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