@camstack/addon-pipeline 1.1.23 → 1.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/audio-codec-ffmpeg/index.js +1 -1
- package/dist/audio-codec-ffmpeg/index.mjs +1 -1
- package/dist/decoder-ffmpeg/index.js +84 -12
- package/dist/decoder-ffmpeg/index.mjs +84 -12
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-pd_-3C0T.mjs → dist-BecXbIzJ.mjs} +64 -2
- package/dist/{dist-CE3a05qT.js → dist-wzWBZ26C.js} +69 -1
- package/dist/frame-handle-plane-BAKiW6t4.js +647 -0
- package/dist/frame-handle-plane-DGg0Aevs.mjs +636 -0
- package/dist/hub-hostname-DAJXlOgV.js +54 -0
- package/dist/hub-hostname-cCknRYKj.mjs +49 -0
- package/dist/{model-download-service-C-IHWnXx-BnQ_awK4.js → model-download-service-C-IHWnXx-DxM2DSns.js} +1 -2
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +305 -10
- package/dist/pipeline-runner/index.mjs +305 -10
- package/dist/recorder/index.js +267 -65
- package/dist/recorder/index.mjs +267 -65
- package/dist/stream-broker/_stub.js +39 -39
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BVFAEkIB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CWPU9tbs.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-wfWYFiTT.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-lpFwIH9e.mjs +26 -0
- package/dist/stream-broker/{hostInit-DHej3Ep9.mjs → hostInit-B7Fx3R_6.mjs} +2 -2
- package/dist/stream-broker/index.js +7 -611
- package/dist/stream-broker/index.mjs +5 -609
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BqXU75it.js → MaskShapeCanvas-DI4BY7W2-CPkQfKnu.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Ct2fpgCd.js → MotionZonesSettings-NcxxQN8r-DlLMQBag.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-a9eFVPPl.js → PrivacyMaskSettings-APgPLF7p-kNcauIAN.js} +1 -1
- package/embed-dist/assets/index-DrJ0ee3f.css +2 -0
- package/embed-dist/assets/index-xpPLFfsT.js +80 -0
- package/embed-dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BFeBlYRQ.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BC1Jt6G8.mjs +0 -26
- package/embed-dist/assets/index-C7SSikPl.css +0 -2
- package/embed-dist/assets/index-DhdVxWXL.js +0 -80
- package/dist/{model-download-service-C-IHWnXx-3Mmeob3l.mjs → model-download-service-C-IHWnXx-BPy6aoAx.mjs} +1 -1
package/dist/recorder/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-
|
|
2
|
-
const require_dist = require("../dist-
|
|
1
|
+
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
|
|
2
|
+
const require_dist = require("../dist-wzWBZ26C.js");
|
|
3
|
+
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
3
4
|
let node_fs = require("node:fs");
|
|
4
5
|
let node_path = require("node:path");
|
|
5
6
|
node_path = require_model_download_service_C_IHWnXx.__toESM(node_path);
|
|
@@ -206,7 +207,7 @@ var RecordingIndex = class {
|
|
|
206
207
|
};
|
|
207
208
|
//#endregion
|
|
208
209
|
//#region src/recorder/segment-store.ts
|
|
209
|
-
var SegmentStore = class {
|
|
210
|
+
var SegmentStore = class SegmentStore {
|
|
210
211
|
deps;
|
|
211
212
|
constructor(deps) {
|
|
212
213
|
this.deps = deps;
|
|
@@ -253,23 +254,68 @@ var SegmentStore = class {
|
|
|
253
254
|
* `location` (`row.locationId === location`) — the caller guarantees this (e.g. the evictable
|
|
254
255
|
* provider feeds rows from `segmentsOnLocation(location)`). Mixed-location rows would be deleted
|
|
255
256
|
* against the wrong location.
|
|
257
|
+
*
|
|
258
|
+
* Bucket-granular fast path (R4): rows are grouped by their hour-bucket dir
|
|
259
|
+
* (`<dev>/<profile>/Y/M/D/H`). When a group covers EVERY indexed segment of
|
|
260
|
+
* its bucket — and the bucket's hour is fully in the past, so no relocate can
|
|
261
|
+
* still land in it (in-flight staging lives in the separate `.rec-tmp`) — the
|
|
262
|
+
* whole dir goes with ONE `removeDir` instead of hundreds of per-file
|
|
263
|
+
* deletes. Partial buckets, current-hour buckets, and a failed removeDir fall
|
|
264
|
+
* back to the per-file `storage.delete` loop.
|
|
256
265
|
*/
|
|
257
266
|
async evict(location, rows) {
|
|
258
267
|
let reclaimed = 0;
|
|
259
268
|
const removed = [];
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
270
|
+
for (const r of rows) {
|
|
271
|
+
const cut = r.path.lastIndexOf("/");
|
|
272
|
+
const bucketDir = cut > 0 ? r.path.slice(0, cut) : "";
|
|
273
|
+
const group = byBucket.get(bucketDir);
|
|
274
|
+
if (group) group.push(r);
|
|
275
|
+
else byBucket.set(bucketDir, [r]);
|
|
276
|
+
}
|
|
277
|
+
for (const [bucketDir, bucketRows] of byBucket) {
|
|
278
|
+
if (bucketDir !== "" && this.bucketFullyEvicted(bucketDir, bucketRows)) try {
|
|
279
|
+
await this.deps.removeDir(location, bucketDir);
|
|
280
|
+
for (const r of bucketRows) {
|
|
281
|
+
removed.push(r.path);
|
|
282
|
+
reclaimed += r.bytes;
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", err);
|
|
287
|
+
}
|
|
288
|
+
for (const r of bucketRows) try {
|
|
289
|
+
await this.deps.deletePath({
|
|
290
|
+
location,
|
|
291
|
+
relativePath: r.path
|
|
292
|
+
});
|
|
293
|
+
removed.push(r.path);
|
|
294
|
+
reclaimed += r.bytes;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
this.deps.logger.warn("segment delete failed", err);
|
|
297
|
+
}
|
|
269
298
|
}
|
|
270
299
|
if (removed.length > 0) this.deps.index.removeSegments(removed);
|
|
271
300
|
return reclaimed;
|
|
272
301
|
}
|
|
302
|
+
/** Hour of one UTC hour-bucket in ms. */
|
|
303
|
+
static HOUR_MS = 36e5;
|
|
304
|
+
/**
|
|
305
|
+
* True when `victims` cover EVERY indexed segment of `bucketDir` AND the
|
|
306
|
+
* bucket's hour has fully elapsed. The current-hour guard matters because
|
|
307
|
+
* `onFinalized` relocates new segments INTO the current bucket concurrently —
|
|
308
|
+
* an rm racing that rename would orphan a just-indexed segment.
|
|
309
|
+
*/
|
|
310
|
+
bucketFullyEvicted(bucketDir, victims) {
|
|
311
|
+
const first = victims[0];
|
|
312
|
+
if (!first) return false;
|
|
313
|
+
const bucketStartMs = Math.floor(first.startMs / SegmentStore.HOUR_MS) * SegmentStore.HOUR_MS;
|
|
314
|
+
if ((this.deps.now?.() ?? Date.now()) < bucketStartMs + SegmentStore.HOUR_MS) return false;
|
|
315
|
+
const victimPaths = new Set(victims.map((v) => v.path));
|
|
316
|
+
const indexed = this.deps.index.segments(first.deviceId, first.profile).filter((s) => s.path.startsWith(`${bucketDir}/`));
|
|
317
|
+
return indexed.length === victimPaths.size && indexed.every((s) => victimPaths.has(s.path));
|
|
318
|
+
}
|
|
273
319
|
};
|
|
274
320
|
//#endregion
|
|
275
321
|
//#region src/recorder/evict-select.ts
|
|
@@ -471,6 +517,129 @@ function subscribeEventCapture(deps) {
|
|
|
471
517
|
});
|
|
472
518
|
}
|
|
473
519
|
//#endregion
|
|
520
|
+
//#region src/recorder/codec-probe.ts
|
|
521
|
+
/** Iterate the boxes fully contained in `bytes[start, end)`. Stops at the
|
|
522
|
+
* first malformed or truncated box (the probe only needs the front boxes). */
|
|
523
|
+
function* boxes(bytes, start, end) {
|
|
524
|
+
let off = start;
|
|
525
|
+
while (off + 8 <= end) {
|
|
526
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + off);
|
|
527
|
+
let size = view.getUint32(0);
|
|
528
|
+
let headerLen = 8;
|
|
529
|
+
if (size === 1) {
|
|
530
|
+
if (off + 16 > end) return;
|
|
531
|
+
const large = view.getBigUint64(8);
|
|
532
|
+
if (large > BigInt(Number.MAX_SAFE_INTEGER)) return;
|
|
533
|
+
size = Number(large);
|
|
534
|
+
headerLen = 16;
|
|
535
|
+
} else if (size === 0) size = end - off;
|
|
536
|
+
if (size < headerLen) return;
|
|
537
|
+
const boxEnd = off + size;
|
|
538
|
+
if (boxEnd > end) return;
|
|
539
|
+
yield {
|
|
540
|
+
type: String.fromCharCode(bytes[off + 4], bytes[off + 5], bytes[off + 6], bytes[off + 7]),
|
|
541
|
+
payloadStart: off + headerLen,
|
|
542
|
+
payloadEnd: boxEnd
|
|
543
|
+
};
|
|
544
|
+
off = boxEnd;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** First child box with `type` inside a payload window, or null. */
|
|
548
|
+
function findBox(bytes, start, end, type) {
|
|
549
|
+
for (const b of boxes(bytes, start, end)) if (b.type === type) return b;
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
/** Descend a container-box path (`moov/trak/mdia/…`), or null when broken. */
|
|
553
|
+
function descend(bytes, path) {
|
|
554
|
+
let start = 0;
|
|
555
|
+
let end = bytes.length;
|
|
556
|
+
let cur = null;
|
|
557
|
+
for (const type of path) {
|
|
558
|
+
cur = findBox(bytes, start, end, type);
|
|
559
|
+
if (!cur) return null;
|
|
560
|
+
start = cur.payloadStart;
|
|
561
|
+
end = cur.payloadEnd;
|
|
562
|
+
}
|
|
563
|
+
return cur;
|
|
564
|
+
}
|
|
565
|
+
/** VisualSampleEntry fixed fields before its child boxes: 6 reserved + 2
|
|
566
|
+
* data_reference_index + 70 bytes (pre_defined/width/height/…/depth). */
|
|
567
|
+
var VISUAL_SAMPLE_ENTRY_FIXED_BYTES = 78;
|
|
568
|
+
/** stsd FullBox payload head: version(1) + flags(3) + entry_count(4). */
|
|
569
|
+
var STSD_HEAD_BYTES = 8;
|
|
570
|
+
var hex2 = (n) => n.toString(16).padStart(2, "0");
|
|
571
|
+
/** `avc1.PPCCLL` from the avcC payload (profile, constraint flags, level). */
|
|
572
|
+
function avcCodecString(bytes, avcC) {
|
|
573
|
+
if (avcC.payloadEnd - avcC.payloadStart < 4) return void 0;
|
|
574
|
+
const p = avcC.payloadStart;
|
|
575
|
+
return `avc1.${hex2(bytes[p + 1])}${hex2(bytes[p + 2])}${hex2(bytes[p + 3])}`;
|
|
576
|
+
}
|
|
577
|
+
/** Reverse the bit order of a 32-bit value (HEVC general_profile_compatibility
|
|
578
|
+
* flags are written MSB-first but the codec string wants them reversed). */
|
|
579
|
+
function reverseBits32(v) {
|
|
580
|
+
let out = 0;
|
|
581
|
+
for (let i = 0; i < 32; i++) out = out << 1 | v >>> i & 1;
|
|
582
|
+
return out >>> 0;
|
|
583
|
+
}
|
|
584
|
+
/** ISO/IEC 14496-15 Annex E HEVC codec string from the hvcC payload. The
|
|
585
|
+
* prefix is the SAMPLE ENTRY code (`hvc1` vs `hev1` — they differ in
|
|
586
|
+
* parameter-set placement, which players honour). */
|
|
587
|
+
function hevcCodecString(bytes, entryType, hvcC) {
|
|
588
|
+
if (hvcC.payloadEnd - hvcC.payloadStart < 13) return void 0;
|
|
589
|
+
const p = hvcC.payloadStart;
|
|
590
|
+
const byte1 = bytes[p + 1];
|
|
591
|
+
const profileSpace = byte1 >> 6 & 3;
|
|
592
|
+
const tier = byte1 >> 5 & 1;
|
|
593
|
+
const profileIdc = byte1 & 31;
|
|
594
|
+
const compat = new DataView(bytes.buffer, bytes.byteOffset + p + 2).getUint32(0);
|
|
595
|
+
const levelIdc = bytes[p + 12];
|
|
596
|
+
const spacePrefix = profileSpace === 0 ? "" : String.fromCharCode(64 + profileSpace);
|
|
597
|
+
const compatHex = reverseBits32(compat).toString(16).toUpperCase();
|
|
598
|
+
let constraints = "";
|
|
599
|
+
for (let i = 5; i >= 0; i--) {
|
|
600
|
+
const b = bytes[p + 6 + i];
|
|
601
|
+
if (b !== 0 || constraints !== "") constraints = `.${b.toString(16).toUpperCase().padStart(2, "0")}${constraints}`;
|
|
602
|
+
}
|
|
603
|
+
return `${entryType}.${spacePrefix}${profileIdc}.${compatHex}.${tier === 0 ? "L" : "H"}${levelIdc}${constraints}`;
|
|
604
|
+
}
|
|
605
|
+
/** Sample-entry codes the probe recognizes, mapped to their config-box type. */
|
|
606
|
+
var VIDEO_SAMPLE_ENTRIES = {
|
|
607
|
+
avc1: "avcC",
|
|
608
|
+
avc3: "avcC",
|
|
609
|
+
hvc1: "hvcC",
|
|
610
|
+
hev1: "hvcC"
|
|
611
|
+
};
|
|
612
|
+
/**
|
|
613
|
+
* Walk an fMP4 fragment's moov for the first recognized video sample entry
|
|
614
|
+
* (avc1/avc3 → avcC, hvc1/hev1 → hvcC) and build its RFC 6381 codec string.
|
|
615
|
+
* Accepts a bounded PREFIX of the fragment (the moov leads the file). Returns
|
|
616
|
+
* `undefined` for anything malformed, truncated, or unrecognized.
|
|
617
|
+
*/
|
|
618
|
+
function codecStringFromFragment(bytes) {
|
|
619
|
+
const moov = descend(bytes, ["moov"]);
|
|
620
|
+
if (!moov) return void 0;
|
|
621
|
+
for (const trak of boxes(bytes, moov.payloadStart, moov.payloadEnd)) {
|
|
622
|
+
if (trak.type !== "trak") continue;
|
|
623
|
+
let cur = trak;
|
|
624
|
+
for (const type of [
|
|
625
|
+
"mdia",
|
|
626
|
+
"minf",
|
|
627
|
+
"stbl",
|
|
628
|
+
"stsd"
|
|
629
|
+
]) cur = cur ? findBox(bytes, cur.payloadStart, cur.payloadEnd, type) : null;
|
|
630
|
+
if (!cur) continue;
|
|
631
|
+
const entriesStart = cur.payloadStart + STSD_HEAD_BYTES;
|
|
632
|
+
for (const entry of boxes(bytes, entriesStart, cur.payloadEnd)) {
|
|
633
|
+
const configType = VIDEO_SAMPLE_ENTRIES[entry.type];
|
|
634
|
+
if (!configType) continue;
|
|
635
|
+
const config = findBox(bytes, entry.payloadStart + VISUAL_SAMPLE_ENTRY_FIXED_BYTES, entry.payloadEnd, configType);
|
|
636
|
+
if (!config) continue;
|
|
637
|
+
const codec = configType === "avcC" ? avcCodecString(bytes, config) : hevcCodecString(bytes, entry.type, config);
|
|
638
|
+
if (codec) return codec;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
//#endregion
|
|
474
643
|
//#region src/recorder/day-bucket.ts
|
|
475
644
|
/** Pure helpers to bucket recording segment starts into local calendar days. */
|
|
476
645
|
var DAY_MS$1 = 1440 * 60 * 1e3;
|
|
@@ -674,6 +843,42 @@ function noPlayback(deviceId) {
|
|
|
674
843
|
function playbackDirFor(dataDir, deviceId) {
|
|
675
844
|
return node_path.default.join(dataDir, "playback", String(deviceId));
|
|
676
845
|
}
|
|
846
|
+
/** Prefix window the CODECS probe reads — the fragment's moov (with its
|
|
847
|
+
* avcC/hvcC) leads the file and is ~1-2KB, so 64KB is a generous bound. */
|
|
848
|
+
var PROBE_PREFIX_BYTES = 65536;
|
|
849
|
+
/** Default bounded prefix reader: `fs.open` + one positioned `read`, never the
|
|
850
|
+
* whole file. */
|
|
851
|
+
async function readPrefixFromDisk(absPath, maxBytes) {
|
|
852
|
+
const fh = await node_fs.promises.open(absPath, "r");
|
|
853
|
+
try {
|
|
854
|
+
const buf = Buffer.alloc(maxBytes);
|
|
855
|
+
const { bytesRead } = await fh.read(buf, 0, maxBytes, 0);
|
|
856
|
+
return buf.subarray(0, bytesRead);
|
|
857
|
+
} finally {
|
|
858
|
+
await fh.close();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* RFC 6381 codec string for one variant: probe the first PRESENT fragment's
|
|
863
|
+
* moov once and cache the result. Failures (unreadable file / unrecognized
|
|
864
|
+
* bytes) return `undefined` — the playlist then omits `CODECS` for that
|
|
865
|
+
* variant, exactly today's behaviour — and are NOT cached, so a later healthy
|
|
866
|
+
* fragment can still populate it.
|
|
867
|
+
*/
|
|
868
|
+
async function probeVariantCodec(readPrefix, cache, deviceId, profile, firstPresentAbs) {
|
|
869
|
+
const key = `${deviceId}/${profile}`;
|
|
870
|
+
const cached = cache.get(key);
|
|
871
|
+
if (cached !== void 0) return cached;
|
|
872
|
+
if (firstPresentAbs === null) return void 0;
|
|
873
|
+
try {
|
|
874
|
+
const prefix = await readPrefix(firstPresentAbs, PROBE_PREFIX_BYTES);
|
|
875
|
+
const codec = codecStringFromFragment(prefix instanceof Uint8Array ? prefix : new Uint8Array(prefix));
|
|
876
|
+
if (codec !== void 0) cache.set(key, codec);
|
|
877
|
+
return codec;
|
|
878
|
+
} catch {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
677
882
|
/**
|
|
678
883
|
* Build a master + per-profile variant HLS playlist on disk for the requested
|
|
679
884
|
* window and return a RELATIVE playback URL through the hub's data-plane
|
|
@@ -683,9 +888,10 @@ function playbackDirFor(dataDir, deviceId) {
|
|
|
683
888
|
* URIs are relative so the master + variants + segments form a self-contained
|
|
684
889
|
* tree the player resolves against the master URL.
|
|
685
890
|
*/
|
|
686
|
-
async function buildPlaybackManifest(deps, deviceId, fromMs, toMs) {
|
|
891
|
+
async function buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs) {
|
|
687
892
|
const base = deps.playbackBaseUrl();
|
|
688
893
|
if (base === null) return noPlayback(deviceId);
|
|
894
|
+
const readPrefix = deps.readFilePrefix ?? readPrefixFromDisk;
|
|
689
895
|
const rootById = new Map(deps.locations().map((l) => [l.id, l.root]));
|
|
690
896
|
const playbackDir = playbackDirFor(deps.dataDir, deviceId);
|
|
691
897
|
const profiles = [...new Set(deps.index.segments(deviceId).map((s) => s.profile))];
|
|
@@ -694,15 +900,18 @@ async function buildPlaybackManifest(deps, deviceId, fromMs, toMs) {
|
|
|
694
900
|
const segs = deps.index.segments(deviceId, profile).filter((s) => s.startMs < toMs && s.startMs + s.durMs > fromMs).toSorted((a, b) => a.startMs - b.startMs);
|
|
695
901
|
if (segs.length === 0) continue;
|
|
696
902
|
const variantSegments = [];
|
|
903
|
+
let firstPresentAbs = null;
|
|
697
904
|
for (const s of segs) {
|
|
698
905
|
const rel = segmentRelPath(deviceId, profile, s.startMs, s.durMs, s.bytes);
|
|
699
906
|
const root = rootById.get(s.locationId);
|
|
700
907
|
if (root === void 0) continue;
|
|
908
|
+
const abs = node_path.default.join(root, rel);
|
|
701
909
|
try {
|
|
702
|
-
await node_fs.promises.stat(
|
|
910
|
+
await node_fs.promises.stat(abs);
|
|
703
911
|
} catch {
|
|
704
912
|
continue;
|
|
705
913
|
}
|
|
914
|
+
if (firstPresentAbs === null) firstPresentAbs = abs;
|
|
706
915
|
variantSegments.push({
|
|
707
916
|
uri: rel.split("/").slice(1).join("/"),
|
|
708
917
|
startMs: s.startMs,
|
|
@@ -710,12 +919,14 @@ async function buildPlaybackManifest(deps, deviceId, fromMs, toMs) {
|
|
|
710
919
|
});
|
|
711
920
|
}
|
|
712
921
|
if (variantSegments.length === 0) continue;
|
|
922
|
+
const codecs = await probeVariantCodec(readPrefix, codecCache, deviceId, profile, firstPresentAbs);
|
|
713
923
|
await node_fs.promises.mkdir(playbackDir, { recursive: true });
|
|
714
924
|
await node_fs.promises.writeFile(node_path.default.join(playbackDir, `${profile}.m3u8`), buildVariantPlaylist(variantSegments), "utf8");
|
|
715
925
|
variants.push({
|
|
716
926
|
profile,
|
|
717
927
|
bandwidth: PROFILE_BANDWIDTH[profile] ?? DEFAULT_BANDWIDTH,
|
|
718
|
-
uri: `${profile}.m3u8
|
|
928
|
+
uri: `${profile}.m3u8`,
|
|
929
|
+
...codecs !== void 0 ? { codecs } : {}
|
|
719
930
|
});
|
|
720
931
|
}
|
|
721
932
|
if (variants.length === 0) return noPlayback(deviceId);
|
|
@@ -757,6 +968,7 @@ function segmentsByLocation(rows) {
|
|
|
757
968
|
*/
|
|
758
969
|
function buildRecordingProvider(deps) {
|
|
759
970
|
const readFile = deps.readFile ?? ((absPath) => node_fs.promises.readFile(absPath));
|
|
971
|
+
const codecCache = /* @__PURE__ */ new Map();
|
|
760
972
|
return {
|
|
761
973
|
getStatus: async ({ deviceId }) => {
|
|
762
974
|
return statusFor(deps, deviceId, await loadDeviceConfig(deps.configStore, deviceId));
|
|
@@ -777,7 +989,7 @@ function buildRecordingProvider(deps) {
|
|
|
777
989
|
days: distinctRecordingDays(deps.index.segments(deviceId).map((s) => s.startMs), fromMs, toMs, tzOffsetMinutes)
|
|
778
990
|
};
|
|
779
991
|
},
|
|
780
|
-
getPlaybackManifest: ({ deviceId, fromMs, toMs }) => buildPlaybackManifest(deps, deviceId, fromMs, toMs),
|
|
992
|
+
getPlaybackManifest: ({ deviceId, fromMs, toMs }) => buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs),
|
|
781
993
|
getStorageUsage: async () => {
|
|
782
994
|
const locations = deps.locations();
|
|
783
995
|
const devices = [];
|
|
@@ -1447,6 +1659,15 @@ var SegmentWriter = class {
|
|
|
1447
1659
|
var DEFAULT_WATCH_INTERVAL_MS = 2e3;
|
|
1448
1660
|
/** Periodic re-evaluation to catch band boundaries crossed with no config change. */
|
|
1449
1661
|
var TICK_MS = 3e4;
|
|
1662
|
+
/**
|
|
1663
|
+
* Staging dir name, created UNDER each recordings location root
|
|
1664
|
+
* (`<locationRoot>/.rec-tmp/<deviceId>/<profile>`). Per-location staging means
|
|
1665
|
+
* the rec-tmp→final relocate is ALWAYS a same-filesystem rename — never the
|
|
1666
|
+
* EXDEV copy+unlink fallback that rewrites every segment (2x disk writes, F1).
|
|
1667
|
+
* The leading dot keeps it out of the recorder's own hydrate walk (which only
|
|
1668
|
+
* descends `<root>/<deviceId>/`) and `parseSegmentPath` (digits-first layout).
|
|
1669
|
+
*/
|
|
1670
|
+
var STAGING_DIR_NAME = ".rec-tmp";
|
|
1450
1671
|
var RecordingController = class {
|
|
1451
1672
|
deps;
|
|
1452
1673
|
active = /* @__PURE__ */ new Map();
|
|
@@ -1645,11 +1866,13 @@ var RecordingController = class {
|
|
|
1645
1866
|
return selected.length > 0 ? selected : assigned;
|
|
1646
1867
|
}
|
|
1647
1868
|
/**
|
|
1648
|
-
* Attach one (device, profile): acquire the broker source, mkdir the
|
|
1649
|
-
* outDir
|
|
1650
|
-
*
|
|
1869
|
+
* Attach one (device, profile): acquire the broker source, mkdir the staging
|
|
1870
|
+
* outDir UNDER the profile's placement root (same-fs relocate — see
|
|
1871
|
+
* {@link STAGING_DIR_NAME}), spawn the SegmentWriter, and start the watcher
|
|
1872
|
+
* that feeds finalized flat segments into `SegmentStore.onFinalized`.
|
|
1651
1873
|
*/
|
|
1652
1874
|
async attachProfile(deviceId, profile, segmentSeconds) {
|
|
1875
|
+
const placement = this.resolvePlacement(profile);
|
|
1653
1876
|
const source = await this.deps.brokerCall(() => this.deps.api.streamBroker.getStreamWithCodec.mutate({
|
|
1654
1877
|
deviceId,
|
|
1655
1878
|
video: "copy",
|
|
@@ -1658,7 +1881,7 @@ var RecordingController = class {
|
|
|
1658
1881
|
...this.deps.consumerHostname !== void 0 ? { hostname: this.deps.consumerHostname } : {},
|
|
1659
1882
|
tag: `recorder:${deviceId}/${profile}`
|
|
1660
1883
|
}));
|
|
1661
|
-
const outDir = node_path.default.join(
|
|
1884
|
+
const outDir = node_path.default.join(placement.root, STAGING_DIR_NAME, String(deviceId), profile);
|
|
1662
1885
|
await node_fs.promises.mkdir(outDir, { recursive: true });
|
|
1663
1886
|
const writer = new SegmentWriter({
|
|
1664
1887
|
rtspUrl: source.url,
|
|
@@ -1672,7 +1895,6 @@ var RecordingController = class {
|
|
|
1672
1895
|
}
|
|
1673
1896
|
});
|
|
1674
1897
|
writer.start();
|
|
1675
|
-
const placement = this.resolvePlacement(profile);
|
|
1676
1898
|
return {
|
|
1677
1899
|
profile,
|
|
1678
1900
|
writer,
|
|
@@ -1797,44 +2019,14 @@ function makeBrokerCall(handle) {
|
|
|
1797
2019
|
* Resolve the host a REMOTE recording node should use to dial the hub-only
|
|
1798
2020
|
* stream-broker's restream port.
|
|
1799
2021
|
*
|
|
1800
|
-
* The
|
|
1801
|
-
*
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
* operator set one explicitly (e.g. the auto-derived host isn't reachable
|
|
1807
|
-
* for RTSP on the recording interface).
|
|
1808
|
-
* 2. `hubUrl` — derived from the agent's `CAMSTACK_HUB_URL` (the address it
|
|
1809
|
-
* already uses to reach the hub). The RTSP restream port differs from the
|
|
1810
|
-
* hub API port, but the HOST is the same — only the host is extracted.
|
|
1811
|
-
*
|
|
1812
|
-
* Returns `undefined` when neither yields a host (the caller logs and the pull
|
|
1813
|
-
* stays hub-local, which simply won't reach a remote node — never a crash).
|
|
1814
|
-
*
|
|
1815
|
-
* Pure: the env string is passed in (not read here) so it can be unit-tested.
|
|
2022
|
+
* The implementation was promoted to the package-shared
|
|
2023
|
+
* `src/shared/hub-hostname.ts` in P2d (cross-node Phase 2) so the
|
|
2024
|
+
* pipeline-runner's remote-source leg shares the exact same resolver instead
|
|
2025
|
+
* of copying it. This module keeps the recorder-facing name as a thin alias —
|
|
2026
|
+
* resolution order and semantics are unchanged (operator override first, then
|
|
2027
|
+
* the host extracted from `CAMSTACK_HUB_URL`).
|
|
1816
2028
|
*/
|
|
1817
|
-
|
|
1818
|
-
const trimmedOverride = override?.trim();
|
|
1819
|
-
if (trimmedOverride !== void 0 && trimmedOverride.length > 0) return trimmedOverride;
|
|
1820
|
-
return extractHost(hubUrl);
|
|
1821
|
-
}
|
|
1822
|
-
/** Extract the bare host from a URL/authority string, scheme + port optional. */
|
|
1823
|
-
function extractHost(raw) {
|
|
1824
|
-
const value = raw?.trim();
|
|
1825
|
-
if (value === void 0 || value.length === 0) return void 0;
|
|
1826
|
-
try {
|
|
1827
|
-
const parsed = new URL(value);
|
|
1828
|
-
if (parsed.hostname.length > 0) return parsed.hostname;
|
|
1829
|
-
} catch {}
|
|
1830
|
-
const authority = value.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "").split("/")[0] ?? "";
|
|
1831
|
-
if (authority.startsWith("[")) {
|
|
1832
|
-
const end = authority.indexOf("]");
|
|
1833
|
-
if (end > 0) return authority.slice(0, end + 1);
|
|
1834
|
-
}
|
|
1835
|
-
const host = authority.split(":")[0] ?? "";
|
|
1836
|
-
return host.length > 0 ? host : void 0;
|
|
1837
|
-
}
|
|
2029
|
+
var resolveRecordingHubHostname = require_hub_hostname.resolveHubHostname;
|
|
1838
2030
|
//#endregion
|
|
1839
2031
|
//#region src/recorder/addon/index.ts
|
|
1840
2032
|
/**
|
|
@@ -1861,13 +2053,18 @@ function extractHost(raw) {
|
|
|
1861
2053
|
* file ships dormant (separate tree, not wired into the manifest yet).
|
|
1862
2054
|
*/
|
|
1863
2055
|
var DEFAULT_CONFIG = {
|
|
1864
|
-
segmentSeconds:
|
|
2056
|
+
segmentSeconds: 10,
|
|
1865
2057
|
watchIntervalMs: 2e3
|
|
1866
2058
|
};
|
|
1867
2059
|
/** Playback-marker window padding around a motion event (EventMap only; the
|
|
1868
|
-
* controller's events-band gating uses the raw trigger ms, not this pad).
|
|
1869
|
-
|
|
1870
|
-
|
|
2060
|
+
* controller's events-band gating uses the raw trigger ms, not this pad).
|
|
2061
|
+
* Derived from the SHARED `EVENT_PAD_MS` (R5/C1): markers are ephemeral
|
|
2062
|
+
* in-RAM and consumed by nothing today, but aligning their window with the
|
|
2063
|
+
* canonical clip pad means that IF they are ever surfaced, their boundaries
|
|
2064
|
+
* match the videoclips/admin-ui clip window for the same event — one pad
|
|
2065
|
+
* convention, not three. */
|
|
2066
|
+
var MARKER_PAD_PRE_MS = require_dist.EVENT_PAD_MS.preMs;
|
|
2067
|
+
var MARKER_PAD_POST_MS = require_dist.EVENT_PAD_MS.postMs;
|
|
1871
2068
|
/** This addon's manifest id + the playback data-plane prefix — together they
|
|
1872
2069
|
* form the hub reverse-proxy client path `/addon/recorder/playback`. */
|
|
1873
2070
|
var RECORDER_ADDON_ID = "recorder";
|
|
@@ -1940,11 +2137,17 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
1940
2137
|
await node_fs.promises.unlink(fromAbs);
|
|
1941
2138
|
}
|
|
1942
2139
|
},
|
|
2140
|
+
removeDir: async (locationId, relDir) => {
|
|
2141
|
+
const loc = this.resolvedLocations.find((l) => l.id === locationId);
|
|
2142
|
+
if (!loc) throw new Error(`recorder: unknown recordings location ${locationId}`);
|
|
2143
|
+
await node_fs.promises.rm(node_path.default.join(loc.root, relDir), {
|
|
2144
|
+
recursive: true,
|
|
2145
|
+
force: true
|
|
2146
|
+
});
|
|
2147
|
+
},
|
|
1943
2148
|
logger: { warn: (message, extras) => this.ctx.logger.warn(message, { meta: { extras } }) }
|
|
1944
2149
|
};
|
|
1945
2150
|
this.segmentStore = new SegmentStore(segmentStoreDeps);
|
|
1946
|
-
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
1947
|
-
const tmpRoot = node_path.default.join(mediaRoot ?? this.ctx.dataDir, "rec-tmp");
|
|
1948
2151
|
const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(BROKER_HOST_NODE_ID));
|
|
1949
2152
|
this.controller = new RecordingController({
|
|
1950
2153
|
api: this.ctx.api,
|
|
@@ -1954,7 +2157,6 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
1954
2157
|
loadConfig: (deviceId) => loadDeviceConfig(this.configStore(), deviceId),
|
|
1955
2158
|
enabledDeviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
|
|
1956
2159
|
locations: () => this.resolvedLocations,
|
|
1957
|
-
tmpRoot,
|
|
1958
2160
|
spawn: node_child_process.spawn,
|
|
1959
2161
|
segmentSeconds: this.config.segmentSeconds,
|
|
1960
2162
|
watchIntervalMs: this.config.watchIntervalMs,
|