@camstack/addon-pipeline 1.1.43 → 1.1.44
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.
- package/dist/audio-analyzer/index.js +5 -4
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/chunk-D6vf50IK.js +28 -0
- package/dist/detection-pipeline/index.js +526 -109
- package/dist/detection-pipeline/index.mjs +520 -104
- package/dist/{dist-GFd8M6KO.mjs → dist-CgBGV988.mjs} +214 -186
- package/dist/{dist-NvwN60Fq.js → dist-CySHUkXY.js} +219 -185
- package/dist/{model-download-service-C-IHWnXx-DxM2DSns.js → model-download-service-C-IHWnXx-BLXzxpRB.js} +3 -29
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +122 -287
- package/dist/pipeline-runner/index.mjs +118 -285
- package/dist/recorder/index.js +60 -20
- package/dist/recorder/index.mjs +57 -18
- package/dist/{frame-handle-plane-E_AsR77T.mjs → remote-source-plane-B4hRwchu.mjs} +219 -2
- package/dist/{frame-handle-plane-B3Fxww8H.js → remote-source-plane-D9m0z1N9.js} +236 -1
- package/dist/session-decode/decode-worker-child.js +86 -2
- package/dist/session-decode/decode-worker-child.mjs +83 -2
- package/dist/stream-broker/_stub.js +550 -523
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CeielP5P.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-G6wtUb3G.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dlagq9X_.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUpF966l.mjs +26 -0
- package/dist/stream-broker/{hostInit-DmjqskOR.mjs → hostInit-BSzP3BIj.mjs} +2 -2
- package/dist/stream-broker/index.js +746 -135
- package/dist/stream-broker/index.mjs +737 -127
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CEsPjwzT.js → MaskShapeCanvas-DI4BY7W2-D2ML5TPi.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-D43fIRgB.js → MotionZonesSettings-NcxxQN8r-TTWGGaqv.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C3wJxG8V.js → PrivacyMaskSettings-APgPLF7p-CJqHU2nU.js} +1 -1
- package/embed-dist/assets/index-C1oaL9s7.js +81 -0
- package/embed-dist/assets/{index-CSFtK41z.css → index-CA4kv43T.css} +1 -1
- package/embed-dist/index.html +2 -2
- package/package.json +2 -2
- 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
- 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
- package/embed-dist/assets/index-CX-rILhw.js +0 -81
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as recordingCapability, B as DeviceType,
|
|
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-CgBGV988.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",
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
-
/**
|
|
2083
|
-
*
|
|
2084
|
-
|
|
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
|
|
2120
|
-
|
|
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:
|
|
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
|
|
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) =>
|
|
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(
|
|
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(
|
|
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
|
-
import { I as errMsg, d as RingBuffer } from "./dist-
|
|
1
|
+
import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-CgBGV988.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
|
-
|
|
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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-CySHUkXY.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
|
+
});
|