@camstack/addon-pipeline 1.1.32 → 1.1.34

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.
Files changed (41) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/audio-codec-ffmpeg/index.js +118 -3
  4. package/dist/audio-codec-ffmpeg/index.mjs +118 -3
  5. package/dist/decoder-ffmpeg/index.js +75 -10
  6. package/dist/decoder-ffmpeg/index.mjs +75 -10
  7. package/dist/decoder-nodeav/index.js +209 -9
  8. package/dist/decoder-nodeav/index.mjs +209 -9
  9. package/dist/detection-pipeline/index.js +1 -1
  10. package/dist/detection-pipeline/index.mjs +1 -1
  11. package/dist/{dist-Cwc0TUQr.js → dist-BrIE85BG.js} +38 -1
  12. package/dist/{dist-DjuGmyG9.mjs → dist-CfSORkv2.mjs} +38 -1
  13. package/dist/{ffmpeg-args-C5GPp8Cw.mjs → ffmpeg-args-CpeRWwL4.mjs} +7 -4
  14. package/dist/{ffmpeg-args-D6h1edXK.js → ffmpeg-args-Dl9mLcOC.js} +7 -4
  15. package/dist/{frame-handle-plane-Bkxz-TTD.mjs → frame-handle-plane-C05rq9Zs.mjs} +108 -17
  16. package/dist/{frame-handle-plane-DQNCTrpC.js → frame-handle-plane-zMju9vmk.js} +108 -17
  17. package/dist/{frame-ring-sink-sY-8XUsH.mjs → frame-ring-sink-1aLGcNNf.mjs} +81 -31
  18. package/dist/{frame-ring-sink-DvdhCFr3.js → frame-ring-sink-CvlcAUOu.js} +86 -30
  19. package/dist/motion-wasm/index.js +1 -1
  20. package/dist/motion-wasm/index.mjs +1 -1
  21. package/dist/pipeline-runner/index.js +86 -16
  22. package/dist/pipeline-runner/index.mjs +85 -17
  23. package/dist/recorder/index.js +1 -1
  24. package/dist/recorder/index.mjs +1 -1
  25. package/dist/stream-broker/_stub.js +1 -1
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPFAbcUo.mjs} +3 -3
  27. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsrrdOBU.mjs} +1 -1
  28. package/dist/stream-broker/{hostInit-B_b3PIZB.mjs → hostInit-C8slrxw1.mjs} +3 -3
  29. package/dist/stream-broker/index.js +165 -27
  30. package/dist/stream-broker/index.mjs +165 -27
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js → MaskShapeCanvas-DI4BY7W2-WUVfFzT-.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DTeuDGCU.js → MotionZonesSettings-NcxxQN8r-CF7FXXLs.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CirtxO3e.js → PrivacyMaskSettings-APgPLF7p-qQCpVob3.js} +1 -1
  35. package/embed-dist/assets/{index-CpUy8OIE.js → index-BNhisVUZ.js} +4 -4
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/dist/constants-CAqYVigN.js +0 -6615
  39. package/dist/constants-oYgW36Ty.mjs +0 -3587
  40. package/dist/dist-D3Ytt-_Y.js +0 -41111
  41. package/dist/dist-DluJuxV5.mjs +0 -37976
@@ -16328,7 +16328,17 @@ var AgentAddonConfigSchema = object({
16328
16328
  });
16329
16329
  var AgentPipelineSettingsSchema = object({
16330
16330
  addonDefaults: record(string(), AgentAddonConfigSchema).readonly(),
16331
- maxCameras: number().int().nonnegative().nullable().default(null)
16331
+ maxCameras: number().int().nonnegative().nullable().default(null),
16332
+ /** Per-node detection weight (relative share for the quota balancer). */
16333
+ detectWeight: number().positive().optional(),
16334
+ /** Node is eligible to run the detection pipeline (decode + inference). */
16335
+ detect: boolean().optional(),
16336
+ /** Node is eligible to host decoder sessions. */
16337
+ decode: boolean().optional(),
16338
+ /** Node is eligible to run audio-analyzer sessions. */
16339
+ audio: boolean().optional(),
16340
+ /** Node is eligible to be the ingest / source-owner (serve the restream). */
16341
+ ingest: boolean().optional()
16332
16342
  });
16333
16343
  var CameraPipelineForAgentSchema = object({
16334
16344
  steps: array(PipelineStepInputSchema).readonly(),
@@ -16634,6 +16644,21 @@ method(object({
16634
16644
  }), object({ success: literal(true) }), {
16635
16645
  kind: "mutation",
16636
16646
  auth: "admin"
16647
+ }), method(object({
16648
+ agentNodeId: string(),
16649
+ detectWeight: number().positive().nullable()
16650
+ }), object({ success: literal(true) }), {
16651
+ kind: "mutation",
16652
+ auth: "admin"
16653
+ }), method(object({
16654
+ agentNodeId: string(),
16655
+ detect: boolean().nullable().optional(),
16656
+ decode: boolean().nullable().optional(),
16657
+ audio: boolean().nullable().optional(),
16658
+ ingest: boolean().nullable().optional()
16659
+ }), object({ success: literal(true) }), {
16660
+ kind: "mutation",
16661
+ auth: "admin"
16637
16662
  }), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
16638
16663
  deviceId: number(),
16639
16664
  addonId: string(),
@@ -22621,6 +22646,18 @@ Object.freeze({
22621
22646
  addonId: null,
22622
22647
  access: "create"
22623
22648
  },
22649
+ "pipelineOrchestrator.setAgentCapabilities": {
22650
+ capName: "pipeline-orchestrator",
22651
+ capScope: "system",
22652
+ addonId: null,
22653
+ access: "create"
22654
+ },
22655
+ "pipelineOrchestrator.setAgentDetectWeight": {
22656
+ capName: "pipeline-orchestrator",
22657
+ capScope: "system",
22658
+ addonId: null,
22659
+ access: "create"
22660
+ },
22624
22661
  "pipelineOrchestrator.setAgentMaxCameras": {
22625
22662
  capName: "pipeline-orchestrator",
22626
22663
  capScope: "system",
@@ -194,6 +194,12 @@ function buildFfmpegDecodeArgs(config, options) {
194
194
  "-i",
195
195
  "pipe:0"
196
196
  ];
197
+ const probeArgs = options.inputUrl !== void 0 ? [] : [
198
+ "-probesize",
199
+ "1M",
200
+ "-analyzeduration",
201
+ "0"
202
+ ];
197
203
  return [
198
204
  ...logBannerArgs("info"),
199
205
  "-nostats",
@@ -201,10 +207,7 @@ function buildFfmpegDecodeArgs(config, options) {
201
207
  "+nobuffer+flush_packets",
202
208
  "-flags",
203
209
  "low_delay",
204
- "-probesize",
205
- "1M",
206
- "-analyzeduration",
207
- "0",
210
+ ...probeArgs,
208
211
  ...hwaccelArgs,
209
212
  ...inputArgs,
210
213
  "-vf",
@@ -194,6 +194,12 @@ function buildFfmpegDecodeArgs(config, options) {
194
194
  "-i",
195
195
  "pipe:0"
196
196
  ];
197
+ const probeArgs = options.inputUrl !== void 0 ? [] : [
198
+ "-probesize",
199
+ "1M",
200
+ "-analyzeduration",
201
+ "0"
202
+ ];
197
203
  return [
198
204
  ...logBannerArgs("info"),
199
205
  "-nostats",
@@ -201,10 +207,7 @@ function buildFfmpegDecodeArgs(config, options) {
201
207
  "+nobuffer+flush_packets",
202
208
  "-flags",
203
209
  "low_delay",
204
- "-probesize",
205
- "1M",
206
- "-analyzeduration",
207
- "0",
210
+ ...probeArgs,
208
211
  ...hwaccelArgs,
209
212
  ...inputArgs,
210
213
  "-vf",
@@ -1,4 +1,4 @@
1
- import { B as errMsg, p as RingBuffer } from "./dist-DjuGmyG9.mjs";
1
+ import { B as errMsg, p as RingBuffer } from "./dist-CfSORkv2.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**
@@ -189,6 +189,7 @@ var FrameHandlePlane = class {
189
189
  resolveStreamInfo;
190
190
  localNodeId;
191
191
  sessionRetryCooldownMs;
192
+ getSeedPackets;
192
193
  subscriptions = /* @__PURE__ */ new Map();
193
194
  sessions = /* @__PURE__ */ new Map();
194
195
  /** Per-format retry gate set when a session lands on a non-local node:
@@ -203,6 +204,9 @@ var FrameHandlePlane = class {
203
204
  /** Re-entrancy guard for `armPendingSessions` — collapses the per-packet
204
205
  * `pushPacket` calls into a single in-flight deferred-arm pass. */
205
206
  armInFlight = false;
207
+ /** DIAG(592): P-frames skipped on an un-bootstrapped session before its
208
+ * first live keyframe. Reset when a keyframe bootstraps the session. */
209
+ unbootstrappedSkips = 0;
206
210
  /** Aggregate decoded-frame rate state — frames fanned out in the current
207
211
  * rolling window, the window's start epoch, and the last computed fps. */
208
212
  framesInWindow = 0;
@@ -214,6 +218,7 @@ var FrameHandlePlane = class {
214
218
  this.resolveStreamInfo = options.resolveStreamInfo;
215
219
  this.localNodeId = options.localNodeId;
216
220
  this.sessionRetryCooldownMs = options.sessionRetryCooldownMs ?? SESSION_RETRY_COOLDOWN_MS;
221
+ this.getSeedPackets = options.getSeedPackets;
217
222
  }
218
223
  /** Number of active handle subscriptions — surfaced for diagnostics. */
219
224
  get subscriberCount() {
@@ -278,6 +283,7 @@ var FrameHandlePlane = class {
278
283
  };
279
284
  this.subscriptions.set(subscriptionId, subscription);
280
285
  await this.ensureSession(input.format, subscriptionId);
286
+ this.reconcileSessionFps(input.format);
281
287
  this.logger?.info("frame-handle subscription added", { meta: {
282
288
  subscriptionId,
283
289
  format: input.format,
@@ -290,6 +296,37 @@ var FrameHandlePlane = class {
290
296
  };
291
297
  }
292
298
  /**
299
+ * The emit `maxFps` a format's decoder session should run at — the MAX of all
300
+ * current subscribers' `maxFps` for that format (fallback `DEFAULT_HINT_FPS`
301
+ * when none, e.g. mid-teardown). The decoder emits at this rate; each
302
+ * subscriber then down-samples to its own `maxFps` on read (latest-wins).
303
+ */
304
+ computeFormatFps(format) {
305
+ let max = 0;
306
+ for (const sub of this.subscriptions.values()) if (sub.format === format && sub.maxFps > max) max = sub.maxFps;
307
+ return max > 0 ? max : DEFAULT_HINT_FPS;
308
+ }
309
+ /**
310
+ * Recompute a format session's emit `maxFps` after a subscriber joins/leaves
311
+ * and push it to the live decoder via `updateConfig` when it changed — so the
312
+ * decoder emits only as fast as the busiest reader needs (saving the
313
+ * scale+convert+shm-write for frames nobody reads).
314
+ */
315
+ reconcileSessionFps(format) {
316
+ const session = this.sessions.get(format);
317
+ if (!session || !session.proxy) return;
318
+ const target = this.computeFormatFps(format);
319
+ if (target === session.targetFps) return;
320
+ session.targetFps = target;
321
+ session.proxy.updateConfig({ maxFps: target }).catch((err) => {
322
+ this.logger?.warn("frame-handle plane: updateConfig(maxFps) failed", { meta: {
323
+ format,
324
+ maxFps: target,
325
+ error: errMsg(err)
326
+ } });
327
+ });
328
+ }
329
+ /**
293
330
  * Drain up to `maxCount` `FrameHandle`s for a subscription, latest-wins.
294
331
  * An unknown subscription id returns `[]` (the consumer may have been
295
332
  * torn down concurrently).
@@ -320,6 +357,7 @@ var FrameHandlePlane = class {
320
357
  this.sessions.delete(subscription.format);
321
358
  await this.destroySession(session);
322
359
  } else this.scheduleLinger(session);
360
+ else this.reconcileSessionFps(subscription.format);
323
361
  }
324
362
  this.logger?.info("frame-handle subscription removed", { meta: {
325
363
  subscriptionId,
@@ -373,10 +411,23 @@ var FrameHandlePlane = class {
373
411
  });
374
412
  this.feedSessions(packet);
375
413
  }
376
- /** Forward a video `EncodedPacket` to every live shm decoder session. */
414
+ /** Forward a video `EncodedPacket` to every live PUSH-mode shm decoder session. */
377
415
  feedSessions(packet) {
378
416
  for (const session of this.sessions.values()) {
379
417
  if (!session.proxy) continue;
418
+ if (session.pullMode) continue;
419
+ if (!session.bootstrapped) {
420
+ if (!packet.keyframe) {
421
+ this.unbootstrappedSkips = (this.unbootstrappedSkips ?? 0) + 1;
422
+ continue;
423
+ }
424
+ this.logger?.debug("frame-handle plane: session bootstrapped from live keyframe", { meta: {
425
+ format: session.format,
426
+ pFramesSkippedBefore: this.unbootstrappedSkips ?? 0
427
+ } });
428
+ this.unbootstrappedSkips = 0;
429
+ session.bootstrapped = true;
430
+ }
380
431
  session.proxy.pushPacket(packet).catch((err) => {
381
432
  const msg = errMsg(err);
382
433
  this.logger?.warn("frame-handle plane: decoder push error", { meta: {
@@ -404,10 +455,14 @@ var FrameHandlePlane = class {
404
455
  *
405
456
  * `triggerPacket` is the packet whose arrival armed the pass. Because
406
457
  * `ensureSession` is async, the synchronous `feedSessions` in `pushPacket`
407
- * runs before the session lands in `this.sessions` so this method feeds
408
- * the triggering packet to each session it just created, otherwise that
409
- * first (keyframe-carrying) packet would be lost and a push-mode H264/H265
410
- * decoder would stall waiting for SPS/PPS.
458
+ * runs before the session lands in `this.sessions`, so the triggering packet
459
+ * would be lost to the new session. `startSessionDecoder` normally bootstraps
460
+ * it by replaying the broker's keyframe-aligned seed GOP; only when NO seed
461
+ * was available (no `getSeedPackets`, or the ring held no keyframe yet) is the
462
+ * session left un-bootstrapped — and then this method hands it the triggering
463
+ * packet, but ONLY if that packet is a keyframe. A P-frame trigger is left for
464
+ * `feedSessions` to skip until the first live keyframe, so a push-mode decoder
465
+ * never sees a slice referencing an unseen param set.
411
466
  */
412
467
  async armPendingSessions(triggerPacket) {
413
468
  if (this.disposed || this.armInFlight) return;
@@ -423,12 +478,15 @@ var FrameHandlePlane = class {
423
478
  if (this.disposed) return;
424
479
  await this.ensureSession(format, subscriptionId);
425
480
  const armed = this.sessions.get(format);
426
- if (armed?.proxy) armed.proxy.pushPacket(triggerPacket).catch((err) => {
427
- this.logger?.warn("frame-handle plane: decoder push error", { meta: {
428
- format,
429
- error: errMsg(err)
430
- } });
431
- });
481
+ if (armed?.proxy && !armed.bootstrapped && triggerPacket.keyframe) {
482
+ armed.bootstrapped = true;
483
+ armed.proxy.pushPacket(triggerPacket).catch((err) => {
484
+ this.logger?.warn("frame-handle plane: decoder push error", { meta: {
485
+ format,
486
+ error: errMsg(err)
487
+ } });
488
+ });
489
+ }
432
490
  }
433
491
  } finally {
434
492
  this.armInFlight = false;
@@ -522,6 +580,9 @@ var FrameHandlePlane = class {
522
580
  proxy: null,
523
581
  nodeId: null,
524
582
  subscriberIds,
583
+ bootstrapped: false,
584
+ pullMode: false,
585
+ targetFps: this.computeFormatFps(format),
525
586
  lingerTimer: null
526
587
  };
527
588
  let started = false;
@@ -561,9 +622,11 @@ var FrameHandlePlane = class {
561
622
  } });
562
623
  return false;
563
624
  }
625
+ const targetFps = this.computeFormatFps(format);
626
+ session.targetFps = targetFps;
564
627
  const { sessionId, nodeId } = await this.decoderApi.createSession({
565
628
  codec: info.codec,
566
- maxFps: 0,
629
+ maxFps: targetFps,
567
630
  outputFormat: format,
568
631
  scale: 1,
569
632
  frameSink: "shm",
@@ -592,6 +655,8 @@ var FrameHandlePlane = class {
592
655
  const proxy = new DecoderSessionProxy(this.decoderApi, sessionId);
593
656
  session.proxy = proxy;
594
657
  session.nodeId = nodeId;
658
+ session.bootstrapped = false;
659
+ session.pullMode = false;
595
660
  proxy.startHandlePolling((handle) => {
596
661
  this.fanoutHandle(format, handle);
597
662
  }).catch((err) => {
@@ -600,12 +665,38 @@ var FrameHandlePlane = class {
600
665
  error: errMsg(err)
601
666
  } });
602
667
  });
603
- if (info.pullModeUrl) proxy.openStream(info.pullModeUrl).catch((err) => {
604
- this.logger?.error("frame-handle plane: pull-mode openStream failed", { meta: {
668
+ if (info.pullModeUrl) {
669
+ session.pullMode = true;
670
+ session.bootstrapped = true;
671
+ proxy.openStream(info.pullModeUrl).catch((err) => {
672
+ this.logger?.error("frame-handle plane: pull-mode openStream failed", { meta: {
673
+ format,
674
+ error: errMsg(err)
675
+ } });
676
+ });
677
+ this.logger?.info("frame-handle plane: shm decoder session created (pull-mode RTSP)", { meta: {
605
678
  format,
606
- error: errMsg(err)
679
+ codec: info.codec,
680
+ sessionId,
681
+ nodeId,
682
+ pullModeUrl: info.pullModeUrl
607
683
  } });
608
- });
684
+ return true;
685
+ }
686
+ const seed = this.getSeedPackets?.() ?? [];
687
+ if (seed.length > 0) {
688
+ for (const seedPacket of seed) await proxy.pushPacket(seedPacket).catch((err) => {
689
+ this.logger?.warn("frame-handle plane: seed-replay push error", { meta: {
690
+ format,
691
+ error: errMsg(err)
692
+ } });
693
+ });
694
+ session.bootstrapped = true;
695
+ this.logger?.debug("frame-handle plane: session bootstrapped from seed GOP", { meta: {
696
+ format,
697
+ seedPackets: seed.length
698
+ } });
699
+ }
609
700
  this.logger?.info("frame-handle plane: shm decoder session created", { meta: {
610
701
  format,
611
702
  codec: info.codec,
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-Cwc0TUQr.js");
1
+ const require_dist = require("./dist-BrIE85BG.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**
@@ -189,6 +189,7 @@ var FrameHandlePlane = class {
189
189
  resolveStreamInfo;
190
190
  localNodeId;
191
191
  sessionRetryCooldownMs;
192
+ getSeedPackets;
192
193
  subscriptions = /* @__PURE__ */ new Map();
193
194
  sessions = /* @__PURE__ */ new Map();
194
195
  /** Per-format retry gate set when a session lands on a non-local node:
@@ -203,6 +204,9 @@ var FrameHandlePlane = class {
203
204
  /** Re-entrancy guard for `armPendingSessions` — collapses the per-packet
204
205
  * `pushPacket` calls into a single in-flight deferred-arm pass. */
205
206
  armInFlight = false;
207
+ /** DIAG(592): P-frames skipped on an un-bootstrapped session before its
208
+ * first live keyframe. Reset when a keyframe bootstraps the session. */
209
+ unbootstrappedSkips = 0;
206
210
  /** Aggregate decoded-frame rate state — frames fanned out in the current
207
211
  * rolling window, the window's start epoch, and the last computed fps. */
208
212
  framesInWindow = 0;
@@ -214,6 +218,7 @@ var FrameHandlePlane = class {
214
218
  this.resolveStreamInfo = options.resolveStreamInfo;
215
219
  this.localNodeId = options.localNodeId;
216
220
  this.sessionRetryCooldownMs = options.sessionRetryCooldownMs ?? SESSION_RETRY_COOLDOWN_MS;
221
+ this.getSeedPackets = options.getSeedPackets;
217
222
  }
218
223
  /** Number of active handle subscriptions — surfaced for diagnostics. */
219
224
  get subscriberCount() {
@@ -278,6 +283,7 @@ var FrameHandlePlane = class {
278
283
  };
279
284
  this.subscriptions.set(subscriptionId, subscription);
280
285
  await this.ensureSession(input.format, subscriptionId);
286
+ this.reconcileSessionFps(input.format);
281
287
  this.logger?.info("frame-handle subscription added", { meta: {
282
288
  subscriptionId,
283
289
  format: input.format,
@@ -290,6 +296,37 @@ var FrameHandlePlane = class {
290
296
  };
291
297
  }
292
298
  /**
299
+ * The emit `maxFps` a format's decoder session should run at — the MAX of all
300
+ * current subscribers' `maxFps` for that format (fallback `DEFAULT_HINT_FPS`
301
+ * when none, e.g. mid-teardown). The decoder emits at this rate; each
302
+ * subscriber then down-samples to its own `maxFps` on read (latest-wins).
303
+ */
304
+ computeFormatFps(format) {
305
+ let max = 0;
306
+ for (const sub of this.subscriptions.values()) if (sub.format === format && sub.maxFps > max) max = sub.maxFps;
307
+ return max > 0 ? max : DEFAULT_HINT_FPS;
308
+ }
309
+ /**
310
+ * Recompute a format session's emit `maxFps` after a subscriber joins/leaves
311
+ * and push it to the live decoder via `updateConfig` when it changed — so the
312
+ * decoder emits only as fast as the busiest reader needs (saving the
313
+ * scale+convert+shm-write for frames nobody reads).
314
+ */
315
+ reconcileSessionFps(format) {
316
+ const session = this.sessions.get(format);
317
+ if (!session || !session.proxy) return;
318
+ const target = this.computeFormatFps(format);
319
+ if (target === session.targetFps) return;
320
+ session.targetFps = target;
321
+ session.proxy.updateConfig({ maxFps: target }).catch((err) => {
322
+ this.logger?.warn("frame-handle plane: updateConfig(maxFps) failed", { meta: {
323
+ format,
324
+ maxFps: target,
325
+ error: require_dist.errMsg(err)
326
+ } });
327
+ });
328
+ }
329
+ /**
293
330
  * Drain up to `maxCount` `FrameHandle`s for a subscription, latest-wins.
294
331
  * An unknown subscription id returns `[]` (the consumer may have been
295
332
  * torn down concurrently).
@@ -320,6 +357,7 @@ var FrameHandlePlane = class {
320
357
  this.sessions.delete(subscription.format);
321
358
  await this.destroySession(session);
322
359
  } else this.scheduleLinger(session);
360
+ else this.reconcileSessionFps(subscription.format);
323
361
  }
324
362
  this.logger?.info("frame-handle subscription removed", { meta: {
325
363
  subscriptionId,
@@ -373,10 +411,23 @@ var FrameHandlePlane = class {
373
411
  });
374
412
  this.feedSessions(packet);
375
413
  }
376
- /** Forward a video `EncodedPacket` to every live shm decoder session. */
414
+ /** Forward a video `EncodedPacket` to every live PUSH-mode shm decoder session. */
377
415
  feedSessions(packet) {
378
416
  for (const session of this.sessions.values()) {
379
417
  if (!session.proxy) continue;
418
+ if (session.pullMode) continue;
419
+ if (!session.bootstrapped) {
420
+ if (!packet.keyframe) {
421
+ this.unbootstrappedSkips = (this.unbootstrappedSkips ?? 0) + 1;
422
+ continue;
423
+ }
424
+ this.logger?.debug("frame-handle plane: session bootstrapped from live keyframe", { meta: {
425
+ format: session.format,
426
+ pFramesSkippedBefore: this.unbootstrappedSkips ?? 0
427
+ } });
428
+ this.unbootstrappedSkips = 0;
429
+ session.bootstrapped = true;
430
+ }
380
431
  session.proxy.pushPacket(packet).catch((err) => {
381
432
  const msg = require_dist.errMsg(err);
382
433
  this.logger?.warn("frame-handle plane: decoder push error", { meta: {
@@ -404,10 +455,14 @@ var FrameHandlePlane = class {
404
455
  *
405
456
  * `triggerPacket` is the packet whose arrival armed the pass. Because
406
457
  * `ensureSession` is async, the synchronous `feedSessions` in `pushPacket`
407
- * runs before the session lands in `this.sessions` so this method feeds
408
- * the triggering packet to each session it just created, otherwise that
409
- * first (keyframe-carrying) packet would be lost and a push-mode H264/H265
410
- * decoder would stall waiting for SPS/PPS.
458
+ * runs before the session lands in `this.sessions`, so the triggering packet
459
+ * would be lost to the new session. `startSessionDecoder` normally bootstraps
460
+ * it by replaying the broker's keyframe-aligned seed GOP; only when NO seed
461
+ * was available (no `getSeedPackets`, or the ring held no keyframe yet) is the
462
+ * session left un-bootstrapped — and then this method hands it the triggering
463
+ * packet, but ONLY if that packet is a keyframe. A P-frame trigger is left for
464
+ * `feedSessions` to skip until the first live keyframe, so a push-mode decoder
465
+ * never sees a slice referencing an unseen param set.
411
466
  */
412
467
  async armPendingSessions(triggerPacket) {
413
468
  if (this.disposed || this.armInFlight) return;
@@ -423,12 +478,15 @@ var FrameHandlePlane = class {
423
478
  if (this.disposed) return;
424
479
  await this.ensureSession(format, subscriptionId);
425
480
  const armed = this.sessions.get(format);
426
- if (armed?.proxy) armed.proxy.pushPacket(triggerPacket).catch((err) => {
427
- this.logger?.warn("frame-handle plane: decoder push error", { meta: {
428
- format,
429
- error: require_dist.errMsg(err)
430
- } });
431
- });
481
+ if (armed?.proxy && !armed.bootstrapped && triggerPacket.keyframe) {
482
+ armed.bootstrapped = true;
483
+ armed.proxy.pushPacket(triggerPacket).catch((err) => {
484
+ this.logger?.warn("frame-handle plane: decoder push error", { meta: {
485
+ format,
486
+ error: require_dist.errMsg(err)
487
+ } });
488
+ });
489
+ }
432
490
  }
433
491
  } finally {
434
492
  this.armInFlight = false;
@@ -522,6 +580,9 @@ var FrameHandlePlane = class {
522
580
  proxy: null,
523
581
  nodeId: null,
524
582
  subscriberIds,
583
+ bootstrapped: false,
584
+ pullMode: false,
585
+ targetFps: this.computeFormatFps(format),
525
586
  lingerTimer: null
526
587
  };
527
588
  let started = false;
@@ -561,9 +622,11 @@ var FrameHandlePlane = class {
561
622
  } });
562
623
  return false;
563
624
  }
625
+ const targetFps = this.computeFormatFps(format);
626
+ session.targetFps = targetFps;
564
627
  const { sessionId, nodeId } = await this.decoderApi.createSession({
565
628
  codec: info.codec,
566
- maxFps: 0,
629
+ maxFps: targetFps,
567
630
  outputFormat: format,
568
631
  scale: 1,
569
632
  frameSink: "shm",
@@ -592,6 +655,8 @@ var FrameHandlePlane = class {
592
655
  const proxy = new DecoderSessionProxy(this.decoderApi, sessionId);
593
656
  session.proxy = proxy;
594
657
  session.nodeId = nodeId;
658
+ session.bootstrapped = false;
659
+ session.pullMode = false;
595
660
  proxy.startHandlePolling((handle) => {
596
661
  this.fanoutHandle(format, handle);
597
662
  }).catch((err) => {
@@ -600,12 +665,38 @@ var FrameHandlePlane = class {
600
665
  error: require_dist.errMsg(err)
601
666
  } });
602
667
  });
603
- if (info.pullModeUrl) proxy.openStream(info.pullModeUrl).catch((err) => {
604
- this.logger?.error("frame-handle plane: pull-mode openStream failed", { meta: {
668
+ if (info.pullModeUrl) {
669
+ session.pullMode = true;
670
+ session.bootstrapped = true;
671
+ proxy.openStream(info.pullModeUrl).catch((err) => {
672
+ this.logger?.error("frame-handle plane: pull-mode openStream failed", { meta: {
673
+ format,
674
+ error: require_dist.errMsg(err)
675
+ } });
676
+ });
677
+ this.logger?.info("frame-handle plane: shm decoder session created (pull-mode RTSP)", { meta: {
605
678
  format,
606
- error: require_dist.errMsg(err)
679
+ codec: info.codec,
680
+ sessionId,
681
+ nodeId,
682
+ pullModeUrl: info.pullModeUrl
607
683
  } });
608
- });
684
+ return true;
685
+ }
686
+ const seed = this.getSeedPackets?.() ?? [];
687
+ if (seed.length > 0) {
688
+ for (const seedPacket of seed) await proxy.pushPacket(seedPacket).catch((err) => {
689
+ this.logger?.warn("frame-handle plane: seed-replay push error", { meta: {
690
+ format,
691
+ error: require_dist.errMsg(err)
692
+ } });
693
+ });
694
+ session.bootstrapped = true;
695
+ this.logger?.debug("frame-handle plane: session bootstrapped from seed GOP", { meta: {
696
+ format,
697
+ seedPackets: seed.length
698
+ } });
699
+ }
609
700
  this.logger?.info("frame-handle plane: shm decoder session created", { meta: {
610
701
  format,
611
702
  codec: info.codec,