@camstack/addon-pipeline 1.2.342 → 1.2.344

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 (35) hide show
  1. package/dist/audio-analyzer/index.js +5 -4
  2. package/dist/audio-analyzer/index.mjs +5 -4
  3. package/dist/{custom-model-catalog-BjPZrWX3.mjs → custom-model-catalog-CG8fXDWC.mjs} +3 -3
  4. package/dist/{custom-model-catalog-DPBG06HE.js → custom-model-catalog-Cu1C2kTA.js} +3 -3
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-w_gA9z8W.js → dist-DNsARIUD.js} +98 -349
  8. package/dist/{dist-BWzBHFEB.mjs → dist-DUr_0s5c.mjs} +98 -349
  9. package/dist/{model-catalogs-Dseq1bmN.mjs → model-catalogs-BMsnPwTI.mjs} +1 -1
  10. package/dist/{model-catalogs-ll-ggTUb.js → model-catalogs-Y509H8MT.js} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-BTsWSPyQ.js → node-D-U3T-h-.js} +1 -1
  14. package/dist/{node-B7gAWBnv.mjs → node-DhrskFZ_.mjs} +1 -1
  15. package/dist/pipeline-runner/index.js +11 -13
  16. package/dist/pipeline-runner/index.mjs +11 -13
  17. package/dist/{process-memory-Nq3qwp-q.js → process-memory-BUrTDsRR.js} +1 -1
  18. package/dist/{process-memory-BYs3sxnT.mjs → process-memory-DXtI2GWk.mjs} +1 -1
  19. package/dist/recorder/index.js +2 -13
  20. package/dist/recorder/index.mjs +2 -13
  21. package/dist/{segment-demux-js-C7Bk1l_R.mjs → segment-demux-js-Bv7s-_uv.mjs} +1 -1
  22. package/dist/{segment-demux-js-D4kTUDck.js → segment-demux-js-vZ6iniOU.js} +1 -1
  23. package/dist/stream-broker/_stub.js +48 -130
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-By8a97Br.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BH3o1g9i.mjs} +3 -3
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C2VoXjqc.mjs +26 -0
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DATpA6mb.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-BU65uPCY.mjs → hostInit-op1zWltj.mjs} +3 -3
  30. package/dist/stream-broker/index.js +49 -1693
  31. package/dist/stream-broker/index.mjs +54 -1696
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/package.json +1 -6
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B204qFeL.mjs +0 -26
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Czhfk3hD.mjs +0 -26
@@ -3,12 +3,12 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-w_gA9z8W.js");
7
- const require_node = require("../node-BTsWSPyQ.js");
6
+ const require_dist = require("../dist-DNsARIUD.js");
7
+ const require_node = require("../node-D-U3T-h-.js");
8
8
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
9
9
  const require_remote_restream = require("../remote-restream-CO36Sr30.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
11
- const require_segment_demux_js = require("../segment-demux-js-D4kTUDck.js");
11
+ const require_segment_demux_js = require("../segment-demux-js-vZ6iniOU.js");
12
12
  let node_os = require("node:os");
13
13
  node_os = require_chunk.__toESM(node_os);
14
14
  let node_crypto = require("node:crypto");
@@ -565,16 +565,6 @@ var STREAM_BROKER_RETIRED_ROOT_KEYS = [
565
565
  }
566
566
  ];
567
567
  //#endregion
568
- //#region src/stream-broker/stream-broker/ffmpeg-args-common.ts
569
- /** The `-hide_banner -loglevel <level>` preamble every broker ffmpeg site opens with. */
570
- function logBannerArgs(level) {
571
- return [
572
- "-hide_banner",
573
- "-loglevel",
574
- level
575
- ];
576
- }
577
- //#endregion
578
568
  //#region src/shared/ffmpeg-decode-args.ts
579
569
  /** Default DRM render node used for the VA-API / QSV hardware decode path. */
580
570
  var DEFAULT_VAAPI_RENDER_DEVICE = "/dev/dri/renderD128";
@@ -7711,15 +7701,15 @@ function isH265IrapAccessUnit(annexB) {
7711
7701
  //#endregion
7712
7702
  //#region src/stream-broker/stream-broker/annexb-param-sets.ts
7713
7703
  /**
7714
- * Annex-B parameter-set detection for the decoder-priming path.
7704
+ * Annex-B parameter-set detection for the clip-priming path.
7715
7705
  *
7716
- * The frame-handle-plane decoder (`ffmpeg -f h264|hevc -i pipe:0`) initialises
7717
- * only once it sees the codec parameter sets (H.264 SPS+PPS / H.265 SPS+PPS,
7718
- * plus VPS) in its Annex-B input. Most cameras carry them in-band with each
7719
- * IDR; some (Reolink high-profile native streams) ship them ONLY in the SDP.
7720
- * The broker harvests those SDP param sets (`sdpParameterSets`) but must not
7721
- * blindly re-inject them ahead of every keyframe — that would duplicate the
7722
- * sets for well-behaved cameras. This scan lets the broker inject only when a
7706
+ * A decoder of a pre-buffer clip (`prebuffer-clip.ts`) initialises only once it
7707
+ * sees the codec parameter sets (H.264 SPS+PPS / H.265 SPS+PPS, plus VPS) in
7708
+ * its Annex-B input. Most cameras carry them in-band with each IDR; some
7709
+ * (Reolink high-profile native streams) ship them ONLY in the SDP. The broker
7710
+ * harvests those SDP param sets (`sdpParameterSets`) but must not blindly
7711
+ * re-inject them ahead of every keyframe — that would duplicate the sets for
7712
+ * well-behaved cameras. This scan lets the clip writer inject only when a
7723
7713
  * keyframe's own NAL stream is missing them.
7724
7714
  *
7725
7715
  * Pure byte scanning: no allocation beyond the walk; safe on any Annex-B blob.
@@ -7765,20 +7755,6 @@ function annexBHasParameterSets(data, codec) {
7765
7755
  }
7766
7756
  return hasSps && hasPps;
7767
7757
  }
7768
- /**
7769
- * Concatenate raw parameter-set NAL payloads into a single Annex-B buffer
7770
- * (each prefixed with a 4-byte start code) — the shape the decoder's stdin
7771
- * expects. Mirrors the existing SDP-param-set replay for encoded subscribers.
7772
- */
7773
- function concatParameterSetsAnnexB(parameterSets) {
7774
- const startCode = Buffer.from([
7775
- 0,
7776
- 0,
7777
- 0,
7778
- 1
7779
- ]);
7780
- return Buffer.concat(parameterSets.flatMap((ps) => [startCode, ps]));
7781
- }
7782
7758
  //#endregion
7783
7759
  //#region src/stream-broker/stream-broker/prebuffer-clip.ts
7784
7760
  /** Profiles cheapest-first — the default picks the first one assigned. */
@@ -11370,806 +11346,6 @@ var EncodedRingBuffer = class {
11370
11346
  }
11371
11347
  };
11372
11348
  //#endregion
11373
- //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
11374
- /**
11375
- * Long-poll window handed to `pullFrames`/`pullHandles`: an empty drain blocks
11376
- * decoder-side until a frame commits or this elapses, replacing the old ~1kHz
11377
- * `setTimeout(…,1)` busy-spin with a single await-notify round-trip. Kept
11378
- * modest so a hung UDS/Moleculer transport can't pin the call; the decoder
11379
- * cancels the waiter on `destroySession` so teardown never waits it out.
11380
- */
11381
- var POLL_WAIT_MS = 50;
11382
- var DecoderSessionProxy = class {
11383
- api;
11384
- sessionId;
11385
- polling = false;
11386
- constructor(api, sessionId) {
11387
- this.api = api;
11388
- this.sessionId = sessionId;
11389
- }
11390
- /**
11391
- * Guarantee an empty drain cannot re-poll faster than `POLL_WAIT_MS`,
11392
- * regardless of the peer. A WELL-BEHAVED decoder blocks server-side for
11393
- * `waitMs` (await-notify long-poll), so `elapsedMs >= POLL_WAIT_MS` and this
11394
- * is a no-op. But the loop's liveness must NOT depend on peer courtesy: a
11395
- * peer that resolves `[]` instantly — the dormant `decoder-nodeav.pullHandles`
11396
- * stub, an old decoder that predates `waitMs`, a test fake — would otherwise
11397
- * turn `startPolling`/`startHandlePolling` into a ZERO-YIELD microtask spin
11398
- * that pins the CPU, starves every timer in the process, and allocates until
11399
- * the heap dies (observed: a 4 GB OOM in under 15 s).
11400
- */
11401
- async paceEmptyDrain(drainStartedAt) {
11402
- const remainderMs = POLL_WAIT_MS - (Date.now() - drainStartedAt);
11403
- if (remainderMs > 0) await new Promise((resolve) => setTimeout(resolve, remainderMs));
11404
- }
11405
- async pushPacket(packet) {
11406
- await this.api.pushPacket({
11407
- sessionId: this.sessionId,
11408
- packet
11409
- });
11410
- }
11411
- async openStream(url) {
11412
- await this.api.openStream({
11413
- sessionId: this.sessionId,
11414
- url
11415
- });
11416
- }
11417
- async startPolling(onFrame) {
11418
- this.polling = true;
11419
- while (this.polling) {
11420
- const drainStartedAt = Date.now();
11421
- const frames = await this.api.pullFrames({
11422
- sessionId: this.sessionId,
11423
- maxCount: 4,
11424
- waitMs: POLL_WAIT_MS
11425
- });
11426
- for (const frame of frames) onFrame(frame);
11427
- if (frames.length === 0 && this.polling) await this.paceEmptyDrain(drainStartedAt);
11428
- }
11429
- }
11430
- /**
11431
- * Poll a `frameSink: 'shm'` session for `FrameHandle`s (Phase 5 / D9).
11432
- * Mirrors `startPolling` but drains `pullHandles` — the decoder has
11433
- * already written the pixels into a shared-memory ring, so what crosses
11434
- * the cap boundary is the tiny serialisable handle. Runs until
11435
- * `stopPolling`, `destroy`, or the session is destroyed externally.
11436
- *
11437
- * When the decoder reports the session is gone (`unknown sessionId` /
11438
- * `Service … not found`) the loop exits gracefully — those errors are
11439
- * expected during decoder restart/shutdown and must not be re-thrown to
11440
- * the caller's `.catch()` handler, which would otherwise spin forever.
11441
- */
11442
- async startHandlePolling(onHandle) {
11443
- this.polling = true;
11444
- while (this.polling) try {
11445
- const drainStartedAt = Date.now();
11446
- const handles = await this.api.pullHandles({
11447
- sessionId: this.sessionId,
11448
- maxCount: 4,
11449
- waitMs: POLL_WAIT_MS
11450
- });
11451
- for (const handle of handles) onHandle(handle);
11452
- if (handles.length === 0 && this.polling) await this.paceEmptyDrain(drainStartedAt);
11453
- } catch (err) {
11454
- this.polling = false;
11455
- const msg = err instanceof Error ? err.message : String(err);
11456
- if (!msg.includes("unknown sessionId") && !msg.includes("Service") && !msg.includes("not found")) throw err;
11457
- return;
11458
- }
11459
- }
11460
- stopPolling() {
11461
- this.polling = false;
11462
- }
11463
- async updateConfig(config) {
11464
- await this.api.updateConfig({
11465
- sessionId: this.sessionId,
11466
- config
11467
- });
11468
- }
11469
- async getStats() {
11470
- return this.api.getStats({ sessionId: this.sessionId });
11471
- }
11472
- async destroy() {
11473
- this.stopPolling();
11474
- await this.api.destroySession({ sessionId: this.sessionId });
11475
- }
11476
- };
11477
- //#endregion
11478
- //#region src/stream-broker/stream-broker/frame-handle-plane.ts
11479
- /**
11480
- * `FrameHandlePlane` — the broker's shared-memory frame-handle surface
11481
- * (Phase 5 / D9).
11482
- *
11483
- * This is the handle-based replacement for `StreamBroker.onDecodedFrame`'s
11484
- * live-object callback path. Instead of fanning pixel `DecodedFrame`s out to
11485
- * in-process callbacks (which cannot cross a process boundary), the broker
11486
- * publishes zero-pixel, serialisable `FrameHandle`s a consumer drains over
11487
- * tRPC and reads back from a shared-memory ring with a `FrameRingReader`.
11488
- *
11489
- * ## Ring-per-format model
11490
- *
11491
- * A `frameSink: 'shm'` decoder session produces exactly ONE pixel format
11492
- * (the decoder's scaler is configured for it). The broker therefore keeps
11493
- * one decoder session — hence one shm ring — per `(brokerId, format)` that a
11494
- * consumer actually subscribes to:
11495
- *
11496
- * - motion subscribes `gray` → one `gray` decoder session + ring
11497
- * - detection subscribes `rgb` → one `rgb` decoder session + ring
11498
- *
11499
- * Only the formats consumers ask for are materialised. There is NO
11500
- * broker-side `sharp` conversion any more — each consumer reads the exact
11501
- * format it requested. A session is reference-counted by its subscriber set
11502
- * and torn down when the last subscriber of that format leaves.
11503
- *
11504
- * ## fps throttling
11505
- *
11506
- * Throttling is implicit: the ring is latest-wins, so a slow consumer that
11507
- * polls `pullFrameHandles` infrequently simply receives the newest handles
11508
- * and the intervening frames are dropped at the ring (a recycled slot the
11509
- * consumer never read). The optional `maxFps` from `subscribeFrames` is NOT
11510
- * enforced here — it is echoed back to the consumer as a reader-side polling
11511
- * cadence hint.
11512
- *
11513
- * ## Packet feed
11514
- *
11515
- * The plane's decoder sessions are push-mode by default: the broker forwards
11516
- * every video `EncodedPacket` to `pushPacket`. For a pull-mode decoder the
11517
- * broker supplies a `pullModeUrl` and the plane calls `openStream` once the
11518
- * session exists.
11519
- */
11520
- /**
11521
- * Collapse a hierarchical Moleculer nodeID (`hub/child-abc`) to its owning
11522
- * node (`hub`). Frame-plane co-location only cares about the physical node, not
11523
- * which forked child within it hosts the session.
11524
- */
11525
- function collapseNodeId(nodeId) {
11526
- const slash = nodeId.indexOf("/");
11527
- return slash >= 0 ? nodeId.slice(0, slash) : nodeId;
11528
- }
11529
- /**
11530
- * Decide whether a decoder session created on `decoderNodeId` is co-located
11531
- * with the broker whose local node is `brokerNodeId`.
11532
- *
11533
- * The broker's shm frame ring is NODE-LOCAL: a decoder session on any other
11534
- * node can never deliver frame handles to this broker's consumers, so a
11535
- * mis-placed session must be rejected. Both ids are collapsed (`hub/child`→
11536
- * `hub`) before comparison.
11537
- *
11538
- * A `null` `brokerNodeId` means the broker's own node is unknown — enforcement
11539
- * is skipped (legacy behavior, accept any placement).
11540
- */
11541
- function isDecoderNodeColocated(brokerNodeId, decoderNodeId) {
11542
- if (brokerNodeId === null) return true;
11543
- return collapseNodeId(brokerNodeId) === collapseNodeId(decoderNodeId);
11544
- }
11545
- /** Per-subscription handle-queue capacity. A handle is tiny; 64 absorbs a
11546
- * burst between two `pullFrameHandles` polls without unbounded growth. */
11547
- var HANDLE_QUEUE_CAPACITY = 64;
11548
- /** Default reader-side cadence hint when a subscriber omits `maxFps`. */
11549
- var DEFAULT_HINT_FPS = 5;
11550
- /** Rolling window for the aggregate `decodeFps` rate, ms. Mirrors the
11551
- * broker's 2s `inputFps` window so the two stats are comparable. */
11552
- var DECODE_FPS_WINDOW_MS = 2e3;
11553
- /**
11554
- * Keep-warm grace before a decoder session with no subscribers is torn down.
11555
- *
11556
- * Subscribers flap on ~30s cadences (occupancy recheck bursts, detection
11557
- * motion-gating watch↔active, onboard gray gating). Destroying the
11558
- * ffmpeg/VAAPI decode session the instant the last subscriber leaves spawns +
11559
- * destroys a full session every cycle — wasted setup/teardown. Chosen >30s so
11560
- * consecutive bursts and watch↔active flips reuse the warm session, yet a
11561
- * genuinely idle session still frees its ffmpeg within ~a minute.
11562
- */
11563
- var SESSION_LINGER_MS = 45e3;
11564
- /**
11565
- * Short keep-warm grace for a PURE-OCCUPANCY session (every remaining
11566
- * subscriber is a periodic on-motion occupancy recheck, tag `'occupancy'`).
11567
- *
11568
- * Occupancy rechecks subscribe to decoded frames for ~1s every ~30s. The
11569
- * normal `SESSION_LINGER_MS` (45s) EXCEEDS that ~30s interval, so the linger
11570
- * timer is perpetually cancelled by the next recheck before it can fire — the
11571
- * decoder never tears down and decodes continuously at source-rate to serve a
11572
- * 1s/30s consumer (~30x wasted decode; multiplied across cameras this is the
11573
- * iGPU baseline that saturated the decoder). A pure-occupancy session instead
11574
- * tears down between polls: short enough to fire well before the next ~30s
11575
- * recheck, long enough to absorb the ~1s poll plus minor jitter. Re-dial is
11576
- * cheap on GPU-scale decode and the restream burst re-primes the live edge
11577
- * fast, so on-demand re-arm on the next recheck is fine.
11578
- */
11579
- var OCCUPANCY_LINGER_MS = 5e3;
11580
- /** Subscription tag emitted by the periodic on-motion occupancy recheck. A
11581
- * session whose subscribers have only ever carried this tag gets the short
11582
- * `OCCUPANCY_LINGER_MS` teardown; any other subscriber biases to the long
11583
- * `SESSION_LINGER_MS` warm linger (interactive viewers, detection, motion). */
11584
- var OCCUPANCY_TAG = "occupancy";
11585
- /**
11586
- * Default retry cooldown after a decoder session lands on a NON-LOCAL node
11587
- * (see `FrameHandlePlaneOptions.sessionRetryCooldownMs`). Long enough to
11588
- * turn a per-packet storm into a slow probe; short enough that the plane
11589
- * recovers within seconds once the local decoder respawns.
11590
- */
11591
- var SESSION_RETRY_COOLDOWN_MS = 1e4;
11592
- /**
11593
- * Owns the broker's shared-memory frame-handle subscriptions and the
11594
- * per-format `frameSink: 'shm'` decoder sessions that feed them.
11595
- */
11596
- var FrameHandlePlane = class {
11597
- decoderApi;
11598
- logger;
11599
- resolveStreamInfo;
11600
- localNodeId;
11601
- sessionRetryCooldownMs;
11602
- getSeedPackets;
11603
- isDebugEnabled;
11604
- subscriptions = /* @__PURE__ */ new Map();
11605
- sessions = /* @__PURE__ */ new Map();
11606
- /** Per-format retry gate set when a session lands on a non-local node:
11607
- * `ensureSession` skips creation until the epoch-ms deadline passes. */
11608
- sessionRetryCooldownUntil = /* @__PURE__ */ new Map();
11609
- /** In-flight `ensureSession` creation per format. Coalesces concurrent
11610
- * same-format callers onto ONE decoder session — without it two callers
11611
- * both pass the `sessions.get` check, both `createSession` (two ffmpeg),
11612
- * and the second registration orphans the first with no `destroySession`. */
11613
- sessionCreating = /* @__PURE__ */ new Map();
11614
- disposed = false;
11615
- /** Re-entrancy guard for `armPendingSessions` — collapses the per-packet
11616
- * `pushPacket` calls into a single in-flight deferred-arm pass. */
11617
- armInFlight = false;
11618
- /** DIAG(592): P-frames skipped on an un-bootstrapped session before its
11619
- * first live keyframe. Reset when a keyframe bootstraps the session. */
11620
- unbootstrappedSkips = 0;
11621
- /** Aggregate decoded-frame rate state — frames fanned out in the current
11622
- * rolling window, the window's start epoch, and the last computed fps. */
11623
- framesInWindow = 0;
11624
- windowStartMs = Date.now();
11625
- decodedFps = 0;
11626
- constructor(options) {
11627
- this.decoderApi = options.decoderApi;
11628
- this.logger = options.logger;
11629
- this.resolveStreamInfo = options.resolveStreamInfo;
11630
- this.localNodeId = options.localNodeId;
11631
- this.sessionRetryCooldownMs = options.sessionRetryCooldownMs ?? SESSION_RETRY_COOLDOWN_MS;
11632
- this.getSeedPackets = options.getSeedPackets;
11633
- this.isDebugEnabled = options.isDebugEnabled;
11634
- }
11635
- /** Number of active handle subscriptions — surfaced for diagnostics. */
11636
- get subscriberCount() {
11637
- return this.subscriptions.size;
11638
- }
11639
- /**
11640
- * Moleculer nodeIDs of the decoder providers currently hosting this plane's
11641
- * `frameSink: 'shm'` sessions. Used by the broker-manager's per-agent
11642
- * hwaccel-change handler to decide which brokers need a decoder rotation.
11643
- */
11644
- decoderNodeIds() {
11645
- const ids = [];
11646
- for (const session of this.sessions.values()) if (session.nodeId) ids.push(session.nodeId);
11647
- return ids;
11648
- }
11649
- /**
11650
- * Rotate every shm decoder session hosted on `agentNodeId` — destroy and
11651
- * rebuild it so the new session re-pulls the latest per-agent decoder
11652
- * preferences (hwaccel backend, …). Subscriptions are preserved: the same
11653
- * `subscriptionId`s read from the rebuilt session's fresh ring. A no-op for
11654
- * sessions on other nodes. Returns the number of sessions rotated.
11655
- */
11656
- async rotate(agentNodeId, reason) {
11657
- if (this.disposed) return 0;
11658
- let rotated = 0;
11659
- for (const session of this.sessions.values()) {
11660
- if ((session.nodeId && session.nodeId.includes("/") ? session.nodeId.split("/")[0] : session.nodeId) !== agentNodeId) continue;
11661
- this.logger?.info("frame-handle plane: rotating shm decoder session", { meta: {
11662
- format: session.format,
11663
- reason,
11664
- nodeId: session.nodeId
11665
- } });
11666
- await this.destroySession(session);
11667
- let restarted = false;
11668
- try {
11669
- restarted = await this.startSessionDecoder(session);
11670
- } finally {
11671
- if (!restarted) this.sessions.delete(session.format);
11672
- }
11673
- if (restarted) rotated += 1;
11674
- }
11675
- return rotated;
11676
- }
11677
- /**
11678
- * Register a frame-handle subscription. Spins up (or reuses) a
11679
- * `frameSink: 'shm'` decoder session producing `format`. The returned
11680
- * `subscriptionId` is the handle the consumer passes to
11681
- * `pullFrameHandles` / `unsubscribe`.
11682
- */
11683
- async subscribe(input) {
11684
- if (this.disposed) throw new Error("FrameHandlePlane: subscribe after dispose");
11685
- const subscriptionId = `fh-${(0, node_crypto.randomUUID)()}`;
11686
- const maxFps = input.maxFps !== void 0 && input.maxFps > 0 ? input.maxFps : DEFAULT_HINT_FPS;
11687
- const subscription = {
11688
- id: subscriptionId,
11689
- format: input.format,
11690
- maxFps,
11691
- tag: input.tag ?? "unknown",
11692
- subscribedAt: Date.now(),
11693
- queue: new require_dist.RingBuffer(HANDLE_QUEUE_CAPACITY),
11694
- framesDelivered: 0
11695
- };
11696
- this.subscriptions.set(subscriptionId, subscription);
11697
- await this.ensureSession(input.format, subscriptionId);
11698
- this.reconcileSessionFps(input.format);
11699
- this.logger?.info("frame-handle subscription added", { meta: {
11700
- subscriptionId,
11701
- format: input.format,
11702
- tag: subscription.tag,
11703
- maxFps
11704
- } });
11705
- return {
11706
- subscriptionId,
11707
- maxFps
11708
- };
11709
- }
11710
- /**
11711
- * The emit `maxFps` a format's decoder session should run at — the MAX of all
11712
- * current subscribers' `maxFps` for that format (fallback `DEFAULT_HINT_FPS`
11713
- * when none, e.g. mid-teardown). The decoder emits at this rate; each
11714
- * subscriber then down-samples to its own `maxFps` on read (latest-wins).
11715
- */
11716
- computeFormatFps(format) {
11717
- let max = 0;
11718
- for (const sub of this.subscriptions.values()) if (sub.format === format && sub.maxFps > max) max = sub.maxFps;
11719
- return max > 0 ? max : DEFAULT_HINT_FPS;
11720
- }
11721
- /**
11722
- * Recompute a format session's emit `maxFps` after a subscriber joins/leaves
11723
- * and push it to the live decoder via `updateConfig` when it changed — so the
11724
- * decoder emits only as fast as the busiest reader needs (saving the
11725
- * scale+convert+shm-write for frames nobody reads).
11726
- */
11727
- reconcileSessionFps(format) {
11728
- const session = this.sessions.get(format);
11729
- if (!session || !session.proxy) return;
11730
- const target = this.computeFormatFps(format);
11731
- if (target === session.targetFps) return;
11732
- session.targetFps = target;
11733
- session.proxy.updateConfig({ maxFps: target }).catch((err) => {
11734
- this.logger?.warn("frame-handle plane: updateConfig(maxFps) failed", { meta: {
11735
- format,
11736
- maxFps: target,
11737
- error: require_dist.errMsg(err)
11738
- } });
11739
- });
11740
- }
11741
- /**
11742
- * Drain up to `maxCount` `FrameHandle`s for a subscription, latest-wins.
11743
- * An unknown subscription id returns `[]` (the consumer may have been
11744
- * torn down concurrently).
11745
- */
11746
- pullHandles(subscriptionId, maxCount) {
11747
- const subscription = this.subscriptions.get(subscriptionId);
11748
- if (!subscription) return [];
11749
- const stale = subscription.queue.size - maxCount;
11750
- if (stale > 0) subscription.queue.drain(stale);
11751
- const handles = subscription.queue.drain(maxCount);
11752
- subscription.framesDelivered += handles.length;
11753
- return handles;
11754
- }
11755
- /**
11756
- * Release a subscription. When it was the last reader of its format the
11757
- * underlying decoder session is destroyed (and its shm segment unlinked
11758
- * by the decoder). Returns `true` when a known subscription was released.
11759
- */
11760
- async unsubscribe(subscriptionId, immediate = false) {
11761
- const subscription = this.subscriptions.get(subscriptionId);
11762
- if (!subscription) return false;
11763
- this.subscriptions.delete(subscriptionId);
11764
- const session = this.sessions.get(subscription.format);
11765
- if (session) {
11766
- session.subscriberIds.delete(subscriptionId);
11767
- if (session.subscriberIds.size === 0) if (immediate) {
11768
- this.clearLinger(session);
11769
- this.sessions.delete(subscription.format);
11770
- await this.destroySession(session);
11771
- } else this.scheduleLinger(session);
11772
- else this.reconcileSessionFps(subscription.format);
11773
- }
11774
- this.logger?.info("frame-handle subscription removed", { meta: {
11775
- subscriptionId,
11776
- format: subscription.format
11777
- } });
11778
- return true;
11779
- }
11780
- /**
11781
- * Arm the keep-warm teardown timer for a session whose last subscriber just
11782
- * left. Clears any existing timer first (idempotent). On fire it re-checks
11783
- * `subscriberIds.size === 0` — a subscriber may have returned in the interim,
11784
- * in which case the timer is a no-op — and only then removes the session and
11785
- * destroys the decoder. The timer is `unref`'d so a lingering warm session
11786
- * never keeps the process alive.
11787
- */
11788
- scheduleLinger(session) {
11789
- this.clearLinger(session);
11790
- const occupancyOnly = !session.hasNonOccupancySubscriberEver;
11791
- const lingerMs = occupancyOnly ? OCCUPANCY_LINGER_MS : SESSION_LINGER_MS;
11792
- this.logger?.debug(occupancyOnly ? "frame-handle plane: short occupancy linger scheduled" : "frame-handle plane: keep-warm linger scheduled", { meta: {
11793
- format: session.format,
11794
- lingerMs,
11795
- occupancyOnly
11796
- } });
11797
- const timer = setTimeout(() => {
11798
- session.lingerTimer = null;
11799
- if (session.subscriberIds.size > 0) return;
11800
- this.sessions.delete(session.format);
11801
- this.destroySession(session).catch(() => {});
11802
- }, lingerMs);
11803
- timer.unref?.();
11804
- session.lingerTimer = timer;
11805
- }
11806
- /**
11807
- * Latch `session.hasNonOccupancySubscriberEver` when the subscription
11808
- * `subscriptionId` carries any tag other than `OCCUPANCY_TAG`. Called at
11809
- * every point a subscription is attached to a session so the keep-warm
11810
- * decision only fast-tears-down sessions that have been PURELY on-demand
11811
- * occupancy rechecks. Unknown ids (mid-teardown) are ignored.
11812
- */
11813
- noteSessionSubscriberTag(session, subscriptionId) {
11814
- const subscription = this.subscriptions.get(subscriptionId);
11815
- if (subscription && subscription.tag !== OCCUPANCY_TAG) session.hasNonOccupancySubscriberEver = true;
11816
- }
11817
- /** Cancel a session's pending keep-warm teardown timer, if any. */
11818
- clearLinger(session) {
11819
- if (session.lingerTimer) {
11820
- clearTimeout(session.lingerTimer);
11821
- session.lingerTimer = null;
11822
- }
11823
- }
11824
- /**
11825
- * Forward a video `EncodedPacket` to every active shm decoder session.
11826
- * Push-mode decoders consume this; a pull-mode decoder ignores it (it
11827
- * reads its own pipe via `openStream`). Audio packets are not forwarded.
11828
- *
11829
- * The packet feed doubles as the plane's stream-ready signal: the broker
11830
- * only calls `pushPacket` once it has a source and the first keyframe has
11831
- * landed — exactly the point `resolveStreamInfo` starts returning a
11832
- * descriptor. Each packet therefore triggers `armPendingSessions`, which
11833
- * re-attempts session creation for any deferred subscription so a lone
11834
- * first subscriber on a not-yet-started broker self-arms without a
11835
- * redundant re-`subscribe`.
11836
- */
11837
- pushPacket(packet) {
11838
- if (this.disposed || packet.type !== "video") return;
11839
- this.armPendingSessions(packet).catch((err) => {
11840
- this.logger?.warn("frame-handle plane: arm pending sessions error", { meta: { error: require_dist.errMsg(err) } });
11841
- });
11842
- this.feedSessions(packet);
11843
- }
11844
- /** Forward a video `EncodedPacket` to every live PUSH-mode shm decoder session. */
11845
- feedSessions(packet) {
11846
- for (const session of this.sessions.values()) {
11847
- if (!session.proxy) continue;
11848
- if (session.pullMode) continue;
11849
- if (!session.bootstrapped) {
11850
- if (!packet.keyframe) {
11851
- this.unbootstrappedSkips = (this.unbootstrappedSkips ?? 0) + 1;
11852
- continue;
11853
- }
11854
- this.logger?.debug("frame-handle plane: session bootstrapped from live keyframe", { meta: {
11855
- format: session.format,
11856
- pFramesSkippedBefore: this.unbootstrappedSkips ?? 0
11857
- } });
11858
- this.unbootstrappedSkips = 0;
11859
- session.bootstrapped = true;
11860
- }
11861
- session.proxy.pushPacket(packet).catch((err) => {
11862
- const msg = require_dist.errMsg(err);
11863
- this.logger?.warn("frame-handle plane: decoder push error", { meta: {
11864
- format: session.format,
11865
- error: msg
11866
- } });
11867
- if (msg.includes("unknown sessionId") || msg.includes("Service") || msg.includes("not found")) {
11868
- this.logger?.info("frame-handle plane: invalidating stale proxy", { meta: { format: session.format } });
11869
- this.destroySession(session).catch(() => {});
11870
- this.sessions.delete(session.format);
11871
- }
11872
- });
11873
- }
11874
- }
11875
- /**
11876
- * Re-attempt session creation for every subscription whose format has no
11877
- * live `FormatSession` yet — the subscriptions whose `startSessionDecoder`
11878
- * was deferred because the broker had no stream at `subscribe` time. Driven
11879
- * off the `pushPacket` feed (event-driven, no polling timer); a no-op once
11880
- * every subscribed format already owns a session.
11881
- *
11882
- * `armInFlight` collapses re-entrant calls: `pushPacket` fires per packet,
11883
- * but `ensureSession` is async — without the guard a burst of packets would
11884
- * launch overlapping decoder-creation round-trips for the same format.
11885
- *
11886
- * `triggerPacket` is the packet whose arrival armed the pass. Because
11887
- * `ensureSession` is async, the synchronous `feedSessions` in `pushPacket`
11888
- * runs before the session lands in `this.sessions`, so the triggering packet
11889
- * would be lost to the new session. `startSessionDecoder` normally bootstraps
11890
- * it by replaying the broker's keyframe-aligned seed GOP; only when NO seed
11891
- * was available (no `getSeedPackets`, or the ring held no keyframe yet) is the
11892
- * session left un-bootstrapped — and then this method hands it the triggering
11893
- * packet, but ONLY if that packet is a keyframe. A P-frame trigger is left for
11894
- * `feedSessions` to skip until the first live keyframe, so a push-mode decoder
11895
- * never sees a slice referencing an unseen param set.
11896
- */
11897
- async armPendingSessions(triggerPacket) {
11898
- if (this.disposed || this.armInFlight) return;
11899
- const pending = /* @__PURE__ */ new Map();
11900
- for (const subscription of this.subscriptions.values()) {
11901
- if (this.sessions.has(subscription.format)) continue;
11902
- if (!pending.has(subscription.format)) pending.set(subscription.format, subscription.id);
11903
- }
11904
- if (pending.size === 0) return;
11905
- this.armInFlight = true;
11906
- try {
11907
- for (const [format, subscriptionId] of pending) {
11908
- if (this.disposed) return;
11909
- await this.ensureSession(format, subscriptionId);
11910
- const armed = this.sessions.get(format);
11911
- if (armed?.proxy && !armed.bootstrapped && triggerPacket.keyframe) {
11912
- armed.bootstrapped = true;
11913
- armed.proxy.pushPacket(triggerPacket).catch((err) => {
11914
- this.logger?.warn("frame-handle plane: decoder push error", { meta: {
11915
- format,
11916
- error: require_dist.errMsg(err)
11917
- } });
11918
- });
11919
- }
11920
- }
11921
- } finally {
11922
- this.armInFlight = false;
11923
- }
11924
- }
11925
- /**
11926
- * Aggregate decoded-frame rate across every `frameSink: 'shm'` session in
11927
- * this plane, frames/s. Each `fanoutHandle` call is one decoded frame
11928
- * delivered into the plane; the rate is recomputed over a rolling
11929
- * `DECODE_FPS_WINDOW_MS` window. Returns the last computed value between
11930
- * window rolls, and `0` while no session is producing.
11931
- */
11932
- decodeFps() {
11933
- this.rollDecodeFpsWindow();
11934
- return this.decodedFps;
11935
- }
11936
- /**
11937
- * Roll the decoded-fps window when it has elapsed: convert frames seen in
11938
- * the window into a per-second rate, then reset the counter. Called both
11939
- * on every fanout and on every `decodeFps()` read so a stalled session
11940
- * (no fanout) decays its rate to `0` rather than reporting a stale value.
11941
- */
11942
- rollDecodeFpsWindow() {
11943
- const now = Date.now();
11944
- const windowMs = now - this.windowStartMs;
11945
- if (windowMs < DECODE_FPS_WINDOW_MS) return;
11946
- this.decodedFps = this.framesInWindow / windowMs * 1e3;
11947
- this.framesInWindow = 0;
11948
- this.windowStartMs = now;
11949
- }
11950
- /** Diagnostic snapshot of every active frame-handle subscription. */
11951
- listSubscribers() {
11952
- return [...this.subscriptions.values()].map((s) => ({
11953
- tag: s.tag,
11954
- subscribedAt: s.subscribedAt,
11955
- maxFps: s.maxFps,
11956
- framesDelivered: s.framesDelivered
11957
- }));
11958
- }
11959
- /**
11960
- * Force-release every subscription whose tag matches `tag`. Returns the
11961
- * number released. Decoder sessions wind down when their last subscriber
11962
- * leaves, same as a normal `unsubscribe`.
11963
- */
11964
- async killByTag(tag) {
11965
- const victims = [...this.subscriptions.values()].filter((s) => s.tag === tag).map((s) => s.id);
11966
- for (const id of victims) await this.unsubscribe(id, true);
11967
- return victims.length;
11968
- }
11969
- /** Tear down every subscription + decoder session. Idempotent. */
11970
- async dispose() {
11971
- if (this.disposed) return;
11972
- this.disposed = true;
11973
- this.subscriptions.clear();
11974
- const sessions = [...this.sessions.values()];
11975
- this.sessions.clear();
11976
- await Promise.all(sessions.map((s) => this.destroySession(s)));
11977
- }
11978
- /**
11979
- * Ensure a `frameSink: 'shm'` decoder session for `format` exists and add
11980
- * `subscriptionId` to its reader set. Reuses the session when one already
11981
- * runs for that format.
11982
- *
11983
- * When a session is created fresh, its reader set is seeded with EVERY
11984
- * subscription currently reading `format`, not just `subscriptionId` — a
11985
- * deferred-arm pass (`armPendingSessions`) may create the session long
11986
- * after several subscriptions of that format have registered, and the
11987
- * session is reference-counted by `subscriberIds`. Seeding only the one id
11988
- * would let an `unsubscribe` of that id tear the session down while its
11989
- * sibling subscriptions still read it.
11990
- */
11991
- async ensureSession(format, subscriptionId) {
11992
- const existing = this.sessions.get(format);
11993
- if (existing) {
11994
- this.clearLinger(existing);
11995
- existing.subscriberIds.add(subscriptionId);
11996
- this.noteSessionSubscriberTag(existing, subscriptionId);
11997
- return;
11998
- }
11999
- const inflight = this.sessionCreating.get(format);
12000
- if (inflight) {
12001
- await inflight;
12002
- const session = this.sessions.get(format);
12003
- if (session) {
12004
- session.subscriberIds.add(subscriptionId);
12005
- this.noteSessionSubscriberTag(session, subscriptionId);
12006
- }
12007
- return;
12008
- }
12009
- if (Date.now() < (this.sessionRetryCooldownUntil.get(format) ?? 0)) return;
12010
- const creation = (async () => {
12011
- const subscriberIds = new Set([subscriptionId]);
12012
- for (const subscription of this.subscriptions.values()) if (subscription.format === format) subscriberIds.add(subscription.id);
12013
- const session = {
12014
- format,
12015
- proxy: null,
12016
- nodeId: null,
12017
- subscriberIds,
12018
- bootstrapped: false,
12019
- pullMode: false,
12020
- targetFps: this.computeFormatFps(format),
12021
- lingerTimer: null,
12022
- hasNonOccupancySubscriberEver: false
12023
- };
12024
- for (const id of subscriberIds) this.noteSessionSubscriberTag(session, id);
12025
- let started = false;
12026
- try {
12027
- started = await this.startSessionDecoder(session);
12028
- } catch (err) {
12029
- this.sessionRetryCooldownUntil.set(format, Date.now() + this.sessionRetryCooldownMs);
12030
- throw err;
12031
- }
12032
- if (started) this.sessions.set(format, session);
12033
- })();
12034
- this.sessionCreating.set(format, creation);
12035
- try {
12036
- await creation;
12037
- } finally {
12038
- this.sessionCreating.delete(format);
12039
- }
12040
- }
12041
- /**
12042
- * Create the `frameSink: 'shm'` decoder for a `FormatSession` and start
12043
- * draining its handle queue. Mutates `session.proxy` / `session.nodeId` in
12044
- * place so a `rotate()` can rebuild a registered session without disturbing
12045
- * its `subscriberIds`. Returns `false` when no stream / unsupported codec
12046
- * means no decoder could be created.
12047
- */
12048
- async startSessionDecoder(session) {
12049
- const { format } = session;
12050
- const info = this.resolveStreamInfo();
12051
- if (!info) {
12052
- this.logger?.info("frame-handle plane: no stream — session deferred", { meta: { format } });
12053
- return false;
12054
- }
12055
- if (!await this.decoderApi.supportsCodec({ codec: info.codec })) {
12056
- this.logger?.warn("frame-handle plane: codec unsupported — session skipped", { meta: {
12057
- format,
12058
- codec: info.codec
12059
- } });
12060
- return false;
12061
- }
12062
- const targetFps = this.computeFormatFps(format);
12063
- session.targetFps = targetFps;
12064
- const { sessionId, nodeId } = await this.decoderApi.createSession({
12065
- codec: info.codec,
12066
- maxFps: targetFps,
12067
- outputFormat: format,
12068
- scale: 1,
12069
- frameSink: "shm",
12070
- ...info.numericDeviceId !== void 0 ? { deviceId: info.numericDeviceId } : {},
12071
- tag: `${info.tag}:shm:${format}`,
12072
- ...this.isDebugEnabled?.() ? { debug: true } : {}
12073
- });
12074
- if (!isDecoderNodeColocated(this.localNodeId, nodeId)) {
12075
- this.sessionRetryCooldownUntil.set(format, Date.now() + this.sessionRetryCooldownMs);
12076
- this.logger?.warn("frame-handle plane: decoder session landed on a non-local node — destroying (shm ring is node-local)", { meta: {
12077
- format,
12078
- requestedNode: this.localNodeId,
12079
- gotNode: nodeId,
12080
- sessionId,
12081
- retryInMs: this.sessionRetryCooldownMs
12082
- } });
12083
- await this.decoderApi.destroySession({ sessionId }).catch((err) => {
12084
- this.logger?.warn("frame-handle plane: failed to destroy mis-placed decoder session", { meta: {
12085
- format,
12086
- sessionId,
12087
- error: require_dist.errMsg(err)
12088
- } });
12089
- });
12090
- return false;
12091
- }
12092
- this.sessionRetryCooldownUntil.delete(format);
12093
- const proxy = new DecoderSessionProxy(this.decoderApi, sessionId);
12094
- session.proxy = proxy;
12095
- session.nodeId = nodeId;
12096
- session.bootstrapped = false;
12097
- session.pullMode = false;
12098
- proxy.startHandlePolling((handle) => {
12099
- this.fanoutHandle(format, handle);
12100
- }).catch((err) => {
12101
- this.logger?.warn("frame-handle plane: handle polling error", { meta: {
12102
- format,
12103
- error: require_dist.errMsg(err)
12104
- } });
12105
- });
12106
- if (info.pullModeUrl) {
12107
- session.pullMode = true;
12108
- session.bootstrapped = true;
12109
- proxy.openStream(info.pullModeUrl).catch((err) => {
12110
- this.logger?.error("frame-handle plane: pull-mode openStream failed", { meta: {
12111
- format,
12112
- error: require_dist.errMsg(err)
12113
- } });
12114
- });
12115
- this.logger?.info("frame-handle plane: shm decoder session created (pull-mode RTSP)", { meta: {
12116
- format,
12117
- codec: info.codec,
12118
- sessionId,
12119
- nodeId,
12120
- pullModeUrl: info.pullModeUrl
12121
- } });
12122
- return true;
12123
- }
12124
- const seed = this.getSeedPackets?.() ?? [];
12125
- if (seed.length > 0) {
12126
- for (const seedPacket of seed) await proxy.pushPacket(seedPacket).catch((err) => {
12127
- this.logger?.warn("frame-handle plane: seed-replay push error", { meta: {
12128
- format,
12129
- error: require_dist.errMsg(err)
12130
- } });
12131
- });
12132
- session.bootstrapped = true;
12133
- this.logger?.debug("frame-handle plane: session bootstrapped from seed GOP", { meta: {
12134
- format,
12135
- seedPackets: seed.length
12136
- } });
12137
- }
12138
- this.logger?.info("frame-handle plane: shm decoder session created", { meta: {
12139
- format,
12140
- codec: info.codec,
12141
- sessionId,
12142
- nodeId
12143
- } });
12144
- return true;
12145
- }
12146
- /** Push one decoded `FrameHandle` to every subscription of `format`. */
12147
- fanoutHandle(format, handle) {
12148
- this.rollDecodeFpsWindow();
12149
- this.framesInWindow += 1;
12150
- for (const subscription of this.subscriptions.values()) {
12151
- if (subscription.format !== format) continue;
12152
- subscription.queue.push(handle);
12153
- }
12154
- }
12155
- /** Destroy a decoder session, swallowing teardown errors. */
12156
- async destroySession(session) {
12157
- this.clearLinger(session);
12158
- const proxy = session.proxy;
12159
- session.proxy = null;
12160
- session.nodeId = null;
12161
- if (!proxy) return;
12162
- try {
12163
- await proxy.destroy();
12164
- } catch (err) {
12165
- this.logger?.warn("frame-handle plane: session destroy failed", { meta: {
12166
- format: session.format,
12167
- error: require_dist.errMsg(err)
12168
- } });
12169
- }
12170
- }
12171
- };
12172
- //#endregion
12173
11349
  //#region src/stream-broker/stream-broker/g711-mulaw.ts
12174
11350
  /**
12175
11351
  * G.711 µ-law encoder — converts 16-bit signed PCM samples to µ-law (ITU-T G.711).
@@ -12866,8 +12042,8 @@ var MAX_RECONNECT_DELAY_MS = 3e4;
12866
12042
  var SUSPEND_GRACE_MS = 5e3;
12867
12043
  /**
12868
12044
  * Grace window before an idle audio-codec decode session is torn down
12869
- * after the last audio consumer detaches. Mirrors the frame-handle /
12870
- * suspend occupancy-linger pattern: a brief gap between consumers (e.g.
12045
+ * after the last audio consumer detaches. Mirrors the suspend linger
12046
+ * pattern: a brief gap between consumers (e.g.
12871
12047
  * a WebRTC renegotiation or an analyzer re-subscribe) must not thrash
12872
12048
  * the upstream decode session create/destroy.
12873
12049
  */
@@ -12905,8 +12081,6 @@ var StreamBroker = class StreamBroker {
12905
12081
  * falls back to the source passed to `start()`.
12906
12082
  */
12907
12083
  sourceProvider = null;
12908
- /** Resolver for this broker's own RTSP restream URL — see `setRestreamUrlResolver`. */
12909
- restreamUrlResolver = null;
12910
12084
  /**
12911
12085
  * Manager-supplied notifier that asks the publishing addon to
12912
12086
  * refresh its loopback transport (e.g. re-run
@@ -12931,25 +12105,8 @@ var StreamBroker = class StreamBroker {
12931
12105
  lastKickAt = 0;
12932
12106
  /** Detected or configured codec — persists across reconnects */
12933
12107
  detectedCodec;
12934
- /**
12935
- * The shared-memory frame-handle surface (Phase 5 / D9) — the broker's sole
12936
- * decoded-frame path. Lazily created on the first `subscribeFrameHandles`
12937
- * call; owns the per-format `frameSink: 'shm'` decoder sessions a handle
12938
- * consumer reads from. `null` until a consumer subscribes.
12939
- */
12940
- frameHandlePlane = null;
12941
- /** One-shot latch for the "seed ring not filled" line — see `pushEncodedPacket`. */
12942
- seedRingSkipLogged = false;
12943
- decoderApi;
12944
12108
  audioCodecApi;
12945
12109
  logger;
12946
- /**
12947
- * This broker's OWN local Moleculer node id, threaded from the manager
12948
- * (`ctx.kernel.localNodeId`). Passed to the `FrameHandlePlane` so it can
12949
- * reject decoder sessions that land on a different node (the shm frame ring
12950
- * is node-local). `null` = unknown → co-location enforcement skipped.
12951
- */
12952
- localNodeId;
12953
12110
  startedAt = Date.now();
12954
12111
  /**
12955
12112
  * How many times `start()` has actually dialed. `> 0` at dial time means the
@@ -13041,9 +12198,9 @@ var StreamBroker = class StreamBroker {
13041
12198
  * The lazy branch is re-entered on every materialize poll tick, so stopping
13042
12199
  * the pump is not enough on its own: the very next tick would re-open a
13043
12200
  * window that real bytes have already closed, and the consumer would get a
13044
- * still picture painted over live video every second. Reset exactly where
13045
- * `firstKeyframeReceived` is — broker stop and suspend — so a battery camera
13046
- * that goes back to sleep gets its sentry again on the next wake.
12201
+ * still picture painted over live video every second. Reset on broker stop
12202
+ * and suspend, so a battery camera that goes back to sleep gets its sentry
12203
+ * again on the next wake.
13047
12204
  */
13048
12205
  sentryWindowClosed = false;
13049
12206
  reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
@@ -13221,17 +12378,6 @@ var StreamBroker = class StreamBroker {
13221
12378
  pipeServer;
13222
12379
  rtspRestreamer;
13223
12380
  preBuffer;
13224
- /**
13225
- * Keyframe-aligned GOP of the EXACT packets fed to the decoder plane (i.e.
13226
- * param-set prepended). Replayed to a freshly-armed shm decoder session so it
13227
- * initialises on a keyframe (SPS/PPS) instead of the next live P-frame — the
13228
- * decoder cold-start fix for push-mode sessions. Populated for ALL source
13229
- * types (unlike `preBuffer`, which push-only sources fill for the WebRTC
13230
- * late-join path): an rfc4571 Reolink source has an EMPTY `preBuffer`, so this
13231
- * is the only GOP available to seed its decoder. `getPackets()` returns just
13232
- * the current GOP (from the last keyframe), so a modest retention suffices.
13233
- */
13234
- decoderSeedBuffer;
13235
12381
  encodedCallbacks = /* @__PURE__ */ new Map();
13236
12382
  /**
13237
12383
  * Source-RTP video subscribers — receive raw on-wire RTP bytes
@@ -13270,13 +12416,6 @@ var StreamBroker = class StreamBroker {
13270
12416
  * so it gets a scoped logger child.
13271
12417
  */
13272
12418
  audioChunkPlane;
13273
- /**
13274
- * Gate for feeding encoded packets to the decoder: H.264/H.265 decoders
13275
- * need the SPS/PPS carried in the first keyframe to initialise, so the
13276
- * broker withholds packets from the shm frame plane until the first
13277
- * keyframe arrives.
13278
- */
13279
- firstKeyframeReceived = false;
13280
12419
  /** Timer for push-mode stall detection. */
13281
12420
  pushStallTimer;
13282
12421
  /**
@@ -13462,20 +12601,11 @@ var StreamBroker = class StreamBroker {
13462
12601
  * (`isClampedByBytes`).
13463
12602
  */
13464
12603
  static CLIP_RING_MAX_BYTES = 16 * 1024 * 1024;
13465
- /**
13466
- * Retention for the decoder-seed GOP ring. Only ever needs to hold the
13467
- * current GOP (a fresh keyframe evicts the previous GOP on read), so a few
13468
- * seconds covers every realistic keyframe interval; cameras with a GOP longer
13469
- * than this fall back to the plane's live-keyframe bootstrap.
13470
- */
13471
- static DECODER_SEED_BUFFER_SEC = 6;
13472
- constructor(deviceId, decoderApi, logger, audioCodecApi, localNodeId) {
12604
+ constructor(deviceId, logger, audioCodecApi) {
13473
12605
  this.deviceId = deviceId;
13474
12606
  this.numericDeviceId = deviceIdFromBrokerId$1(deviceId);
13475
- this.decoderApi = decoderApi;
13476
12607
  this.audioCodecApi = audioCodecApi ?? null;
13477
12608
  this.logger = logger;
13478
- this.localNodeId = localNodeId ?? null;
13479
12609
  this.pipeServer = new StreamPipeServer(logger?.child("pipe-server"));
13480
12610
  this.rtspRestreamer = new RtspRestreamer(deviceId);
13481
12611
  if (logger) this.rtspRestreamer.setLogger(logger.child("rtsp"));
@@ -13488,7 +12618,6 @@ var StreamBroker = class StreamBroker {
13488
12618
  };
13489
12619
  });
13490
12620
  this.preBuffer = new EncodedRingBuffer(StreamBroker.DEFAULT_PRE_BUFFER_SEC);
13491
- this.decoderSeedBuffer = new EncodedRingBuffer(StreamBroker.DECODER_SEED_BUFFER_SEC);
13492
12621
  this.audioChunkPlane = new AudioChunkPlane(logger?.child("audio-chunk-plane"), () => this.checkDemand());
13493
12622
  this.rtspRestreamer.onSessionCountChanged(() => this.checkDemand());
13494
12623
  this.pipeServer.onClientCountChanged(() => this.checkDemand());
@@ -13535,22 +12664,6 @@ var StreamBroker = class StreamBroker {
13535
12664
  };
13536
12665
  }
13537
12666
  /**
13538
- * Inject a manager-owned resolver for this broker's OWN RTSP restream URL
13539
- * (`rtsp://127.0.0.1:<port>/<token>[/muted]`). When the source is an RTP
13540
- * source ({@link isRtpSource}), the frame-handle decoder decodes via PULL
13541
- * mode from this restream instead of the broker's hand-depacketized AnnexB
13542
- * push feed — ffmpeg's own RTSP/RTP demuxer does the FU-A reassembly, which
13543
- * is robust where the AnnexB depacketizer chokes on heavily-fragmented 4K
13544
- * NALs (Reolink rfc4571 produced 0 decoded frames on the push path; the same
13545
- * stream decodes at full fps through the RTSP restream): RTP is passed
13546
- * through to the consumer's demuxer.
13547
- * Returns `null` before a restream token exists → falls back to the
13548
- * push path.
13549
- */
13550
- setRestreamUrlResolver(resolver) {
13551
- this.restreamUrlResolver = resolver;
13552
- }
13553
- /**
13554
12667
  * Inject where the "ffmpeg parser" ingress ({@link startFfmpegSourceReader})
13555
12668
  * reads the node's pinned ffmpeg: the manager's live config, asked per dial.
13556
12669
  */
@@ -13885,18 +12998,11 @@ var StreamBroker = class StreamBroker {
13885
12998
  clearTimeout(this.pushStallTimer);
13886
12999
  this.pushStallTimer = void 0;
13887
13000
  }
13888
- if (this.frameHandlePlane) {
13889
- const plane = this.frameHandlePlane;
13890
- this.frameHandlePlane = null;
13891
- await plane.dispose();
13892
- }
13893
13001
  await this.pipeServer.stop();
13894
13002
  this.rtspRestreamer.destroy();
13895
13003
  this.preBuffer.clear();
13896
- this.decoderSeedBuffer.clear();
13897
13004
  this.clipBuffer?.clear();
13898
13005
  this.pendingParamNals = [];
13899
- this.firstKeyframeReceived = false;
13900
13006
  this.encodedCallbacks.clear();
13901
13007
  this.audioChunkPlane.dispose();
13902
13008
  }
@@ -14147,7 +13253,6 @@ var StreamBroker = class StreamBroker {
14147
13253
  hasLiveIntent() {
14148
13254
  if (this.encodedCallbacks.size > 0) return true;
14149
13255
  if (this.rtpVideoCallbacks.size > 0) return true;
14150
- if ((this.frameHandlePlane?.subscriberCount ?? 0) > 0) return true;
14151
13256
  if (this.audioChunkPlane.subscriberCount > 0) return true;
14152
13257
  if (this.rtspRestreamer.getLiveIntentSessionCount() > 0) return true;
14153
13258
  if (this.pipeServer.getClientCount() > 0) return true;
@@ -14157,9 +13262,9 @@ var StreamBroker = class StreamBroker {
14157
13262
  }
14158
13263
  /**
14159
13264
  * True when at least one consumer needs DECODED audio. The audio-specific
14160
- * subset of {@link hasDemand} — deliberately excludes the video
14161
- * frame-handle plane so a video-only viewer never spins up the
14162
- * AAC/Opus decode chain. `encodedCallbacks` covers WebRTC (its G.711
13265
+ * subset of {@link hasDemand} — deliberately excludes video-only
13266
+ * consumers so a video-only viewer never spins up the AAC/Opus decode
13267
+ * chain. `encodedCallbacks` covers WebRTC (its G.711
14163
13268
  * egress needs decoded PCM); `audioChunkPlane` covers the analyzer.
14164
13269
  */
14165
13270
  hasAudioConsumer() {
@@ -14359,9 +13464,7 @@ var StreamBroker = class StreamBroker {
14359
13464
  this.sentryWindowClosed = false;
14360
13465
  this.reconcilePrestreamWindow();
14361
13466
  this.preBuffer.clear();
14362
- this.decoderSeedBuffer.clear();
14363
13467
  this.clipBuffer?.clear();
14364
- this.firstKeyframeReceived = false;
14365
13468
  this.pendingParamNals = [];
14366
13469
  this.rtspRestreamer.resetStreamState();
14367
13470
  this.totalBytes = 0;
@@ -14499,61 +13602,8 @@ var StreamBroker = class StreamBroker {
14499
13602
  if (packet.type === "video") {
14500
13603
  this.pipeServer.broadcast(packet.data);
14501
13604
  if (!this.rtspRestreamer.rtpPassthroughMode) this.rtspRestreamer.pushPacket(packet);
14502
- if (!this.firstKeyframeReceived && packet.keyframe && packet.isPlaceholder !== true) {
14503
- this.firstKeyframeReceived = true;
14504
- this.logger?.info("First keyframe received — decoder can start");
14505
- }
14506
- }
14507
- if (packet.type === "video" && this.firstKeyframeReceived && packet.isPlaceholder !== true) {
14508
- const plane = this.frameHandlePlane;
14509
- if (plane) {
14510
- const decoderPacket = this.decoderPacketWithParamSets(packet);
14511
- this.decoderSeedBuffer.push(decoderPacket);
14512
- plane.pushPacket(decoderPacket);
14513
- } else if (!this.seedRingSkipLogged) {
14514
- this.seedRingSkipLogged = true;
14515
- this.logger?.info("decoder seed ring not filled — no frame-handle consumer on this broker", {
14516
- tags: { deviceId: this.numericDeviceId },
14517
- meta: { seedSec: StreamBroker.DECODER_SEED_BUFFER_SEC }
14518
- });
14519
- }
14520
13605
  }
14521
13606
  }
14522
- /**
14523
- * Return the packet to feed the decoder plane, prepending the SDP-harvested
14524
- * SPS/PPS to a keyframe that lacks them in-band.
14525
- *
14526
- * The plane seeds a freshly-armed decoder session with the SINGLE keyframe
14527
- * packet that triggered the arm (`armPendingSessions`), so the parameter sets
14528
- * MUST travel inside that same packet — a separate param-set packet pushed
14529
- * just before it reaches only already-live sessions and is lost on the
14530
- * arming one. Unlike the encoded-subscriber path (`emitSdpParamSetsTo`), the
14531
- * decoder plane is otherwise never handed `sdpParameterSets`. Cameras that
14532
- * ship parameter sets ONLY in the SDP (Reolink high-profile native streams)
14533
- * therefore starve the `ffmpeg -i pipe:0` decoder of SPS/PPS, which loops
14534
- * `non-existing PPS → no frame` and produces zero decoded frames (verified
14535
- * live on device 592: WebRTC worked, detection decoded nothing). Only
14536
- * keyframes actually missing the sets are rewritten — well-behaved cameras
14537
- * are returned untouched — and the check runs per keyframe so a decoder
14538
- * restart mid-stream re-primes on the next IDR. The returned object is a
14539
- * fresh copy; the original `packet` fanned out to the other consumers above
14540
- * (WebRTC, restreamers, pre-buffer) is never mutated.
14541
- */
14542
- decoderPacketWithParamSets(packet) {
14543
- if (!packet.keyframe) return packet;
14544
- const paramSets = this.sdpParameterSets;
14545
- if (!paramSets || paramSets.length === 0) return packet;
14546
- if (annexBHasParameterSets(packet.data, packet.codec)) return packet;
14547
- if (this.streamingDebug || CH_RTP.on) this.logger?.info("Prepending SDP param sets to decoder keyframe (in-band SPS/PPS absent)", { meta: {
14548
- codec: packet.codec,
14549
- psCount: paramSets.length,
14550
- keyframeBytes: packet.data.length
14551
- } });
14552
- return {
14553
- ...packet,
14554
- data: Buffer.concat([concatParameterSetsAnnexB(paramSets), packet.data])
14555
- };
14556
- }
14557
13607
  onEncodedData(callback, attribution = UNKNOWN_ATTRIBUTION) {
14558
13608
  this.subscriberIdCounter += 1;
14559
13609
  const entry = {
@@ -14832,7 +13882,7 @@ var StreamBroker = class StreamBroker {
14832
13882
  *
14833
13883
  * Every ring this broker owns is sized from it, each fed by the source type
14834
13884
  * it belongs to: the source-RTP ring (RTSP cameras, the history a join burst
14835
- * is cut from), the Annex-B `preBuffer` (push sources), and the decoder seed.
13885
+ * is cut from) and the Annex-B `preBuffer` (push sources).
14836
13886
  * That is the correction this record exists for — the seconds used to size
14837
13887
  * ONLY the Annex-B ring, which is never fed for an RTP source, so on every
14838
13888
  * warm camera in this fleet the configured seconds retained nothing while two
@@ -14842,10 +13892,7 @@ var StreamBroker = class StreamBroker {
14842
13892
  const next = require_restream_intent.prebufferRetentionMs(config);
14843
13893
  this.setPreBufferEnabled(config.enabled);
14844
13894
  this.setPreBufferDuration(config.seconds);
14845
- if (next === this.prebufferMs) return;
14846
13895
  this.prebufferMs = next;
14847
- const seedSec = Math.max(StreamBroker.DECODER_SEED_BUFFER_SEC, Math.ceil(next / 1e3) + 2);
14848
- this.decoderSeedBuffer.setDuration(seedSec);
14849
13896
  }
14850
13897
  /**
14851
13898
  * How far back the ring is kept — the prebuffer, and nothing else.
@@ -14878,33 +13925,6 @@ var StreamBroker = class StreamBroker {
14878
13925
  getPreRollBudget() {
14879
13926
  return preRollBudgetBytes(this.getPreRollRetentionMs(), this.bitrateKbps);
14880
13927
  }
14881
- /**
14882
- * The seed replayed into a freshly-armed PUSH-mode decoder session.
14883
- *
14884
- * The current GOP (`getPackets()`) while pre-roll is off — today's behaviour —
14885
- * and the same keyframe-aligned pre-roll window otherwise, cut from the seed
14886
- * ring's whole retention rather than from its last keyframe. This is the
14887
- * push/Annex-B counterpart of {@link getRtpPreRollBuffer}: RTP sources decode
14888
- * in PULL mode off the restream and take their pre-roll as the join burst,
14889
- * push sources are fed packet by packet and take it here. One selector serves
14890
- * both, so a fix to the boundary cannot land on only one plane.
14891
- */
14892
- getDecoderSeedPackets() {
14893
- if (this.prebufferMs <= 0) return this.decoderSeedBuffer.getPackets();
14894
- const all = this.decoderSeedBuffer.getAllPackets();
14895
- if (all.length === 0) return [];
14896
- const newestPts = all[all.length - 1].pts;
14897
- const marks = [];
14898
- for (let i = 0; i < all.length; i++) {
14899
- const packet = all[i];
14900
- if (packet.keyframe && packet.type === "video") marks.push({
14901
- index: i,
14902
- ageMs: newestPts - packet.pts
14903
- });
14904
- }
14905
- const selection = selectPreRollStart(marks, this.prebufferMs);
14906
- return selection.startIndex < 0 ? [] : all.slice(selection.startIndex);
14907
- }
14908
13928
  /** Retained keyframe AUs as ages in ms of media time behind `newestTs`. */
14909
13929
  rtpPreRollMarks(newestTs) {
14910
13930
  return this.rtpRingKeyframeMarks.map((mark) => ({
@@ -15038,7 +14058,7 @@ var StreamBroker = class StreamBroker {
15038
14058
  * does NOT re-emit EncodedPackets; it only derives the per-packet
15039
14059
  * keyframe/param-set flags the pre-buffer capture needs (via a
15040
14060
  * flag-only depacketizer that never touches the AnnexB plane), keeping
15041
- * the AnnexB consumers (decoder, restreamer) single-fed by the provider.
14061
+ * the AnnexB consumers (pipe, restreamer) single-fed by the provider.
15042
14062
  *
15043
14063
  * The pre-buffer is captured even with zero subscribers attached — a
15044
14064
  * viewer that connects mid-GOP must be able to replay the GOP that
@@ -15066,7 +14086,7 @@ var StreamBroker = class StreamBroker {
15066
14086
  * EncodedPackets via `handleDepacketizedNal`), this callback ONLY sets
15067
14087
  * `_lastNalKeyframe`/`_lastNalParamSet` — the push provider owns the
15068
14088
  * AnnexB plane through `pushEncodedPacket`, so re-emitting here would
15069
- * double-feed the decoder/restreamer. push-rtp has no SDP `onVideoTrack`,
14089
+ * double-feed the pipe/restreamer. push-rtp has no SDP `onVideoTrack`,
15070
14090
  * so the codec comes from the source declaration (`detectedCodec`, seeded
15071
14091
  * in `start()`). Dropped on teardown (`resetRtpPipeline`) so a redial
15072
14092
  * starts with clean FU reassembly state.
@@ -15140,82 +14160,11 @@ var StreamBroker = class StreamBroker {
15140
14160
  if (released) this.checkDemand();
15141
14161
  return released;
15142
14162
  }
15143
- /**
15144
- * Resolve the stream descriptor the `FrameHandlePlane` needs to create a
15145
- * `frameSink: 'shm'` decoder session — codec, numeric device id, log tag,
15146
- * and (for a pull-mode decoder) the local pipe URL. Returns `null` before
15147
- * the broker has a source.
15148
- */
15149
- resolveFrameHandleStreamInfo() {
15150
- const codec = this.detectedCodec ?? this.source?.videoCodec;
15151
- if (!codec || !this.source) return null;
15152
- const slash = this.deviceId.indexOf("/");
15153
- const numericDeviceId = slash >= 0 ? Number.parseInt(this.deviceId.slice(0, slash), 10) : Number.parseInt(this.deviceId, 10);
15154
- const pullModeUrl = this.isRtpSource() ? this.restreamUrlResolver?.() ?? null : null;
15155
- return {
15156
- codec,
15157
- ...Number.isFinite(numericDeviceId) ? { numericDeviceId } : {},
15158
- tag: `broker:${this.deviceId}`,
15159
- pullModeUrl
15160
- };
15161
- }
15162
- /** Lazily construct the frame-handle plane, returning `null` with no decoder. */
15163
- ensureFrameHandlePlane() {
15164
- if (this.frameHandlePlane) return this.frameHandlePlane;
15165
- if (!this.decoderApi) {
15166
- this.logger?.warn("subscribeFrameHandles: no decoder API — frame handles unavailable");
15167
- return null;
15168
- }
15169
- this.frameHandlePlane = new FrameHandlePlane({
15170
- decoderApi: this.decoderApi,
15171
- logger: this.logger?.child("frame-handle-plane"),
15172
- resolveStreamInfo: () => this.resolveFrameHandleStreamInfo(),
15173
- localNodeId: this.localNodeId,
15174
- getSeedPackets: () => this.getDecoderSeedPackets(),
15175
- isDebugEnabled: () => this.streamingDebug
15176
- });
15177
- return this.frameHandlePlane;
15178
- }
15179
- /**
15180
- * Subscribe to this broker's shared-memory frame-handle stream — the broker's
15181
- * decoded-frame surface. The broker spins up (or reuses) a `frameSink: 'shm'`
15182
- * decoder session producing `input.format` and returns a `subscriptionId`
15183
- * the consumer polls via `pullFrameHandles`. fps throttling is implicit
15184
- * (latest-wins ring reads); `input.maxFps` is echoed as a cadence hint.
15185
- */
15186
- async subscribeFrameHandles(input) {
15187
- const plane = this.ensureFrameHandlePlane();
15188
- if (!plane) throw new Error("stream-broker: no decoder available for frame-handle subscription");
15189
- const { subscriptionId, maxFps } = await plane.subscribe({
15190
- format: input.format,
15191
- maxFps: input.maxFps,
15192
- tag: input.tag
15193
- });
15194
- this.checkDemand();
15195
- return {
15196
- subscriptionId,
15197
- maxFps
15198
- };
15199
- }
15200
- /** Drain up to `maxCount` `FrameHandle`s for a frame-handle subscription. */
15201
- pullFrameHandles(subscriptionId, maxCount) {
15202
- return this.frameHandlePlane?.pullHandles(subscriptionId, maxCount) ?? [];
15203
- }
15204
- /** Release a frame-handle subscription. Returns `true` when it existed. */
15205
- async unsubscribeFrameHandles(subscriptionId) {
15206
- const plane = this.frameHandlePlane;
15207
- if (!plane) return false;
15208
- const released = await plane.unsubscribe(subscriptionId);
15209
- if (released && plane.subscriberCount === 0) this.checkDemand();
15210
- return released;
15211
- }
15212
14163
  getStats() {
15213
14164
  return {
15214
14165
  status: this._suspended ? "idle" : this._status,
15215
14166
  inputFps: this.encodedInputFps,
15216
- decodeFps: this.frameHandlePlane?.decodeFps() ?? 0,
15217
14167
  encodedSubscribers: this.encodedCallbacks.size + this.rtpVideoCallbacks.size,
15218
- decodedSubscribers: this.frameHandlePlane?.subscriberCount ?? 0,
15219
14168
  uptimeMs: Date.now() - this.startedAt,
15220
14169
  bitrateKbps: this.bitrateKbps,
15221
14170
  idrIntervalMs: this.idrIntervalMs,
@@ -15227,7 +14176,6 @@ var StreamBroker = class StreamBroker {
15227
14176
  preBufferSec: this.preBuffer.getDuration(),
15228
14177
  preBufferMs: this.preBuffer.getBufferedDurationMs(),
15229
14178
  preBufferPackets: this.preBuffer.getPacketCount(),
15230
- decoderNodeId: this.frameHandlePlane?.decoderNodeIds()[0] ?? null,
15231
14179
  audio: this.audioTrackInfo ?? null,
15232
14180
  audioMuted: this.audioMuted,
15233
14181
  audioMutedDropped: this.audioMutedDropped
@@ -15235,8 +14183,8 @@ var StreamBroker = class StreamBroker {
15235
14183
  }
15236
14184
  /**
15237
14185
  * Per-broker consumer inventory (Phase 10). Returns the live subscriber
15238
- * list grouped by transport — every RTSP client, decoded-frame
15239
- * subscriber, and audio-chunk subscriber. Used by the admin UI's
14186
+ * list grouped by transport — every RTSP client, encoded subscriber and
14187
+ * audio-chunk subscriber. Used by the admin UI's
15240
14188
  * Streams tab and by operators diagnosing "phantom" holders keeping a
15241
14189
  * broker open.
15242
14190
  */
@@ -15270,13 +14218,6 @@ var StreamBroker = class StreamBroker {
15270
14218
  });
15271
14219
  return {
15272
14220
  rtsp: this.rtspRestreamer.listSessionInfos(),
15273
- decoded: (this.frameHandlePlane?.listSubscribers() ?? []).map((s) => ({
15274
- tag: s.tag,
15275
- subscribedAt: s.subscribedAt,
15276
- maxFps: s.maxFps,
15277
- framesDelivered: s.framesDelivered,
15278
- framesDropped: 0
15279
- })),
15280
14221
  audio: this.audioChunkPlane.listSubscribers().map((s) => ({
15281
14222
  tag: s.tag,
15282
14223
  subscribedAt: s.subscribedAt,
@@ -15290,33 +14231,12 @@ var StreamBroker = class StreamBroker {
15290
14231
  /**
15291
14232
  * Force-disconnect a single consumer. Matches by channel:
15292
14233
  * - `rtsp`: by `sessionId` (calls `RtspRestreamer.killSession`).
15293
- * - `decoded`: by `tag` — releases every shm frame-handle subscription
15294
- * whose tag matches (the plane winds the decoder session down once its
15295
- * last subscriber leaves).
15296
14234
  * - `audio`: by `tag` — drops every audio subscriber whose tag matches,
15297
14235
  * then re-evaluates demand.
15298
14236
  * Returns `true` when at least one consumer was dropped.
15299
14237
  */
15300
14238
  killClient(channel, handle) {
15301
14239
  if (channel === "rtsp") return this.rtspRestreamer.killSession(handle);
15302
- if (channel === "decoded") {
15303
- const plane = this.frameHandlePlane;
15304
- if (!plane) return false;
15305
- if (!plane.listSubscribers().some((s) => s.tag === handle)) return false;
15306
- plane.killByTag(handle).then((killed) => {
15307
- this.logger?.info("frame-handle subscribers force-disconnected", { meta: {
15308
- handle,
15309
- killed
15310
- } });
15311
- this.checkDemand();
15312
- }).catch((err) => {
15313
- this.logger?.warn("frame-handle killByTag failed", { meta: {
15314
- handle,
15315
- error: require_dist.errMsg(err)
15316
- } });
15317
- });
15318
- return true;
15319
- }
15320
14240
  const killed = this.audioChunkPlane.killByTag(handle);
15321
14241
  if (killed === 0) return false;
15322
14242
  this.logger?.info("audio subscribers force-disconnected", { meta: {
@@ -15570,7 +14490,7 @@ var StreamBroker = class StreamBroker {
15570
14490
  * Shared RTP-stream callback factory. Used by both the native RTSP
15571
14491
  * reader and the RFC 4571 reader — they ingest RTP from different
15572
14492
  * transports but the broker's downstream pipeline (depacketize →
15573
- * AnnexB → prebuffer / pipe / decoder / restream) is identical.
14493
+ * AnnexB → prebuffer / pipe / restream) is identical.
15574
14494
  *
15575
14495
  * `label` is the source-tag we attach to log messages so it's clear
15576
14496
  * which transport surfaced the event. `onTerminate` is the
@@ -15804,7 +14724,7 @@ var StreamBroker = class StreamBroker {
15804
14724
  /**
15805
14725
  * Handle a complete NAL from the RTP depacketizer.
15806
14726
  * Converts to EncodedPacket and feeds all Annex-B consumers
15807
- * (prebuffer, pipe server, decoder, restreamers).
14727
+ * (prebuffer, pipe server, restreamers).
15808
14728
  *
15809
14729
  * Called synchronously from processPacket — sets _lastNalKeyframe and
15810
14730
  * _lastNalParamSet so the onVideoRtp callback can forward them to the
@@ -15814,7 +14734,7 @@ var StreamBroker = class StreamBroker {
15814
14734
  * Pending parameter-set NALs (SPS/PPS for H.264, VPS/SPS/PPS for H.265).
15815
14735
  * Cameras emit these as separate RTP packets before the IDR/IRAP frame.
15816
14736
  * We buffer them here and prepend to the next VCL NAL so downstream
15817
- * consumers (WebRTC, prebuffer, decoder) receive complete access units
14737
+ * consumers (WebRTC, prebuffer, restream) receive complete access units
15818
14738
  * with a single `EncodedPacket` that contains SPS+PPS+IDR together.
15819
14739
  */
15820
14740
  pendingParamNals = [];
@@ -16171,32 +15091,6 @@ var StreamBroker = class StreamBroker {
16171
15091
  this._status = "error";
16172
15092
  }, PUSH_STALL_TIMEOUT_MS);
16173
15093
  }
16174
- /**
16175
- * Moleculer nodeID of a decoder provider hosting one of this broker's shm
16176
- * frame-plane sessions. `null` when the plane has no session. Used by the
16177
- * broker-manager's `rotateDecodersOnNode` to drop only the sessions
16178
- * affected by a per-agent event (e.g. an hwaccel override change).
16179
- */
16180
- getDecoderNodeId() {
16181
- return this.frameHandlePlane?.decoderNodeIds()[0] ?? null;
16182
- }
16183
- /**
16184
- * Force this broker's shm frame-plane decoder sessions hosted on
16185
- * `agentNodeId` to rotate — destroy and rebuild so they re-pull the latest
16186
- * per-agent preferences (hwaccel backend, …). A no-op when the broker has
16187
- * no frame plane. Subscriptions survive the rotation.
16188
- */
16189
- async rotateDecoderSession(reason, agentNodeId) {
16190
- const plane = this.frameHandlePlane;
16191
- if (!plane) return;
16192
- const target = agentNodeId ?? plane.decoderNodeIds()[0]?.split("/")[0];
16193
- if (!target) return;
16194
- this.logger?.info("rotating shm decoder sessions", { meta: {
16195
- reason,
16196
- agentNodeId: target
16197
- } });
16198
- await plane.rotate(target, reason);
16199
- }
16200
15094
  destroyNativeClient() {
16201
15095
  if (this.nativeClient) {
16202
15096
  this.nativeClient.destroy();
@@ -16210,7 +15104,7 @@ var StreamBroker = class StreamBroker {
16210
15104
  * Reolink lib's `createRfc4571TcpServer`). Same downstream pipeline as
16211
15105
  * the native RTSP reader: SDP arrives out-of-band on the StreamSource,
16212
15106
  * RTP packets feed the same depacketizer, AnnexB output goes through
16213
- * the same prebuffer / pipe / decoder paths.
15107
+ * the same prebuffer / pipe / restream paths.
16214
15108
  *
16215
15109
  * `source.metadata.sdp` is required and must describe the video
16216
15110
  * (and optionally audio) track the publisher is multiplexing on the
@@ -17402,15 +16296,9 @@ var StreamBrokerManager = class StreamBrokerManager {
17402
16296
  streamOfflineReasonByBroker = /* @__PURE__ */ new Map();
17403
16297
  streamHealthTimer;
17404
16298
  /**
17405
- * Routes a frame-handle `subscriptionId` (Phase 5 / D9) back to its owning
17406
- * brokerId so `pullFrameHandles` / `unsubscribeFrames` resolve in O(1)
17407
- * without scanning every broker.
17408
- */
17409
- frameSubscriptionBroker = /* @__PURE__ */ new Map();
17410
- /**
17411
16299
  * Routes an audio-chunk `subscriptionId` (Phase 5 / D9) back to its owning
17412
16300
  * brokerId so `pullAudioChunks` / `unsubscribeAudioChunks` resolve in O(1)
17413
- * without scanning every broker. Mirrors `frameSubscriptionBroker`.
16301
+ * without scanning every broker.
17414
16302
  */
17415
16303
  audioSubscriptionBroker = /* @__PURE__ */ new Map();
17416
16304
  /**
@@ -17427,7 +16315,6 @@ var StreamBrokerManager = class StreamBrokerManager {
17427
16315
  * it down on idle / retract. Populated by {@link attachDerivedFeeder}.
17428
16316
  */
17429
16317
  derivedFeeders = /* @__PURE__ */ new Map();
17430
- staticDecoders;
17431
16318
  logger;
17432
16319
  /** Public accessor for same-process callers (e.g. `CameraStreamsProvider.pickStream`)
17433
16320
  * that want to emit diagnostics through the manager's logger. Internal callers
@@ -17558,12 +16445,12 @@ var StreamBrokerManager = class StreamBrokerManager {
17558
16445
  /**
17559
16446
  * Injected factory for the inner broker a `RemoteEncodedSourceBroker`
17560
16447
  * drives. Production wraps the real `StreamBroker` — this leg never decodes
17561
- * locally, so the decoder/audio-codec caps and local-node pin are unused
17562
- * (mirrors the recommendation in `RemoteEncodedSourceBroker`'s doc comment).
16448
+ * audio locally, so the audio-codec cap is unused (mirrors the
16449
+ * recommendation in `RemoteEncodedSourceBroker`'s doc comment).
17563
16450
  * Tests substitute a fake via `setRemoteEncodedBrokerFactory` so no real
17564
16451
  * RTSP dial happens.
17565
16452
  */
17566
- createRemoteEncodedBroker = (deps) => new StreamBroker(deps.brokerId, null, deps.logger, null, null);
16453
+ createRemoteEncodedBroker = (deps) => new StreamBroker(deps.brokerId, deps.logger, null);
17567
16454
  /**
17568
16455
  * Transcode-pipeline manager — backs `getStreamWithCodec`. Lazily
17569
16456
  * created on first acquire, lives until `destroyAll`.
@@ -17615,8 +16502,7 @@ var StreamBrokerManager = class StreamBrokerManager {
17615
16502
  hwAccel: "copy",
17616
16503
  threadCount: 0
17617
16504
  };
17618
- constructor(staticDecoders, logger) {
17619
- this.staticDecoders = staticDecoders ?? [];
16505
+ constructor(logger) {
17620
16506
  this.logger = logger;
17621
16507
  this.rtspProvider = new RtspRestreamProviderImpl(this.rtspServer, logger.child("rtsp-restream"));
17622
16508
  }
@@ -17883,11 +16769,12 @@ var StreamBrokerManager = class StreamBrokerManager {
17883
16769
  }
17884
16770
  /** The decoder addon's answer for this node — before the process quarantine. */
17885
16771
  async readEgressHwAccel(deviceId, override) {
17886
- const decoderApi = this.buildDecoderCapApi(deviceId);
17887
16772
  return require_dist.resolveEgressDecodeHwAccel({
17888
16773
  readDecoderBackend: async () => {
17889
- if (!decoderApi) throw new Error("decoder cap not reachable from this broker");
17890
- return (await decoderApi.getInfo()).probedBestHwaccel ?? "";
16774
+ const api = this.api;
16775
+ if (!api) throw new Error("decoder cap not reachable from this broker");
16776
+ const pin = this.localNodeId ? require_dist.nodePin(this.localNodeId) : void 0;
16777
+ return (await api.decoder.getInfo.query(void 0, pin)).probedBestHwaccel ?? "";
17891
16778
  },
17892
16779
  readKernelPreferred: async () => {
17893
16780
  if (!this.hwaccel) return [];
@@ -17913,173 +16800,9 @@ var StreamBrokerManager = class StreamBrokerManager {
17913
16800
  async releaseEgressTranscode(input) {
17914
16801
  return this.getEgressTranscodeManager().release(input);
17915
16802
  }
17916
- buildDecoderCapApi(_deviceId) {
17917
- const localApi = this.buildLocalDecoderCapApi();
17918
- const apiDecoder = () => {
17919
- if (!this.api) return null;
17920
- const decoder = this.api["decoder"];
17921
- return decoder ? decoder : null;
17922
- };
17923
- const pin = this.localNodeId ? require_dist.nodePin(this.localNodeId) : void 0;
17924
- return {
17925
- supportsCodec: async (input) => {
17926
- const proxy = apiDecoder();
17927
- if (proxy) return proxy.supportsCodec.query(input, pin);
17928
- if (!localApi) return false;
17929
- return localApi.supportsCodec(input);
17930
- },
17931
- getInfo: async () => {
17932
- const proxy = apiDecoder();
17933
- if (proxy) return proxy.getInfo.query(void 0, pin);
17934
- if (!localApi) return {
17935
- id: "none",
17936
- name: "No decoder"
17937
- };
17938
- return localApi.getInfo();
17939
- },
17940
- createSession: async (config) => {
17941
- const proxy = apiDecoder();
17942
- if (proxy) return proxy.createSession.query(config, pin);
17943
- if (!localApi) throw new Error("No decoder provider available");
17944
- return localApi.createSession(config);
17945
- },
17946
- pushPacket: async (input) => {
17947
- const proxy = apiDecoder();
17948
- if (proxy) return proxy.pushPacket.query(input, pin);
17949
- if (!localApi) return;
17950
- return localApi.pushPacket(input);
17951
- },
17952
- pullFrames: async (input) => {
17953
- const proxy = apiDecoder();
17954
- if (proxy) return proxy.pullFrames.query(input, pin);
17955
- if (!localApi) return [];
17956
- return localApi.pullFrames(input);
17957
- },
17958
- pullHandles: async (input) => {
17959
- const proxy = apiDecoder();
17960
- if (proxy) return proxy.pullHandles.query(input, pin);
17961
- if (!localApi) return [];
17962
- return localApi.pullHandles(input);
17963
- },
17964
- destroySession: async (input) => {
17965
- const proxy = apiDecoder();
17966
- if (proxy) return proxy.destroySession.query(input, pin);
17967
- if (!localApi) return;
17968
- return localApi.destroySession(input);
17969
- },
17970
- openStream: async (input) => {
17971
- const proxy = apiDecoder();
17972
- if (proxy) return proxy.openStream.query(input, pin);
17973
- if (!localApi) return;
17974
- return localApi.openStream(input);
17975
- },
17976
- updateConfig: async (input) => {
17977
- const proxy = apiDecoder();
17978
- if (proxy) return proxy.updateConfig.query(input, pin);
17979
- if (!localApi) return;
17980
- return localApi.updateConfig(input);
17981
- },
17982
- getStats: async (input) => {
17983
- const proxy = apiDecoder();
17984
- if (proxy) return proxy.getStats.query(input, pin);
17985
- if (!localApi) return {
17986
- inputFps: 0,
17987
- outputFps: 0,
17988
- avgDecodeTimeMs: 0,
17989
- droppedFrames: 0
17990
- };
17991
- return localApi.getStats(input);
17992
- }
17993
- };
17994
- }
17995
- buildLocalDecoderCapApi() {
17996
- if (this.staticDecoders.length === 0) return null;
17997
- const FRAME_BUFFER_CAPACITY = 32;
17998
- const frameBuffers = /* @__PURE__ */ new Map();
17999
- const sessions = /* @__PURE__ */ new Map();
18000
- const unsubscribers = /* @__PURE__ */ new Map();
18001
- let nextId = 0;
18002
- const resolveDecoder = async (codec) => {
18003
- for (const p of this.staticDecoders) if (await p.supportsCodec({ codec })) return p;
18004
- return null;
18005
- };
18006
- return {
18007
- supportsCodec: async (input) => await resolveDecoder(input.codec) !== null,
18008
- getInfo: async () => {
18009
- const provider = this.staticDecoders[0];
18010
- if (!provider) return {
18011
- id: "none",
18012
- name: "No decoder"
18013
- };
18014
- return {
18015
- id: provider.id,
18016
- name: provider.name,
18017
- isPullMode: provider.isPullMode,
18018
- priority: provider.priority
18019
- };
18020
- },
18021
- createSession: async (config) => {
18022
- const provider = await resolveDecoder(config.codec);
18023
- if (!provider) throw new Error(`No decoder provider for codec "${config.codec}"`);
18024
- const session = await provider.createSession(config);
18025
- const sessionId = `dec-${++nextId}`;
18026
- sessions.set(sessionId, session);
18027
- const buffer = new require_dist.RingBuffer(FRAME_BUFFER_CAPACITY);
18028
- frameBuffers.set(sessionId, buffer);
18029
- const unsub = session.onFrame((frame) => {
18030
- buffer.push(frame);
18031
- });
18032
- unsubscribers.set(sessionId, unsub);
18033
- return {
18034
- sessionId,
18035
- nodeId: "local"
18036
- };
18037
- },
18038
- pushPacket: async (input) => {
18039
- const session = sessions.get(input.sessionId);
18040
- if (session) session.pushPacket(input.packet);
18041
- },
18042
- pullFrames: async (input) => {
18043
- const buffer = frameBuffers.get(input.sessionId);
18044
- if (!buffer) return [];
18045
- return buffer.drain(input.maxCount);
18046
- },
18047
- pullHandles: async () => [],
18048
- destroySession: async (input) => {
18049
- const session = sessions.get(input.sessionId);
18050
- if (session) {
18051
- const unsub = unsubscribers.get(input.sessionId);
18052
- if (unsub) unsub();
18053
- sessions.delete(input.sessionId);
18054
- frameBuffers.delete(input.sessionId);
18055
- unsubscribers.delete(input.sessionId);
18056
- await session.destroy();
18057
- }
18058
- },
18059
- openStream: async (input) => {
18060
- const session = sessions.get(input.sessionId);
18061
- if (session?.openStream) await session.openStream(input.url);
18062
- },
18063
- updateConfig: async (input) => {
18064
- const session = sessions.get(input.sessionId);
18065
- if (session) session.updateConfig(input.config);
18066
- },
18067
- getStats: async (input) => {
18068
- const session = sessions.get(input.sessionId);
18069
- if (!session) return {
18070
- inputFps: 0,
18071
- outputFps: 0,
18072
- avgDecodeTimeMs: 0,
18073
- droppedFrames: 0
18074
- };
18075
- return session.getStats();
18076
- }
18077
- };
18078
- }
18079
16803
  /**
18080
- * Wire the addon's `ctx.api` proxy. Required for the broker to
18081
- * resolve the `decoder` capability via tRPC — see
18082
- * `buildDecoderCapApi` for the full rationale.
16804
+ * Wire the addon's `ctx.api` proxy — the broker's route to other caps
16805
+ * (the stream catalog, the decoder's per-node hwaccel probe, …).
18083
16806
  */
18084
16807
  setApiAccess(api, localNodeId, hwaccel) {
18085
16808
  this.api = api;
@@ -20072,59 +18795,20 @@ var StreamBrokerManager = class StreamBrokerManager {
20072
18795
  * Resolve the live `StreamBroker` instance for a broker id. In-process
20073
18796
  * only — a `StreamBroker` is not tRPC-serialisable, so this is reachable
20074
18797
  * solely from same-process callers (device-scoped providers, the WebRTC
20075
- * server). Cross-process frame access goes through the shm `subscribeFrames`
20076
- * surface; cross-process stats through `getBrokerStats` / `listClients`.
18798
+ * server). Cross-process stats go through `getBrokerStats` / `listClients`.
20077
18799
  */
20078
18800
  async getBroker(input) {
20079
18801
  return this.brokers.get(input.brokerId) ?? null;
20080
18802
  }
20081
18803
  /**
20082
- * Open a `FrameHandle` subscription on a broker — the handle-based
20083
- * replacement for the live-object `onDecodedFrame` callback. Routed over
20084
- * tRPC, so a consumer in a different process can subscribe; the consumer
20085
- * then polls `pullFrameHandles` and feeds each handle to a
20086
- * `FrameRingReader`.
18804
+ * Drop every `audioSubscriptionBroker` entry pointing at `brokerId`. Called
18805
+ * on broker teardown so a destroyed broker leaves no dangling
18806
+ * `subscriptionId → brokerId` rows (a slow consumer that never calls
18807
+ * `unsubscribeAudioChunks` would otherwise leak a map entry per stream).
20087
18808
  */
20088
- async subscribeFrames(input) {
20089
- await this.ensureBrokerForId(input.brokerId);
20090
- const broker = this.brokers.get(input.brokerId);
20091
- if (!broker) throw new Error(`stream-broker: no broker for "${input.brokerId}"`);
20092
- const result = await broker.subscribeFrameHandles({
20093
- format: input.format,
20094
- maxFps: input.maxFps,
20095
- tag: input.tag
20096
- });
20097
- this.frameSubscriptionBroker.set(result.subscriptionId, input.brokerId);
20098
- return result;
20099
- }
20100
- /** Drain `FrameHandle`s for a subscription opened via `subscribeFrames`. */
20101
- async pullFrameHandles(input) {
20102
- const brokerId = this.frameSubscriptionBroker.get(input.subscriptionId);
20103
- if (!brokerId) return [];
20104
- const broker = this.brokers.get(brokerId);
20105
- if (!broker) return [];
20106
- return broker.pullFrameHandles(input.subscriptionId, input.maxCount);
20107
- }
20108
- /**
20109
- * Drop every `frameSubscriptionBroker` + `audioSubscriptionBroker` entry
20110
- * pointing at `brokerId`. Called on broker teardown so a destroyed broker
20111
- * leaves no dangling `subscriptionId → brokerId` rows (a slow consumer that
20112
- * never calls `unsubscribeFrames` / `unsubscribeAudioChunks` would otherwise
20113
- * leak a map entry per stream).
20114
- */
20115
- sweepFrameSubscriptions(brokerId) {
20116
- for (const [subscriptionId, mappedBrokerId] of this.frameSubscriptionBroker) if (mappedBrokerId === brokerId) this.frameSubscriptionBroker.delete(subscriptionId);
18809
+ sweepAudioSubscriptions(brokerId) {
20117
18810
  for (const [subscriptionId, mappedBrokerId] of this.audioSubscriptionBroker) if (mappedBrokerId === brokerId) this.audioSubscriptionBroker.delete(subscriptionId);
20118
18811
  }
20119
- /** Release a frame-handle subscription. */
20120
- async unsubscribeFrames(input) {
20121
- const brokerId = this.frameSubscriptionBroker.get(input.subscriptionId);
20122
- if (!brokerId) return { released: false };
20123
- this.frameSubscriptionBroker.delete(input.subscriptionId);
20124
- const broker = this.brokers.get(brokerId);
20125
- if (!broker) return { released: false };
20126
- return { released: await broker.unsubscribeFrameHandles(input.subscriptionId) };
20127
- }
20128
18812
  /**
20129
18813
  * Open a decoded audio-chunk subscription on a broker — the poll-based,
20130
18814
  * tRPC-reachable replacement for the live-object `onDecodedAudioChunk`
@@ -20165,9 +18849,7 @@ var StreamBrokerManager = class StreamBrokerManager {
20165
18849
  if (!broker) return {
20166
18850
  status: "stopped",
20167
18851
  inputFps: 0,
20168
- decodeFps: 0,
20169
18852
  encodedSubscribers: 0,
20170
- decodedSubscribers: 0,
20171
18853
  uptimeMs: 0,
20172
18854
  bitrateKbps: 0,
20173
18855
  idrIntervalMs: 0,
@@ -20178,7 +18860,6 @@ var StreamBrokerManager = class StreamBrokerManager {
20178
18860
  preBufferSec: 0,
20179
18861
  preBufferMs: 0,
20180
18862
  preBufferPackets: 0,
20181
- decoderNodeId: null,
20182
18863
  audio: null
20183
18864
  };
20184
18865
  return broker.getStats();
@@ -20187,7 +18868,6 @@ var StreamBrokerManager = class StreamBrokerManager {
20187
18868
  const broker = this.brokers.get(input.brokerId);
20188
18869
  if (!broker) return {
20189
18870
  rtsp: [],
20190
- decoded: [],
20191
18871
  audio: [],
20192
18872
  encoded: [],
20193
18873
  pipeClients: 0,
@@ -20200,28 +18880,6 @@ var StreamBrokerManager = class StreamBrokerManager {
20200
18880
  if (!broker) return { killed: false };
20201
18881
  return { killed: broker.killClient(input.channel, input.handle) };
20202
18882
  }
20203
- async rotateDecodersOnNode(agentNodeId, reason) {
20204
- let rotated = 0;
20205
- for (const broker of this.brokers.values()) {
20206
- if (!(broker.getDecoderNodeId()?.split("/")[0] === agentNodeId)) continue;
20207
- try {
20208
- await broker.rotateDecoderSession(reason, agentNodeId);
20209
- rotated++;
20210
- } catch (err) {
20211
- const parsedBrokerId = parseBrokerId(broker.deviceId);
20212
- this.logger.warn("decoder rotation failed", {
20213
- ...parsedBrokerId ? { tags: { deviceId: parsedBrokerId.deviceId } } : {},
20214
- meta: {
20215
- brokerId: broker.deviceId,
20216
- agentNodeId,
20217
- reason,
20218
- error: require_dist.errMsg(err)
20219
- }
20220
- });
20221
- }
20222
- }
20223
- return rotated;
20224
- }
20225
18883
  /** Raw broker instances — internal use only. */
20226
18884
  getBrokerInstances() {
20227
18885
  return [...this.brokers.values()];
@@ -20322,7 +18980,6 @@ var StreamBrokerManager = class StreamBrokerManager {
20322
18980
  this.brokers.clear();
20323
18981
  this.streamHealthByBroker.clear();
20324
18982
  this.streamOfflineReasonByBroker.clear();
20325
- this.frameSubscriptionBroker.clear();
20326
18983
  this.audioSubscriptionBroker.clear();
20327
18984
  await this.rtspServer.stop();
20328
18985
  }
@@ -22120,8 +20777,7 @@ var StreamBrokerManager = class StreamBrokerManager {
22120
20777
  camStreamId
22121
20778
  });
22122
20779
  const source = this.buildSourceFromCamStream(cam);
22123
- const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
22124
- broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
20780
+ const broker = new StreamBroker(brokerId, brokerLogger, buildAudioCodecCapApi(this.api));
22125
20781
  this.bindBrokerFfmpeg(broker);
22126
20782
  if (this.costRegistry !== null) broker.setCostRegistry(this.costRegistry);
22127
20783
  this.applyPrebufferConfig(broker, deviceId, camStreamId);
@@ -22187,7 +20843,7 @@ var StreamBrokerManager = class StreamBrokerManager {
22187
20843
  await broker.stop();
22188
20844
  this.brokers.delete(brokerId);
22189
20845
  this.sentryFrames.delete(brokerId);
22190
- this.sweepFrameSubscriptions(brokerId);
20846
+ this.sweepAudioSubscriptions(brokerId);
22191
20847
  this.rtspProvider.persistTokens();
22192
20848
  this.syncProfileRestreamers(deviceId);
22193
20849
  }
@@ -37658,7 +36314,6 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
37658
36314
  resolution: null,
37659
36315
  status: null,
37660
36316
  inputFps: null,
37661
- decodeFps: null,
37662
36317
  bitrateKbps: null
37663
36318
  });
37664
36319
  for (const slot of assignedSlots) {
@@ -37678,7 +36333,6 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
37678
36333
  resolution: cam?.resolution ?? slot.resolution ?? null,
37679
36334
  status: stats?.status ?? null,
37680
36335
  inputFps: stats?.inputFps ?? null,
37681
- decodeFps: stats?.decodeFps ?? null,
37682
36336
  bitrateKbps: stats?.bitrateKbps ?? null
37683
36337
  });
37684
36338
  }
@@ -37700,7 +36354,6 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
37700
36354
  resolution: cam.resolution ?? null,
37701
36355
  status: stats?.status ?? null,
37702
36356
  inputFps: stats?.inputFps ?? null,
37703
- decodeFps: stats?.decodeFps ?? null,
37704
36357
  bitrateKbps: stats?.bitrateKbps ?? null
37705
36358
  });
37706
36359
  }
@@ -38072,7 +36725,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
38072
36725
  this.ctx?.logger?.warn?.("event-loop stall — every session in this process froze", { meta: { stallMs } });
38073
36726
  } });
38074
36727
  this.loopStallMonitor.start();
38075
- this.brokerManager = new StreamBrokerManager(void 0, this.ctx.logger);
36728
+ this.brokerManager = new StreamBrokerManager(this.ctx.logger);
38076
36729
  this.brokerManager.setCostRegistry(this.costRegistry);
38077
36730
  this.brokerManager.setDefaultPreBufferSec(this.config.defaultPreBufferSec);
38078
36731
  this.brokerManager.setDefaultJoinBurstMaxSpanMs(this.config.joinBurstMaxSpanMs);
@@ -38194,47 +36847,6 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
38194
36847
  return { rtspTokenRowsDeleted };
38195
36848
  });
38196
36849
  await this.brokerManager.startRtspServer(this.config.rtspPort);
38197
- this.subscribe({ category: require_dist.EventCategory.PipelineAgentHwaccelChanged }, (event) => {
38198
- const { agentNodeId, reason } = event.data;
38199
- if (typeof agentNodeId !== "string" || agentNodeId.length === 0) return;
38200
- const manager = this.brokerManager;
38201
- if (!manager) return;
38202
- const reasonLabel = typeof reason === "string" ? reason : "hwaccel-changed";
38203
- manager.rotateDecodersOnNode(agentNodeId, reasonLabel).then((rotated) => {
38204
- if (rotated > 0) this.ctx.logger.info("decoders rotated for hwaccel change", {
38205
- tags: { nodeId: agentNodeId },
38206
- meta: {
38207
- rotated,
38208
- reason: reasonLabel
38209
- }
38210
- });
38211
- }).catch((err) => {
38212
- this.ctx.logger.warn("hwaccel-driven decoder rotation failed", {
38213
- tags: { nodeId: agentNodeId },
38214
- meta: { error: require_dist.errMsg(err) }
38215
- });
38216
- });
38217
- });
38218
- this.watchCapability(["decoder"], { onDown: (nodeId, capName) => {
38219
- if (capName !== "decoder") return;
38220
- const manager = this.brokerManager;
38221
- if (!manager) return;
38222
- const reasonLabel = `decoder-down:${nodeId}`;
38223
- manager.rotateDecodersOnNode(nodeId, reasonLabel).then((rotated) => {
38224
- if (rotated > 0) this.ctx.logger.info("decoders rotated after provider went down", {
38225
- tags: { nodeId },
38226
- meta: {
38227
- rotated,
38228
- reason: reasonLabel
38229
- }
38230
- });
38231
- }).catch((err) => {
38232
- this.ctx.logger.warn("decoder-down rotation failed", {
38233
- tags: { nodeId },
38234
- meta: { error: require_dist.errMsg(err) }
38235
- });
38236
- });
38237
- } });
38238
36850
  const reconcileAll = (reason) => {
38239
36851
  this.brokerManager?.reconcileAllCatalogs().catch((err) => {
38240
36852
  this.ctx.logger.warn("catalog reconcile failed", { meta: {
@@ -38710,260 +37322,6 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
38710
37322
  }
38711
37323
  };
38712
37324
  //#endregion
38713
- //#region src/stream-broker/stream-broker/frame-dropper.ts
38714
- var FrameDropper = class {
38715
- intervalMs;
38716
- lastPassedAt = -Infinity;
38717
- constructor(maxFps) {
38718
- this.intervalMs = maxFps > 0 ? 1e3 / maxFps : 0;
38719
- }
38720
- shouldKeep() {
38721
- if (this.intervalMs === 0) return true;
38722
- const now = Date.now();
38723
- if (now - this.lastPassedAt >= this.intervalMs) {
38724
- this.lastPassedAt = now;
38725
- return true;
38726
- }
38727
- return false;
38728
- }
38729
- setMaxFps(maxFps) {
38730
- this.intervalMs = maxFps > 0 ? 1e3 / maxFps : 0;
38731
- }
38732
- };
38733
- //#endregion
38734
- //#region src/stream-broker/stream-broker/ffmpeg-decoder-session.ts
38735
- /** Minimal no-op logger for default parameter */
38736
- var noopLogger = {
38737
- debug() {},
38738
- info() {},
38739
- warn() {},
38740
- error() {},
38741
- child() {
38742
- return noopLogger;
38743
- },
38744
- withTags() {
38745
- return noopLogger;
38746
- }
38747
- };
38748
- var SOI = Buffer.from([255, 216]);
38749
- var EOI = Buffer.from([255, 217]);
38750
- function codecToInputFormat(codec) {
38751
- switch (codec) {
38752
- case "h265":
38753
- case "hevc": return "hevc";
38754
- case "mjpeg": return "mjpeg";
38755
- default: return "h264";
38756
- }
38757
- }
38758
- function buildFfmpegArgs(config) {
38759
- const inputFormat = codecToInputFormat(config.codec);
38760
- const args = [
38761
- ...logBannerArgs("error"),
38762
- "-fflags",
38763
- "+nobuffer+flush_packets",
38764
- "-flags",
38765
- "low_delay",
38766
- "-probesize",
38767
- "32",
38768
- "-analyzeduration",
38769
- "0",
38770
- "-f",
38771
- inputFormat,
38772
- "-i",
38773
- "pipe:0"
38774
- ];
38775
- if (config.scale > 1) args.push("-vf", `scale=iw/${config.scale}:ih/${config.scale}`);
38776
- args.push("-f", "image2pipe", "-vcodec", "mjpeg", "-q:v", "3", "-threads", "1", "pipe:1");
38777
- return args;
38778
- }
38779
- var FfmpegDecoderSession = class {
38780
- config;
38781
- frameDropper;
38782
- process = null;
38783
- frameCallbacks = /* @__PURE__ */ new Set();
38784
- outputBuffer = Buffer.alloc(0);
38785
- destroyed = false;
38786
- logger;
38787
- /** The pinned ffmpeg to spawn (D540). Required: there is no PATH default. */
38788
- ffmpegPath;
38789
- cachedWidth = 0;
38790
- cachedHeight = 0;
38791
- inputPackets = 0;
38792
- outputFrames = 0;
38793
- droppedFrames = 0;
38794
- totalDecodeTimeMs = 0;
38795
- decodeCount = 0;
38796
- startTime = Date.now();
38797
- constructor(config, ffmpegPath, logger = noopLogger) {
38798
- this.config = { ...config };
38799
- this.ffmpegPath = ffmpegPath;
38800
- this.logger = logger;
38801
- this.frameDropper = new FrameDropper(config.maxFps);
38802
- this.spawnFfmpeg();
38803
- }
38804
- spawnFfmpeg() {
38805
- if (this.destroyed) return;
38806
- this.killFfmpeg();
38807
- this.outputBuffer = Buffer.alloc(0);
38808
- this.cachedWidth = 0;
38809
- this.cachedHeight = 0;
38810
- const args = buildFfmpegArgs(this.config);
38811
- const child = (0, node_child_process.spawn)(this.ffmpegPath, args);
38812
- this.process = child;
38813
- child.stdin?.on("error", () => {});
38814
- child.stdout?.on("data", (chunk) => {
38815
- if (child === this.process) this.handleOutputData(chunk);
38816
- });
38817
- child.stderr?.on("data", (data) => {
38818
- if (child !== this.process) return;
38819
- const line = data.toString().trim();
38820
- if (line) this.logger.debug("ffmpeg stderr", { meta: { line } });
38821
- });
38822
- child.on("error", (err) => {
38823
- if (child !== this.process) return;
38824
- this.logger.error("FFmpeg decoder spawn error", { meta: { error: err.message } });
38825
- });
38826
- child.on("close", (_code, _signal) => {
38827
- if (child === this.process && !this.destroyed) this.process = null;
38828
- });
38829
- }
38830
- killFfmpeg() {
38831
- const child = this.process;
38832
- if (!child) return;
38833
- this.process = null;
38834
- child.stdout?.removeAllListeners("data");
38835
- child.stderr?.removeAllListeners("data");
38836
- child.removeAllListeners("close");
38837
- try {
38838
- child.kill("SIGKILL");
38839
- } catch {}
38840
- }
38841
- handleOutputData(chunk) {
38842
- this.outputBuffer = Buffer.concat([this.outputBuffer, chunk]);
38843
- let searchFrom = 0;
38844
- while (true) {
38845
- const soiIndex = this.outputBuffer.indexOf(SOI, searchFrom);
38846
- if (soiIndex === -1) break;
38847
- const eoiIndex = this.outputBuffer.indexOf(EOI, soiIndex + 2);
38848
- if (eoiIndex === -1) break;
38849
- const frameEnd = eoiIndex + 2;
38850
- const jpegData = this.outputBuffer.subarray(soiIndex, frameEnd);
38851
- searchFrom = frameEnd;
38852
- this.emitFrame(Buffer.from(jpegData));
38853
- }
38854
- if (searchFrom > 0) this.outputBuffer = Buffer.from(this.outputBuffer.subarray(searchFrom));
38855
- }
38856
- emitFrame(data) {
38857
- const decodeStart = Date.now();
38858
- if (!this.frameDropper.shouldKeep()) {
38859
- this.droppedFrames++;
38860
- return;
38861
- }
38862
- const decodeTime = Date.now() - decodeStart;
38863
- this.totalDecodeTimeMs += decodeTime;
38864
- this.decodeCount++;
38865
- this.outputFrames++;
38866
- if (this.cachedWidth === 0) {
38867
- const dims = parseJpegDimensions(data);
38868
- this.cachedWidth = dims.width;
38869
- this.cachedHeight = dims.height;
38870
- }
38871
- const frame = {
38872
- data,
38873
- width: this.cachedWidth,
38874
- height: this.cachedHeight,
38875
- format: "jpeg",
38876
- timestamp: Date.now()
38877
- };
38878
- for (const cb of this.frameCallbacks) cb(frame);
38879
- }
38880
- pushPacket(packet) {
38881
- if (this.destroyed || !this.process?.stdin) return;
38882
- this.inputPackets++;
38883
- try {
38884
- this.process.stdin.write(packet.data);
38885
- } catch {}
38886
- }
38887
- onFrame(callback) {
38888
- this.frameCallbacks.add(callback);
38889
- return () => {
38890
- this.frameCallbacks.delete(callback);
38891
- };
38892
- }
38893
- updateConfig(update) {
38894
- const needsRestart = update.scale !== void 0 && update.scale !== this.config.scale || update.outputFormat !== void 0 && update.outputFormat !== this.config.outputFormat || update.codec !== void 0 && update.codec !== this.config.codec;
38895
- this.config = {
38896
- ...this.config,
38897
- ...update
38898
- };
38899
- if (update.maxFps !== void 0) this.frameDropper.setMaxFps(update.maxFps);
38900
- if (needsRestart) this.spawnFfmpeg();
38901
- }
38902
- async destroy() {
38903
- if (this.destroyed) return;
38904
- this.destroyed = true;
38905
- this.killFfmpeg();
38906
- this.frameCallbacks.clear();
38907
- }
38908
- getStats() {
38909
- const uptimeSec = Math.max((Date.now() - this.startTime) / 1e3, 1);
38910
- return {
38911
- inputFps: this.inputPackets / uptimeSec,
38912
- outputFps: this.outputFrames / uptimeSec,
38913
- avgDecodeTimeMs: this.decodeCount > 0 ? this.totalDecodeTimeMs / this.decodeCount : 0,
38914
- droppedFrames: this.droppedFrames
38915
- };
38916
- }
38917
- };
38918
- /**
38919
- * Parse JPEG SOF0/SOF2 marker to extract image dimensions.
38920
- * Scans for 0xFF 0xC0 (baseline) or 0xFF 0xC2 (progressive) markers.
38921
- * Returns {width: 0, height: 0} if not found.
38922
- */
38923
- function parseJpegDimensions(data) {
38924
- for (let i = 0; i < data.length - 8; i++) if (data[i] === 255 && (data[i + 1] === 192 || data[i + 1] === 194)) {
38925
- const height = data[i + 5] << 8 | data[i + 6];
38926
- return {
38927
- width: data[i + 7] << 8 | data[i + 8],
38928
- height
38929
- };
38930
- }
38931
- return {
38932
- width: 0,
38933
- height: 0
38934
- };
38935
- }
38936
- //#endregion
38937
- //#region src/stream-broker/stream-broker/ffmpeg-decoder-provider.ts
38938
- var SUPPORTED_CODECS = new Set([
38939
- "h264",
38940
- "h265",
38941
- "hevc",
38942
- "mjpeg"
38943
- ]);
38944
- /**
38945
- * A static decoder provider for stand-alone broker harnesses. Production
38946
- * decodes through the `decoder` cap; this exists for the manual streaming
38947
- * diagnostics. It is handed the pinned ffmpeg (D540) — there is no PATH
38948
- * default for it to fall back to.
38949
- */
38950
- var FfmpegDecoderProvider = class {
38951
- ffmpegPath;
38952
- id = "ffmpeg";
38953
- name = "FFmpeg Decoder";
38954
- /** Software decoder — used as fallback when hardware decoders are unavailable. */
38955
- priority = 50;
38956
- async supportsCodec(input) {
38957
- return SUPPORTED_CODECS.has(input.codec);
38958
- }
38959
- constructor(ffmpegPath) {
38960
- this.ffmpegPath = ffmpegPath;
38961
- }
38962
- async createSession(config) {
38963
- return new FfmpegDecoderSession(config, this.ffmpegPath);
38964
- }
38965
- };
38966
- //#endregion
38967
37325
  //#region src/stream-broker/frame-transport/codecs/length-prefix-codec.ts
38968
37326
  var LENGTH_PREFIX_BYTES = 4;
38969
37327
  /** Hard cap — 2^31-1 to stay safely within uint32 BE range and V8 Buffer limits. */
@@ -39928,8 +38286,6 @@ function fromBase64Url(s) {
39928
38286
  //#region src/stream-broker/index.ts
39929
38287
  var stream_broker_default = StreamBrokerAddon;
39930
38288
  //#endregion
39931
- exports.DecoderSessionProxy = DecoderSessionProxy;
39932
- exports.FfmpegDecoderProvider = FfmpegDecoderProvider;
39933
38289
  exports.InProcessTransport = InProcessTransport;
39934
38290
  exports.LengthPrefixCodec = LengthPrefixCodec;
39935
38291
  exports.StreamBroker = StreamBroker;