@camstack/addon-pipeline 1.1.43 → 1.1.45

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 (36) hide show
  1. package/dist/audio-analyzer/index.js +5 -4
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/chunk-D6vf50IK.js +28 -0
  4. package/dist/detection-pipeline/index.js +2714 -2028
  5. package/dist/detection-pipeline/index.mjs +2708 -2023
  6. package/dist/{dist-NvwN60Fq.js → dist-CjxqjgEv.js} +255 -185
  7. package/dist/{dist-GFd8M6KO.mjs → dist-D04numuO.mjs} +250 -186
  8. package/dist/{model-download-service-C-IHWnXx-DxM2DSns.js → model-download-service-C-IHWnXx-BLXzxpRB.js} +3 -29
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +122 -287
  12. package/dist/pipeline-runner/index.mjs +118 -285
  13. package/dist/recorder/index.js +60 -20
  14. package/dist/recorder/index.mjs +57 -18
  15. package/dist/{frame-handle-plane-B3Fxww8H.js → remote-source-plane-CLuxyDV2.js} +236 -1
  16. package/dist/{frame-handle-plane-E_AsR77T.mjs → remote-source-plane-D31IILFN.mjs} +219 -2
  17. package/dist/session-decode/decode-worker-child.js +86 -2
  18. package/dist/session-decode/decode-worker-child.mjs +83 -2
  19. package/dist/stream-broker/_stub.js +550 -523
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CeielP5P.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BGKueo9D.mjs} +2 -2
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-D0sKyXiy.mjs +26 -0
  22. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-8BUVCFIR.mjs +26 -0
  23. package/dist/stream-broker/{hostInit-DmjqskOR.mjs → hostInit-DazV2RCq.mjs} +2 -2
  24. package/dist/stream-broker/index.js +746 -135
  25. package/dist/stream-broker/index.mjs +737 -127
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CEsPjwzT.js → MaskShapeCanvas-DI4BY7W2-D_F4a_3H.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-D43fIRgB.js → MotionZonesSettings-NcxxQN8r-CfTBrn4G.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C3wJxG8V.js → PrivacyMaskSettings-APgPLF7p-BWYkeoRj.js} +1 -1
  30. package/embed-dist/assets/{index-CSFtK41z.css → index-CA4kv43T.css} +1 -1
  31. package/embed-dist/assets/index-_ccEdNW7.js +81 -0
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +2 -2
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.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-dGka8yZY.mjs +0 -26
  36. package/embed-dist/assets/index-CX-rILhw.js +0 -81
@@ -1,4 +1,4 @@
1
- import { A as recordingCapability, B as DeviceType, I as errMsg, J as selectAssignedProfileSlots, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, at as string, it as record, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-GFd8M6KO.mjs";
1
+ import { A as recordingCapability, B as DeviceType, D as nodePin, I as errMsg, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, Y as selectAssignedProfileSlots, at as record, ot as string, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-D04numuO.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
4
  import { spawn } from "node:child_process";
@@ -223,7 +223,10 @@ var SegmentStore = class SegmentStore {
223
223
  try {
224
224
  bytes = await this.deps.statBytes(input.flatAbsPath);
225
225
  } catch (err) {
226
- this.deps.logger.warn("segment stat failed", err);
226
+ this.deps.logger.warn("segment stat failed", {
227
+ tags: { deviceId: input.deviceId },
228
+ error: err
229
+ });
227
230
  return null;
228
231
  }
229
232
  if (bytes <= 0) return null;
@@ -232,7 +235,10 @@ var SegmentStore = class SegmentStore {
232
235
  try {
233
236
  await this.deps.moveFile(input.flatAbsPath, toAbs);
234
237
  } catch (err) {
235
- this.deps.logger.warn("segment relocate failed", err);
238
+ this.deps.logger.warn("segment relocate failed", {
239
+ tags: { deviceId: input.deviceId },
240
+ error: err
241
+ });
236
242
  return null;
237
243
  }
238
244
  const row = {
@@ -282,7 +288,10 @@ var SegmentStore = class SegmentStore {
282
288
  }
283
289
  continue;
284
290
  } catch (err) {
285
- this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", err);
291
+ this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", {
292
+ tags: { deviceId: bucketRows[0]?.deviceId },
293
+ error: err
294
+ });
286
295
  }
287
296
  for (const r of bucketRows) try {
288
297
  await this.deps.deletePath({
@@ -292,7 +301,10 @@ var SegmentStore = class SegmentStore {
292
301
  removed.push(r.path);
293
302
  reclaimed += r.bytes;
294
303
  } catch (err) {
295
- this.deps.logger.warn("segment delete failed", err);
304
+ this.deps.logger.warn("segment delete failed", {
305
+ tags: { deviceId: r.deviceId },
306
+ error: err
307
+ });
296
308
  }
297
309
  }
298
310
  if (removed.length > 0) this.deps.index.removeSegments(removed);
@@ -1862,7 +1874,7 @@ var RecordingController = class {
1862
1874
  * assigned sources is ignored (record every assigned source — minimum of 1).
1863
1875
  */
1864
1876
  async resolveProfiles(deviceId, override) {
1865
- const assigned = selectAssignedProfileSlots(await this.deps.brokerCall(() => this.deps.api.streamBroker.listAllProfileSlots.query()), deviceId).map((slot) => slot.profile);
1877
+ const assigned = selectAssignedProfileSlots(await this.deps.brokerCall(() => this.deps.api.streamBroker.listAllProfileSlots.query(void 0, nodePin(this.deps.ownerNodeId))), deviceId).map((slot) => slot.profile);
1866
1878
  if (!override || override.length === 0) return assigned;
1867
1879
  const selected = assigned.filter((p) => override.includes(p));
1868
1880
  return selected.length > 0 ? selected : assigned;
@@ -1882,7 +1894,7 @@ var RecordingController = class {
1882
1894
  profile,
1883
1895
  ...this.deps.consumerHostname !== void 0 ? { hostname: this.deps.consumerHostname } : {},
1884
1896
  tag: `recorder:${deviceId}/${profile}`
1885
- }));
1897
+ }, nodePin(this.deps.ownerNodeId)));
1886
1898
  const outDir = path.join(placement.root, STAGING_DIR_NAME, String(deviceId), profile);
1887
1899
  await promises.mkdir(outDir, { recursive: true });
1888
1900
  const deviceLog = this.deps.logger.withTags({ deviceId });
@@ -1968,7 +1980,7 @@ var RecordingController = class {
1968
1980
  r.writer.stop();
1969
1981
  } catch {}
1970
1982
  try {
1971
- await this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey });
1983
+ await this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey }, nodePin(this.deps.ownerNodeId));
1972
1984
  } catch (err) {
1973
1985
  this.deps.logger.warn("recorder: releaseStreamWithCodec failed", {
1974
1986
  tags: { deviceId },
@@ -2079,9 +2091,13 @@ var MARKER_PAD_POST_MS = EVENT_PAD_MS.postMs;
2079
2091
  * form the hub reverse-proxy client path `/addon/recorder/playback`. */
2080
2092
  var RECORDER_ADDON_ID = "recorder";
2081
2093
  var PLAYBACK_PREFIX = "playback";
2082
- /** Node the hub-only `stream-broker` runs on. Every recorder on the hub or on
2083
- * a designated agent gates on, and pulls from, THIS node's broker. */
2084
- var BROKER_HOST_NODE_ID = "hub";
2094
+ /** Narrows the `unknown` extras `SegmentStore`'s logger contract accepts down
2095
+ * to the structured `{ tags, error }` shape the device-scoped call sites
2096
+ * (stat/relocate failures) pass — as opposed to the raw caught error most
2097
+ * other call sites still pass directly. */
2098
+ function isSegmentStoreLogExtras(extras) {
2099
+ return typeof extras === "object" && extras !== null && ("tags" in extras || "error" in extras);
2100
+ }
2085
2101
  var RecorderV2Addon = class extends BaseAddon {
2086
2102
  nodeId = "hub";
2087
2103
  /** True when THIS node is the designated recording node (`recordingNodeId`).
@@ -2116,14 +2132,15 @@ var RecorderV2Addon = class extends BaseAddon {
2116
2132
  } });
2117
2133
  return { providers: [] };
2118
2134
  }
2119
- const consumerHostname = this.nodeId === BROKER_HOST_NODE_ID ? void 0 : resolveRecordingHubHostname(this.config.recordingHubHostname, process.env["CAMSTACK_HUB_URL"]);
2120
- if (this.nodeId !== BROKER_HOST_NODE_ID) {
2135
+ const ingestOwner = await this.resolveIngestOwner();
2136
+ const consumerHostname = this.nodeId === ingestOwner.ownerNodeId ? void 0 : ingestOwner.reachableHost ?? resolveRecordingHubHostname(this.config.recordingHubHostname, process.env["CAMSTACK_HUB_URL"]);
2137
+ if (this.nodeId !== ingestOwner.ownerNodeId) {
2121
2138
  this.ctx.logger.info("recorder is a REMOTE recording node — pulling broker streams cross-node", { meta: {
2122
2139
  ownNodeId: this.nodeId,
2123
- brokerHost: BROKER_HOST_NODE_ID,
2140
+ brokerHost: ingestOwner.ownerNodeId,
2124
2141
  consumerHostname: consumerHostname ?? "(unresolved)"
2125
2142
  } });
2126
- if (consumerHostname === void 0) this.ctx.logger.warn("recorder: could not resolve the hub host for cross-node pull — set the `recordingHubHostname` global setting or CAMSTACK_HUB_URL; recording will not reach this node", { meta: { ownNodeId: this.nodeId } });
2143
+ if (consumerHostname === void 0) this.ctx.logger.warn("recorder: could not resolve the ingest owner host for cross-node pull — set the `recordingHubHostname` global setting or CAMSTACK_HUB_URL; recording will not reach this node", { meta: { ownNodeId: this.nodeId } });
2127
2144
  }
2128
2145
  const segmentStoreDeps = {
2129
2146
  index: this.index,
@@ -2148,14 +2165,21 @@ var RecorderV2Addon = class extends BaseAddon {
2148
2165
  force: true
2149
2166
  });
2150
2167
  },
2151
- logger: { warn: (message, extras) => this.ctx.logger.warn(message, { meta: { extras } }) }
2168
+ logger: { warn: (message, extras) => {
2169
+ if (isSegmentStoreLogExtras(extras)) this.ctx.logger.warn(message, {
2170
+ tags: extras.tags,
2171
+ meta: { error: extras.error }
2172
+ });
2173
+ else this.ctx.logger.warn(message, { meta: { extras } });
2174
+ } }
2152
2175
  };
2153
2176
  this.segmentStore = new SegmentStore(segmentStoreDeps);
2154
- const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(BROKER_HOST_NODE_ID));
2177
+ const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(ingestOwner.ownerNodeId));
2155
2178
  this.controller = new RecordingController({
2156
2179
  api: this.ctx.api,
2157
2180
  logger: this.ctx.logger,
2158
2181
  nodeId: this.nodeId,
2182
+ ownerNodeId: ingestOwner.ownerNodeId,
2159
2183
  segmentStore: this.segmentStore,
2160
2184
  loadConfig: (deviceId) => loadDeviceConfig(this.configStore(), deviceId),
2161
2185
  enabledDeviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
@@ -2164,7 +2188,7 @@ var RecorderV2Addon = class extends BaseAddon {
2164
2188
  segmentSeconds: this.config.segmentSeconds,
2165
2189
  watchIntervalMs: this.config.watchIntervalMs,
2166
2190
  ...consumerHostname !== void 0 ? { consumerHostname } : {},
2167
- onBrokerReady: (handler) => this.ctx.onCapabilityStateChange("stream-broker", brokerScope(BROKER_HOST_NODE_ID), (state) => {
2191
+ onBrokerReady: (handler) => this.ctx.onCapabilityStateChange("stream-broker", brokerScope(ingestOwner.ownerNodeId), (state) => {
2168
2192
  if (state === "ready") handler();
2169
2193
  }),
2170
2194
  brokerCall: makeBrokerCall(brokerHandle)
@@ -2290,6 +2314,21 @@ var RecorderV2Addon = class extends BaseAddon {
2290
2314
  return this.resolvedLocations;
2291
2315
  }
2292
2316
  /**
2317
+ * Resolve the cluster's camera-source owner. Graceful degrade: the
2318
+ * `pipeline-orchestrator` cap may not be mounted yet (pre-image-release),
2319
+ * in which case we fall back to `{ ownerNodeId: 'hub' }` — the same default
2320
+ * `getIngestOwner` itself returns absent an explicit `clusterRoles.ingestNode`,
2321
+ * so an unresolved owner is byte-identical to today's hub-only behaviour.
2322
+ */
2323
+ async resolveIngestOwner() {
2324
+ try {
2325
+ return await this.ctx.api.pipelineOrchestrator.getIngestOwner.query();
2326
+ } catch (err) {
2327
+ this.ctx.logger.warn("recorder: getIngestOwner unavailable — defaulting to hub (cap not mounted yet?)", { meta: { error: errMsg(err) } });
2328
+ return { ownerNodeId: "hub" };
2329
+ }
2330
+ }
2331
+ /**
2293
2332
  * Roots the playback data-plane handler searches, IN ORDER: `<dataDir>/playback`
2294
2333
  * FIRST (the freshly-written master/variant `.m3u8` playlists), then every
2295
2334
  * recordings location root (the segment `.m4s` files at
@@ -1,4 +1,4 @@
1
- const require_dist = require("./dist-NvwN60Fq.js");
1
+ const require_dist = require("./dist-CjxqjgEv.js");
2
2
  let node_crypto = require("node:crypto");
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**
@@ -781,6 +781,223 @@ var FrameHandlePlane = class {
781
781
  }
782
782
  };
783
783
  //#endregion
784
+ //#region src/pipeline-runner/remote-source-plane.ts
785
+ /**
786
+ * Runner-side mode selection (pure): whether an attach payload routes this
787
+ * camera through the remote-source leg. Absent `frameSource` (a pre-P2c
788
+ * payload) and `local-broker` both take today's co-located broker path —
789
+ * the rollout-OFF safety invariant.
790
+ */
791
+ function isRemoteRestream(frameSource) {
792
+ return frameSource !== void 0 && frameSource.kind === "remote-restream";
793
+ }
794
+ /**
795
+ * Resolve WHICH broker profile a camStream id feeds for a device (pure).
796
+ *
797
+ * `getStreamWithCodec` targets a PROFILE (its assigned camStream), while the
798
+ * runner is dispatched with camStream ids (`motionStreamId` /
799
+ * `detectionStreamId`). The broker's profile slots carry the mapping
800
+ * (`sourceCamStreamId`); a stream no slot feeds returns `null` — the acquire
801
+ * fails fast and the poller's backoff retries (the assignment may still be
802
+ * propagating).
803
+ */
804
+ function profileForStreamId(slots, deviceId, streamId) {
805
+ for (const slot of slots) if (slot.deviceId === deviceId && slot.sourceCamStreamId === streamId) return slot.profile;
806
+ return null;
807
+ }
808
+ var RemoteSourcePlane = class {
809
+ api;
810
+ logger;
811
+ deviceId;
812
+ ownNodeId;
813
+ ownerNodeId;
814
+ hostname;
815
+ /** Single-flight entry creation per camStream id. */
816
+ entries = /* @__PURE__ */ new Map();
817
+ disposed = false;
818
+ constructor(options) {
819
+ this.api = options.api;
820
+ this.logger = options.logger.withTags({ deviceId: options.deviceId });
821
+ this.deviceId = options.deviceId;
822
+ this.ownNodeId = options.ownNodeId;
823
+ this.ownerNodeId = options.ownerNodeId;
824
+ this.hostname = options.hostname;
825
+ }
826
+ /**
827
+ * A {@link FrameHandleSource} for one camStream id, pluggable into
828
+ * `startFrameHandlePoller`. Motion (`gray`) and detection (`rgb`)
829
+ * subscriptions of the same stream share ONE acquired restream and ONE
830
+ * satellite plane (per-format decoder sessions inside it — hub parity).
831
+ */
832
+ makeSource(streamId) {
833
+ return {
834
+ subscribe: async (input) => {
835
+ if (this.disposed) throw new Error("RemoteSourcePlane: subscribe after dispose");
836
+ const entry = await this.ensureEntry(streamId);
837
+ const result = await entry.plane.subscribe({
838
+ format: input.format,
839
+ maxFps: input.maxFps,
840
+ tag: input.tag
841
+ });
842
+ entry.subscriptionIds.add(result.subscriptionId);
843
+ return result;
844
+ },
845
+ pullHandles: async (input) => {
846
+ const entry = await this.peekEntry(streamId);
847
+ if (!entry) return [];
848
+ return entry.plane.pullHandles(input.subscriptionId, input.maxCount);
849
+ },
850
+ unsubscribe: async (input) => {
851
+ const entry = await this.peekEntry(streamId);
852
+ if (!entry) return;
853
+ entry.subscriptionIds.delete(input.subscriptionId);
854
+ await entry.plane.unsubscribe(input.subscriptionId).catch(() => {});
855
+ if (entry.subscriptionIds.size === 0) {
856
+ this.entries.delete(streamId);
857
+ await this.teardownEntry(entry, streamId);
858
+ }
859
+ }
860
+ };
861
+ }
862
+ /** Dispose every entry: satellite planes destroyed, restreams released. Idempotent. */
863
+ async dispose() {
864
+ if (this.disposed) return;
865
+ this.disposed = true;
866
+ const pending = [...this.entries.entries()];
867
+ this.entries.clear();
868
+ for (const [streamId, entryPromise] of pending) try {
869
+ const entry = await entryPromise;
870
+ await this.teardownEntry(entry, streamId);
871
+ } catch {}
872
+ }
873
+ /** The live entry for a stream, or `null` (never creates one). */
874
+ async peekEntry(streamId) {
875
+ const pending = this.entries.get(streamId);
876
+ if (!pending) return null;
877
+ try {
878
+ return await pending;
879
+ } catch {
880
+ return null;
881
+ }
882
+ }
883
+ /** Single-flight: acquire the restream + build the satellite plane once per stream. */
884
+ ensureEntry(streamId) {
885
+ const existing = this.entries.get(streamId);
886
+ if (existing) return existing;
887
+ const creation = this.createEntry(streamId);
888
+ this.entries.set(streamId, creation);
889
+ creation.catch(() => {
890
+ if (this.entries.get(streamId) === creation) this.entries.delete(streamId);
891
+ });
892
+ return creation;
893
+ }
894
+ async createEntry(streamId) {
895
+ const { api, deviceId, ownNodeId, ownerNodeId } = this;
896
+ if (this.hostname === void 0) throw new Error("RemoteSourcePlane: owner hostname unresolvable — set frameSource.hubHostnameOverride or CAMSTACK_HUB_URL (a 127.0.0.1 restream URL would dial the wrong box)");
897
+ const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(void 0, require_dist.nodePin(ownerNodeId)), deviceId, streamId);
898
+ if (profile === null) throw new Error(`RemoteSourcePlane: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
899
+ const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
900
+ deviceId,
901
+ video: "copy",
902
+ audio: "none",
903
+ profile,
904
+ hostname: this.hostname,
905
+ tag: `runner:${ownNodeId}:remote-src:${deviceId}/${streamId}`
906
+ }, require_dist.nodePin(ownerNodeId));
907
+ const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
908
+ const plane = new FrameHandlePlane({
909
+ decoderApi: this.buildPinnedDecoderCapApi(),
910
+ logger: this.logger,
911
+ localNodeId: ownNodeId,
912
+ resolveStreamInfo: () => ({
913
+ codec,
914
+ numericDeviceId: deviceId,
915
+ tag: `runner-remote:${deviceId}/${streamId}`,
916
+ pullModeUrl: rtpSource.url
917
+ })
918
+ });
919
+ this.logger.info("remote-source plane: owner restream acquired", { meta: {
920
+ streamId,
921
+ profile,
922
+ codec,
923
+ pipelineKey: rtpSource.pipelineKey,
924
+ hostname: this.hostname
925
+ } });
926
+ return {
927
+ plane,
928
+ pipelineKey: rtpSource.pipelineKey,
929
+ subscriptionIds: /* @__PURE__ */ new Set(),
930
+ released: false
931
+ };
932
+ }
933
+ /** Destroy an entry's satellite plane and release its restream refcount once. */
934
+ async teardownEntry(entry, streamId) {
935
+ await entry.plane.dispose().catch((err) => {
936
+ this.logger.warn("remote-source plane: satellite plane dispose failed", { meta: {
937
+ streamId,
938
+ error: require_dist.errMsg(err)
939
+ } });
940
+ });
941
+ if (entry.released) return;
942
+ entry.released = true;
943
+ await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }, require_dist.nodePin(this.ownerNodeId)).catch((err) => {
944
+ this.logger.debug("remote-source plane: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
945
+ streamId,
946
+ pipelineKey: entry.pipelineKey,
947
+ error: require_dist.errMsg(err)
948
+ } });
949
+ });
950
+ }
951
+ /**
952
+ * Every decoder call pinned to THIS runner's node — mirror of the hub
953
+ * broker's wrapper (`stream-broker-manager.buildDecoderCapApi`): the
954
+ * `decoder` cap is a singleton whose UNPINNED calls resolve to the
955
+ * cluster-elected node, which is generally NOT this agent. Pinning create
956
+ * AND every session op to the same local node keeps the session + its shm
957
+ * ring here, where this plane's readers live.
958
+ */
959
+ buildPinnedDecoderCapApi() {
960
+ const api = this.api;
961
+ const pin = require_dist.nodePin(this.ownNodeId);
962
+ const toWireBytes = (bytes) => {
963
+ const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
964
+ copy.set(bytes);
965
+ return copy;
966
+ };
967
+ return {
968
+ supportsCodec: async (input) => api.decoder.supportsCodec.query({ ...input }, pin),
969
+ getInfo: async () => api.decoder.getInfo.query(void 0, pin),
970
+ createSession: async (config) => api.decoder.createSession.query({ ...config }, pin),
971
+ pushPacket: async (input) => {
972
+ await api.decoder.pushPacket.query({
973
+ sessionId: input.sessionId,
974
+ packet: {
975
+ ...input.packet,
976
+ data: toWireBytes(input.packet.data)
977
+ }
978
+ }, pin);
979
+ },
980
+ pullFrames: async (input) => {
981
+ return (await api.decoder.pullFrames.query({ ...input }, pin)).map((frame) => ({
982
+ ...frame,
983
+ data: Buffer.from(frame.data)
984
+ }));
985
+ },
986
+ pullHandles: async (input) => api.decoder.pullHandles.query({ ...input }, pin),
987
+ destroySession: async (input) => {
988
+ await api.decoder.destroySession.query({ ...input }, pin);
989
+ },
990
+ openStream: async (input) => {
991
+ await api.decoder.openStream.query({ ...input }, pin);
992
+ },
993
+ updateConfig: async (input) => {
994
+ await api.decoder.updateConfig.query({ ...input }, pin);
995
+ },
996
+ getStats: async (input) => api.decoder.getStats.query({ ...input }, pin)
997
+ };
998
+ }
999
+ };
1000
+ //#endregion
784
1001
  Object.defineProperty(exports, "DecoderSessionProxy", {
785
1002
  enumerable: true,
786
1003
  get: function() {
@@ -793,3 +1010,21 @@ Object.defineProperty(exports, "FrameHandlePlane", {
793
1010
  return FrameHandlePlane;
794
1011
  }
795
1012
  });
1013
+ Object.defineProperty(exports, "RemoteSourcePlane", {
1014
+ enumerable: true,
1015
+ get: function() {
1016
+ return RemoteSourcePlane;
1017
+ }
1018
+ });
1019
+ Object.defineProperty(exports, "isRemoteRestream", {
1020
+ enumerable: true,
1021
+ get: function() {
1022
+ return isRemoteRestream;
1023
+ }
1024
+ });
1025
+ Object.defineProperty(exports, "profileForStreamId", {
1026
+ enumerable: true,
1027
+ get: function() {
1028
+ return profileForStreamId;
1029
+ }
1030
+ });
@@ -1,4 +1,4 @@
1
- import { I as errMsg, d as RingBuffer } from "./dist-GFd8M6KO.mjs";
1
+ import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-D04numuO.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  //#region src/stream-broker/stream-broker/decoder-session-proxy.ts
4
4
  /**
@@ -781,4 +781,221 @@ var FrameHandlePlane = class {
781
781
  }
782
782
  };
783
783
  //#endregion
784
- export { DecoderSessionProxy as n, FrameHandlePlane as t };
784
+ //#region src/pipeline-runner/remote-source-plane.ts
785
+ /**
786
+ * Runner-side mode selection (pure): whether an attach payload routes this
787
+ * camera through the remote-source leg. Absent `frameSource` (a pre-P2c
788
+ * payload) and `local-broker` both take today's co-located broker path —
789
+ * the rollout-OFF safety invariant.
790
+ */
791
+ function isRemoteRestream(frameSource) {
792
+ return frameSource !== void 0 && frameSource.kind === "remote-restream";
793
+ }
794
+ /**
795
+ * Resolve WHICH broker profile a camStream id feeds for a device (pure).
796
+ *
797
+ * `getStreamWithCodec` targets a PROFILE (its assigned camStream), while the
798
+ * runner is dispatched with camStream ids (`motionStreamId` /
799
+ * `detectionStreamId`). The broker's profile slots carry the mapping
800
+ * (`sourceCamStreamId`); a stream no slot feeds returns `null` — the acquire
801
+ * fails fast and the poller's backoff retries (the assignment may still be
802
+ * propagating).
803
+ */
804
+ function profileForStreamId(slots, deviceId, streamId) {
805
+ for (const slot of slots) if (slot.deviceId === deviceId && slot.sourceCamStreamId === streamId) return slot.profile;
806
+ return null;
807
+ }
808
+ var RemoteSourcePlane = class {
809
+ api;
810
+ logger;
811
+ deviceId;
812
+ ownNodeId;
813
+ ownerNodeId;
814
+ hostname;
815
+ /** Single-flight entry creation per camStream id. */
816
+ entries = /* @__PURE__ */ new Map();
817
+ disposed = false;
818
+ constructor(options) {
819
+ this.api = options.api;
820
+ this.logger = options.logger.withTags({ deviceId: options.deviceId });
821
+ this.deviceId = options.deviceId;
822
+ this.ownNodeId = options.ownNodeId;
823
+ this.ownerNodeId = options.ownerNodeId;
824
+ this.hostname = options.hostname;
825
+ }
826
+ /**
827
+ * A {@link FrameHandleSource} for one camStream id, pluggable into
828
+ * `startFrameHandlePoller`. Motion (`gray`) and detection (`rgb`)
829
+ * subscriptions of the same stream share ONE acquired restream and ONE
830
+ * satellite plane (per-format decoder sessions inside it — hub parity).
831
+ */
832
+ makeSource(streamId) {
833
+ return {
834
+ subscribe: async (input) => {
835
+ if (this.disposed) throw new Error("RemoteSourcePlane: subscribe after dispose");
836
+ const entry = await this.ensureEntry(streamId);
837
+ const result = await entry.plane.subscribe({
838
+ format: input.format,
839
+ maxFps: input.maxFps,
840
+ tag: input.tag
841
+ });
842
+ entry.subscriptionIds.add(result.subscriptionId);
843
+ return result;
844
+ },
845
+ pullHandles: async (input) => {
846
+ const entry = await this.peekEntry(streamId);
847
+ if (!entry) return [];
848
+ return entry.plane.pullHandles(input.subscriptionId, input.maxCount);
849
+ },
850
+ unsubscribe: async (input) => {
851
+ const entry = await this.peekEntry(streamId);
852
+ if (!entry) return;
853
+ entry.subscriptionIds.delete(input.subscriptionId);
854
+ await entry.plane.unsubscribe(input.subscriptionId).catch(() => {});
855
+ if (entry.subscriptionIds.size === 0) {
856
+ this.entries.delete(streamId);
857
+ await this.teardownEntry(entry, streamId);
858
+ }
859
+ }
860
+ };
861
+ }
862
+ /** Dispose every entry: satellite planes destroyed, restreams released. Idempotent. */
863
+ async dispose() {
864
+ if (this.disposed) return;
865
+ this.disposed = true;
866
+ const pending = [...this.entries.entries()];
867
+ this.entries.clear();
868
+ for (const [streamId, entryPromise] of pending) try {
869
+ const entry = await entryPromise;
870
+ await this.teardownEntry(entry, streamId);
871
+ } catch {}
872
+ }
873
+ /** The live entry for a stream, or `null` (never creates one). */
874
+ async peekEntry(streamId) {
875
+ const pending = this.entries.get(streamId);
876
+ if (!pending) return null;
877
+ try {
878
+ return await pending;
879
+ } catch {
880
+ return null;
881
+ }
882
+ }
883
+ /** Single-flight: acquire the restream + build the satellite plane once per stream. */
884
+ ensureEntry(streamId) {
885
+ const existing = this.entries.get(streamId);
886
+ if (existing) return existing;
887
+ const creation = this.createEntry(streamId);
888
+ this.entries.set(streamId, creation);
889
+ creation.catch(() => {
890
+ if (this.entries.get(streamId) === creation) this.entries.delete(streamId);
891
+ });
892
+ return creation;
893
+ }
894
+ async createEntry(streamId) {
895
+ const { api, deviceId, ownNodeId, ownerNodeId } = this;
896
+ if (this.hostname === void 0) throw new Error("RemoteSourcePlane: owner hostname unresolvable — set frameSource.hubHostnameOverride or CAMSTACK_HUB_URL (a 127.0.0.1 restream URL would dial the wrong box)");
897
+ const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(void 0, nodePin(ownerNodeId)), deviceId, streamId);
898
+ if (profile === null) throw new Error(`RemoteSourcePlane: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
899
+ const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
900
+ deviceId,
901
+ video: "copy",
902
+ audio: "none",
903
+ profile,
904
+ hostname: this.hostname,
905
+ tag: `runner:${ownNodeId}:remote-src:${deviceId}/${streamId}`
906
+ }, nodePin(ownerNodeId));
907
+ const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
908
+ const plane = new FrameHandlePlane({
909
+ decoderApi: this.buildPinnedDecoderCapApi(),
910
+ logger: this.logger,
911
+ localNodeId: ownNodeId,
912
+ resolveStreamInfo: () => ({
913
+ codec,
914
+ numericDeviceId: deviceId,
915
+ tag: `runner-remote:${deviceId}/${streamId}`,
916
+ pullModeUrl: rtpSource.url
917
+ })
918
+ });
919
+ this.logger.info("remote-source plane: owner restream acquired", { meta: {
920
+ streamId,
921
+ profile,
922
+ codec,
923
+ pipelineKey: rtpSource.pipelineKey,
924
+ hostname: this.hostname
925
+ } });
926
+ return {
927
+ plane,
928
+ pipelineKey: rtpSource.pipelineKey,
929
+ subscriptionIds: /* @__PURE__ */ new Set(),
930
+ released: false
931
+ };
932
+ }
933
+ /** Destroy an entry's satellite plane and release its restream refcount once. */
934
+ async teardownEntry(entry, streamId) {
935
+ await entry.plane.dispose().catch((err) => {
936
+ this.logger.warn("remote-source plane: satellite plane dispose failed", { meta: {
937
+ streamId,
938
+ error: errMsg(err)
939
+ } });
940
+ });
941
+ if (entry.released) return;
942
+ entry.released = true;
943
+ await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }, nodePin(this.ownerNodeId)).catch((err) => {
944
+ this.logger.debug("remote-source plane: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
945
+ streamId,
946
+ pipelineKey: entry.pipelineKey,
947
+ error: errMsg(err)
948
+ } });
949
+ });
950
+ }
951
+ /**
952
+ * Every decoder call pinned to THIS runner's node — mirror of the hub
953
+ * broker's wrapper (`stream-broker-manager.buildDecoderCapApi`): the
954
+ * `decoder` cap is a singleton whose UNPINNED calls resolve to the
955
+ * cluster-elected node, which is generally NOT this agent. Pinning create
956
+ * AND every session op to the same local node keeps the session + its shm
957
+ * ring here, where this plane's readers live.
958
+ */
959
+ buildPinnedDecoderCapApi() {
960
+ const api = this.api;
961
+ const pin = nodePin(this.ownNodeId);
962
+ const toWireBytes = (bytes) => {
963
+ const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
964
+ copy.set(bytes);
965
+ return copy;
966
+ };
967
+ return {
968
+ supportsCodec: async (input) => api.decoder.supportsCodec.query({ ...input }, pin),
969
+ getInfo: async () => api.decoder.getInfo.query(void 0, pin),
970
+ createSession: async (config) => api.decoder.createSession.query({ ...config }, pin),
971
+ pushPacket: async (input) => {
972
+ await api.decoder.pushPacket.query({
973
+ sessionId: input.sessionId,
974
+ packet: {
975
+ ...input.packet,
976
+ data: toWireBytes(input.packet.data)
977
+ }
978
+ }, pin);
979
+ },
980
+ pullFrames: async (input) => {
981
+ return (await api.decoder.pullFrames.query({ ...input }, pin)).map((frame) => ({
982
+ ...frame,
983
+ data: Buffer.from(frame.data)
984
+ }));
985
+ },
986
+ pullHandles: async (input) => api.decoder.pullHandles.query({ ...input }, pin),
987
+ destroySession: async (input) => {
988
+ await api.decoder.destroySession.query({ ...input }, pin);
989
+ },
990
+ openStream: async (input) => {
991
+ await api.decoder.openStream.query({ ...input }, pin);
992
+ },
993
+ updateConfig: async (input) => {
994
+ await api.decoder.updateConfig.query({ ...input }, pin);
995
+ },
996
+ getStats: async (input) => api.decoder.getStats.query({ ...input }, pin)
997
+ };
998
+ }
999
+ };
1000
+ //#endregion
1001
+ export { DecoderSessionProxy as a, FrameHandlePlane as i, isRemoteRestream as n, profileForStreamId as r, RemoteSourcePlane as t };