@camstack/addon-pipeline 1.1.23 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/audio-codec-ffmpeg/index.js +1 -1
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -1
  5. package/dist/decoder-ffmpeg/index.js +84 -12
  6. package/dist/decoder-ffmpeg/index.mjs +84 -12
  7. package/dist/detection-pipeline/index.js +2 -2
  8. package/dist/detection-pipeline/index.mjs +2 -2
  9. package/dist/{dist-pd_-3C0T.mjs → dist-BecXbIzJ.mjs} +64 -2
  10. package/dist/{dist-CE3a05qT.js → dist-wzWBZ26C.js} +69 -1
  11. package/dist/frame-handle-plane-BAKiW6t4.js +647 -0
  12. package/dist/frame-handle-plane-DGg0Aevs.mjs +636 -0
  13. package/dist/hub-hostname-DAJXlOgV.js +54 -0
  14. package/dist/hub-hostname-cCknRYKj.mjs +49 -0
  15. package/dist/{model-download-service-C-IHWnXx-BnQ_awK4.js → model-download-service-C-IHWnXx-DxM2DSns.js} +1 -2
  16. package/dist/motion-wasm/index.js +1 -1
  17. package/dist/motion-wasm/index.mjs +1 -1
  18. package/dist/pipeline-runner/index.js +305 -10
  19. package/dist/pipeline-runner/index.mjs +305 -10
  20. package/dist/recorder/index.js +267 -65
  21. package/dist/recorder/index.mjs +267 -65
  22. package/dist/stream-broker/_stub.js +39 -39
  23. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BVFAEkIB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CWPU9tbs.mjs} +2 -2
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-wfWYFiTT.mjs +26 -0
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-lpFwIH9e.mjs +26 -0
  26. package/dist/stream-broker/{hostInit-DHej3Ep9.mjs → hostInit-B7Fx3R_6.mjs} +2 -2
  27. package/dist/stream-broker/index.js +7 -611
  28. package/dist/stream-broker/index.mjs +5 -609
  29. package/dist/stream-broker/remoteEntry.js +1 -1
  30. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BqXU75it.js → MaskShapeCanvas-DI4BY7W2-CPkQfKnu.js} +1 -1
  31. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Ct2fpgCd.js → MotionZonesSettings-NcxxQN8r-DlLMQBag.js} +1 -1
  32. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-a9eFVPPl.js → PrivacyMaskSettings-APgPLF7p-kNcauIAN.js} +1 -1
  33. package/embed-dist/assets/index-DrJ0ee3f.css +2 -0
  34. package/embed-dist/assets/index-xpPLFfsT.js +80 -0
  35. package/embed-dist/index.html +2 -2
  36. package/package.json +1 -1
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BFeBlYRQ.mjs +0 -26
  38. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BC1Jt6G8.mjs +0 -26
  39. package/embed-dist/assets/index-C7SSikPl.css +0 -2
  40. package/embed-dist/assets/index-DhdVxWXL.js +0 -80
  41. package/dist/{model-download-service-C-IHWnXx-3Mmeob3l.mjs → model-download-service-C-IHWnXx-BPy6aoAx.mjs} +1 -1
@@ -2,17 +2,18 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BnQ_awK4.js");
6
- const require_dist = require("../dist-CE3a05qT.js");
5
+ const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
6
+ const require_dist = require("../dist-wzWBZ26C.js");
7
+ const require_frame_handle_plane = require("../frame-handle-plane-BAKiW6t4.js");
7
8
  const require_frame_dropper = require("../frame-dropper-7RTo_YyG.js");
9
+ let node_crypto = require("node:crypto");
10
+ node_crypto = require_model_download_service_C_IHWnXx.__toESM(node_crypto);
8
11
  let node_fs = require("node:fs");
9
12
  node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
10
13
  let node_path = require("node:path");
11
14
  node_path = require_model_download_service_C_IHWnXx.__toESM(node_path);
12
15
  let node_os = require("node:os");
13
16
  node_os = require_model_download_service_C_IHWnXx.__toESM(node_os);
14
- let node_crypto = require("node:crypto");
15
- node_crypto = require_model_download_service_C_IHWnXx.__toESM(node_crypto);
16
17
  let node_child_process = require("node:child_process");
17
18
  let node_dns_promises = require("node:dns/promises");
18
19
  let node_module = require("node:module");
@@ -8750,611 +8751,6 @@ var EncodedRingBuffer = class {
8750
8751
  }
8751
8752
  };
8752
8753
  //#endregion
8753
- //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
8754
- var DecoderSessionProxy = class {
8755
- api;
8756
- sessionId;
8757
- polling = false;
8758
- constructor(api, sessionId) {
8759
- this.api = api;
8760
- this.sessionId = sessionId;
8761
- }
8762
- async pushPacket(packet) {
8763
- await this.api.pushPacket({
8764
- sessionId: this.sessionId,
8765
- packet
8766
- });
8767
- }
8768
- async openStream(url) {
8769
- await this.api.openStream({
8770
- sessionId: this.sessionId,
8771
- url
8772
- });
8773
- }
8774
- async startPolling(onFrame) {
8775
- this.polling = true;
8776
- while (this.polling) {
8777
- const frames = await this.api.pullFrames({
8778
- sessionId: this.sessionId,
8779
- maxCount: 4
8780
- });
8781
- for (const frame of frames) onFrame(frame);
8782
- if (frames.length === 0) await new Promise((r) => setTimeout(r, 1));
8783
- }
8784
- }
8785
- /**
8786
- * Poll a `frameSink: 'shm'` session for `FrameHandle`s (Phase 5 / D9).
8787
- * Mirrors `startPolling` but drains `pullHandles` — the decoder has
8788
- * already written the pixels into a shared-memory ring, so what crosses
8789
- * the cap boundary is the tiny serialisable handle. Runs until
8790
- * `stopPolling`, `destroy`, or the session is destroyed externally.
8791
- *
8792
- * When the decoder reports the session is gone (`unknown sessionId` /
8793
- * `Service … not found`) the loop exits gracefully — those errors are
8794
- * expected during decoder restart/shutdown and must not be re-thrown to
8795
- * the caller's `.catch()` handler, which would otherwise spin forever.
8796
- */
8797
- async startHandlePolling(onHandle) {
8798
- this.polling = true;
8799
- while (this.polling) try {
8800
- const handles = await this.api.pullHandles({
8801
- sessionId: this.sessionId,
8802
- maxCount: 4
8803
- });
8804
- for (const handle of handles) onHandle(handle);
8805
- if (handles.length === 0) await new Promise((r) => setTimeout(r, 1));
8806
- } catch (err) {
8807
- this.polling = false;
8808
- const msg = err instanceof Error ? err.message : String(err);
8809
- if (!msg.includes("unknown sessionId") && !msg.includes("Service") && !msg.includes("not found")) throw err;
8810
- return;
8811
- }
8812
- }
8813
- stopPolling() {
8814
- this.polling = false;
8815
- }
8816
- async updateConfig(config) {
8817
- await this.api.updateConfig({
8818
- sessionId: this.sessionId,
8819
- config
8820
- });
8821
- }
8822
- async getStats() {
8823
- return this.api.getStats({ sessionId: this.sessionId });
8824
- }
8825
- async destroy() {
8826
- this.stopPolling();
8827
- await this.api.destroySession({ sessionId: this.sessionId });
8828
- }
8829
- };
8830
- //#endregion
8831
- //#region src/stream-broker/stream-broker/frame-handle-plane.ts
8832
- /**
8833
- * `FrameHandlePlane` — the broker's shared-memory frame-handle surface
8834
- * (Phase 5 / D9).
8835
- *
8836
- * This is the handle-based replacement for `StreamBroker.onDecodedFrame`'s
8837
- * live-object callback path. Instead of fanning pixel `DecodedFrame`s out to
8838
- * in-process callbacks (which cannot cross a process boundary), the broker
8839
- * publishes zero-pixel, serialisable `FrameHandle`s a consumer drains over
8840
- * tRPC and reads back from a shared-memory ring with a `FrameRingReader`.
8841
- *
8842
- * ## Ring-per-format model
8843
- *
8844
- * A `frameSink: 'shm'` decoder session produces exactly ONE pixel format
8845
- * (the decoder's scaler is configured for it). The broker therefore keeps
8846
- * one decoder session — hence one shm ring — per `(brokerId, format)` that a
8847
- * consumer actually subscribes to:
8848
- *
8849
- * - motion subscribes `gray` → one `gray` decoder session + ring
8850
- * - detection subscribes `rgb` → one `rgb` decoder session + ring
8851
- *
8852
- * Only the formats consumers ask for are materialised. There is NO
8853
- * broker-side `sharp` conversion any more — each consumer reads the exact
8854
- * format it requested. A session is reference-counted by its subscriber set
8855
- * and torn down when the last subscriber of that format leaves.
8856
- *
8857
- * ## fps throttling
8858
- *
8859
- * Throttling is implicit: the ring is latest-wins, so a slow consumer that
8860
- * polls `pullFrameHandles` infrequently simply receives the newest handles
8861
- * and the intervening frames are dropped at the ring (a recycled slot the
8862
- * consumer never read). The optional `maxFps` from `subscribeFrames` is NOT
8863
- * enforced here — it is echoed back to the consumer as a reader-side polling
8864
- * cadence hint.
8865
- *
8866
- * ## Packet feed
8867
- *
8868
- * The plane's decoder sessions are push-mode by default: the broker forwards
8869
- * every video `EncodedPacket` to `pushPacket`. For a pull-mode decoder the
8870
- * broker supplies a `pullModeUrl` and the plane calls `openStream` once the
8871
- * session exists.
8872
- */
8873
- /**
8874
- * Collapse a hierarchical Moleculer nodeID (`hub/child-abc`) to its owning
8875
- * node (`hub`). Frame-plane co-location only cares about the physical node, not
8876
- * which forked child within it hosts the session.
8877
- */
8878
- function collapseNodeId(nodeId) {
8879
- const slash = nodeId.indexOf("/");
8880
- return slash >= 0 ? nodeId.slice(0, slash) : nodeId;
8881
- }
8882
- /**
8883
- * Decide whether a decoder session created on `decoderNodeId` is co-located
8884
- * with the broker whose local node is `brokerNodeId`.
8885
- *
8886
- * The broker's shm frame ring is NODE-LOCAL: a decoder session on any other
8887
- * node can never deliver frame handles to this broker's consumers, so a
8888
- * mis-placed session must be rejected. Both ids are collapsed (`hub/child`→
8889
- * `hub`) before comparison.
8890
- *
8891
- * A `null` `brokerNodeId` means the broker's own node is unknown — enforcement
8892
- * is skipped (legacy behavior, accept any placement).
8893
- */
8894
- function isDecoderNodeColocated(brokerNodeId, decoderNodeId) {
8895
- if (brokerNodeId === null) return true;
8896
- return collapseNodeId(brokerNodeId) === collapseNodeId(decoderNodeId);
8897
- }
8898
- /** Per-subscription handle-queue capacity. A handle is tiny; 64 absorbs a
8899
- * burst between two `pullFrameHandles` polls without unbounded growth. */
8900
- var HANDLE_QUEUE_CAPACITY = 64;
8901
- /** Default reader-side cadence hint when a subscriber omits `maxFps`. */
8902
- var DEFAULT_HINT_FPS = 5;
8903
- /** Rolling window for the aggregate `decodeFps` rate, ms. Mirrors the
8904
- * broker's 2s `inputFps` window so the two stats are comparable. */
8905
- var DECODE_FPS_WINDOW_MS = 2e3;
8906
- /**
8907
- * Keep-warm grace before a decoder session with no subscribers is torn down.
8908
- *
8909
- * Subscribers flap on ~30s cadences (occupancy recheck bursts, detection
8910
- * motion-gating watch↔active, onboard gray gating). Destroying the
8911
- * ffmpeg/VAAPI decode session the instant the last subscriber leaves spawns +
8912
- * destroys a full session every cycle — wasted setup/teardown. Chosen >30s so
8913
- * consecutive bursts and watch↔active flips reuse the warm session, yet a
8914
- * genuinely idle session still frees its ffmpeg within ~a minute.
8915
- */
8916
- var SESSION_LINGER_MS = 45e3;
8917
- /**
8918
- * Owns the broker's shared-memory frame-handle subscriptions and the
8919
- * per-format `frameSink: 'shm'` decoder sessions that feed them.
8920
- */
8921
- var FrameHandlePlane = class {
8922
- decoderApi;
8923
- logger;
8924
- resolveStreamInfo;
8925
- localNodeId;
8926
- subscriptions = /* @__PURE__ */ new Map();
8927
- sessions = /* @__PURE__ */ new Map();
8928
- /** In-flight `ensureSession` creation per format. Coalesces concurrent
8929
- * same-format callers onto ONE decoder session — without it two callers
8930
- * both pass the `sessions.get` check, both `createSession` (two ffmpeg),
8931
- * and the second registration orphans the first with no `destroySession`. */
8932
- sessionCreating = /* @__PURE__ */ new Map();
8933
- disposed = false;
8934
- /** Re-entrancy guard for `armPendingSessions` — collapses the per-packet
8935
- * `pushPacket` calls into a single in-flight deferred-arm pass. */
8936
- armInFlight = false;
8937
- /** Aggregate decoded-frame rate state — frames fanned out in the current
8938
- * rolling window, the window's start epoch, and the last computed fps. */
8939
- framesInWindow = 0;
8940
- windowStartMs = Date.now();
8941
- decodedFps = 0;
8942
- constructor(options) {
8943
- this.decoderApi = options.decoderApi;
8944
- this.logger = options.logger;
8945
- this.resolveStreamInfo = options.resolveStreamInfo;
8946
- this.localNodeId = options.localNodeId;
8947
- }
8948
- /** Number of active handle subscriptions — surfaced for diagnostics. */
8949
- get subscriberCount() {
8950
- return this.subscriptions.size;
8951
- }
8952
- /**
8953
- * Moleculer nodeIDs of the decoder providers currently hosting this plane's
8954
- * `frameSink: 'shm'` sessions. Used by the broker-manager's per-agent
8955
- * hwaccel-change handler to decide which brokers need a decoder rotation.
8956
- */
8957
- decoderNodeIds() {
8958
- const ids = [];
8959
- for (const session of this.sessions.values()) if (session.nodeId) ids.push(session.nodeId);
8960
- return ids;
8961
- }
8962
- /**
8963
- * Rotate every shm decoder session hosted on `agentNodeId` — destroy and
8964
- * rebuild it so the new session re-pulls the latest per-agent decoder
8965
- * preferences (hwaccel backend, …). Subscriptions are preserved: the same
8966
- * `subscriptionId`s read from the rebuilt session's fresh ring. A no-op for
8967
- * sessions on other nodes. Returns the number of sessions rotated.
8968
- */
8969
- async rotate(agentNodeId, reason) {
8970
- if (this.disposed) return 0;
8971
- let rotated = 0;
8972
- for (const session of this.sessions.values()) {
8973
- if ((session.nodeId && session.nodeId.includes("/") ? session.nodeId.split("/")[0] : session.nodeId) !== agentNodeId) continue;
8974
- this.logger?.info("frame-handle plane: rotating shm decoder session", { meta: {
8975
- format: session.format,
8976
- reason,
8977
- nodeId: session.nodeId
8978
- } });
8979
- await this.destroySession(session);
8980
- await this.startSessionDecoder(session);
8981
- rotated += 1;
8982
- }
8983
- return rotated;
8984
- }
8985
- /**
8986
- * Register a frame-handle subscription. Spins up (or reuses) a
8987
- * `frameSink: 'shm'` decoder session producing `format`. The returned
8988
- * `subscriptionId` is the handle the consumer passes to
8989
- * `pullFrameHandles` / `unsubscribe`.
8990
- */
8991
- async subscribe(input) {
8992
- if (this.disposed) throw new Error("FrameHandlePlane: subscribe after dispose");
8993
- const subscriptionId = `fh-${(0, node_crypto.randomUUID)()}`;
8994
- const maxFps = input.maxFps !== void 0 && input.maxFps > 0 ? input.maxFps : DEFAULT_HINT_FPS;
8995
- const subscription = {
8996
- id: subscriptionId,
8997
- format: input.format,
8998
- maxFps,
8999
- tag: input.tag ?? "unknown",
9000
- subscribedAt: Date.now(),
9001
- queue: new require_dist.RingBuffer(HANDLE_QUEUE_CAPACITY),
9002
- framesDelivered: 0
9003
- };
9004
- this.subscriptions.set(subscriptionId, subscription);
9005
- await this.ensureSession(input.format, subscriptionId);
9006
- this.logger?.info("frame-handle subscription added", { meta: {
9007
- subscriptionId,
9008
- format: input.format,
9009
- tag: subscription.tag,
9010
- maxFps
9011
- } });
9012
- return {
9013
- subscriptionId,
9014
- maxFps
9015
- };
9016
- }
9017
- /**
9018
- * Drain up to `maxCount` `FrameHandle`s for a subscription, latest-wins.
9019
- * An unknown subscription id returns `[]` (the consumer may have been
9020
- * torn down concurrently).
9021
- */
9022
- pullHandles(subscriptionId, maxCount) {
9023
- const subscription = this.subscriptions.get(subscriptionId);
9024
- if (!subscription) return [];
9025
- const stale = subscription.queue.size - maxCount;
9026
- if (stale > 0) subscription.queue.drain(stale);
9027
- const handles = subscription.queue.drain(maxCount);
9028
- subscription.framesDelivered += handles.length;
9029
- return handles;
9030
- }
9031
- /**
9032
- * Release a subscription. When it was the last reader of its format the
9033
- * underlying decoder session is destroyed (and its shm segment unlinked
9034
- * by the decoder). Returns `true` when a known subscription was released.
9035
- */
9036
- async unsubscribe(subscriptionId, immediate = false) {
9037
- const subscription = this.subscriptions.get(subscriptionId);
9038
- if (!subscription) return false;
9039
- this.subscriptions.delete(subscriptionId);
9040
- const session = this.sessions.get(subscription.format);
9041
- if (session) {
9042
- session.subscriberIds.delete(subscriptionId);
9043
- if (session.subscriberIds.size === 0) if (immediate) {
9044
- this.clearLinger(session);
9045
- this.sessions.delete(subscription.format);
9046
- await this.destroySession(session);
9047
- } else this.scheduleLinger(session);
9048
- }
9049
- this.logger?.info("frame-handle subscription removed", { meta: {
9050
- subscriptionId,
9051
- format: subscription.format
9052
- } });
9053
- return true;
9054
- }
9055
- /**
9056
- * Arm the keep-warm teardown timer for a session whose last subscriber just
9057
- * left. Clears any existing timer first (idempotent). On fire it re-checks
9058
- * `subscriberIds.size === 0` — a subscriber may have returned in the interim,
9059
- * in which case the timer is a no-op — and only then removes the session and
9060
- * destroys the decoder. The timer is `unref`'d so a lingering warm session
9061
- * never keeps the process alive.
9062
- */
9063
- scheduleLinger(session) {
9064
- this.clearLinger(session);
9065
- const timer = setTimeout(() => {
9066
- session.lingerTimer = null;
9067
- if (session.subscriberIds.size > 0) return;
9068
- this.sessions.delete(session.format);
9069
- this.destroySession(session).catch(() => {});
9070
- }, SESSION_LINGER_MS);
9071
- timer.unref?.();
9072
- session.lingerTimer = timer;
9073
- }
9074
- /** Cancel a session's pending keep-warm teardown timer, if any. */
9075
- clearLinger(session) {
9076
- if (session.lingerTimer) {
9077
- clearTimeout(session.lingerTimer);
9078
- session.lingerTimer = null;
9079
- }
9080
- }
9081
- /**
9082
- * Forward a video `EncodedPacket` to every active shm decoder session.
9083
- * Push-mode decoders consume this; a pull-mode decoder ignores it (it
9084
- * reads its own pipe via `openStream`). Audio packets are not forwarded.
9085
- *
9086
- * The packet feed doubles as the plane's stream-ready signal: the broker
9087
- * only calls `pushPacket` once it has a source and the first keyframe has
9088
- * landed — exactly the point `resolveStreamInfo` starts returning a
9089
- * descriptor. Each packet therefore triggers `armPendingSessions`, which
9090
- * re-attempts session creation for any deferred subscription so a lone
9091
- * first subscriber on a not-yet-started broker self-arms without a
9092
- * redundant re-`subscribe`.
9093
- */
9094
- pushPacket(packet) {
9095
- if (this.disposed || packet.type !== "video") return;
9096
- this.armPendingSessions(packet).catch((err) => {
9097
- this.logger?.warn("frame-handle plane: arm pending sessions error", { meta: { error: require_dist.errMsg(err) } });
9098
- });
9099
- this.feedSessions(packet);
9100
- }
9101
- /** Forward a video `EncodedPacket` to every live shm decoder session. */
9102
- feedSessions(packet) {
9103
- for (const session of this.sessions.values()) {
9104
- if (!session.proxy) continue;
9105
- session.proxy.pushPacket(packet).catch((err) => {
9106
- const msg = require_dist.errMsg(err);
9107
- this.logger?.warn("frame-handle plane: decoder push error", { meta: {
9108
- format: session.format,
9109
- error: msg
9110
- } });
9111
- if (msg.includes("unknown sessionId") || msg.includes("Service") || msg.includes("not found")) {
9112
- this.logger?.info("frame-handle plane: invalidating stale proxy", { meta: { format: session.format } });
9113
- this.destroySession(session).catch(() => {});
9114
- this.sessions.delete(session.format);
9115
- }
9116
- });
9117
- }
9118
- }
9119
- /**
9120
- * Re-attempt session creation for every subscription whose format has no
9121
- * live `FormatSession` yet — the subscriptions whose `startSessionDecoder`
9122
- * was deferred because the broker had no stream at `subscribe` time. Driven
9123
- * off the `pushPacket` feed (event-driven, no polling timer); a no-op once
9124
- * every subscribed format already owns a session.
9125
- *
9126
- * `armInFlight` collapses re-entrant calls: `pushPacket` fires per packet,
9127
- * but `ensureSession` is async — without the guard a burst of packets would
9128
- * launch overlapping decoder-creation round-trips for the same format.
9129
- *
9130
- * `triggerPacket` is the packet whose arrival armed the pass. Because
9131
- * `ensureSession` is async, the synchronous `feedSessions` in `pushPacket`
9132
- * runs before the session lands in `this.sessions` — so this method feeds
9133
- * the triggering packet to each session it just created, otherwise that
9134
- * first (keyframe-carrying) packet would be lost and a push-mode H264/H265
9135
- * decoder would stall waiting for SPS/PPS.
9136
- */
9137
- async armPendingSessions(triggerPacket) {
9138
- if (this.disposed || this.armInFlight) return;
9139
- const pending = /* @__PURE__ */ new Map();
9140
- for (const subscription of this.subscriptions.values()) {
9141
- if (this.sessions.has(subscription.format)) continue;
9142
- if (!pending.has(subscription.format)) pending.set(subscription.format, subscription.id);
9143
- }
9144
- if (pending.size === 0) return;
9145
- this.armInFlight = true;
9146
- try {
9147
- for (const [format, subscriptionId] of pending) {
9148
- if (this.disposed) return;
9149
- await this.ensureSession(format, subscriptionId);
9150
- const armed = this.sessions.get(format);
9151
- if (armed?.proxy) armed.proxy.pushPacket(triggerPacket).catch((err) => {
9152
- this.logger?.warn("frame-handle plane: decoder push error", { meta: {
9153
- format,
9154
- error: require_dist.errMsg(err)
9155
- } });
9156
- });
9157
- }
9158
- } finally {
9159
- this.armInFlight = false;
9160
- }
9161
- }
9162
- /**
9163
- * Aggregate decoded-frame rate across every `frameSink: 'shm'` session in
9164
- * this plane, frames/s. Each `fanoutHandle` call is one decoded frame
9165
- * delivered into the plane; the rate is recomputed over a rolling
9166
- * `DECODE_FPS_WINDOW_MS` window. Returns the last computed value between
9167
- * window rolls, and `0` while no session is producing.
9168
- */
9169
- decodeFps() {
9170
- this.rollDecodeFpsWindow();
9171
- return this.decodedFps;
9172
- }
9173
- /**
9174
- * Roll the decoded-fps window when it has elapsed: convert frames seen in
9175
- * the window into a per-second rate, then reset the counter. Called both
9176
- * on every fanout and on every `decodeFps()` read so a stalled session
9177
- * (no fanout) decays its rate to `0` rather than reporting a stale value.
9178
- */
9179
- rollDecodeFpsWindow() {
9180
- const now = Date.now();
9181
- const windowMs = now - this.windowStartMs;
9182
- if (windowMs < DECODE_FPS_WINDOW_MS) return;
9183
- this.decodedFps = this.framesInWindow / windowMs * 1e3;
9184
- this.framesInWindow = 0;
9185
- this.windowStartMs = now;
9186
- }
9187
- /** Diagnostic snapshot of every active frame-handle subscription. */
9188
- listSubscribers() {
9189
- return [...this.subscriptions.values()].map((s) => ({
9190
- tag: s.tag,
9191
- subscribedAt: s.subscribedAt,
9192
- maxFps: s.maxFps,
9193
- framesDelivered: s.framesDelivered
9194
- }));
9195
- }
9196
- /**
9197
- * Force-release every subscription whose tag matches `tag`. Returns the
9198
- * number released. Decoder sessions wind down when their last subscriber
9199
- * leaves, same as a normal `unsubscribe`.
9200
- */
9201
- async killByTag(tag) {
9202
- const victims = [...this.subscriptions.values()].filter((s) => s.tag === tag).map((s) => s.id);
9203
- for (const id of victims) await this.unsubscribe(id, true);
9204
- return victims.length;
9205
- }
9206
- /** Tear down every subscription + decoder session. Idempotent. */
9207
- async dispose() {
9208
- if (this.disposed) return;
9209
- this.disposed = true;
9210
- this.subscriptions.clear();
9211
- const sessions = [...this.sessions.values()];
9212
- this.sessions.clear();
9213
- await Promise.all(sessions.map((s) => this.destroySession(s)));
9214
- }
9215
- /**
9216
- * Ensure a `frameSink: 'shm'` decoder session for `format` exists and add
9217
- * `subscriptionId` to its reader set. Reuses the session when one already
9218
- * runs for that format.
9219
- *
9220
- * When a session is created fresh, its reader set is seeded with EVERY
9221
- * subscription currently reading `format`, not just `subscriptionId` — a
9222
- * deferred-arm pass (`armPendingSessions`) may create the session long
9223
- * after several subscriptions of that format have registered, and the
9224
- * session is reference-counted by `subscriberIds`. Seeding only the one id
9225
- * would let an `unsubscribe` of that id tear the session down while its
9226
- * sibling subscriptions still read it.
9227
- */
9228
- async ensureSession(format, subscriptionId) {
9229
- const existing = this.sessions.get(format);
9230
- if (existing) {
9231
- this.clearLinger(existing);
9232
- existing.subscriberIds.add(subscriptionId);
9233
- return;
9234
- }
9235
- const inflight = this.sessionCreating.get(format);
9236
- if (inflight) {
9237
- await inflight;
9238
- this.sessions.get(format)?.subscriberIds.add(subscriptionId);
9239
- return;
9240
- }
9241
- const creation = (async () => {
9242
- const subscriberIds = new Set([subscriptionId]);
9243
- for (const subscription of this.subscriptions.values()) if (subscription.format === format) subscriberIds.add(subscription.id);
9244
- const session = {
9245
- format,
9246
- proxy: null,
9247
- nodeId: null,
9248
- subscriberIds,
9249
- lingerTimer: null
9250
- };
9251
- if (await this.startSessionDecoder(session)) this.sessions.set(format, session);
9252
- })();
9253
- this.sessionCreating.set(format, creation);
9254
- try {
9255
- await creation;
9256
- } finally {
9257
- this.sessionCreating.delete(format);
9258
- }
9259
- }
9260
- /**
9261
- * Create the `frameSink: 'shm'` decoder for a `FormatSession` and start
9262
- * draining its handle queue. Mutates `session.proxy` / `session.nodeId` in
9263
- * place so a `rotate()` can rebuild a registered session without disturbing
9264
- * its `subscriberIds`. Returns `false` when no stream / unsupported codec
9265
- * means no decoder could be created.
9266
- */
9267
- async startSessionDecoder(session) {
9268
- const { format } = session;
9269
- const info = this.resolveStreamInfo();
9270
- if (!info) {
9271
- this.logger?.info("frame-handle plane: no stream — session deferred", { meta: { format } });
9272
- return false;
9273
- }
9274
- if (!await this.decoderApi.supportsCodec({ codec: info.codec })) {
9275
- this.logger?.warn("frame-handle plane: codec unsupported — session skipped", { meta: {
9276
- format,
9277
- codec: info.codec
9278
- } });
9279
- return false;
9280
- }
9281
- const { sessionId, nodeId } = await this.decoderApi.createSession({
9282
- codec: info.codec,
9283
- maxFps: 0,
9284
- outputFormat: format,
9285
- scale: 1,
9286
- frameSink: "shm",
9287
- ...info.numericDeviceId !== void 0 ? { deviceId: info.numericDeviceId } : {},
9288
- tag: `${info.tag}:shm:${format}`
9289
- });
9290
- if (!isDecoderNodeColocated(this.localNodeId, nodeId)) {
9291
- this.logger?.warn("frame-handle plane: decoder session landed on a non-local node — destroying (shm ring is node-local)", { meta: {
9292
- format,
9293
- requestedNode: this.localNodeId,
9294
- gotNode: nodeId,
9295
- sessionId
9296
- } });
9297
- await this.decoderApi.destroySession({ sessionId }).catch((err) => {
9298
- this.logger?.warn("frame-handle plane: failed to destroy mis-placed decoder session", { meta: {
9299
- format,
9300
- sessionId,
9301
- error: require_dist.errMsg(err)
9302
- } });
9303
- });
9304
- return false;
9305
- }
9306
- const proxy = new DecoderSessionProxy(this.decoderApi, sessionId);
9307
- session.proxy = proxy;
9308
- session.nodeId = nodeId;
9309
- proxy.startHandlePolling((handle) => {
9310
- this.fanoutHandle(format, handle);
9311
- }).catch((err) => {
9312
- this.logger?.warn("frame-handle plane: handle polling error", { meta: {
9313
- format,
9314
- error: require_dist.errMsg(err)
9315
- } });
9316
- });
9317
- if (info.pullModeUrl) proxy.openStream(info.pullModeUrl).catch((err) => {
9318
- this.logger?.error("frame-handle plane: pull-mode openStream failed", { meta: {
9319
- format,
9320
- error: require_dist.errMsg(err)
9321
- } });
9322
- });
9323
- this.logger?.info("frame-handle plane: shm decoder session created", { meta: {
9324
- format,
9325
- codec: info.codec,
9326
- sessionId,
9327
- nodeId
9328
- } });
9329
- return true;
9330
- }
9331
- /** Push one decoded `FrameHandle` to every subscription of `format`. */
9332
- fanoutHandle(format, handle) {
9333
- this.rollDecodeFpsWindow();
9334
- this.framesInWindow += 1;
9335
- for (const subscription of this.subscriptions.values()) {
9336
- if (subscription.format !== format) continue;
9337
- subscription.queue.push(handle);
9338
- }
9339
- }
9340
- /** Destroy a decoder session, swallowing teardown errors. */
9341
- async destroySession(session) {
9342
- this.clearLinger(session);
9343
- const proxy = session.proxy;
9344
- session.proxy = null;
9345
- session.nodeId = null;
9346
- if (!proxy) return;
9347
- try {
9348
- await proxy.destroy();
9349
- } catch (err) {
9350
- this.logger?.warn("frame-handle plane: session destroy failed", { meta: {
9351
- format: session.format,
9352
- error: require_dist.errMsg(err)
9353
- } });
9354
- }
9355
- }
9356
- };
9357
- //#endregion
9358
8754
  //#region src/stream-broker/stream-broker/g711-mulaw.ts
9359
8755
  /**
9360
8756
  * G.711 µ-law encoder — converts 16-bit signed PCM samples to µ-law (ITU-T G.711).
@@ -10602,7 +9998,7 @@ var StreamBroker = class StreamBroker {
10602
9998
  this.logger?.warn("subscribeFrameHandles: no decoder API — frame handles unavailable");
10603
9999
  return null;
10604
10000
  }
10605
- this.frameHandlePlane = new FrameHandlePlane({
10001
+ this.frameHandlePlane = new require_frame_handle_plane.FrameHandlePlane({
10606
10002
  decoderApi: this.decoderApi,
10607
10003
  logger: this.logger?.child("frame-handle-plane"),
10608
10004
  resolveStreamInfo: () => this.resolveFrameHandleStreamInfo(),
@@ -19508,7 +18904,7 @@ function fromBase64Url(s) {
19508
18904
  //#region src/stream-broker/index.ts
19509
18905
  var stream_broker_default = StreamBrokerAddon;
19510
18906
  //#endregion
19511
- exports.DecoderSessionProxy = DecoderSessionProxy;
18907
+ exports.DecoderSessionProxy = require_frame_handle_plane.DecoderSessionProxy;
19512
18908
  exports.FfmpegDecoderProvider = FfmpegDecoderProvider;
19513
18909
  exports.InProcessTransport = InProcessTransport;
19514
18910
  exports.LengthPrefixCodec = LengthPrefixCodec;