@camstack/system 1.2.259 → 1.2.261

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.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Asking again for a source that has not republished yet.
3
+ *
4
+ * A camera provider re-announces its streams a moment after the hub comes up,
5
+ * and a grid opened inside that window used to fail outright:
6
+ *
7
+ * ```
8
+ * camera grid: could not acquire every source — releasing the ones already taken
9
+ * {acquired=0 error="getStreamWithCodec: profile \"low\" source \"native:sub\"
10
+ * not published for device 592"}
11
+ * ```
12
+ *
13
+ * Observed twice on the hub at 12:02 on 2026-09-18 and gone by the next attempt.
14
+ * It is the same shape as a WebRTC broker being rebuilt: the answer changes with
15
+ * TIME, and the consumer was deciding on the first ask.
16
+ *
17
+ * The retry is BOUNDED and it changes no verdict — a camera that really has no
18
+ * such profile still fails, with the provider's own error, after a wait that is
19
+ * reported. What it stops is a grid refusing to open for the first few seconds
20
+ * of every hub's life.
21
+ */
22
+ export interface AcquireRetryDeps {
23
+ /** Wall-clock ceiling for the whole acquire, across every attempt. */
24
+ readonly budgetMs: number;
25
+ readonly now: () => number;
26
+ readonly sleep: () => Promise<void>;
27
+ /** Called before each retry with the ms spent so far — for the log line. */
28
+ readonly onRetry?: (waitedMs: number) => void;
29
+ }
30
+ export declare function acquireWithRetry<T>(attempt: () => Promise<T>, deps: AcquireRetryDeps): Promise<T>;
@@ -209,6 +209,19 @@ var GridCameraDevice = class extends require_dist.BaseDevice {
209
209
  }
210
210
  };
211
211
  //#endregion
212
+ //#region src/builtins/camera-grid/acquire-retry.ts
213
+ async function acquireWithRetry(attempt, deps) {
214
+ const startedAt = deps.now();
215
+ for (;;) try {
216
+ return await attempt();
217
+ } catch (error) {
218
+ const waited = deps.now() - startedAt;
219
+ if (waited >= deps.budgetMs) throw error;
220
+ deps.onRetry?.(waited);
221
+ await deps.sleep();
222
+ }
223
+ }
224
+ //#endregion
212
225
  //#region src/builtins/camera-grid/grid-instances.ts
213
226
  /**
214
227
  * The operator-written grid rows.
@@ -1590,7 +1603,6 @@ function inputPlanFor(source) {
1590
1603
  rtspTransport: "tcp",
1591
1604
  fflags: ["+discardcorrupt"],
1592
1605
  lowDelay: true,
1593
- useWallclockTimestamps: true,
1594
1606
  analyzeDurationUs: ANALYZE_DURATION_US,
1595
1607
  probeSizeBytes: PROBE_SIZE_BYTES
1596
1608
  };
@@ -1701,6 +1713,13 @@ async function startGridChild(ctx, plan, sources, onEnded) {
1701
1713
  //#region src/builtins/camera-grid/addon.ts
1702
1714
  var INTEGRATION_NAME = "Camera Grids";
1703
1715
  var RECONCILE_MS = 6e4;
1716
+ /**
1717
+ * How long a source acquire may keep asking while a provider is still coming up.
1718
+ * Long enough to cover the republish window after a hub restart, short enough
1719
+ * that a genuinely missing profile fails while the operator is still looking.
1720
+ */
1721
+ var SOURCE_ACQUIRE_BUDGET_MS = 8e3;
1722
+ var SOURCE_ACQUIRE_PAUSE_MS = 700;
1704
1723
  var DEFAULTS = {
1705
1724
  cameraGrids: [],
1706
1725
  cameraGridTombstones: []
@@ -2013,12 +2032,27 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2013
2032
  deps: {
2014
2033
  logger: this.ctx.logger,
2015
2034
  acquireSource: async (sourceDeviceId) => {
2016
- const dial = gridSourceDial(await this.ctx.api.streamBroker.getStreamWithCodec.mutate({
2035
+ const dial = gridSourceDial(await acquireWithRetry(() => this.ctx.api.streamBroker.getStreamWithCodec.mutate({
2017
2036
  deviceId: sourceDeviceId,
2018
2037
  video: "copy",
2019
2038
  audio: "none",
2020
2039
  profile,
2021
2040
  tag: `camera-grid:${String(deviceId)}:${profile}`
2041
+ }), {
2042
+ budgetMs: SOURCE_ACQUIRE_BUDGET_MS,
2043
+ now: () => Date.now(),
2044
+ sleep: () => new Promise((resolve) => setTimeout(resolve, SOURCE_ACQUIRE_PAUSE_MS)),
2045
+ onRetry: (waitedMs) => {
2046
+ this.ctx.logger.info("camera grid: a source has not republished yet — asking again", {
2047
+ tags: { deviceId },
2048
+ meta: {
2049
+ instanceId,
2050
+ profile,
2051
+ sourceDeviceId,
2052
+ waitedMs
2053
+ }
2054
+ });
2055
+ }
2022
2056
  }));
2023
2057
  this.ctx.logger.info("camera grid: source acquired", {
2024
2058
  tags: { deviceId },
@@ -204,6 +204,19 @@ var GridCameraDevice = class extends BaseDevice {
204
204
  }
205
205
  };
206
206
  //#endregion
207
+ //#region src/builtins/camera-grid/acquire-retry.ts
208
+ async function acquireWithRetry(attempt, deps) {
209
+ const startedAt = deps.now();
210
+ for (;;) try {
211
+ return await attempt();
212
+ } catch (error) {
213
+ const waited = deps.now() - startedAt;
214
+ if (waited >= deps.budgetMs) throw error;
215
+ deps.onRetry?.(waited);
216
+ await deps.sleep();
217
+ }
218
+ }
219
+ //#endregion
207
220
  //#region src/builtins/camera-grid/grid-instances.ts
208
221
  /**
209
222
  * The operator-written grid rows.
@@ -1585,7 +1598,6 @@ function inputPlanFor(source) {
1585
1598
  rtspTransport: "tcp",
1586
1599
  fflags: ["+discardcorrupt"],
1587
1600
  lowDelay: true,
1588
- useWallclockTimestamps: true,
1589
1601
  analyzeDurationUs: ANALYZE_DURATION_US,
1590
1602
  probeSizeBytes: PROBE_SIZE_BYTES
1591
1603
  };
@@ -1696,6 +1708,13 @@ async function startGridChild(ctx, plan, sources, onEnded) {
1696
1708
  //#region src/builtins/camera-grid/addon.ts
1697
1709
  var INTEGRATION_NAME = "Camera Grids";
1698
1710
  var RECONCILE_MS = 6e4;
1711
+ /**
1712
+ * How long a source acquire may keep asking while a provider is still coming up.
1713
+ * Long enough to cover the republish window after a hub restart, short enough
1714
+ * that a genuinely missing profile fails while the operator is still looking.
1715
+ */
1716
+ var SOURCE_ACQUIRE_BUDGET_MS = 8e3;
1717
+ var SOURCE_ACQUIRE_PAUSE_MS = 700;
1699
1718
  var DEFAULTS = {
1700
1719
  cameraGrids: [],
1701
1720
  cameraGridTombstones: []
@@ -2008,12 +2027,27 @@ var CameraGridAddon = class extends BaseAddon {
2008
2027
  deps: {
2009
2028
  logger: this.ctx.logger,
2010
2029
  acquireSource: async (sourceDeviceId) => {
2011
- const dial = gridSourceDial(await this.ctx.api.streamBroker.getStreamWithCodec.mutate({
2030
+ const dial = gridSourceDial(await acquireWithRetry(() => this.ctx.api.streamBroker.getStreamWithCodec.mutate({
2012
2031
  deviceId: sourceDeviceId,
2013
2032
  video: "copy",
2014
2033
  audio: "none",
2015
2034
  profile,
2016
2035
  tag: `camera-grid:${String(deviceId)}:${profile}`
2036
+ }), {
2037
+ budgetMs: SOURCE_ACQUIRE_BUDGET_MS,
2038
+ now: () => Date.now(),
2039
+ sleep: () => new Promise((resolve) => setTimeout(resolve, SOURCE_ACQUIRE_PAUSE_MS)),
2040
+ onRetry: (waitedMs) => {
2041
+ this.ctx.logger.info("camera grid: a source has not republished yet — asking again", {
2042
+ tags: { deviceId },
2043
+ meta: {
2044
+ instanceId,
2045
+ profile,
2046
+ sourceDeviceId,
2047
+ waitedMs
2048
+ }
2049
+ });
2050
+ }
2017
2051
  }));
2018
2052
  this.ctx.logger.info("camera grid: source acquired", {
2019
2053
  tags: { deviceId },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.259",
3
+ "version": "1.2.261",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",