@camstack/addon-pipeline 1.2.27 → 1.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1336,7 +1336,24 @@ function forkDecodeWorker(_source) {
|
|
|
1336
1336
|
//#endregion
|
|
1337
1337
|
//#region src/session-decode/guarded-session-decode.ts
|
|
1338
1338
|
/**
|
|
1339
|
-
*
|
|
1339
|
+
* 2s → 5s → 15s → 30s, then 30s forever.
|
|
1340
|
+
*
|
|
1341
|
+
* Indefinite is deliberate. The failure this recovers from is "the broker has
|
|
1342
|
+
* no profile bound yet" or "the broker call failed right now" — both of which
|
|
1343
|
+
* clear on their own, often minutes later (a source re-dialing, a runner
|
|
1344
|
+
* restarting). A bounded retry would turn a slow recovery back into the
|
|
1345
|
+
* permanent outage this exists to prevent, and teardown already stops it: a
|
|
1346
|
+
* detach aborts the loop, so nothing retries for a camera nobody wants.
|
|
1347
|
+
*/
|
|
1348
|
+
var DEFAULT_ACQUIRE_RETRY_DELAYS_MS = [
|
|
1349
|
+
2e3,
|
|
1350
|
+
5e3,
|
|
1351
|
+
15e3,
|
|
1352
|
+
3e4
|
|
1353
|
+
];
|
|
1354
|
+
/**
|
|
1355
|
+
* Guards the in-flight restream-acquire against a detach/unsubscribe race,
|
|
1356
|
+
* and retries an acquire that fails.
|
|
1340
1357
|
*
|
|
1341
1358
|
* `acquireSessionDecodeRestream` + forking the decode worker both take real
|
|
1342
1359
|
* time (a broker round-trip, then a process fork). If the caller's teardown
|
|
@@ -1348,29 +1365,71 @@ function forkDecodeWorker(_source) {
|
|
|
1348
1365
|
*
|
|
1349
1366
|
* This helper returns the teardown SYNCHRONOUSLY. The acquire + pump-start
|
|
1350
1367
|
* run in the background; an `aborted` flag recorded by the (possibly
|
|
1351
|
-
* already-called) teardown is checked right after
|
|
1352
|
-
* if set,
|
|
1368
|
+
* already-called) teardown is checked right after each acquire resolves —
|
|
1369
|
+
* if set, any restream is released immediately and the pump is NEVER
|
|
1353
1370
|
* started (no orphan worker). Otherwise the pump starts and the teardown,
|
|
1354
1371
|
* once called, stops the pump then releases the restream.
|
|
1372
|
+
*
|
|
1373
|
+
* **A failed acquire is retried, not swallowed.** The teardown handed back
|
|
1374
|
+
* here is stored by the caller (as `attachment.detectionUnsubscribe`) the
|
|
1375
|
+
* instant this returns, so a `return` on failure left that handle permanently
|
|
1376
|
+
* non-null with no pump behind it: `onEnded` could never fire because nothing
|
|
1377
|
+
* had ever started, the camera's phase stayed `active`, and not one frame
|
|
1378
|
+
* arrived again. The motion path was hardened against this shape by a frame
|
|
1379
|
+
* freshness window (`MOTION_FRESHNESS_WINDOW_MS`); this is the detection
|
|
1380
|
+
* path's half.
|
|
1355
1381
|
*/
|
|
1356
1382
|
function startGuardedSessionDecode(deps) {
|
|
1357
1383
|
let aborted = false;
|
|
1358
1384
|
let stopPump = null;
|
|
1359
1385
|
let release = null;
|
|
1386
|
+
let retryTimer = null;
|
|
1387
|
+
const delays = deps.acquireRetryDelaysMs && deps.acquireRetryDelaysMs.length > 0 ? deps.acquireRetryDelaysMs : DEFAULT_ACQUIRE_RETRY_DELAYS_MS;
|
|
1388
|
+
/** Resolves after the backoff for `attempt`, or immediately once aborted. */
|
|
1389
|
+
const waitBackoff = (attempt) => new Promise((resolve) => {
|
|
1390
|
+
const ms = delays[Math.min(attempt, delays.length - 1)] ?? delays[delays.length - 1] ?? 2e3;
|
|
1391
|
+
retryTimer = setTimeout(() => {
|
|
1392
|
+
retryTimer = null;
|
|
1393
|
+
resolve();
|
|
1394
|
+
}, ms);
|
|
1395
|
+
retryTimer.unref?.();
|
|
1396
|
+
});
|
|
1360
1397
|
(async () => {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1398
|
+
for (let attempt = 0; !aborted; attempt++) {
|
|
1399
|
+
let acquired = null;
|
|
1400
|
+
try {
|
|
1401
|
+
acquired = await deps.acquire();
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
(attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: acquire failed — retrying", { meta: {
|
|
1404
|
+
attempt: attempt + 1,
|
|
1405
|
+
error: require_dist.errMsg(err)
|
|
1406
|
+
} });
|
|
1407
|
+
await waitBackoff(attempt);
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (aborted) {
|
|
1411
|
+
if (acquired) acquired.release();
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (!acquired) {
|
|
1415
|
+
(attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: no broker profile bound yet — retrying", { meta: { attempt: attempt + 1 } });
|
|
1416
|
+
await waitBackoff(attempt);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (attempt > 0) deps.logger.info("session-decode: acquire succeeded after retry", { meta: { attempts: attempt + 1 } });
|
|
1420
|
+
release = acquired.release;
|
|
1421
|
+
stopPump = deps.startPump(acquired.source);
|
|
1365
1422
|
return;
|
|
1366
1423
|
}
|
|
1367
|
-
release = acquired.release;
|
|
1368
|
-
stopPump = deps.startPump(acquired.source);
|
|
1369
1424
|
})().catch((err) => {
|
|
1370
1425
|
deps.logger.warn("session-decode: guarded start failed", { meta: { error: require_dist.errMsg(err) } });
|
|
1371
1426
|
});
|
|
1372
1427
|
return () => {
|
|
1373
1428
|
aborted = true;
|
|
1429
|
+
if (retryTimer !== null) {
|
|
1430
|
+
clearTimeout(retryTimer);
|
|
1431
|
+
retryTimer = null;
|
|
1432
|
+
}
|
|
1374
1433
|
stopPump?.();
|
|
1375
1434
|
if (release) release();
|
|
1376
1435
|
};
|
|
@@ -1330,7 +1330,24 @@ function forkDecodeWorker(_source) {
|
|
|
1330
1330
|
//#endregion
|
|
1331
1331
|
//#region src/session-decode/guarded-session-decode.ts
|
|
1332
1332
|
/**
|
|
1333
|
-
*
|
|
1333
|
+
* 2s → 5s → 15s → 30s, then 30s forever.
|
|
1334
|
+
*
|
|
1335
|
+
* Indefinite is deliberate. The failure this recovers from is "the broker has
|
|
1336
|
+
* no profile bound yet" or "the broker call failed right now" — both of which
|
|
1337
|
+
* clear on their own, often minutes later (a source re-dialing, a runner
|
|
1338
|
+
* restarting). A bounded retry would turn a slow recovery back into the
|
|
1339
|
+
* permanent outage this exists to prevent, and teardown already stops it: a
|
|
1340
|
+
* detach aborts the loop, so nothing retries for a camera nobody wants.
|
|
1341
|
+
*/
|
|
1342
|
+
var DEFAULT_ACQUIRE_RETRY_DELAYS_MS = [
|
|
1343
|
+
2e3,
|
|
1344
|
+
5e3,
|
|
1345
|
+
15e3,
|
|
1346
|
+
3e4
|
|
1347
|
+
];
|
|
1348
|
+
/**
|
|
1349
|
+
* Guards the in-flight restream-acquire against a detach/unsubscribe race,
|
|
1350
|
+
* and retries an acquire that fails.
|
|
1334
1351
|
*
|
|
1335
1352
|
* `acquireSessionDecodeRestream` + forking the decode worker both take real
|
|
1336
1353
|
* time (a broker round-trip, then a process fork). If the caller's teardown
|
|
@@ -1342,29 +1359,71 @@ function forkDecodeWorker(_source) {
|
|
|
1342
1359
|
*
|
|
1343
1360
|
* This helper returns the teardown SYNCHRONOUSLY. The acquire + pump-start
|
|
1344
1361
|
* run in the background; an `aborted` flag recorded by the (possibly
|
|
1345
|
-
* already-called) teardown is checked right after
|
|
1346
|
-
* if set,
|
|
1362
|
+
* already-called) teardown is checked right after each acquire resolves —
|
|
1363
|
+
* if set, any restream is released immediately and the pump is NEVER
|
|
1347
1364
|
* started (no orphan worker). Otherwise the pump starts and the teardown,
|
|
1348
1365
|
* once called, stops the pump then releases the restream.
|
|
1366
|
+
*
|
|
1367
|
+
* **A failed acquire is retried, not swallowed.** The teardown handed back
|
|
1368
|
+
* here is stored by the caller (as `attachment.detectionUnsubscribe`) the
|
|
1369
|
+
* instant this returns, so a `return` on failure left that handle permanently
|
|
1370
|
+
* non-null with no pump behind it: `onEnded` could never fire because nothing
|
|
1371
|
+
* had ever started, the camera's phase stayed `active`, and not one frame
|
|
1372
|
+
* arrived again. The motion path was hardened against this shape by a frame
|
|
1373
|
+
* freshness window (`MOTION_FRESHNESS_WINDOW_MS`); this is the detection
|
|
1374
|
+
* path's half.
|
|
1349
1375
|
*/
|
|
1350
1376
|
function startGuardedSessionDecode(deps) {
|
|
1351
1377
|
let aborted = false;
|
|
1352
1378
|
let stopPump = null;
|
|
1353
1379
|
let release = null;
|
|
1380
|
+
let retryTimer = null;
|
|
1381
|
+
const delays = deps.acquireRetryDelaysMs && deps.acquireRetryDelaysMs.length > 0 ? deps.acquireRetryDelaysMs : DEFAULT_ACQUIRE_RETRY_DELAYS_MS;
|
|
1382
|
+
/** Resolves after the backoff for `attempt`, or immediately once aborted. */
|
|
1383
|
+
const waitBackoff = (attempt) => new Promise((resolve) => {
|
|
1384
|
+
const ms = delays[Math.min(attempt, delays.length - 1)] ?? delays[delays.length - 1] ?? 2e3;
|
|
1385
|
+
retryTimer = setTimeout(() => {
|
|
1386
|
+
retryTimer = null;
|
|
1387
|
+
resolve();
|
|
1388
|
+
}, ms);
|
|
1389
|
+
retryTimer.unref?.();
|
|
1390
|
+
});
|
|
1354
1391
|
(async () => {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1392
|
+
for (let attempt = 0; !aborted; attempt++) {
|
|
1393
|
+
let acquired = null;
|
|
1394
|
+
try {
|
|
1395
|
+
acquired = await deps.acquire();
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
(attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: acquire failed — retrying", { meta: {
|
|
1398
|
+
attempt: attempt + 1,
|
|
1399
|
+
error: errMsg(err)
|
|
1400
|
+
} });
|
|
1401
|
+
await waitBackoff(attempt);
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
if (aborted) {
|
|
1405
|
+
if (acquired) acquired.release();
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (!acquired) {
|
|
1409
|
+
(attempt === 0 ? deps.logger.warn : deps.logger.debug).call(deps.logger, "session-decode: no broker profile bound yet — retrying", { meta: { attempt: attempt + 1 } });
|
|
1410
|
+
await waitBackoff(attempt);
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
if (attempt > 0) deps.logger.info("session-decode: acquire succeeded after retry", { meta: { attempts: attempt + 1 } });
|
|
1414
|
+
release = acquired.release;
|
|
1415
|
+
stopPump = deps.startPump(acquired.source);
|
|
1359
1416
|
return;
|
|
1360
1417
|
}
|
|
1361
|
-
release = acquired.release;
|
|
1362
|
-
stopPump = deps.startPump(acquired.source);
|
|
1363
1418
|
})().catch((err) => {
|
|
1364
1419
|
deps.logger.warn("session-decode: guarded start failed", { meta: { error: errMsg(err) } });
|
|
1365
1420
|
});
|
|
1366
1421
|
return () => {
|
|
1367
1422
|
aborted = true;
|
|
1423
|
+
if (retryTimer !== null) {
|
|
1424
|
+
clearTimeout(retryTimer);
|
|
1425
|
+
retryTimer = null;
|
|
1426
|
+
}
|
|
1368
1427
|
stopPump?.();
|
|
1369
1428
|
if (release) release();
|
|
1370
1429
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.28",
|
|
4
4
|
"description": "CamStack Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|