@camstack/addon-pipeline 1.2.11 → 1.2.13
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 +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-DNjy_sr4.js → dist-BUd2xSjP.js} +2243 -2075
- package/dist/{dist-owmpaeIY.mjs → dist-DuI0Ypuu.mjs} +2243 -2075
- package/dist/{fmp4-keyframes-BzTPoPVn.js → fmp4-keyframes-5s8T9Mng.js} +42 -7
- package/dist/{fmp4-keyframes-qOgJBjMO.mjs → fmp4-keyframes-CV0X1mGy.mjs} +37 -12
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +2 -2
- package/dist/pipeline-runner/index.mjs +2 -2
- package/dist/recorder/index.js +183 -49
- package/dist/recorder/index.mjs +183 -49
- package/dist/{step-definitions-C2bNlww3.js → step-definitions-Ck4_6GKK.js} +1 -1
- package/dist/{step-definitions-CkvctwuS.mjs → step-definitions-DqNWVeYB.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CkiyBstP.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-5EJfCCjk.mjs} +2 -2
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DHQsa56B.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BDeEvhV5.mjs} +1 -1
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js--zEZtV2N.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BGtpZkgz.mjs} +1 -1
- package/dist/stream-broker/{hostInit-BC07ueda.mjs → hostInit-QaJMh7Ec.mjs} +2 -2
- package/dist/stream-broker/index.js +1186 -337
- package/dist/stream-broker/index.mjs +1187 -338
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CftzR7Pj.js → MaskShapeCanvas-DI4BY7W2-Di-nvxaR.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DPgVIdEn.js → MotionZonesSettings-NcxxQN8r-C6Nj76Lm.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-D0HqiXHW.js → PrivacyMaskSettings-APgPLF7p-DwVoJunF.js} +1 -1
- package/embed-dist/assets/index-CtDVz0vI.js +116 -0
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/embed-dist/assets/index-DRtbV_en.js +0 -116
|
@@ -6631,12 +6631,16 @@ function readTrack(file, segment) {
|
|
|
6631
6631
|
});
|
|
6632
6632
|
}
|
|
6633
6633
|
/**
|
|
6634
|
-
* Demux a recorded
|
|
6635
|
-
* WebCodecs decoder config, or null if the segment
|
|
6636
|
-
* decodable
|
|
6637
|
-
* epoch ms; the first sample's cts is the zero reference.
|
|
6634
|
+
* Demux a recorded `.m4s` fragment and return ALL its samples (sync + delta) in
|
|
6635
|
+
* decode order plus the WebCodecs decoder config, or null if the segment
|
|
6636
|
+
* carries no video / no decodable sample. `segmentStartMs` anchors each sample's
|
|
6637
|
+
* `ts` to absolute epoch ms; the first sample's cts is the zero reference.
|
|
6638
|
+
*
|
|
6639
|
+
* No decode, no ffmpeg — byte copies only. This is the general form used by the
|
|
6640
|
+
* recorded-playback demux; {@link extractKeyframes} filters its output to sync
|
|
6641
|
+
* samples for the scrub-pack service.
|
|
6638
6642
|
*/
|
|
6639
|
-
async function
|
|
6643
|
+
async function extractSamples(segment, segmentStartMs) {
|
|
6640
6644
|
const file = import_mp4box_all.createFile();
|
|
6641
6645
|
const ready = await readTrack(file, segment);
|
|
6642
6646
|
if (!ready) return null;
|
|
@@ -6647,10 +6651,10 @@ async function extractKeyframes(segment, segmentStartMs) {
|
|
|
6647
6651
|
file.onSamples = (_id, _user, samples) => {
|
|
6648
6652
|
for (const s of samples) {
|
|
6649
6653
|
if (firstCts === null) firstCts = s.cts;
|
|
6650
|
-
if (!s.is_sync) continue;
|
|
6651
6654
|
const ts = segmentStartMs + Math.round((s.cts - firstCts) / s.timescale * 1e3);
|
|
6652
6655
|
collected.push({
|
|
6653
6656
|
ts,
|
|
6657
|
+
isSync: s.is_sync,
|
|
6654
6658
|
data: new Uint8Array(s.data)
|
|
6655
6659
|
});
|
|
6656
6660
|
}
|
|
@@ -6664,7 +6668,26 @@ async function extractKeyframes(segment, segmentStartMs) {
|
|
|
6664
6668
|
return collected.length > 0 ? {
|
|
6665
6669
|
codec,
|
|
6666
6670
|
description,
|
|
6667
|
-
|
|
6671
|
+
samples: collected
|
|
6672
|
+
} : null;
|
|
6673
|
+
}
|
|
6674
|
+
/**
|
|
6675
|
+
* Demux a recorded LOW `.m4s` segment and return its sync samples plus the
|
|
6676
|
+
* WebCodecs decoder config, or null if the segment carries no video / no
|
|
6677
|
+
* decodable keyframe. `segmentStartMs` anchors keyframe timestamps to absolute
|
|
6678
|
+
* epoch ms; the first sample's cts is the zero reference.
|
|
6679
|
+
*/
|
|
6680
|
+
async function extractKeyframes(segment, segmentStartMs) {
|
|
6681
|
+
const extracted = await extractSamples(segment, segmentStartMs);
|
|
6682
|
+
if (!extracted) return null;
|
|
6683
|
+
const keyframes = extracted.samples.filter((s) => s.isSync).map((s) => ({
|
|
6684
|
+
ts: s.ts,
|
|
6685
|
+
data: s.data
|
|
6686
|
+
}));
|
|
6687
|
+
return keyframes.length > 0 ? {
|
|
6688
|
+
codec: extracted.codec,
|
|
6689
|
+
description: extracted.description,
|
|
6690
|
+
keyframes
|
|
6668
6691
|
} : null;
|
|
6669
6692
|
}
|
|
6670
6693
|
//#endregion
|
|
@@ -6674,3 +6697,15 @@ Object.defineProperty(exports, "extractKeyframes", {
|
|
|
6674
6697
|
return extractKeyframes;
|
|
6675
6698
|
}
|
|
6676
6699
|
});
|
|
6700
|
+
Object.defineProperty(exports, "extractSamples", {
|
|
6701
|
+
enumerable: true,
|
|
6702
|
+
get: function() {
|
|
6703
|
+
return extractSamples;
|
|
6704
|
+
}
|
|
6705
|
+
});
|
|
6706
|
+
Object.defineProperty(exports, "require_mp4box_all", {
|
|
6707
|
+
enumerable: true,
|
|
6708
|
+
get: function() {
|
|
6709
|
+
return require_mp4box_all;
|
|
6710
|
+
}
|
|
6711
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { c as __commonJSMin, u as __toESM } from "./model-download-service-Cp9f4dk6-WO-foD7W.mjs";
|
|
2
|
-
//#
|
|
3
|
-
|
|
4
|
-
var import_mp4box_all = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports) => {
|
|
2
|
+
//#region ../../node_modules/mp4box/dist/mp4box.all.js
|
|
3
|
+
var require_mp4box_all = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
5
4
|
var Log = (function() {
|
|
6
5
|
var start = /* @__PURE__ */ new Date();
|
|
7
6
|
var LOG_LEVEL_ERROR = 4;
|
|
@@ -6566,7 +6565,10 @@ var import_mp4box_all = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin((
|
|
|
6566
6565
|
return file;
|
|
6567
6566
|
};
|
|
6568
6567
|
if (typeof exports !== "undefined") exports.createFile = MP4Box.createFile;
|
|
6569
|
-
}))
|
|
6568
|
+
}));
|
|
6569
|
+
//#endregion
|
|
6570
|
+
//#region src/recorder/scrub/fmp4-keyframes.ts
|
|
6571
|
+
var import_mp4box_all = /* @__PURE__ */ __toESM(require_mp4box_all());
|
|
6570
6572
|
/**
|
|
6571
6573
|
* Normalize an RFC 6381 codec string for WebCodecs HEVC. Apple/WebKit
|
|
6572
6574
|
* WebCodecs only accepts the `hvc1.*` fourcc — a `hev1.*` string (what mp4box
|
|
@@ -6629,12 +6631,16 @@ function readTrack(file, segment) {
|
|
|
6629
6631
|
});
|
|
6630
6632
|
}
|
|
6631
6633
|
/**
|
|
6632
|
-
* Demux a recorded
|
|
6633
|
-
* WebCodecs decoder config, or null if the segment
|
|
6634
|
-
* decodable
|
|
6635
|
-
* epoch ms; the first sample's cts is the zero reference.
|
|
6634
|
+
* Demux a recorded `.m4s` fragment and return ALL its samples (sync + delta) in
|
|
6635
|
+
* decode order plus the WebCodecs decoder config, or null if the segment
|
|
6636
|
+
* carries no video / no decodable sample. `segmentStartMs` anchors each sample's
|
|
6637
|
+
* `ts` to absolute epoch ms; the first sample's cts is the zero reference.
|
|
6638
|
+
*
|
|
6639
|
+
* No decode, no ffmpeg — byte copies only. This is the general form used by the
|
|
6640
|
+
* recorded-playback demux; {@link extractKeyframes} filters its output to sync
|
|
6641
|
+
* samples for the scrub-pack service.
|
|
6636
6642
|
*/
|
|
6637
|
-
async function
|
|
6643
|
+
async function extractSamples(segment, segmentStartMs) {
|
|
6638
6644
|
const file = import_mp4box_all.createFile();
|
|
6639
6645
|
const ready = await readTrack(file, segment);
|
|
6640
6646
|
if (!ready) return null;
|
|
@@ -6645,10 +6651,10 @@ async function extractKeyframes(segment, segmentStartMs) {
|
|
|
6645
6651
|
file.onSamples = (_id, _user, samples) => {
|
|
6646
6652
|
for (const s of samples) {
|
|
6647
6653
|
if (firstCts === null) firstCts = s.cts;
|
|
6648
|
-
if (!s.is_sync) continue;
|
|
6649
6654
|
const ts = segmentStartMs + Math.round((s.cts - firstCts) / s.timescale * 1e3);
|
|
6650
6655
|
collected.push({
|
|
6651
6656
|
ts,
|
|
6657
|
+
isSync: s.is_sync,
|
|
6652
6658
|
data: new Uint8Array(s.data)
|
|
6653
6659
|
});
|
|
6654
6660
|
}
|
|
@@ -6662,8 +6668,27 @@ async function extractKeyframes(segment, segmentStartMs) {
|
|
|
6662
6668
|
return collected.length > 0 ? {
|
|
6663
6669
|
codec,
|
|
6664
6670
|
description,
|
|
6665
|
-
|
|
6671
|
+
samples: collected
|
|
6672
|
+
} : null;
|
|
6673
|
+
}
|
|
6674
|
+
/**
|
|
6675
|
+
* Demux a recorded LOW `.m4s` segment and return its sync samples plus the
|
|
6676
|
+
* WebCodecs decoder config, or null if the segment carries no video / no
|
|
6677
|
+
* decodable keyframe. `segmentStartMs` anchors keyframe timestamps to absolute
|
|
6678
|
+
* epoch ms; the first sample's cts is the zero reference.
|
|
6679
|
+
*/
|
|
6680
|
+
async function extractKeyframes(segment, segmentStartMs) {
|
|
6681
|
+
const extracted = await extractSamples(segment, segmentStartMs);
|
|
6682
|
+
if (!extracted) return null;
|
|
6683
|
+
const keyframes = extracted.samples.filter((s) => s.isSync).map((s) => ({
|
|
6684
|
+
ts: s.ts,
|
|
6685
|
+
data: s.data
|
|
6686
|
+
}));
|
|
6687
|
+
return keyframes.length > 0 ? {
|
|
6688
|
+
codec: extracted.codec,
|
|
6689
|
+
description: extracted.description,
|
|
6690
|
+
keyframes
|
|
6666
6691
|
} : null;
|
|
6667
6692
|
}
|
|
6668
6693
|
//#endregion
|
|
6669
|
-
export { extractKeyframes as t };
|
|
6694
|
+
export { extractSamples as n, require_mp4box_all as r, extractKeyframes as t };
|
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-BUd2xSjP.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as motionDetectionCapability, G as DeviceType, H as BaseAddon, T as evaluateZoneRules, q as hydrateSchema } from "../dist-
|
|
1
|
+
import { A as motionDetectionCapability, G as DeviceType, H as BaseAddon, T as evaluateZoneRules, q as hydrateSchema } from "../dist-DuI0Ypuu.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
//#region src/motion-wasm/wasm-motion-detector.ts
|
|
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
6
|
+
const require_dist = require("../dist-BUd2xSjP.js");
|
|
7
7
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
8
8
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
9
9
|
const require_worker_protocol = require("../worker-protocol-DextwlTX.js");
|
|
10
|
-
const require_step_definitions = require("../step-definitions-
|
|
10
|
+
const require_step_definitions = require("../step-definitions-Ck4_6GKK.js");
|
|
11
11
|
let node_child_process = require("node:child_process");
|
|
12
12
|
let node_url = require("node:url");
|
|
13
13
|
let sharp = require("sharp");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { H as BaseAddon, K as createEvent, N as pipelineRunnerCapability, S as defineCustomActions, V as errMsg, at as lazy, b as customAction, ct as object, ft as EventCategory, j as nodePin, nt as array, rt as boolean, s as DEVICE_BACKEND_TO_FORMAT, st as number, tt as _enum, ut as string } from "../dist-
|
|
1
|
+
import { H as BaseAddon, K as createEvent, N as pipelineRunnerCapability, S as defineCustomActions, V as errMsg, at as lazy, b as customAction, ct as object, ft as EventCategory, j as nodePin, nt as array, rt as boolean, s as DEVICE_BACKEND_TO_FORMAT, st as number, tt as _enum, ut as string } from "../dist-DuI0Ypuu.mjs";
|
|
2
2
|
import { n as profileForStreamId, t as isRemoteRestream } from "../remote-restream-BeHi78PZ.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { t as isWorkerReply } from "../worker-protocol-D7RzZIla.mjs";
|
|
5
|
-
import { a as getStepDefinition } from "../step-definitions-
|
|
5
|
+
import { a as getStepDefinition } from "../step-definitions-DqNWVeYB.mjs";
|
|
6
6
|
import { fork } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import sharp from "sharp";
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-BUd2xSjP.js");
|
|
3
3
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
4
4
|
const require_model_download_service_Cp9f4dk6 = require("../model-download-service-Cp9f4dk6-fsdDExML.js");
|
|
5
|
-
const require_fmp4_keyframes = require("../fmp4-keyframes-
|
|
5
|
+
const require_fmp4_keyframes = require("../fmp4-keyframes-5s8T9Mng.js");
|
|
6
6
|
let node_child_process = require("node:child_process");
|
|
7
7
|
let node_fs = require("node:fs");
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -953,15 +953,6 @@ function noPlayback(deviceId) {
|
|
|
953
953
|
function playbackDirFor(dataDir, deviceId) {
|
|
954
954
|
return node_path.default.join(dataDir, "playback", String(deviceId));
|
|
955
955
|
}
|
|
956
|
-
/**
|
|
957
|
-
* Absolute path of ONE profile's variant playlist inside the playback dir.
|
|
958
|
-
* `buildPlaybackManifest` writes `<profile>.m3u8` next to the master whenever
|
|
959
|
-
* that profile has overlapping footage — the export engine renders this variant
|
|
960
|
-
* (a single-profile source) rather than the multi-variant master.
|
|
961
|
-
*/
|
|
962
|
-
function playbackVariantPlaylistPath(dataDir, deviceId, profile) {
|
|
963
|
-
return node_path.default.join(playbackDirFor(dataDir, deviceId), `${profile}.m3u8`);
|
|
964
|
-
}
|
|
965
956
|
/** Prefix window the CODECS probe reads — the fragment's moov (with its
|
|
966
957
|
* avcC/hvcC) leads the file and is ~1-2KB, so 64KB is a generous bound. */
|
|
967
958
|
var PROBE_PREFIX_BYTES = 65536;
|
|
@@ -999,6 +990,43 @@ async function probeVariantCodec(readPrefix, cache, deviceId, profile, firstPres
|
|
|
999
990
|
}
|
|
1000
991
|
}
|
|
1001
992
|
/**
|
|
993
|
+
* Collect the segments of one (device, profile) overlapping `[fromMs, toMs)`
|
|
994
|
+
* that are actually present on disk, as playlist entries in `uriMode`'s shape.
|
|
995
|
+
*
|
|
996
|
+
* Absolute resolution is the SAME idiom as the scrub path's
|
|
997
|
+
* `resolveSegmentAbsPath`: `<root for the row's locationId>/<relative path>`.
|
|
998
|
+
* A row whose location is unknown, or whose file has been evicted since the
|
|
999
|
+
* index row was written, is skipped so the playlist never references a
|
|
1000
|
+
* non-existent segment.
|
|
1001
|
+
*/
|
|
1002
|
+
async function collectRangeSegments(deps, deviceId, profile, fromMs, toMs, uriMode) {
|
|
1003
|
+
const rootById = new Map(deps.locations().map((l) => [l.id, l.root]));
|
|
1004
|
+
const rows = deps.index.segments(deviceId, profile).filter((s) => s.startMs < toMs && s.startMs + s.durMs > fromMs).toSorted((a, b) => a.startMs - b.startMs);
|
|
1005
|
+
const segments = [];
|
|
1006
|
+
let firstPresentAbs = null;
|
|
1007
|
+
for (const s of rows) {
|
|
1008
|
+
const rel = segmentRelPath(deviceId, profile, s.startMs, s.durMs, s.bytes);
|
|
1009
|
+
const root = rootById.get(s.locationId);
|
|
1010
|
+
if (root === void 0) continue;
|
|
1011
|
+
const abs = node_path.default.join(root, rel);
|
|
1012
|
+
try {
|
|
1013
|
+
await node_fs.promises.stat(abs);
|
|
1014
|
+
} catch {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (firstPresentAbs === null) firstPresentAbs = abs;
|
|
1018
|
+
segments.push({
|
|
1019
|
+
uri: uriMode === "absolute" ? abs : rel.split("/").slice(1).join("/"),
|
|
1020
|
+
startMs: s.startMs,
|
|
1021
|
+
durMs: s.durMs
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
segments,
|
|
1026
|
+
firstPresentAbs
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1002
1030
|
* Build a master + per-profile variant HLS playlist on disk for the requested
|
|
1003
1031
|
* window and return a RELATIVE playback URL through the hub's data-plane
|
|
1004
1032
|
* reverse-proxy. Only segments actually present on disk are referenced (a
|
|
@@ -1011,32 +1039,11 @@ async function buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs) {
|
|
|
1011
1039
|
const base = deps.playbackBaseUrl();
|
|
1012
1040
|
if (base === null) return noPlayback(deviceId);
|
|
1013
1041
|
const readPrefix = deps.readFilePrefix ?? readPrefixFromDisk;
|
|
1014
|
-
const rootById = new Map(deps.locations().map((l) => [l.id, l.root]));
|
|
1015
1042
|
const playbackDir = playbackDirFor(deps.dataDir, deviceId);
|
|
1016
1043
|
const profiles = [...new Set(deps.index.segments(deviceId).map((s) => s.profile))];
|
|
1017
1044
|
const variants = [];
|
|
1018
1045
|
for (const profile of profiles) {
|
|
1019
|
-
const
|
|
1020
|
-
if (segs.length === 0) continue;
|
|
1021
|
-
const variantSegments = [];
|
|
1022
|
-
let firstPresentAbs = null;
|
|
1023
|
-
for (const s of segs) {
|
|
1024
|
-
const rel = segmentRelPath(deviceId, profile, s.startMs, s.durMs, s.bytes);
|
|
1025
|
-
const root = rootById.get(s.locationId);
|
|
1026
|
-
if (root === void 0) continue;
|
|
1027
|
-
const abs = node_path.default.join(root, rel);
|
|
1028
|
-
try {
|
|
1029
|
-
await node_fs.promises.stat(abs);
|
|
1030
|
-
} catch {
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
if (firstPresentAbs === null) firstPresentAbs = abs;
|
|
1034
|
-
variantSegments.push({
|
|
1035
|
-
uri: rel.split("/").slice(1).join("/"),
|
|
1036
|
-
startMs: s.startMs,
|
|
1037
|
-
durMs: s.durMs
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1046
|
+
const { segments: variantSegments, firstPresentAbs } = await collectRangeSegments(deps, deviceId, profile, fromMs, toMs, "playback-relative");
|
|
1040
1047
|
if (variantSegments.length === 0) continue;
|
|
1041
1048
|
const codecs = await probeVariantCodec(readPrefix, codecCache, deviceId, profile, firstPresentAbs);
|
|
1042
1049
|
await node_fs.promises.mkdir(playbackDir, { recursive: true });
|
|
@@ -1279,6 +1286,48 @@ function buildRecordingProvider(deps) {
|
|
|
1279
1286
|
};
|
|
1280
1287
|
}
|
|
1281
1288
|
//#endregion
|
|
1289
|
+
//#region src/recorder/addon/export-source-playlist.ts
|
|
1290
|
+
/**
|
|
1291
|
+
* The ffmpeg INPUT playlist for one recording export.
|
|
1292
|
+
*
|
|
1293
|
+
* Exports must NOT reuse the playback variant playlist. That file lives under
|
|
1294
|
+
* `<dataDir>/playback` while its segments live under a recordings LOCATION root,
|
|
1295
|
+
* and the two trees are unified only by the HTTP data-plane's ordered multi-root
|
|
1296
|
+
* search. ffmpeg has no such fallback — it resolves every segment URI against
|
|
1297
|
+
* the playlist's own directory — so a playback playlist handed to `-i` ENOENTs
|
|
1298
|
+
* on every segment and dies with `AVERROR_INVALIDDATA` (exit 183).
|
|
1299
|
+
*
|
|
1300
|
+
* So each export gets its OWN playlist whose segment URIs are ABSOLUTE on-disk
|
|
1301
|
+
* paths, written at a PER-EXPORT path. Per-export is load-bearing twice over:
|
|
1302
|
+
* the playback playlist sits at a shared fixed path that `buildPlaybackManifest`
|
|
1303
|
+
* rewrites on every playback request, so an operator scrubbing the same camera
|
|
1304
|
+
* mid-render would otherwise swap ffmpeg's input out from under it.
|
|
1305
|
+
*
|
|
1306
|
+
* The file is transient — `ExportEngine` deletes it when the render finishes
|
|
1307
|
+
* (success, failure or cancel).
|
|
1308
|
+
*/
|
|
1309
|
+
/**
|
|
1310
|
+
* Absolute path of ONE export's source playlist:
|
|
1311
|
+
* `<exportsDir>/<deviceId>/<exportId>.src.m3u8`. Unique per export id, so two
|
|
1312
|
+
* concurrent renders (and any concurrent playback request) never share a file.
|
|
1313
|
+
*/
|
|
1314
|
+
function exportSourcePlaylistPath(exportsDir, deviceId, exportId) {
|
|
1315
|
+
return node_path.default.join(exportsDir, String(deviceId), `${exportId}.src.m3u8`);
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Write the export's source playlist and return its absolute path, or null when
|
|
1319
|
+
* no segment of the requested profile is present on disk for the range (the
|
|
1320
|
+
* engine then fails the job with 'no footage for range').
|
|
1321
|
+
*/
|
|
1322
|
+
async function writeExportSourcePlaylist(input) {
|
|
1323
|
+
const { segments } = await collectRangeSegments(input.deps, input.deviceId, input.profile, input.fromMs, input.toMs, "absolute");
|
|
1324
|
+
if (segments.length === 0) return null;
|
|
1325
|
+
const playlistPath = exportSourcePlaylistPath(input.exportsDir, input.deviceId, input.exportId);
|
|
1326
|
+
await node_fs.promises.mkdir(node_path.default.dirname(playlistPath), { recursive: true });
|
|
1327
|
+
await node_fs.promises.writeFile(playlistPath, buildVariantPlaylist(segments), "utf8");
|
|
1328
|
+
return playlistPath;
|
|
1329
|
+
}
|
|
1330
|
+
//#endregion
|
|
1282
1331
|
//#region src/recorder/addon/export-store.ts
|
|
1283
1332
|
/**
|
|
1284
1333
|
* Recording-export persistence for the recorder addon.
|
|
@@ -1478,16 +1527,22 @@ function buildExportArgs(input) {
|
|
|
1478
1527
|
* Render engine for recording exports.
|
|
1479
1528
|
*
|
|
1480
1529
|
* A FIFO queue with ONE concurrent ffmpeg render (exports are heavy; serialising
|
|
1481
|
-
* protects the recording node). Each job:
|
|
1482
|
-
* playlist → build argv (`buildExportArgs`) → spawn ffmpeg →
|
|
1483
|
-
* stderr for progress → on exit 0 stat the file and mark
|
|
1484
|
-
* mark `failed` and remove the partial
|
|
1530
|
+
* protects the recording node). Each job: write the range's per-export source
|
|
1531
|
+
* playlist (`resolvePlaylist`) → build argv (`buildExportArgs`) → spawn ffmpeg →
|
|
1532
|
+
* parse `time=` from stderr for progress → on exit 0 stat the file and mark
|
|
1533
|
+
* `ready`; on non-zero mark `failed` and remove the partial; drop the source
|
|
1534
|
+
* playlist whatever the outcome. `cancel` SIGKILLs an in-flight render.
|
|
1485
1535
|
* State transitions patch the DurableState store and emit telemetry events.
|
|
1486
1536
|
*
|
|
1487
1537
|
* Unlike `SegmentWriter` there is NO auto-restart — a render runs once; boot
|
|
1488
1538
|
* re-queue of interrupted jobs is the provider's concern.
|
|
1489
1539
|
*/
|
|
1490
|
-
|
|
1540
|
+
/** Stderr lines retained for the failure LOG. ffmpeg prints one line per failed
|
|
1541
|
+
* segment before the decisive "Error opening input" summary, so a short tail can
|
|
1542
|
+
* push the actual cause out of the window (it very nearly did — the line that
|
|
1543
|
+
* identified the ENOENT'd segment paths was the 12th). Bounded, because a long
|
|
1544
|
+
* render writes one progress line per second. */
|
|
1545
|
+
var STDERR_TAIL_LINES$1 = 40;
|
|
1491
1546
|
var TIME_RE = /time=(\d+):(\d+):(\d+(?:\.\d+)?)/;
|
|
1492
1547
|
/** Expected OUTPUT duration (seconds) for progress scaling. */
|
|
1493
1548
|
function expectedOutSeconds(rec) {
|
|
@@ -1556,6 +1611,22 @@ var ExportEngine = class {
|
|
|
1556
1611
|
await this.fail(id, "no footage for range");
|
|
1557
1612
|
return;
|
|
1558
1613
|
}
|
|
1614
|
+
try {
|
|
1615
|
+
await this.spawnRender(id, rec, playlist);
|
|
1616
|
+
} finally {
|
|
1617
|
+
if (this.deps.cleanupPlaylist) try {
|
|
1618
|
+
await this.deps.cleanupPlaylist(playlist);
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
this.deps.logger.warn("export: source playlist cleanup failed", { meta: {
|
|
1621
|
+
exportId: id,
|
|
1622
|
+
playlist,
|
|
1623
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1624
|
+
} });
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
/** Spawn ffmpeg for one render and resolve when it has exited + finalized. */
|
|
1629
|
+
async spawnRender(id, rec, playlist) {
|
|
1559
1630
|
const outPath = this.deps.outPath(rec);
|
|
1560
1631
|
const args = buildExportArgs({
|
|
1561
1632
|
inputPlaylist: playlist,
|
|
@@ -1563,6 +1634,17 @@ var ExportEngine = class {
|
|
|
1563
1634
|
options: rec.options
|
|
1564
1635
|
});
|
|
1565
1636
|
const totalSec = expectedOutSeconds(rec);
|
|
1637
|
+
this.deps.logger.info("export render starting", { meta: {
|
|
1638
|
+
exportId: id,
|
|
1639
|
+
deviceId: rec.deviceId,
|
|
1640
|
+
profile: rec.profile,
|
|
1641
|
+
playlist,
|
|
1642
|
+
outPath
|
|
1643
|
+
} });
|
|
1644
|
+
this.deps.logger.debug("export ffmpeg argv", { meta: {
|
|
1645
|
+
exportId: id,
|
|
1646
|
+
args
|
|
1647
|
+
} });
|
|
1566
1648
|
await new Promise((resolve) => {
|
|
1567
1649
|
const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
|
|
1568
1650
|
this.activeProc = proc;
|
|
@@ -1601,8 +1683,14 @@ var ExportEngine = class {
|
|
|
1601
1683
|
return;
|
|
1602
1684
|
}
|
|
1603
1685
|
if (code !== 0) {
|
|
1686
|
+
this.deps.logger.error("export ffmpeg failed", { meta: {
|
|
1687
|
+
exportId: id,
|
|
1688
|
+
code,
|
|
1689
|
+
outPath,
|
|
1690
|
+
stderr: stderrTail.join("\n")
|
|
1691
|
+
} });
|
|
1604
1692
|
await this.deps.removeFile(outPath);
|
|
1605
|
-
await this.fail(id, `ffmpeg exited ${code}: ${stderrTail.join(" | ")}`);
|
|
1693
|
+
await this.fail(id, `ffmpeg exited ${code}: ${stderrTail.slice(-12).join(" | ")}`);
|
|
1606
1694
|
return;
|
|
1607
1695
|
}
|
|
1608
1696
|
let bytes = 0;
|
|
@@ -1618,12 +1706,21 @@ var ExportEngine = class {
|
|
|
1618
1706
|
fileBytes: bytes,
|
|
1619
1707
|
error: null
|
|
1620
1708
|
});
|
|
1709
|
+
this.deps.logger.info("export render ready", { meta: {
|
|
1710
|
+
exportId: id,
|
|
1711
|
+
outPath,
|
|
1712
|
+
bytes
|
|
1713
|
+
} });
|
|
1621
1714
|
this.deps.emit(require_dist.EventCategory.RecordingExportCompleted, {
|
|
1622
1715
|
exportId: id,
|
|
1623
1716
|
fileBytes: bytes
|
|
1624
1717
|
});
|
|
1625
1718
|
}
|
|
1626
1719
|
async fail(id, error) {
|
|
1720
|
+
this.deps.logger.warn("export render failed", { meta: {
|
|
1721
|
+
exportId: id,
|
|
1722
|
+
error
|
|
1723
|
+
} });
|
|
1627
1724
|
await patchExport(this.deps.state, id, {
|
|
1628
1725
|
state: "failed",
|
|
1629
1726
|
progressPct: null,
|
|
@@ -2213,7 +2310,7 @@ function startSegmentWatcher(deps) {
|
|
|
2213
2310
|
}
|
|
2214
2311
|
//#endregion
|
|
2215
2312
|
//#region src/recorder/addon/ffmpeg-args.ts
|
|
2216
|
-
/** ffmpeg argv: pull RTSP (TCP), copy video / transcode audio to
|
|
2313
|
+
/** ffmpeg argv: pull RTSP (TCP), copy video / transcode audio to Opus, write
|
|
2217
2314
|
* fragmented-mp4 segments named by epoch seconds FLAT under `outDir`, plus a
|
|
2218
2315
|
* rolling `live.m3u8` playlist. The recorder segment-watcher tails the
|
|
2219
2316
|
* playlist and hands each finalized flat file to `SegmentStore.onFinalized`,
|
|
@@ -2232,7 +2329,9 @@ function buildPassthroughArgs(a) {
|
|
|
2232
2329
|
"copy",
|
|
2233
2330
|
...a.videoCodec === "H265" ? ["-tag:v", "hvc1"] : [],
|
|
2234
2331
|
"-c:a",
|
|
2235
|
-
"
|
|
2332
|
+
"libopus",
|
|
2333
|
+
"-b:a",
|
|
2334
|
+
"48k",
|
|
2236
2335
|
"-f",
|
|
2237
2336
|
"segment",
|
|
2238
2337
|
"-segment_time",
|
|
@@ -2920,8 +3019,12 @@ var ScrubPackService = class {
|
|
|
2920
3019
|
this.inflight.set(key, run);
|
|
2921
3020
|
return run;
|
|
2922
3021
|
}
|
|
3022
|
+
resolveCacheDir() {
|
|
3023
|
+
const c = this.deps.cacheDir;
|
|
3024
|
+
return typeof c === "function" ? c() : c;
|
|
3025
|
+
}
|
|
2923
3026
|
cachePath(deviceId, windowStartMs) {
|
|
2924
|
-
return node_path.default.join(this.
|
|
3027
|
+
return node_path.default.join(this.resolveCacheDir(), String(deviceId), utcDay(windowStartMs), `${windowStartMs}.pack`);
|
|
2925
3028
|
}
|
|
2926
3029
|
async getOrBuildInner(deviceId, windowStartMs) {
|
|
2927
3030
|
const now = (this.deps.now ?? Date.now)();
|
|
@@ -2998,14 +3101,15 @@ var ScrubPackService = class {
|
|
|
2998
3101
|
/** Remove per-device day directories older than the retention horizon. */
|
|
2999
3102
|
async prune(retentionDays) {
|
|
3000
3103
|
const cutoff = (this.deps.now ?? Date.now)() - retentionDays * 864e5;
|
|
3104
|
+
const cacheDir = this.resolveCacheDir();
|
|
3001
3105
|
let deviceDirs = [];
|
|
3002
3106
|
try {
|
|
3003
|
-
deviceDirs = await node_fs.promises.readdir(
|
|
3107
|
+
deviceDirs = await node_fs.promises.readdir(cacheDir);
|
|
3004
3108
|
} catch {
|
|
3005
3109
|
return;
|
|
3006
3110
|
}
|
|
3007
3111
|
for (const dev of deviceDirs) {
|
|
3008
|
-
const devPath = node_path.default.join(
|
|
3112
|
+
const devPath = node_path.default.join(cacheDir, dev);
|
|
3009
3113
|
let days = [];
|
|
3010
3114
|
try {
|
|
3011
3115
|
days = await node_fs.promises.readdir(devPath);
|
|
@@ -3440,8 +3544,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3440
3544
|
}
|
|
3441
3545
|
});
|
|
3442
3546
|
const exportsState = this.exportsStore();
|
|
3443
|
-
const
|
|
3444
|
-
const exportOutPath = (rec) => node_path.default.join(
|
|
3547
|
+
const exportsDir = node_path.default.join(this.ctx.dataDir, EXPORTS_PREFIX);
|
|
3548
|
+
const exportOutPath = (rec) => node_path.default.join(exportsDir, String(rec.deviceId), `${rec.id}.mp4`);
|
|
3445
3549
|
const removeExportFile = async (p) => {
|
|
3446
3550
|
await node_fs.promises.rm(p, { force: true });
|
|
3447
3551
|
};
|
|
@@ -3450,9 +3554,18 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3450
3554
|
logger: this.ctx.logger.child("export"),
|
|
3451
3555
|
state: exportsState,
|
|
3452
3556
|
resolvePlaylist: async (rec) => {
|
|
3453
|
-
|
|
3454
|
-
return
|
|
3557
|
+
await node_fs.promises.mkdir(node_path.default.dirname(exportOutPath(rec)), { recursive: true });
|
|
3558
|
+
return writeExportSourcePlaylist({
|
|
3559
|
+
deps: recordingDeps,
|
|
3560
|
+
exportsDir,
|
|
3561
|
+
exportId: rec.id,
|
|
3562
|
+
deviceId: rec.deviceId,
|
|
3563
|
+
profile: rec.profile,
|
|
3564
|
+
fromMs: rec.fromMs,
|
|
3565
|
+
toMs: rec.toMs
|
|
3566
|
+
});
|
|
3455
3567
|
},
|
|
3568
|
+
cleanupPlaylist: removeExportFile,
|
|
3456
3569
|
outPath: exportOutPath,
|
|
3457
3570
|
statBytes: async (p) => (await node_fs.promises.stat(p)).size,
|
|
3458
3571
|
removeFile: removeExportFile,
|
|
@@ -3491,7 +3604,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3491
3604
|
const abs = this.resolveSegmentAbsPath(row);
|
|
3492
3605
|
return new Uint8Array(await node_fs.promises.readFile(abs));
|
|
3493
3606
|
},
|
|
3494
|
-
cacheDir:
|
|
3607
|
+
cacheDir: () => this.resolveScrubCacheRoot(),
|
|
3495
3608
|
logger: this.ctx.logger.child("ScrubPacks")
|
|
3496
3609
|
});
|
|
3497
3610
|
this.scrubPackService = scrubService;
|
|
@@ -3625,6 +3738,10 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3625
3738
|
this.ctx.logger.warn("recorder: no recordings storage locations resolved — index not hydrated");
|
|
3626
3739
|
return;
|
|
3627
3740
|
}
|
|
3741
|
+
node_fs.promises.rm(node_path.default.join(this.ctx.dataDir, "scrub-packs"), {
|
|
3742
|
+
recursive: true,
|
|
3743
|
+
force: true
|
|
3744
|
+
}).catch(() => {});
|
|
3628
3745
|
try {
|
|
3629
3746
|
await this.controller?.start();
|
|
3630
3747
|
} catch (err) {
|
|
@@ -3914,6 +4031,23 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3914
4031
|
return node_path.default.join(loc.root, row.path);
|
|
3915
4032
|
}
|
|
3916
4033
|
/**
|
|
4034
|
+
* Cache root for the scrub packs — a hidden `.scrub-packs` dir UNDER a
|
|
4035
|
+
* recordings storage root (the `recordingsLow` location the packs derive from,
|
|
4036
|
+
* else the first resolved location). Packs are a regenerable keyframe cache of
|
|
4037
|
+
* the LOW segments; keeping them beside the recordings (a top-level mount, NOT
|
|
4038
|
+
* backed up) — instead of in `addons-data` — keeps the backup perimeter pure
|
|
4039
|
+
* operator state so it can be tarred whole with no exclusion list. See ADR-0027.
|
|
4040
|
+
*
|
|
4041
|
+
* Resolved lazily: `resolvedLocations` is only populated in `onHubReachable`,
|
|
4042
|
+
* after the ScrubPackService is constructed in `onInitialize`. getOrBuild/prune
|
|
4043
|
+
* run only at request time / on a delayed timer, by when locations are ready.
|
|
4044
|
+
*/
|
|
4045
|
+
resolveScrubCacheRoot() {
|
|
4046
|
+
const loc = this.resolvedLocations.find((l) => l.type === "recordingsLow") ?? this.resolvedLocations[0];
|
|
4047
|
+
if (loc === void 0) return node_path.default.join(this.ctx.dataDir, "scrub-packs");
|
|
4048
|
+
return node_path.default.join(loc.root, ".scrub-packs");
|
|
4049
|
+
}
|
|
4050
|
+
/**
|
|
3917
4051
|
* Scrub-pack request handler: `GET /<deviceId>/<windowStartSec>` →
|
|
3918
4052
|
* `application/octet-stream` pack | 204 (no footage) | 400 (bad path / window
|
|
3919
4053
|
* not aligned to `SCRUB_WINDOW_MS`) | 500. Immutable (fully past) windows carry
|