@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
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
1
|
+
import { $ as boolean, D as nodePin, H as createEvent, I as errMsg, K as makeSourceBrokerId, L as BaseAddon, Q as array, V as EventCategory, Z as _enum, _ as customAction, it as object, k as pipelineRunnerCapability, ot as string, rt as number, tt as lazy, y as defineCustomActions } from "../dist-CgBGV988.mjs";
|
|
2
|
+
import { n as isRemoteRestream, r as profileForStreamId, t as RemoteSourcePlane } from "../remote-source-plane-B4hRwchu.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { t as isWorkerReply } from "../worker-protocol-pk7qdYXt.mjs";
|
|
5
5
|
import { FrameRingReaderCache } from "@camstack/shm-ring";
|
|
@@ -422,221 +422,6 @@ function startPolling(options, source, subscriptionId, resolvedMaxFps, lifecycle
|
|
|
422
422
|
};
|
|
423
423
|
}
|
|
424
424
|
//#endregion
|
|
425
|
-
//#region src/pipeline-runner/remote-source-plane.ts
|
|
426
|
-
/**
|
|
427
|
-
* Runner-side mode selection (pure): whether an attach payload routes this
|
|
428
|
-
* camera through the remote-source leg. Absent `frameSource` (a pre-P2c
|
|
429
|
-
* payload) and `local-broker` both take today's co-located broker path —
|
|
430
|
-
* the rollout-OFF safety invariant.
|
|
431
|
-
*/
|
|
432
|
-
function isRemoteRestream(frameSource) {
|
|
433
|
-
return frameSource !== void 0 && frameSource.kind === "remote-restream";
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Resolve WHICH broker profile a camStream id feeds for a device (pure).
|
|
437
|
-
*
|
|
438
|
-
* `getStreamWithCodec` targets a PROFILE (its assigned camStream), while the
|
|
439
|
-
* runner is dispatched with camStream ids (`motionStreamId` /
|
|
440
|
-
* `detectionStreamId`). The broker's profile slots carry the mapping
|
|
441
|
-
* (`sourceCamStreamId`); a stream no slot feeds returns `null` — the acquire
|
|
442
|
-
* fails fast and the poller's backoff retries (the assignment may still be
|
|
443
|
-
* propagating).
|
|
444
|
-
*/
|
|
445
|
-
function profileForStreamId(slots, deviceId, streamId) {
|
|
446
|
-
for (const slot of slots) if (slot.deviceId === deviceId && slot.sourceCamStreamId === streamId) return slot.profile;
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
var RemoteSourcePlane = class {
|
|
450
|
-
api;
|
|
451
|
-
logger;
|
|
452
|
-
deviceId;
|
|
453
|
-
ownNodeId;
|
|
454
|
-
hostname;
|
|
455
|
-
/** Single-flight entry creation per camStream id. */
|
|
456
|
-
entries = /* @__PURE__ */ new Map();
|
|
457
|
-
disposed = false;
|
|
458
|
-
constructor(options) {
|
|
459
|
-
this.api = options.api;
|
|
460
|
-
this.logger = options.logger.withTags({ deviceId: options.deviceId });
|
|
461
|
-
this.deviceId = options.deviceId;
|
|
462
|
-
this.ownNodeId = options.ownNodeId;
|
|
463
|
-
this.hostname = options.hostname;
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* A {@link FrameHandleSource} for one camStream id, pluggable into
|
|
467
|
-
* `startFrameHandlePoller`. Motion (`gray`) and detection (`rgb`)
|
|
468
|
-
* subscriptions of the same stream share ONE acquired restream and ONE
|
|
469
|
-
* satellite plane (per-format decoder sessions inside it — hub parity).
|
|
470
|
-
*/
|
|
471
|
-
makeSource(streamId) {
|
|
472
|
-
return {
|
|
473
|
-
subscribe: async (input) => {
|
|
474
|
-
if (this.disposed) throw new Error("RemoteSourcePlane: subscribe after dispose");
|
|
475
|
-
const entry = await this.ensureEntry(streamId);
|
|
476
|
-
const result = await entry.plane.subscribe({
|
|
477
|
-
format: input.format,
|
|
478
|
-
maxFps: input.maxFps,
|
|
479
|
-
tag: input.tag
|
|
480
|
-
});
|
|
481
|
-
entry.subscriptionIds.add(result.subscriptionId);
|
|
482
|
-
return result;
|
|
483
|
-
},
|
|
484
|
-
pullHandles: async (input) => {
|
|
485
|
-
const entry = await this.peekEntry(streamId);
|
|
486
|
-
if (!entry) return [];
|
|
487
|
-
return entry.plane.pullHandles(input.subscriptionId, input.maxCount);
|
|
488
|
-
},
|
|
489
|
-
unsubscribe: async (input) => {
|
|
490
|
-
const entry = await this.peekEntry(streamId);
|
|
491
|
-
if (!entry) return;
|
|
492
|
-
entry.subscriptionIds.delete(input.subscriptionId);
|
|
493
|
-
await entry.plane.unsubscribe(input.subscriptionId).catch(() => {});
|
|
494
|
-
if (entry.subscriptionIds.size === 0) {
|
|
495
|
-
this.entries.delete(streamId);
|
|
496
|
-
await this.teardownEntry(entry, streamId);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
/** Dispose every entry: satellite planes destroyed, restreams released. Idempotent. */
|
|
502
|
-
async dispose() {
|
|
503
|
-
if (this.disposed) return;
|
|
504
|
-
this.disposed = true;
|
|
505
|
-
const pending = [...this.entries.entries()];
|
|
506
|
-
this.entries.clear();
|
|
507
|
-
for (const [streamId, entryPromise] of pending) try {
|
|
508
|
-
const entry = await entryPromise;
|
|
509
|
-
await this.teardownEntry(entry, streamId);
|
|
510
|
-
} catch {}
|
|
511
|
-
}
|
|
512
|
-
/** The live entry for a stream, or `null` (never creates one). */
|
|
513
|
-
async peekEntry(streamId) {
|
|
514
|
-
const pending = this.entries.get(streamId);
|
|
515
|
-
if (!pending) return null;
|
|
516
|
-
try {
|
|
517
|
-
return await pending;
|
|
518
|
-
} catch {
|
|
519
|
-
return null;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
/** Single-flight: acquire the restream + build the satellite plane once per stream. */
|
|
523
|
-
ensureEntry(streamId) {
|
|
524
|
-
const existing = this.entries.get(streamId);
|
|
525
|
-
if (existing) return existing;
|
|
526
|
-
const creation = this.createEntry(streamId);
|
|
527
|
-
this.entries.set(streamId, creation);
|
|
528
|
-
creation.catch(() => {
|
|
529
|
-
if (this.entries.get(streamId) === creation) this.entries.delete(streamId);
|
|
530
|
-
});
|
|
531
|
-
return creation;
|
|
532
|
-
}
|
|
533
|
-
async createEntry(streamId) {
|
|
534
|
-
const { api, deviceId, ownNodeId } = this;
|
|
535
|
-
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)");
|
|
536
|
-
const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(), deviceId, streamId);
|
|
537
|
-
if (profile === null) throw new Error(`RemoteSourcePlane: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
|
|
538
|
-
const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
|
|
539
|
-
deviceId,
|
|
540
|
-
video: "copy",
|
|
541
|
-
audio: "none",
|
|
542
|
-
profile,
|
|
543
|
-
hostname: this.hostname,
|
|
544
|
-
tag: `runner:${ownNodeId}:remote-src:${deviceId}/${streamId}`
|
|
545
|
-
});
|
|
546
|
-
const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
|
|
547
|
-
const plane = new FrameHandlePlane({
|
|
548
|
-
decoderApi: this.buildPinnedDecoderCapApi(),
|
|
549
|
-
logger: this.logger,
|
|
550
|
-
localNodeId: ownNodeId,
|
|
551
|
-
resolveStreamInfo: () => ({
|
|
552
|
-
codec,
|
|
553
|
-
numericDeviceId: deviceId,
|
|
554
|
-
tag: `runner-remote:${deviceId}/${streamId}`,
|
|
555
|
-
pullModeUrl: rtpSource.url
|
|
556
|
-
})
|
|
557
|
-
});
|
|
558
|
-
this.logger.info("remote-source plane: owner restream acquired", { meta: {
|
|
559
|
-
streamId,
|
|
560
|
-
profile,
|
|
561
|
-
codec,
|
|
562
|
-
pipelineKey: rtpSource.pipelineKey,
|
|
563
|
-
hostname: this.hostname
|
|
564
|
-
} });
|
|
565
|
-
return {
|
|
566
|
-
plane,
|
|
567
|
-
pipelineKey: rtpSource.pipelineKey,
|
|
568
|
-
subscriptionIds: /* @__PURE__ */ new Set(),
|
|
569
|
-
released: false
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
/** Destroy an entry's satellite plane and release its restream refcount once. */
|
|
573
|
-
async teardownEntry(entry, streamId) {
|
|
574
|
-
await entry.plane.dispose().catch((err) => {
|
|
575
|
-
this.logger.warn("remote-source plane: satellite plane dispose failed", { meta: {
|
|
576
|
-
streamId,
|
|
577
|
-
error: errMsg(err)
|
|
578
|
-
} });
|
|
579
|
-
});
|
|
580
|
-
if (entry.released) return;
|
|
581
|
-
entry.released = true;
|
|
582
|
-
await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }).catch((err) => {
|
|
583
|
-
this.logger.debug("remote-source plane: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
|
|
584
|
-
streamId,
|
|
585
|
-
pipelineKey: entry.pipelineKey,
|
|
586
|
-
error: errMsg(err)
|
|
587
|
-
} });
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Every decoder call pinned to THIS runner's node — mirror of the hub
|
|
592
|
-
* broker's wrapper (`stream-broker-manager.buildDecoderCapApi`): the
|
|
593
|
-
* `decoder` cap is a singleton whose UNPINNED calls resolve to the
|
|
594
|
-
* cluster-elected node, which is generally NOT this agent. Pinning create
|
|
595
|
-
* AND every session op to the same local node keeps the session + its shm
|
|
596
|
-
* ring here, where this plane's readers live.
|
|
597
|
-
*/
|
|
598
|
-
buildPinnedDecoderCapApi() {
|
|
599
|
-
const api = this.api;
|
|
600
|
-
const pin = nodePin(this.ownNodeId);
|
|
601
|
-
const toWireBytes = (bytes) => {
|
|
602
|
-
const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
|
|
603
|
-
copy.set(bytes);
|
|
604
|
-
return copy;
|
|
605
|
-
};
|
|
606
|
-
return {
|
|
607
|
-
supportsCodec: async (input) => api.decoder.supportsCodec.query({ ...input }, pin),
|
|
608
|
-
getInfo: async () => api.decoder.getInfo.query(void 0, pin),
|
|
609
|
-
createSession: async (config) => api.decoder.createSession.query({ ...config }, pin),
|
|
610
|
-
pushPacket: async (input) => {
|
|
611
|
-
await api.decoder.pushPacket.query({
|
|
612
|
-
sessionId: input.sessionId,
|
|
613
|
-
packet: {
|
|
614
|
-
...input.packet,
|
|
615
|
-
data: toWireBytes(input.packet.data)
|
|
616
|
-
}
|
|
617
|
-
}, pin);
|
|
618
|
-
},
|
|
619
|
-
pullFrames: async (input) => {
|
|
620
|
-
return (await api.decoder.pullFrames.query({ ...input }, pin)).map((frame) => ({
|
|
621
|
-
...frame,
|
|
622
|
-
data: Buffer.from(frame.data)
|
|
623
|
-
}));
|
|
624
|
-
},
|
|
625
|
-
pullHandles: async (input) => api.decoder.pullHandles.query({ ...input }, pin),
|
|
626
|
-
destroySession: async (input) => {
|
|
627
|
-
await api.decoder.destroySession.query({ ...input }, pin);
|
|
628
|
-
},
|
|
629
|
-
openStream: async (input) => {
|
|
630
|
-
await api.decoder.openStream.query({ ...input }, pin);
|
|
631
|
-
},
|
|
632
|
-
updateConfig: async (input) => {
|
|
633
|
-
await api.decoder.updateConfig.query({ ...input }, pin);
|
|
634
|
-
},
|
|
635
|
-
getStats: async (input) => api.decoder.getStats.query({ ...input }, pin)
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
//#endregion
|
|
640
425
|
//#region src/pipeline-runner/frame-queue.ts
|
|
641
426
|
/**
|
|
642
427
|
* Latest-frame-only buffer. Keeps only the most recent item, dropping all
|
|
@@ -1924,21 +1709,25 @@ function startSessionDecodePump(deps) {
|
|
|
1924
1709
|
const decoded = await toDecodedFrame(frame, format);
|
|
1925
1710
|
deps.onDecodedFrame(decoded);
|
|
1926
1711
|
} catch (err) {
|
|
1927
|
-
deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", {
|
|
1928
|
-
deviceId: deps.source.deviceId,
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1712
|
+
deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", {
|
|
1713
|
+
tags: { deviceId: deps.source.deviceId },
|
|
1714
|
+
meta: {
|
|
1715
|
+
format,
|
|
1716
|
+
error: errMsg(err)
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1932
1719
|
} finally {
|
|
1933
1720
|
frame.image.close();
|
|
1934
1721
|
}
|
|
1935
1722
|
}
|
|
1936
1723
|
} catch (err) {
|
|
1937
|
-
deps.logger.warn("session-decode pump: frame loop ended with error", {
|
|
1938
|
-
deviceId: deps.source.deviceId,
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1724
|
+
deps.logger.warn("session-decode pump: frame loop ended with error", {
|
|
1725
|
+
tags: { deviceId: deps.source.deviceId },
|
|
1726
|
+
meta: {
|
|
1727
|
+
format,
|
|
1728
|
+
error: errMsg(err)
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1942
1731
|
} finally {
|
|
1943
1732
|
releaseSpawnSlotOnce();
|
|
1944
1733
|
}
|
|
@@ -2061,9 +1850,95 @@ function resolveMotionFrameSource(config, sharedWithDetection) {
|
|
|
2061
1850
|
function resolveSessionDecodeHostname(config, hubUrl) {
|
|
2062
1851
|
const frameSource = config.frameSource;
|
|
2063
1852
|
if (!isRemoteRestream(frameSource)) return void 0;
|
|
1853
|
+
return resolveOwnerRestreamHost(frameSource, hubUrl);
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* The camera-source OWNER node for a session-decode acquire, or `undefined`
|
|
1857
|
+
* when the source is node-local (co-located, pre-cross-node). Mirrors
|
|
1858
|
+
* `resolveSessionDecodeHostname`: only a `remote-restream` frame source names an
|
|
1859
|
+
* owner. Used to `nodePin` the broker calls at the actual owner.
|
|
1860
|
+
*/
|
|
1861
|
+
function resolveSessionDecodeOwnerNodeId(config) {
|
|
1862
|
+
const frameSource = config.frameSource;
|
|
1863
|
+
if (!isRemoteRestream(frameSource)) return void 0;
|
|
1864
|
+
return frameSource.ownerNodeId;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* The host a cross-node consumer dials for the owner's compressed restream.
|
|
1868
|
+
* Precedence: the orchestrator-resolved `ownerReachableHost` (the per-node
|
|
1869
|
+
* Cluster-UI override) → the operator `hubHostnameOverride` escape hatch →
|
|
1870
|
+
* the `CAMSTACK_HUB_URL`-derived host (the auto-detected Moleculer address).
|
|
1871
|
+
* `undefined` only when none resolve (the acquire then fails loudly rather
|
|
1872
|
+
* than silently dialing `127.0.0.1` on a remote node).
|
|
1873
|
+
*/
|
|
1874
|
+
function resolveOwnerRestreamHost(frameSource, hubUrl) {
|
|
1875
|
+
const reachable = frameSource.ownerReachableHost?.trim();
|
|
1876
|
+
if (reachable !== void 0 && reachable.length > 0) return reachable;
|
|
2064
1877
|
return resolveHubHostname(frameSource.hubHostnameOverride, hubUrl);
|
|
2065
1878
|
}
|
|
2066
1879
|
/**
|
|
1880
|
+
* Acquire the broker's COMPRESSED passthrough restream for one camStream and
|
|
1881
|
+
* wrap it as a {@link VideoFrameSource} the decode worker can dial. Mirrors
|
|
1882
|
+
* {@link RemoteSourcePlane}'s acquire: resolve the profile the camStream
|
|
1883
|
+
* feeds, then `getStreamWithCodec({ video: 'copy' })` (refcounted — rides the
|
|
1884
|
+
* broker's single source pull, never a second camera dial). No `hostname`:
|
|
1885
|
+
* the P1 flagged camera is HUB-LOCAL, so the returned `127.0.0.1` URL is
|
|
1886
|
+
* dialable from this hub-resident runner. Returns `null` (loudly logged) when
|
|
1887
|
+
* no profile is bound yet — the caller opens no subscription.
|
|
1888
|
+
*
|
|
1889
|
+
* Extracted as a standalone exported function (rather than a private method)
|
|
1890
|
+
* so the owner-node pin threading through the three `streamBroker` calls
|
|
1891
|
+
* (`listAllProfileSlots`, `getStreamWithCodec`, `releaseStreamWithCodec`) is
|
|
1892
|
+
* directly unit-testable without instantiating the addon class. The private
|
|
1893
|
+
* `acquireSessionDecodeRestream` method on {@link PipelineRunnerAddon} is a
|
|
1894
|
+
* thin delegator to this function — its two call sites are unchanged.
|
|
1895
|
+
*/
|
|
1896
|
+
async function acquireSessionDecodeRestreamFrom(api, logger, params) {
|
|
1897
|
+
const { deviceId, streamId, tag, hostname, ownerNodeId } = params;
|
|
1898
|
+
const pin = ownerNodeId !== void 0 ? nodePin(ownerNodeId) : void 0;
|
|
1899
|
+
const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(void 0, pin), deviceId, streamId);
|
|
1900
|
+
if (profile === null) {
|
|
1901
|
+
logger.error("session-decode: no broker profile feeds camStream — cannot acquire restream", {
|
|
1902
|
+
tags: { deviceId },
|
|
1903
|
+
meta: { streamId }
|
|
1904
|
+
});
|
|
1905
|
+
return null;
|
|
1906
|
+
}
|
|
1907
|
+
const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
|
|
1908
|
+
deviceId,
|
|
1909
|
+
video: "copy",
|
|
1910
|
+
audio: "none",
|
|
1911
|
+
profile,
|
|
1912
|
+
tag,
|
|
1913
|
+
...hostname !== void 0 ? { hostname } : {}
|
|
1914
|
+
}, pin);
|
|
1915
|
+
const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
|
|
1916
|
+
const source = {
|
|
1917
|
+
restreamUrl: rtpSource.url,
|
|
1918
|
+
deviceId,
|
|
1919
|
+
codec
|
|
1920
|
+
};
|
|
1921
|
+
const pipelineKey = rtpSource.pipelineKey;
|
|
1922
|
+
let released = false;
|
|
1923
|
+
const release = async () => {
|
|
1924
|
+
if (released) return;
|
|
1925
|
+
released = true;
|
|
1926
|
+
await api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey }, pin).catch((err) => {
|
|
1927
|
+
logger.debug("session-decode: releaseStreamWithCodec failed (benign on hub restart)", {
|
|
1928
|
+
tags: { deviceId },
|
|
1929
|
+
meta: {
|
|
1930
|
+
pipelineKey,
|
|
1931
|
+
error: errMsg(err)
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
});
|
|
1935
|
+
};
|
|
1936
|
+
return {
|
|
1937
|
+
source,
|
|
1938
|
+
release
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
2067
1942
|
* Interval for periodic metric snapshot emission. ~1 Hz strikes the
|
|
2068
1943
|
* balance between UI freshness (overlay phase / fps numbers feel live)
|
|
2069
1944
|
* and bus traffic (one snapshot per attached camera + one per node
|
|
@@ -2624,7 +2499,6 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2624
2499
|
if (config.motionSources.includes("analyzer")) attachment.motionUnsubscribe = await this.subscribeMotionFrames(config);
|
|
2625
2500
|
const stepsCount = config.steps?.length ?? 0;
|
|
2626
2501
|
const dispatch = stepsCount > 0 ? `runPipeline(${stepsCount}step${stepsCount === 1 ? "" : "s"})` : config.steps !== void 0 ? "skip(0steps)" : "runFrame(legacy)";
|
|
2627
|
-
const engineLabel = config.engine ? `${config.engine.runtime}+${config.engine.backend}/${config.engine.format}` : "default";
|
|
2628
2502
|
this.ctx.logger.info("attachCamera", {
|
|
2629
2503
|
tags: { deviceId: config.deviceId },
|
|
2630
2504
|
meta: {
|
|
@@ -2634,7 +2508,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2634
2508
|
detectionFps: config.detectionFps,
|
|
2635
2509
|
motionSources: config.motionSources,
|
|
2636
2510
|
dispatch,
|
|
2637
|
-
engine:
|
|
2511
|
+
engine: "node-local"
|
|
2638
2512
|
}
|
|
2639
2513
|
});
|
|
2640
2514
|
return { success: true };
|
|
@@ -2823,12 +2697,13 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2823
2697
|
if (existing) return existing;
|
|
2824
2698
|
const api = this.ctx.api;
|
|
2825
2699
|
if (!api) return null;
|
|
2826
|
-
const hostname =
|
|
2700
|
+
const hostname = resolveOwnerRestreamHost(frameSource, process.env["CAMSTACK_HUB_URL"]);
|
|
2827
2701
|
const plane = new RemoteSourcePlane({
|
|
2828
2702
|
api,
|
|
2829
2703
|
logger: this.ctx.logger,
|
|
2830
2704
|
deviceId: config.deviceId,
|
|
2831
2705
|
ownNodeId: this.nodeId,
|
|
2706
|
+
ownerNodeId: frameSource.ownerNodeId,
|
|
2832
2707
|
hostname
|
|
2833
2708
|
});
|
|
2834
2709
|
this.ctx.logger.info("remote-source leg armed for camera", {
|
|
@@ -2842,57 +2717,18 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2842
2717
|
return plane;
|
|
2843
2718
|
}
|
|
2844
2719
|
/**
|
|
2845
|
-
*
|
|
2846
|
-
*
|
|
2847
|
-
*
|
|
2848
|
-
* feeds, then `getStreamWithCodec({ video: 'copy' })` (refcounted — rides the
|
|
2849
|
-
* broker's single source pull, never a second camera dial). No `hostname`:
|
|
2850
|
-
* the P1 flagged camera is HUB-LOCAL, so the returned `127.0.0.1` URL is
|
|
2851
|
-
* dialable from this hub-resident runner. Returns `null` (loudly logged) when
|
|
2852
|
-
* no profile is bound yet — the caller opens no subscription.
|
|
2720
|
+
* Thin delegator to the module-level {@link acquireSessionDecodeRestreamFrom}
|
|
2721
|
+
* (see its doc for the acquire behavior) — supplies this addon's `ctx.logger`.
|
|
2722
|
+
* Kept as a method so the two call sites need no change.
|
|
2853
2723
|
*/
|
|
2854
|
-
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname) {
|
|
2855
|
-
|
|
2856
|
-
if (profile === null) {
|
|
2857
|
-
this.ctx.logger.error("session-decode: no broker profile feeds camStream — cannot acquire restream", {
|
|
2858
|
-
tags: { deviceId },
|
|
2859
|
-
meta: { streamId }
|
|
2860
|
-
});
|
|
2861
|
-
return null;
|
|
2862
|
-
}
|
|
2863
|
-
const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
|
|
2724
|
+
async acquireSessionDecodeRestream(api, deviceId, streamId, tag, hostname, ownerNodeId) {
|
|
2725
|
+
return acquireSessionDecodeRestreamFrom(api, this.ctx.logger, {
|
|
2864
2726
|
deviceId,
|
|
2865
|
-
|
|
2866
|
-
audio: "none",
|
|
2867
|
-
profile,
|
|
2727
|
+
streamId,
|
|
2868
2728
|
tag,
|
|
2869
|
-
|
|
2729
|
+
hostname,
|
|
2730
|
+
ownerNodeId
|
|
2870
2731
|
});
|
|
2871
|
-
const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
|
|
2872
|
-
const source = {
|
|
2873
|
-
restreamUrl: rtpSource.url,
|
|
2874
|
-
deviceId,
|
|
2875
|
-
codec
|
|
2876
|
-
};
|
|
2877
|
-
const pipelineKey = rtpSource.pipelineKey;
|
|
2878
|
-
let released = false;
|
|
2879
|
-
const release = async () => {
|
|
2880
|
-
if (released) return;
|
|
2881
|
-
released = true;
|
|
2882
|
-
await api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey }).catch((err) => {
|
|
2883
|
-
this.ctx.logger.debug("session-decode: releaseStreamWithCodec failed (benign on hub restart)", {
|
|
2884
|
-
tags: { deviceId },
|
|
2885
|
-
meta: {
|
|
2886
|
-
pipelineKey,
|
|
2887
|
-
error: errMsg(err)
|
|
2888
|
-
}
|
|
2889
|
-
});
|
|
2890
|
-
});
|
|
2891
|
-
};
|
|
2892
|
-
return {
|
|
2893
|
-
source,
|
|
2894
|
-
release
|
|
2895
|
-
};
|
|
2896
2732
|
}
|
|
2897
2733
|
/**
|
|
2898
2734
|
* Register this device's motion to ride the detection session-decode worker
|
|
@@ -2970,7 +2806,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2970
2806
|
if (!runner) return () => {};
|
|
2971
2807
|
return startGuardedSessionDecode({
|
|
2972
2808
|
logger: log,
|
|
2973
|
-
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"])),
|
|
2809
|
+
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.detectionStreamId, `session-decode:detect:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
2974
2810
|
startPump: (source) => {
|
|
2975
2811
|
log.info("session-decode: detection routed through per-session decode worker (rgb)", { meta: {
|
|
2976
2812
|
streamId: config.detectionStreamId,
|
|
@@ -3009,7 +2845,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3009
2845
|
if (!runner) return () => {};
|
|
3010
2846
|
return startGuardedSessionDecode({
|
|
3011
2847
|
logger: log,
|
|
3012
|
-
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.motionStreamId, `session-decode:motion:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"])),
|
|
2848
|
+
acquire: () => this.acquireSessionDecodeRestream(api, config.deviceId, config.motionStreamId, `session-decode:motion:${config.deviceId}`, resolveSessionDecodeHostname(config, process.env["CAMSTACK_HUB_URL"]), resolveSessionDecodeOwnerNodeId(config)),
|
|
3013
2849
|
startPump: (source) => {
|
|
3014
2850
|
log.info("session-decode: motion routed through per-session decode worker (gray)", { meta: {
|
|
3015
2851
|
streamId: config.motionStreamId,
|
|
@@ -3191,9 +3027,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3191
3027
|
log.error("runInference: this.ctx.api not available");
|
|
3192
3028
|
return null;
|
|
3193
3029
|
}
|
|
3194
|
-
const
|
|
3195
|
-
const steps = camConfig?.steps;
|
|
3196
|
-
const engine = camConfig?.engine;
|
|
3030
|
+
const steps = (this.attached.get(deviceId)?.config)?.steps;
|
|
3197
3031
|
if (!steps) {
|
|
3198
3032
|
log.warn("runInference: no steps in attach config — skipping frame (legacy attach?)");
|
|
3199
3033
|
return null;
|
|
@@ -3204,8 +3038,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3204
3038
|
return await api.pipelineExecutor.runPipeline.mutate({
|
|
3205
3039
|
steps: [...steps],
|
|
3206
3040
|
...useHandle ? { frameHandle: handle } : { frame },
|
|
3207
|
-
deviceId
|
|
3208
|
-
...engine ? { engine } : {}
|
|
3041
|
+
deviceId
|
|
3209
3042
|
});
|
|
3210
3043
|
} catch (err) {
|
|
3211
3044
|
const msg = errMsg(err);
|
|
@@ -3563,4 +3396,4 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
3563
3396
|
}
|
|
3564
3397
|
};
|
|
3565
3398
|
//#endregion
|
|
3566
|
-
export { DEFAULT_CONFIG, FrameQueue, PipelineRunner, PipelineTimingSampler, Semaphore, pipelineRunnerBenchActions as customActions, PipelineRunnerAddon as default, resolveMotionFrameSource, resolveSessionDecodeHostname, rgbFrameToGrayFrame, shouldStartOnboardAnalyzer };
|
|
3399
|
+
export { DEFAULT_CONFIG, FrameQueue, PipelineRunner, PipelineTimingSampler, Semaphore, acquireSessionDecodeRestreamFrom, pipelineRunnerBenchActions as customActions, PipelineRunnerAddon as default, resolveMotionFrameSource, resolveSessionDecodeHostname, resolveSessionDecodeOwnerNodeId, rgbFrameToGrayFrame, shouldStartOnboardAnalyzer };
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
const
|
|
2
|
-
const require_dist = require("../dist-
|
|
1
|
+
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
2
|
+
const require_dist = require("../dist-CySHUkXY.js");
|
|
3
3
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
4
|
+
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
|
|
4
5
|
let node_child_process = require("node:child_process");
|
|
5
6
|
let node_fs = require("node:fs");
|
|
6
7
|
let node_path = require("node:path");
|
|
7
|
-
node_path =
|
|
8
|
+
node_path = require_chunk.__toESM(node_path);
|
|
8
9
|
//#region src/recorder/segment-path.ts
|
|
9
10
|
function segmentRelPath(deviceId, profile, startMs, durMs, bytes) {
|
|
10
11
|
if (profile.includes("/")) throw new Error(`segmentRelPath: profile must not contain '/': ${profile}`);
|
|
@@ -224,7 +225,10 @@ var SegmentStore = class SegmentStore {
|
|
|
224
225
|
try {
|
|
225
226
|
bytes = await this.deps.statBytes(input.flatAbsPath);
|
|
226
227
|
} catch (err) {
|
|
227
|
-
this.deps.logger.warn("segment stat failed",
|
|
228
|
+
this.deps.logger.warn("segment stat failed", {
|
|
229
|
+
tags: { deviceId: input.deviceId },
|
|
230
|
+
error: err
|
|
231
|
+
});
|
|
228
232
|
return null;
|
|
229
233
|
}
|
|
230
234
|
if (bytes <= 0) return null;
|
|
@@ -233,7 +237,10 @@ var SegmentStore = class SegmentStore {
|
|
|
233
237
|
try {
|
|
234
238
|
await this.deps.moveFile(input.flatAbsPath, toAbs);
|
|
235
239
|
} catch (err) {
|
|
236
|
-
this.deps.logger.warn("segment relocate failed",
|
|
240
|
+
this.deps.logger.warn("segment relocate failed", {
|
|
241
|
+
tags: { deviceId: input.deviceId },
|
|
242
|
+
error: err
|
|
243
|
+
});
|
|
237
244
|
return null;
|
|
238
245
|
}
|
|
239
246
|
const row = {
|
|
@@ -283,7 +290,10 @@ var SegmentStore = class SegmentStore {
|
|
|
283
290
|
}
|
|
284
291
|
continue;
|
|
285
292
|
} catch (err) {
|
|
286
|
-
this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes",
|
|
293
|
+
this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", {
|
|
294
|
+
tags: { deviceId: bucketRows[0]?.deviceId },
|
|
295
|
+
error: err
|
|
296
|
+
});
|
|
287
297
|
}
|
|
288
298
|
for (const r of bucketRows) try {
|
|
289
299
|
await this.deps.deletePath({
|
|
@@ -293,7 +303,10 @@ var SegmentStore = class SegmentStore {
|
|
|
293
303
|
removed.push(r.path);
|
|
294
304
|
reclaimed += r.bytes;
|
|
295
305
|
} catch (err) {
|
|
296
|
-
this.deps.logger.warn("segment delete failed",
|
|
306
|
+
this.deps.logger.warn("segment delete failed", {
|
|
307
|
+
tags: { deviceId: r.deviceId },
|
|
308
|
+
error: err
|
|
309
|
+
});
|
|
297
310
|
}
|
|
298
311
|
}
|
|
299
312
|
if (removed.length > 0) this.deps.index.removeSegments(removed);
|
|
@@ -1863,7 +1876,7 @@ var RecordingController = class {
|
|
|
1863
1876
|
* assigned sources is ignored (record every assigned source — minimum of 1).
|
|
1864
1877
|
*/
|
|
1865
1878
|
async resolveProfiles(deviceId, override) {
|
|
1866
|
-
const assigned = require_dist.selectAssignedProfileSlots(await this.deps.brokerCall(() => this.deps.api.streamBroker.listAllProfileSlots.query()), deviceId).map((slot) => slot.profile);
|
|
1879
|
+
const assigned = require_dist.selectAssignedProfileSlots(await this.deps.brokerCall(() => this.deps.api.streamBroker.listAllProfileSlots.query(void 0, require_dist.nodePin(this.deps.ownerNodeId))), deviceId).map((slot) => slot.profile);
|
|
1867
1880
|
if (!override || override.length === 0) return assigned;
|
|
1868
1881
|
const selected = assigned.filter((p) => override.includes(p));
|
|
1869
1882
|
return selected.length > 0 ? selected : assigned;
|
|
@@ -1883,7 +1896,7 @@ var RecordingController = class {
|
|
|
1883
1896
|
profile,
|
|
1884
1897
|
...this.deps.consumerHostname !== void 0 ? { hostname: this.deps.consumerHostname } : {},
|
|
1885
1898
|
tag: `recorder:${deviceId}/${profile}`
|
|
1886
|
-
}));
|
|
1899
|
+
}, require_dist.nodePin(this.deps.ownerNodeId)));
|
|
1887
1900
|
const outDir = node_path.default.join(placement.root, STAGING_DIR_NAME, String(deviceId), profile);
|
|
1888
1901
|
await node_fs.promises.mkdir(outDir, { recursive: true });
|
|
1889
1902
|
const deviceLog = this.deps.logger.withTags({ deviceId });
|
|
@@ -1969,7 +1982,7 @@ var RecordingController = class {
|
|
|
1969
1982
|
r.writer.stop();
|
|
1970
1983
|
} catch {}
|
|
1971
1984
|
try {
|
|
1972
|
-
await this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey });
|
|
1985
|
+
await this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey }, require_dist.nodePin(this.deps.ownerNodeId));
|
|
1973
1986
|
} catch (err) {
|
|
1974
1987
|
this.deps.logger.warn("recorder: releaseStreamWithCodec failed", {
|
|
1975
1988
|
tags: { deviceId },
|
|
@@ -2080,9 +2093,13 @@ var MARKER_PAD_POST_MS = require_dist.EVENT_PAD_MS.postMs;
|
|
|
2080
2093
|
* form the hub reverse-proxy client path `/addon/recorder/playback`. */
|
|
2081
2094
|
var RECORDER_ADDON_ID = "recorder";
|
|
2082
2095
|
var PLAYBACK_PREFIX = "playback";
|
|
2083
|
-
/**
|
|
2084
|
-
*
|
|
2085
|
-
|
|
2096
|
+
/** Narrows the `unknown` extras `SegmentStore`'s logger contract accepts down
|
|
2097
|
+
* to the structured `{ tags, error }` shape the device-scoped call sites
|
|
2098
|
+
* (stat/relocate failures) pass — as opposed to the raw caught error most
|
|
2099
|
+
* other call sites still pass directly. */
|
|
2100
|
+
function isSegmentStoreLogExtras(extras) {
|
|
2101
|
+
return typeof extras === "object" && extras !== null && ("tags" in extras || "error" in extras);
|
|
2102
|
+
}
|
|
2086
2103
|
var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
2087
2104
|
nodeId = "hub";
|
|
2088
2105
|
/** True when THIS node is the designated recording node (`recordingNodeId`).
|
|
@@ -2117,14 +2134,15 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
2117
2134
|
} });
|
|
2118
2135
|
return { providers: [] };
|
|
2119
2136
|
}
|
|
2120
|
-
const
|
|
2121
|
-
|
|
2137
|
+
const ingestOwner = await this.resolveIngestOwner();
|
|
2138
|
+
const consumerHostname = this.nodeId === ingestOwner.ownerNodeId ? void 0 : ingestOwner.reachableHost ?? resolveRecordingHubHostname(this.config.recordingHubHostname, process.env["CAMSTACK_HUB_URL"]);
|
|
2139
|
+
if (this.nodeId !== ingestOwner.ownerNodeId) {
|
|
2122
2140
|
this.ctx.logger.info("recorder is a REMOTE recording node — pulling broker streams cross-node", { meta: {
|
|
2123
2141
|
ownNodeId: this.nodeId,
|
|
2124
|
-
brokerHost:
|
|
2142
|
+
brokerHost: ingestOwner.ownerNodeId,
|
|
2125
2143
|
consumerHostname: consumerHostname ?? "(unresolved)"
|
|
2126
2144
|
} });
|
|
2127
|
-
if (consumerHostname === void 0) this.ctx.logger.warn("recorder: could not resolve the
|
|
2145
|
+
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 } });
|
|
2128
2146
|
}
|
|
2129
2147
|
const segmentStoreDeps = {
|
|
2130
2148
|
index: this.index,
|
|
@@ -2149,14 +2167,21 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
2149
2167
|
force: true
|
|
2150
2168
|
});
|
|
2151
2169
|
},
|
|
2152
|
-
logger: { warn: (message, extras) =>
|
|
2170
|
+
logger: { warn: (message, extras) => {
|
|
2171
|
+
if (isSegmentStoreLogExtras(extras)) this.ctx.logger.warn(message, {
|
|
2172
|
+
tags: extras.tags,
|
|
2173
|
+
meta: { error: extras.error }
|
|
2174
|
+
});
|
|
2175
|
+
else this.ctx.logger.warn(message, { meta: { extras } });
|
|
2176
|
+
} }
|
|
2153
2177
|
};
|
|
2154
2178
|
this.segmentStore = new SegmentStore(segmentStoreDeps);
|
|
2155
|
-
const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(
|
|
2179
|
+
const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(ingestOwner.ownerNodeId));
|
|
2156
2180
|
this.controller = new RecordingController({
|
|
2157
2181
|
api: this.ctx.api,
|
|
2158
2182
|
logger: this.ctx.logger,
|
|
2159
2183
|
nodeId: this.nodeId,
|
|
2184
|
+
ownerNodeId: ingestOwner.ownerNodeId,
|
|
2160
2185
|
segmentStore: this.segmentStore,
|
|
2161
2186
|
loadConfig: (deviceId) => loadDeviceConfig(this.configStore(), deviceId),
|
|
2162
2187
|
enabledDeviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
|
|
@@ -2165,7 +2190,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
2165
2190
|
segmentSeconds: this.config.segmentSeconds,
|
|
2166
2191
|
watchIntervalMs: this.config.watchIntervalMs,
|
|
2167
2192
|
...consumerHostname !== void 0 ? { consumerHostname } : {},
|
|
2168
|
-
onBrokerReady: (handler) => this.ctx.onCapabilityStateChange("stream-broker", brokerScope(
|
|
2193
|
+
onBrokerReady: (handler) => this.ctx.onCapabilityStateChange("stream-broker", brokerScope(ingestOwner.ownerNodeId), (state) => {
|
|
2169
2194
|
if (state === "ready") handler();
|
|
2170
2195
|
}),
|
|
2171
2196
|
brokerCall: makeBrokerCall(brokerHandle)
|
|
@@ -2291,6 +2316,21 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
2291
2316
|
return this.resolvedLocations;
|
|
2292
2317
|
}
|
|
2293
2318
|
/**
|
|
2319
|
+
* Resolve the cluster's camera-source owner. Graceful degrade: the
|
|
2320
|
+
* `pipeline-orchestrator` cap may not be mounted yet (pre-image-release),
|
|
2321
|
+
* in which case we fall back to `{ ownerNodeId: 'hub' }` — the same default
|
|
2322
|
+
* `getIngestOwner` itself returns absent an explicit `clusterRoles.ingestNode`,
|
|
2323
|
+
* so an unresolved owner is byte-identical to today's hub-only behaviour.
|
|
2324
|
+
*/
|
|
2325
|
+
async resolveIngestOwner() {
|
|
2326
|
+
try {
|
|
2327
|
+
return await this.ctx.api.pipelineOrchestrator.getIngestOwner.query();
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
this.ctx.logger.warn("recorder: getIngestOwner unavailable — defaulting to hub (cap not mounted yet?)", { meta: { error: require_dist.errMsg(err) } });
|
|
2330
|
+
return { ownerNodeId: "hub" };
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2294
2334
|
* Roots the playback data-plane handler searches, IN ORDER: `<dataDir>/playback`
|
|
2295
2335
|
* FIRST (the freshly-written master/variant `.m3u8` playlists), then every
|
|
2296
2336
|
* recordings location root (the segment `.m4s` files at
|