@camstack/addon-pipeline 1.2.11 → 1.2.12
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-B89zyHyl.js} +1265 -1176
- package/dist/{dist-owmpaeIY.mjs → dist-DMiEnBkk.mjs} +1265 -1176
- 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 +148 -42
- package/dist/recorder/index.mjs +148 -42
- package/dist/{step-definitions-C2bNlww3.js → step-definitions-DLgHGq8f.js} +1 -1
- package/dist/{step-definitions-CkvctwuS.mjs → step-definitions-NeFJ6mFi.mjs} +1 -1
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CkiyBstP.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CAvWbzaf.mjs} +1 -1
- 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-DbC3MMjB.mjs} +1 -1
- package/dist/stream-broker/{hostInit-BC07ueda.mjs → hostInit-Do62nL9c.mjs} +1 -1
- package/dist/stream-broker/index.js +1 -1
- package/dist/stream-broker/index.mjs +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CftzR7Pj.js → MaskShapeCanvas-DI4BY7W2-CNyCTsIC.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DPgVIdEn.js → MotionZonesSettings-NcxxQN8r-DOdX9Yp1.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-D0HqiXHW.js → PrivacyMaskSettings-APgPLF7p-9huXsgpT.js} +1 -1
- package/embed-dist/assets/index-BxOCtm8E.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
|
@@ -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-B89zyHyl.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-DMiEnBkk.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-B89zyHyl.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-DLgHGq8f.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-DMiEnBkk.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-NeFJ6mFi.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,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-B89zyHyl.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
5
|
const require_fmp4_keyframes = require("../fmp4-keyframes-BzTPoPVn.js");
|
|
@@ -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,
|
|
@@ -3440,8 +3537,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3440
3537
|
}
|
|
3441
3538
|
});
|
|
3442
3539
|
const exportsState = this.exportsStore();
|
|
3443
|
-
const
|
|
3444
|
-
const exportOutPath = (rec) => node_path.default.join(
|
|
3540
|
+
const exportsDir = node_path.default.join(this.ctx.dataDir, EXPORTS_PREFIX);
|
|
3541
|
+
const exportOutPath = (rec) => node_path.default.join(exportsDir, String(rec.deviceId), `${rec.id}.mp4`);
|
|
3445
3542
|
const removeExportFile = async (p) => {
|
|
3446
3543
|
await node_fs.promises.rm(p, { force: true });
|
|
3447
3544
|
};
|
|
@@ -3450,9 +3547,18 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
3450
3547
|
logger: this.ctx.logger.child("export"),
|
|
3451
3548
|
state: exportsState,
|
|
3452
3549
|
resolvePlaylist: async (rec) => {
|
|
3453
|
-
|
|
3454
|
-
return
|
|
3550
|
+
await node_fs.promises.mkdir(node_path.default.dirname(exportOutPath(rec)), { recursive: true });
|
|
3551
|
+
return writeExportSourcePlaylist({
|
|
3552
|
+
deps: recordingDeps,
|
|
3553
|
+
exportsDir,
|
|
3554
|
+
exportId: rec.id,
|
|
3555
|
+
deviceId: rec.deviceId,
|
|
3556
|
+
profile: rec.profile,
|
|
3557
|
+
fromMs: rec.fromMs,
|
|
3558
|
+
toMs: rec.toMs
|
|
3559
|
+
});
|
|
3455
3560
|
},
|
|
3561
|
+
cleanupPlaylist: removeExportFile,
|
|
3456
3562
|
outPath: exportOutPath,
|
|
3457
3563
|
statBytes: async (p) => (await node_fs.promises.stat(p)).size,
|
|
3458
3564
|
removeFile: removeExportFile,
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../model-download-service-Cp9f4dk6-WO-foD7W.mjs";
|
|
2
|
-
import { $ as selectAssignedProfileSlots, F as recordingExportCapability, G as DeviceType, H as BaseAddon, L as storageEvictableCapability, P as recordingCapability, V as errMsg, c as EVENT_PAD_MS, f as OpsLogEntrySchema, ft as EventCategory, j as nodePin, k as migrateConfigToBands, lt as record, nt as array, p as RecordingConfigSchema, q as hydrateSchema, u as ExportRecordSchema, ut as string } from "../dist-
|
|
2
|
+
import { $ as selectAssignedProfileSlots, F as recordingExportCapability, G as DeviceType, H as BaseAddon, L as storageEvictableCapability, P as recordingCapability, V as errMsg, c as EVENT_PAD_MS, f as OpsLogEntrySchema, ft as EventCategory, j as nodePin, k as migrateConfigToBands, lt as record, nt as array, p as RecordingConfigSchema, q as hydrateSchema, u as ExportRecordSchema, ut as string } from "../dist-DMiEnBkk.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { t as extractKeyframes } from "../fmp4-keyframes-qOgJBjMO.mjs";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
@@ -951,15 +951,6 @@ function noPlayback(deviceId) {
|
|
|
951
951
|
function playbackDirFor(dataDir, deviceId) {
|
|
952
952
|
return path.join(dataDir, "playback", String(deviceId));
|
|
953
953
|
}
|
|
954
|
-
/**
|
|
955
|
-
* Absolute path of ONE profile's variant playlist inside the playback dir.
|
|
956
|
-
* `buildPlaybackManifest` writes `<profile>.m3u8` next to the master whenever
|
|
957
|
-
* that profile has overlapping footage — the export engine renders this variant
|
|
958
|
-
* (a single-profile source) rather than the multi-variant master.
|
|
959
|
-
*/
|
|
960
|
-
function playbackVariantPlaylistPath(dataDir, deviceId, profile) {
|
|
961
|
-
return path.join(playbackDirFor(dataDir, deviceId), `${profile}.m3u8`);
|
|
962
|
-
}
|
|
963
954
|
/** Prefix window the CODECS probe reads — the fragment's moov (with its
|
|
964
955
|
* avcC/hvcC) leads the file and is ~1-2KB, so 64KB is a generous bound. */
|
|
965
956
|
var PROBE_PREFIX_BYTES = 65536;
|
|
@@ -997,6 +988,43 @@ async function probeVariantCodec(readPrefix, cache, deviceId, profile, firstPres
|
|
|
997
988
|
}
|
|
998
989
|
}
|
|
999
990
|
/**
|
|
991
|
+
* Collect the segments of one (device, profile) overlapping `[fromMs, toMs)`
|
|
992
|
+
* that are actually present on disk, as playlist entries in `uriMode`'s shape.
|
|
993
|
+
*
|
|
994
|
+
* Absolute resolution is the SAME idiom as the scrub path's
|
|
995
|
+
* `resolveSegmentAbsPath`: `<root for the row's locationId>/<relative path>`.
|
|
996
|
+
* A row whose location is unknown, or whose file has been evicted since the
|
|
997
|
+
* index row was written, is skipped so the playlist never references a
|
|
998
|
+
* non-existent segment.
|
|
999
|
+
*/
|
|
1000
|
+
async function collectRangeSegments(deps, deviceId, profile, fromMs, toMs, uriMode) {
|
|
1001
|
+
const rootById = new Map(deps.locations().map((l) => [l.id, l.root]));
|
|
1002
|
+
const rows = deps.index.segments(deviceId, profile).filter((s) => s.startMs < toMs && s.startMs + s.durMs > fromMs).toSorted((a, b) => a.startMs - b.startMs);
|
|
1003
|
+
const segments = [];
|
|
1004
|
+
let firstPresentAbs = null;
|
|
1005
|
+
for (const s of rows) {
|
|
1006
|
+
const rel = segmentRelPath(deviceId, profile, s.startMs, s.durMs, s.bytes);
|
|
1007
|
+
const root = rootById.get(s.locationId);
|
|
1008
|
+
if (root === void 0) continue;
|
|
1009
|
+
const abs = path.join(root, rel);
|
|
1010
|
+
try {
|
|
1011
|
+
await promises.stat(abs);
|
|
1012
|
+
} catch {
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
if (firstPresentAbs === null) firstPresentAbs = abs;
|
|
1016
|
+
segments.push({
|
|
1017
|
+
uri: uriMode === "absolute" ? abs : rel.split("/").slice(1).join("/"),
|
|
1018
|
+
startMs: s.startMs,
|
|
1019
|
+
durMs: s.durMs
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
segments,
|
|
1024
|
+
firstPresentAbs
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1000
1028
|
* Build a master + per-profile variant HLS playlist on disk for the requested
|
|
1001
1029
|
* window and return a RELATIVE playback URL through the hub's data-plane
|
|
1002
1030
|
* reverse-proxy. Only segments actually present on disk are referenced (a
|
|
@@ -1009,32 +1037,11 @@ async function buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs) {
|
|
|
1009
1037
|
const base = deps.playbackBaseUrl();
|
|
1010
1038
|
if (base === null) return noPlayback(deviceId);
|
|
1011
1039
|
const readPrefix = deps.readFilePrefix ?? readPrefixFromDisk;
|
|
1012
|
-
const rootById = new Map(deps.locations().map((l) => [l.id, l.root]));
|
|
1013
1040
|
const playbackDir = playbackDirFor(deps.dataDir, deviceId);
|
|
1014
1041
|
const profiles = [...new Set(deps.index.segments(deviceId).map((s) => s.profile))];
|
|
1015
1042
|
const variants = [];
|
|
1016
1043
|
for (const profile of profiles) {
|
|
1017
|
-
const
|
|
1018
|
-
if (segs.length === 0) continue;
|
|
1019
|
-
const variantSegments = [];
|
|
1020
|
-
let firstPresentAbs = null;
|
|
1021
|
-
for (const s of segs) {
|
|
1022
|
-
const rel = segmentRelPath(deviceId, profile, s.startMs, s.durMs, s.bytes);
|
|
1023
|
-
const root = rootById.get(s.locationId);
|
|
1024
|
-
if (root === void 0) continue;
|
|
1025
|
-
const abs = path.join(root, rel);
|
|
1026
|
-
try {
|
|
1027
|
-
await promises.stat(abs);
|
|
1028
|
-
} catch {
|
|
1029
|
-
continue;
|
|
1030
|
-
}
|
|
1031
|
-
if (firstPresentAbs === null) firstPresentAbs = abs;
|
|
1032
|
-
variantSegments.push({
|
|
1033
|
-
uri: rel.split("/").slice(1).join("/"),
|
|
1034
|
-
startMs: s.startMs,
|
|
1035
|
-
durMs: s.durMs
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1044
|
+
const { segments: variantSegments, firstPresentAbs } = await collectRangeSegments(deps, deviceId, profile, fromMs, toMs, "playback-relative");
|
|
1038
1045
|
if (variantSegments.length === 0) continue;
|
|
1039
1046
|
const codecs = await probeVariantCodec(readPrefix, codecCache, deviceId, profile, firstPresentAbs);
|
|
1040
1047
|
await promises.mkdir(playbackDir, { recursive: true });
|
|
@@ -1277,6 +1284,48 @@ function buildRecordingProvider(deps) {
|
|
|
1277
1284
|
};
|
|
1278
1285
|
}
|
|
1279
1286
|
//#endregion
|
|
1287
|
+
//#region src/recorder/addon/export-source-playlist.ts
|
|
1288
|
+
/**
|
|
1289
|
+
* The ffmpeg INPUT playlist for one recording export.
|
|
1290
|
+
*
|
|
1291
|
+
* Exports must NOT reuse the playback variant playlist. That file lives under
|
|
1292
|
+
* `<dataDir>/playback` while its segments live under a recordings LOCATION root,
|
|
1293
|
+
* and the two trees are unified only by the HTTP data-plane's ordered multi-root
|
|
1294
|
+
* search. ffmpeg has no such fallback — it resolves every segment URI against
|
|
1295
|
+
* the playlist's own directory — so a playback playlist handed to `-i` ENOENTs
|
|
1296
|
+
* on every segment and dies with `AVERROR_INVALIDDATA` (exit 183).
|
|
1297
|
+
*
|
|
1298
|
+
* So each export gets its OWN playlist whose segment URIs are ABSOLUTE on-disk
|
|
1299
|
+
* paths, written at a PER-EXPORT path. Per-export is load-bearing twice over:
|
|
1300
|
+
* the playback playlist sits at a shared fixed path that `buildPlaybackManifest`
|
|
1301
|
+
* rewrites on every playback request, so an operator scrubbing the same camera
|
|
1302
|
+
* mid-render would otherwise swap ffmpeg's input out from under it.
|
|
1303
|
+
*
|
|
1304
|
+
* The file is transient — `ExportEngine` deletes it when the render finishes
|
|
1305
|
+
* (success, failure or cancel).
|
|
1306
|
+
*/
|
|
1307
|
+
/**
|
|
1308
|
+
* Absolute path of ONE export's source playlist:
|
|
1309
|
+
* `<exportsDir>/<deviceId>/<exportId>.src.m3u8`. Unique per export id, so two
|
|
1310
|
+
* concurrent renders (and any concurrent playback request) never share a file.
|
|
1311
|
+
*/
|
|
1312
|
+
function exportSourcePlaylistPath(exportsDir, deviceId, exportId) {
|
|
1313
|
+
return path.join(exportsDir, String(deviceId), `${exportId}.src.m3u8`);
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Write the export's source playlist and return its absolute path, or null when
|
|
1317
|
+
* no segment of the requested profile is present on disk for the range (the
|
|
1318
|
+
* engine then fails the job with 'no footage for range').
|
|
1319
|
+
*/
|
|
1320
|
+
async function writeExportSourcePlaylist(input) {
|
|
1321
|
+
const { segments } = await collectRangeSegments(input.deps, input.deviceId, input.profile, input.fromMs, input.toMs, "absolute");
|
|
1322
|
+
if (segments.length === 0) return null;
|
|
1323
|
+
const playlistPath = exportSourcePlaylistPath(input.exportsDir, input.deviceId, input.exportId);
|
|
1324
|
+
await promises.mkdir(path.dirname(playlistPath), { recursive: true });
|
|
1325
|
+
await promises.writeFile(playlistPath, buildVariantPlaylist(segments), "utf8");
|
|
1326
|
+
return playlistPath;
|
|
1327
|
+
}
|
|
1328
|
+
//#endregion
|
|
1280
1329
|
//#region src/recorder/addon/export-store.ts
|
|
1281
1330
|
/**
|
|
1282
1331
|
* Recording-export persistence for the recorder addon.
|
|
@@ -1476,16 +1525,22 @@ function buildExportArgs(input) {
|
|
|
1476
1525
|
* Render engine for recording exports.
|
|
1477
1526
|
*
|
|
1478
1527
|
* A FIFO queue with ONE concurrent ffmpeg render (exports are heavy; serialising
|
|
1479
|
-
* protects the recording node). Each job:
|
|
1480
|
-
* playlist → build argv (`buildExportArgs`) → spawn ffmpeg →
|
|
1481
|
-
* stderr for progress → on exit 0 stat the file and mark
|
|
1482
|
-
* mark `failed` and remove the partial
|
|
1528
|
+
* protects the recording node). Each job: write the range's per-export source
|
|
1529
|
+
* playlist (`resolvePlaylist`) → build argv (`buildExportArgs`) → spawn ffmpeg →
|
|
1530
|
+
* parse `time=` from stderr for progress → on exit 0 stat the file and mark
|
|
1531
|
+
* `ready`; on non-zero mark `failed` and remove the partial; drop the source
|
|
1532
|
+
* playlist whatever the outcome. `cancel` SIGKILLs an in-flight render.
|
|
1483
1533
|
* State transitions patch the DurableState store and emit telemetry events.
|
|
1484
1534
|
*
|
|
1485
1535
|
* Unlike `SegmentWriter` there is NO auto-restart — a render runs once; boot
|
|
1486
1536
|
* re-queue of interrupted jobs is the provider's concern.
|
|
1487
1537
|
*/
|
|
1488
|
-
|
|
1538
|
+
/** Stderr lines retained for the failure LOG. ffmpeg prints one line per failed
|
|
1539
|
+
* segment before the decisive "Error opening input" summary, so a short tail can
|
|
1540
|
+
* push the actual cause out of the window (it very nearly did — the line that
|
|
1541
|
+
* identified the ENOENT'd segment paths was the 12th). Bounded, because a long
|
|
1542
|
+
* render writes one progress line per second. */
|
|
1543
|
+
var STDERR_TAIL_LINES$1 = 40;
|
|
1489
1544
|
var TIME_RE = /time=(\d+):(\d+):(\d+(?:\.\d+)?)/;
|
|
1490
1545
|
/** Expected OUTPUT duration (seconds) for progress scaling. */
|
|
1491
1546
|
function expectedOutSeconds(rec) {
|
|
@@ -1554,6 +1609,22 @@ var ExportEngine = class {
|
|
|
1554
1609
|
await this.fail(id, "no footage for range");
|
|
1555
1610
|
return;
|
|
1556
1611
|
}
|
|
1612
|
+
try {
|
|
1613
|
+
await this.spawnRender(id, rec, playlist);
|
|
1614
|
+
} finally {
|
|
1615
|
+
if (this.deps.cleanupPlaylist) try {
|
|
1616
|
+
await this.deps.cleanupPlaylist(playlist);
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
this.deps.logger.warn("export: source playlist cleanup failed", { meta: {
|
|
1619
|
+
exportId: id,
|
|
1620
|
+
playlist,
|
|
1621
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1622
|
+
} });
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
/** Spawn ffmpeg for one render and resolve when it has exited + finalized. */
|
|
1627
|
+
async spawnRender(id, rec, playlist) {
|
|
1557
1628
|
const outPath = this.deps.outPath(rec);
|
|
1558
1629
|
const args = buildExportArgs({
|
|
1559
1630
|
inputPlaylist: playlist,
|
|
@@ -1561,6 +1632,17 @@ var ExportEngine = class {
|
|
|
1561
1632
|
options: rec.options
|
|
1562
1633
|
});
|
|
1563
1634
|
const totalSec = expectedOutSeconds(rec);
|
|
1635
|
+
this.deps.logger.info("export render starting", { meta: {
|
|
1636
|
+
exportId: id,
|
|
1637
|
+
deviceId: rec.deviceId,
|
|
1638
|
+
profile: rec.profile,
|
|
1639
|
+
playlist,
|
|
1640
|
+
outPath
|
|
1641
|
+
} });
|
|
1642
|
+
this.deps.logger.debug("export ffmpeg argv", { meta: {
|
|
1643
|
+
exportId: id,
|
|
1644
|
+
args
|
|
1645
|
+
} });
|
|
1564
1646
|
await new Promise((resolve) => {
|
|
1565
1647
|
const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
|
|
1566
1648
|
this.activeProc = proc;
|
|
@@ -1599,8 +1681,14 @@ var ExportEngine = class {
|
|
|
1599
1681
|
return;
|
|
1600
1682
|
}
|
|
1601
1683
|
if (code !== 0) {
|
|
1684
|
+
this.deps.logger.error("export ffmpeg failed", { meta: {
|
|
1685
|
+
exportId: id,
|
|
1686
|
+
code,
|
|
1687
|
+
outPath,
|
|
1688
|
+
stderr: stderrTail.join("\n")
|
|
1689
|
+
} });
|
|
1602
1690
|
await this.deps.removeFile(outPath);
|
|
1603
|
-
await this.fail(id, `ffmpeg exited ${code}: ${stderrTail.join(" | ")}`);
|
|
1691
|
+
await this.fail(id, `ffmpeg exited ${code}: ${stderrTail.slice(-12).join(" | ")}`);
|
|
1604
1692
|
return;
|
|
1605
1693
|
}
|
|
1606
1694
|
let bytes = 0;
|
|
@@ -1616,12 +1704,21 @@ var ExportEngine = class {
|
|
|
1616
1704
|
fileBytes: bytes,
|
|
1617
1705
|
error: null
|
|
1618
1706
|
});
|
|
1707
|
+
this.deps.logger.info("export render ready", { meta: {
|
|
1708
|
+
exportId: id,
|
|
1709
|
+
outPath,
|
|
1710
|
+
bytes
|
|
1711
|
+
} });
|
|
1619
1712
|
this.deps.emit(EventCategory.RecordingExportCompleted, {
|
|
1620
1713
|
exportId: id,
|
|
1621
1714
|
fileBytes: bytes
|
|
1622
1715
|
});
|
|
1623
1716
|
}
|
|
1624
1717
|
async fail(id, error) {
|
|
1718
|
+
this.deps.logger.warn("export render failed", { meta: {
|
|
1719
|
+
exportId: id,
|
|
1720
|
+
error
|
|
1721
|
+
} });
|
|
1625
1722
|
await patchExport(this.deps.state, id, {
|
|
1626
1723
|
state: "failed",
|
|
1627
1724
|
progressPct: null,
|
|
@@ -3438,8 +3535,8 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
3438
3535
|
}
|
|
3439
3536
|
});
|
|
3440
3537
|
const exportsState = this.exportsStore();
|
|
3441
|
-
const
|
|
3442
|
-
const exportOutPath = (rec) => path.join(
|
|
3538
|
+
const exportsDir = path.join(this.ctx.dataDir, EXPORTS_PREFIX);
|
|
3539
|
+
const exportOutPath = (rec) => path.join(exportsDir, String(rec.deviceId), `${rec.id}.mp4`);
|
|
3443
3540
|
const removeExportFile = async (p) => {
|
|
3444
3541
|
await promises.rm(p, { force: true });
|
|
3445
3542
|
};
|
|
@@ -3448,9 +3545,18 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
3448
3545
|
logger: this.ctx.logger.child("export"),
|
|
3449
3546
|
state: exportsState,
|
|
3450
3547
|
resolvePlaylist: async (rec) => {
|
|
3451
|
-
|
|
3452
|
-
return
|
|
3548
|
+
await promises.mkdir(path.dirname(exportOutPath(rec)), { recursive: true });
|
|
3549
|
+
return writeExportSourcePlaylist({
|
|
3550
|
+
deps: recordingDeps,
|
|
3551
|
+
exportsDir,
|
|
3552
|
+
exportId: rec.id,
|
|
3553
|
+
deviceId: rec.deviceId,
|
|
3554
|
+
profile: rec.profile,
|
|
3555
|
+
fromMs: rec.fromMs,
|
|
3556
|
+
toMs: rec.toMs
|
|
3557
|
+
});
|
|
3453
3558
|
},
|
|
3559
|
+
cleanupPlaylist: removeExportFile,
|
|
3454
3560
|
outPath: exportOutPath,
|
|
3455
3561
|
statBytes: async (p) => (await promises.stat(p)).size,
|
|
3456
3562
|
removeFile: removeExportFile,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { E as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-
|
|
1
|
+
import { E as hfModelUrl, a as COCO_TO_MACRO, i as COCO_80_LABELS, r as AUDIO_MACRO_LABELS } from "./dist-DMiEnBkk.mjs";
|
|
2
2
|
//#region src/detection-pipeline/registry/model-catalogs.ts
|
|
3
3
|
var HF_REPO = "camstack/camstack-models";
|
|
4
4
|
var HF_SCRYPTED = "scrypted/plugin-models";
|
|
@@ -2,7 +2,7 @@ import { a as e, c as t, d as n, i as r, l as i, n as a, o, r as s, s as c, t as
|
|
|
2
2
|
import { a as d, i as f, n as p, o as m, r as h, t as g } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
|
|
3
3
|
import { n as _, r as v, t as y } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
|
|
4
4
|
import { n as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
|
|
5
|
-
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-
|
|
5
|
+
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DbC3MMjB.mjs";
|
|
6
6
|
//#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
7
7
|
var C = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), w = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), T = (e) => {
|
|
8
8
|
let t = w(e);
|