@camstack/addon-pipeline 1.2.90 → 1.2.93
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/{addon-utils-yc-chXuC.js → addon-utils-CTRpEiSd.js} +1 -1
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-BJYwWEZ9.mjs → dist-6POVIYmC.mjs} +91 -4
- package/dist/{dist-n4Nbnxx5.js → dist-D9To_r_d.js} +96 -3
- package/dist/{event-loop-stall-monitor-BiPffk0D.mjs → event-loop-stall-monitor-C-VvWOxS.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-DG7QY00k.js → event-loop-stall-monitor-CGacbzXq.js} +1 -1
- package/dist/{lazy-sharp-EoSOHgoj.js → lazy-sharp-C3zOnmiU.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/recorder/index.js +134 -21
- package/dist/recorder/index.mjs +133 -20
- package/dist/session-decode/decode-worker-child.js +2 -2
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CsrMZYLB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-C2woB6bY.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dut56EeQ.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-DgivrPsH.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DsIi4G5Q.mjs} +1 -1
- package/dist/stream-broker/{hostInit-DoFovQmc.mjs → hostInit-VCURcZLt.mjs} +2 -2
- package/dist/stream-broker/index.js +194 -30
- package/dist/stream-broker/index.mjs +194 -30
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-DjuxhGo3.js → worker-protocol-Bq28qobd.js} +1 -1
- package/dist/{worker-protocol-XZ1e36rM.mjs → worker-protocol-PzAKg8mC.mjs} +1 -1
- 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-DNSELPGi.mjs +0 -26
package/dist/recorder/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const require_dist = require("../dist-
|
|
1
|
+
const require_dist = require("../dist-D9To_r_d.js");
|
|
2
2
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
3
|
-
const require_addon_utils = require("../addon-utils-
|
|
3
|
+
const require_addon_utils = require("../addon-utils-CTRpEiSd.js");
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_child_process = require("node:child_process");
|
|
6
6
|
let node_path = require("node:path");
|
|
@@ -1069,6 +1069,7 @@ var RelocateEngine = class {
|
|
|
1069
1069
|
deviceId: input.deviceId ?? null,
|
|
1070
1070
|
profiles: input.profiles ? [...input.profiles] : [],
|
|
1071
1071
|
footageClass: input.footageClass,
|
|
1072
|
+
sinceMs: input.sinceMs ?? null,
|
|
1072
1073
|
throttleMbps: input.throttleMbps ?? DEFAULT_THROTTLE_MBPS,
|
|
1073
1074
|
entities: input.entities ? [...input.entities] : ["segments"],
|
|
1074
1075
|
filesMoved: 0,
|
|
@@ -1129,7 +1130,7 @@ var RelocateEngine = class {
|
|
|
1129
1130
|
const bytesPerMs = job.throttleMbps * 1024 * 1024 / 1e3;
|
|
1130
1131
|
try {
|
|
1131
1132
|
const samePhysicalRoot = await rootsPhysicallySame(from.root, to.root);
|
|
1132
|
-
const segRows = job.entities.includes("segments") === false ? [] : this.deps.index.segmentsOnLocation(from.id).filter((r) => job.deviceId === null || r.deviceId === job.deviceId).filter((r) => job.profiles.length === 0 || job.profiles.includes(r.profile)).filter((r) => profileBelongsToClass(r.profile, job.footageClass)).sort((a, b) => a.startMs - b.startMs);
|
|
1133
|
+
const segRows = job.entities.includes("segments") === false ? [] : this.deps.index.segmentsOnLocation(from.id).filter((r) => job.deviceId === null || r.deviceId === job.deviceId).filter((r) => job.profiles.length === 0 || job.profiles.includes(r.profile)).filter((r) => profileBelongsToClass(r.profile, job.footageClass)).filter((r) => job.sinceMs === null || r.startMs >= job.sinceMs).sort((a, b) => a.startMs - b.startMs);
|
|
1133
1134
|
job.filesTotal = segRows.length;
|
|
1134
1135
|
for (const row of segRows) {
|
|
1135
1136
|
if (job.cancelRequested) break;
|
|
@@ -1165,9 +1166,26 @@ var RelocateEngine = class {
|
|
|
1165
1166
|
}
|
|
1166
1167
|
}
|
|
1167
1168
|
/** Move one segment file and re-point its index row to the target location
|
|
1168
|
-
* (same location-relative path — the layout is identical on every root).
|
|
1169
|
+
* (same location-relative path — the layout is identical on every root).
|
|
1170
|
+
* A source that is already gone (evicted, never flushed, ENOSPC) is dropped
|
|
1171
|
+
* from the index and skipped — one ghost file must not abort a drain. */
|
|
1169
1172
|
async moveSegment(row, from, to, samePhysicalRoot) {
|
|
1170
|
-
if (!samePhysicalRoot)
|
|
1173
|
+
if (!samePhysicalRoot) try {
|
|
1174
|
+
await moveFile(node_path.default.join(from.root, row.path), node_path.default.join(to.root, row.path), row.bytes);
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
if (isEnoent(err)) {
|
|
1177
|
+
this.deps.index.removeSegments([row.path]);
|
|
1178
|
+
this.deps.logger.warn("relocate skipped missing source", {
|
|
1179
|
+
tags: { deviceId: row.deviceId },
|
|
1180
|
+
meta: {
|
|
1181
|
+
path: row.path,
|
|
1182
|
+
locationId: from.id
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
throw err;
|
|
1188
|
+
}
|
|
1171
1189
|
this.deps.index.removeSegments([row.path]);
|
|
1172
1190
|
this.deps.index.addSegment({
|
|
1173
1191
|
...row,
|
|
@@ -1175,6 +1193,9 @@ var RelocateEngine = class {
|
|
|
1175
1193
|
});
|
|
1176
1194
|
}
|
|
1177
1195
|
};
|
|
1196
|
+
function isEnoent(err) {
|
|
1197
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
1198
|
+
}
|
|
1178
1199
|
/** Logical migration classes are profile-derived, never location-derived:
|
|
1179
1200
|
* high+mid are `recordings`; low is `recordingsLow`. */
|
|
1180
1201
|
var profileBelongsToClass = (profile, footageClass) => {
|
|
@@ -4213,6 +4234,7 @@ function buildRecordingProvider(deps) {
|
|
|
4213
4234
|
rescanStorage: async ({ deviceId }) => {
|
|
4214
4235
|
const locations = await deps.refreshLocations();
|
|
4215
4236
|
await deps.hydrateDevice(deviceId, locations);
|
|
4237
|
+
await deps.reconcileHourLedger?.(deviceId);
|
|
4216
4238
|
const config = await loadDeviceConfig(deps.configStore, deviceId);
|
|
4217
4239
|
await deps.opsLog?.append({
|
|
4218
4240
|
op: "rescan",
|
|
@@ -5076,7 +5098,11 @@ async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger
|
|
|
5076
5098
|
try {
|
|
5077
5099
|
entries = await node_fs.promises.readdir(deviceDir, { recursive: true });
|
|
5078
5100
|
} catch (err) {
|
|
5079
|
-
if (err.code
|
|
5101
|
+
if (err.code === "ENOENT") {
|
|
5102
|
+
for (const location of aliases) index.hydrateDevice(deviceId, location.id, []);
|
|
5103
|
+
continue;
|
|
5104
|
+
}
|
|
5105
|
+
logger.warn("recorder: hydrateDevice walk failed for location", {
|
|
5080
5106
|
tags: { deviceId },
|
|
5081
5107
|
meta: {
|
|
5082
5108
|
locationIds: aliases.map((location) => location.id),
|
|
@@ -5104,6 +5130,7 @@ async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger
|
|
|
5104
5130
|
handled += 1;
|
|
5105
5131
|
if (handled % chunkSize === 0) await yieldBetween();
|
|
5106
5132
|
}
|
|
5133
|
+
for (const location of aliases) if (!pathsByLocation.has(location.id)) pathsByLocation.set(location.id, []);
|
|
5107
5134
|
let indexed = 0;
|
|
5108
5135
|
for (const [locationId, relPaths] of pathsByLocation) {
|
|
5109
5136
|
index.hydrateDevice(deviceId, locationId, relPaths);
|
|
@@ -6563,6 +6590,13 @@ var RecordingController = class {
|
|
|
6563
6590
|
* records again.
|
|
6564
6591
|
*/
|
|
6565
6592
|
unrecordableReported = /* @__PURE__ */ new Set();
|
|
6593
|
+
/**
|
|
6594
|
+
* Profiles the last attach skipped (stream not published / getStream threw).
|
|
6595
|
+
* `evaluateDevice` retries only these on the converge tick so a dead `low`
|
|
6596
|
+
* cannot keep `high` dark, without a `listAllProfileSlots` RPC on every
|
|
6597
|
+
* healthy device every 30s.
|
|
6598
|
+
*/
|
|
6599
|
+
skippedProfiles = /* @__PURE__ */ new Map();
|
|
6566
6600
|
restore = null;
|
|
6567
6601
|
/** Cheap, never-delayed: is any pinned writer producing nothing? */
|
|
6568
6602
|
livenessPass = null;
|
|
@@ -6769,7 +6803,10 @@ var RecordingController = class {
|
|
|
6769
6803
|
return;
|
|
6770
6804
|
}
|
|
6771
6805
|
this.unrecordableReported.delete(deviceId);
|
|
6772
|
-
if (this.active.has(deviceId))
|
|
6806
|
+
if (this.active.has(deviceId)) {
|
|
6807
|
+
if (this.skippedProfiles.has(deviceId)) await this.fillMissingProfiles(deviceId, config);
|
|
6808
|
+
return;
|
|
6809
|
+
}
|
|
6773
6810
|
if (this.attaching.has(deviceId)) return;
|
|
6774
6811
|
this.attaching.add(deviceId);
|
|
6775
6812
|
const attaching = this.performAttach(deviceId, config);
|
|
@@ -6820,8 +6857,12 @@ var RecordingController = class {
|
|
|
6820
6857
|
/**
|
|
6821
6858
|
* The attach body — extracted so the {@link attaching} in-flight guard in
|
|
6822
6859
|
* `evaluateDevice` wraps it cleanly. Resolves the profiles, spawns a
|
|
6823
|
-
* SegmentWriter + watcher per profile, and records the set in `active`.
|
|
6824
|
-
*
|
|
6860
|
+
* SegmentWriter + watcher per profile, and records the set in `active`.
|
|
6861
|
+
*
|
|
6862
|
+
* Per-profile: a throw from one slot is logged and skipped. Rolling back the
|
|
6863
|
+
* whole device when a single stream was dead (618 `derived` on `low`) left
|
|
6864
|
+
* ZERO footage for hours. If every profile fails, throw so the restore queue
|
|
6865
|
+
* retries the device.
|
|
6825
6866
|
*/
|
|
6826
6867
|
async performAttach(deviceId, config) {
|
|
6827
6868
|
this.restore?.add(deviceId, true);
|
|
@@ -6829,38 +6870,106 @@ var RecordingController = class {
|
|
|
6829
6870
|
if (profiles.length === 0) throw new Error(`recorder controller: no assigned broker sources for device ${deviceId}`);
|
|
6830
6871
|
const segmentSeconds = config.segmentSeconds ?? this.deps.segmentSeconds;
|
|
6831
6872
|
const recordings = [];
|
|
6832
|
-
|
|
6833
|
-
|
|
6873
|
+
const failed = [];
|
|
6874
|
+
for (const profile of profiles) try {
|
|
6875
|
+
recordings.push(await this.attachProfile(deviceId, profile, segmentSeconds));
|
|
6834
6876
|
} catch (err) {
|
|
6835
|
-
|
|
6836
|
-
|
|
6877
|
+
failed.push(profile);
|
|
6878
|
+
this.deps.logger.warn("recorder: profile attach failed — recording the remaining profiles", {
|
|
6879
|
+
tags: { deviceId },
|
|
6880
|
+
meta: {
|
|
6881
|
+
profile,
|
|
6882
|
+
error: require_dist.errMsg(err)
|
|
6883
|
+
}
|
|
6884
|
+
});
|
|
6837
6885
|
}
|
|
6886
|
+
if (recordings.length === 0) throw new Error(`recorder controller: no profile attached for device ${deviceId}` + (failed.length > 0 ? ` (failed: ${failed.join(",")})` : ""));
|
|
6838
6887
|
if (this.paused) {
|
|
6839
6888
|
await Promise.all(recordings.map((recording) => this.teardownProfile(deviceId, recording)));
|
|
6840
6889
|
return;
|
|
6841
6890
|
}
|
|
6842
6891
|
this.active.set(deviceId, recordings);
|
|
6892
|
+
if (failed.length > 0) this.skippedProfiles.set(deviceId, new Set(failed));
|
|
6893
|
+
else this.skippedProfiles.delete(deviceId);
|
|
6843
6894
|
this.deps.logger.info("recorder pinned broker source(s) for recording", {
|
|
6844
6895
|
tags: { deviceId },
|
|
6845
6896
|
meta: {
|
|
6846
|
-
profiles,
|
|
6897
|
+
profiles: recordings.map((r) => r.profile),
|
|
6898
|
+
failedProfiles: failed,
|
|
6847
6899
|
segmentSeconds,
|
|
6848
6900
|
pipelineKeys: recordings.map((r) => r.pipelineKey)
|
|
6849
6901
|
}
|
|
6850
6902
|
});
|
|
6851
6903
|
}
|
|
6852
6904
|
/**
|
|
6905
|
+
* Retry profiles the last attach skipped, without tearing down writers that
|
|
6906
|
+
* are already producing. Called from `evaluateDevice` when the device is
|
|
6907
|
+
* already in `active` and {@link skippedProfiles} is non-empty. Failures stay
|
|
6908
|
+
* logged; the next converge tick retries.
|
|
6909
|
+
*/
|
|
6910
|
+
async fillMissingProfiles(deviceId, config) {
|
|
6911
|
+
if (this.attaching.has(deviceId)) return;
|
|
6912
|
+
const current = this.active.get(deviceId);
|
|
6913
|
+
const skipped = this.skippedProfiles.get(deviceId);
|
|
6914
|
+
if (!current || !skipped || skipped.size === 0) return;
|
|
6915
|
+
this.attaching.add(deviceId);
|
|
6916
|
+
const filling = this.runFillMissingProfiles(deviceId, config, current, skipped);
|
|
6917
|
+
this.attachingTasks.set(deviceId, filling);
|
|
6918
|
+
try {
|
|
6919
|
+
await filling;
|
|
6920
|
+
} finally {
|
|
6921
|
+
this.attachingTasks.delete(deviceId);
|
|
6922
|
+
this.attaching.delete(deviceId);
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
6925
|
+
async runFillMissingProfiles(deviceId, config, current, skipped) {
|
|
6926
|
+
const have = new Set(current.map((r) => r.profile));
|
|
6927
|
+
const segmentSeconds = config.segmentSeconds ?? this.deps.segmentSeconds;
|
|
6928
|
+
const added = [];
|
|
6929
|
+
for (const profile of [...skipped]) {
|
|
6930
|
+
if (have.has(profile)) {
|
|
6931
|
+
skipped.delete(profile);
|
|
6932
|
+
continue;
|
|
6933
|
+
}
|
|
6934
|
+
try {
|
|
6935
|
+
added.push(await this.attachProfile(deviceId, profile, segmentSeconds));
|
|
6936
|
+
skipped.delete(profile);
|
|
6937
|
+
} catch (err) {
|
|
6938
|
+
this.deps.logger.warn("recorder: profile attach retry failed — keeping the healthy profiles", {
|
|
6939
|
+
tags: { deviceId },
|
|
6940
|
+
meta: {
|
|
6941
|
+
profile,
|
|
6942
|
+
error: require_dist.errMsg(err)
|
|
6943
|
+
}
|
|
6944
|
+
});
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
if (skipped.size === 0) this.skippedProfiles.delete(deviceId);
|
|
6948
|
+
if (added.length === 0) return;
|
|
6949
|
+
const existing = this.active.get(deviceId);
|
|
6950
|
+
if (!existing || this.paused || this.stopped) {
|
|
6951
|
+
await Promise.all(added.map((recording) => this.teardownProfile(deviceId, recording)));
|
|
6952
|
+
return;
|
|
6953
|
+
}
|
|
6954
|
+
this.active.set(deviceId, [...existing, ...added]);
|
|
6955
|
+
this.deps.logger.info("recorder pinned additional broker source(s) for recording", {
|
|
6956
|
+
tags: { deviceId },
|
|
6957
|
+
meta: {
|
|
6958
|
+
profiles: added.map((r) => r.profile),
|
|
6959
|
+
pipelineKeys: added.map((r) => r.pipelineKey)
|
|
6960
|
+
}
|
|
6961
|
+
});
|
|
6962
|
+
}
|
|
6963
|
+
/**
|
|
6853
6964
|
* Pick which profiles to record. The broker enumerates assigned profile slots,
|
|
6854
6965
|
* deduped by physical source (`selectAssignedProfileSlots`) so the same camera
|
|
6855
|
-
* encoder is never recorded twice.
|
|
6856
|
-
*
|
|
6857
|
-
*
|
|
6966
|
+
* encoder is never recorded twice. Default is high+low among those slots
|
|
6967
|
+
* (`resolveRecordingProfiles`); an operator `override` DISABLES the
|
|
6968
|
+
* unselected profiles. A selection matching none of the assigned sources
|
|
6969
|
+
* falls back to the default/assigned set (minimum of 1 when anything is assigned).
|
|
6858
6970
|
*/
|
|
6859
6971
|
async resolveProfiles(deviceId, override) {
|
|
6860
|
-
|
|
6861
|
-
if (!override || override.length === 0) return assigned;
|
|
6862
|
-
const selected = assigned.filter((p) => override.includes(p));
|
|
6863
|
-
return selected.length > 0 ? selected : assigned;
|
|
6972
|
+
return require_dist.resolveRecordingProfiles(require_dist.selectAssignedProfileSlots(await this.deps.brokerCall(() => withDeadline(this.deps.api.streamBroker.listAllProfileSlots.query(void 0, require_dist.nodePin(this.deps.ownerNodeId)), ATTACH_RPC_TIMEOUT_MS, "listAllProfileSlots")), deviceId).map((slot) => slot.profile), override);
|
|
6864
6973
|
}
|
|
6865
6974
|
/**
|
|
6866
6975
|
* Attach one (device, profile): acquire the broker source, mkdir the staging
|
|
@@ -6990,6 +7099,7 @@ var RecordingController = class {
|
|
|
6990
7099
|
const recordings = this.active.get(deviceId);
|
|
6991
7100
|
if (!recordings) return;
|
|
6992
7101
|
this.active.delete(deviceId);
|
|
7102
|
+
this.skippedProfiles.delete(deviceId);
|
|
6993
7103
|
for (const r of recordings) this.lastSegmentAt.delete(idleKey(deviceId, r.profile));
|
|
6994
7104
|
await Promise.all(recordings.map((r) => this.teardownProfile(deviceId, r)));
|
|
6995
7105
|
this.deps.logger.info("recorder unpinned broker source(s) — recording stopped", {
|
|
@@ -8606,6 +8716,9 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
8606
8716
|
refreshLocations: () => this.refreshLocations(),
|
|
8607
8717
|
capacity: (root) => this.locationCapacity(root),
|
|
8608
8718
|
hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
|
|
8719
|
+
reconcileHourLedger: async (deviceId) => {
|
|
8720
|
+
await this.segmentHours?.reconcileFromIndex(this.index, [deviceId], Date.now());
|
|
8721
|
+
},
|
|
8609
8722
|
calendar: this.calendar,
|
|
8610
8723
|
mfraFor: (deviceId, profile, startMs) => this.mfraTables.get(deviceId, profile, startMs),
|
|
8611
8724
|
hydrateWindow: (deviceId, fromMs, toMs) => hydrateWindowFromStorage(this.index, deviceId, fromMs, toMs, this.resolvedLocations, this.ctx.logger),
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as resolveRecordingProfiles, Ct as selectAssignedProfileSlots, Et as array, I as deriveRecordingMode, It as EventCategory, Mt as object, Nt as record, Pt as string, S as RecordingConfigSchema, X as recordingCapability, Z as recordingExportCapability, at as errMsg, bt as nodePin, dt as BaseAddon, f as EVENT_PAD_MS, gt as hydrateSchema, jt as number, l as DEFAULT_EVENTS_BAND_BUFFER_SEC, m as ExportRecordSchema, mt as DeviceType, tt as storageEvictableCapability, v as OpsLogEntrySchema, x as RECORDING_EXPORT_MAX_READ_BYTES } from "../dist-6POVIYmC.mjs";
|
|
2
2
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
3
3
|
import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-A2S9D7pu.mjs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -1068,6 +1068,7 @@ var RelocateEngine = class {
|
|
|
1068
1068
|
deviceId: input.deviceId ?? null,
|
|
1069
1069
|
profiles: input.profiles ? [...input.profiles] : [],
|
|
1070
1070
|
footageClass: input.footageClass,
|
|
1071
|
+
sinceMs: input.sinceMs ?? null,
|
|
1071
1072
|
throttleMbps: input.throttleMbps ?? DEFAULT_THROTTLE_MBPS,
|
|
1072
1073
|
entities: input.entities ? [...input.entities] : ["segments"],
|
|
1073
1074
|
filesMoved: 0,
|
|
@@ -1128,7 +1129,7 @@ var RelocateEngine = class {
|
|
|
1128
1129
|
const bytesPerMs = job.throttleMbps * 1024 * 1024 / 1e3;
|
|
1129
1130
|
try {
|
|
1130
1131
|
const samePhysicalRoot = await rootsPhysicallySame(from.root, to.root);
|
|
1131
|
-
const segRows = job.entities.includes("segments") === false ? [] : this.deps.index.segmentsOnLocation(from.id).filter((r) => job.deviceId === null || r.deviceId === job.deviceId).filter((r) => job.profiles.length === 0 || job.profiles.includes(r.profile)).filter((r) => profileBelongsToClass(r.profile, job.footageClass)).sort((a, b) => a.startMs - b.startMs);
|
|
1132
|
+
const segRows = job.entities.includes("segments") === false ? [] : this.deps.index.segmentsOnLocation(from.id).filter((r) => job.deviceId === null || r.deviceId === job.deviceId).filter((r) => job.profiles.length === 0 || job.profiles.includes(r.profile)).filter((r) => profileBelongsToClass(r.profile, job.footageClass)).filter((r) => job.sinceMs === null || r.startMs >= job.sinceMs).sort((a, b) => a.startMs - b.startMs);
|
|
1132
1133
|
job.filesTotal = segRows.length;
|
|
1133
1134
|
for (const row of segRows) {
|
|
1134
1135
|
if (job.cancelRequested) break;
|
|
@@ -1164,9 +1165,26 @@ var RelocateEngine = class {
|
|
|
1164
1165
|
}
|
|
1165
1166
|
}
|
|
1166
1167
|
/** Move one segment file and re-point its index row to the target location
|
|
1167
|
-
* (same location-relative path — the layout is identical on every root).
|
|
1168
|
+
* (same location-relative path — the layout is identical on every root).
|
|
1169
|
+
* A source that is already gone (evicted, never flushed, ENOSPC) is dropped
|
|
1170
|
+
* from the index and skipped — one ghost file must not abort a drain. */
|
|
1168
1171
|
async moveSegment(row, from, to, samePhysicalRoot) {
|
|
1169
|
-
if (!samePhysicalRoot)
|
|
1172
|
+
if (!samePhysicalRoot) try {
|
|
1173
|
+
await moveFile(path.join(from.root, row.path), path.join(to.root, row.path), row.bytes);
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
if (isEnoent(err)) {
|
|
1176
|
+
this.deps.index.removeSegments([row.path]);
|
|
1177
|
+
this.deps.logger.warn("relocate skipped missing source", {
|
|
1178
|
+
tags: { deviceId: row.deviceId },
|
|
1179
|
+
meta: {
|
|
1180
|
+
path: row.path,
|
|
1181
|
+
locationId: from.id
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
throw err;
|
|
1187
|
+
}
|
|
1170
1188
|
this.deps.index.removeSegments([row.path]);
|
|
1171
1189
|
this.deps.index.addSegment({
|
|
1172
1190
|
...row,
|
|
@@ -1174,6 +1192,9 @@ var RelocateEngine = class {
|
|
|
1174
1192
|
});
|
|
1175
1193
|
}
|
|
1176
1194
|
};
|
|
1195
|
+
function isEnoent(err) {
|
|
1196
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
1197
|
+
}
|
|
1177
1198
|
/** Logical migration classes are profile-derived, never location-derived:
|
|
1178
1199
|
* high+mid are `recordings`; low is `recordingsLow`. */
|
|
1179
1200
|
var profileBelongsToClass = (profile, footageClass) => {
|
|
@@ -4212,6 +4233,7 @@ function buildRecordingProvider(deps) {
|
|
|
4212
4233
|
rescanStorage: async ({ deviceId }) => {
|
|
4213
4234
|
const locations = await deps.refreshLocations();
|
|
4214
4235
|
await deps.hydrateDevice(deviceId, locations);
|
|
4236
|
+
await deps.reconcileHourLedger?.(deviceId);
|
|
4215
4237
|
const config = await loadDeviceConfig(deps.configStore, deviceId);
|
|
4216
4238
|
await deps.opsLog?.append({
|
|
4217
4239
|
op: "rescan",
|
|
@@ -5075,7 +5097,11 @@ async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger
|
|
|
5075
5097
|
try {
|
|
5076
5098
|
entries = await promises.readdir(deviceDir, { recursive: true });
|
|
5077
5099
|
} catch (err) {
|
|
5078
|
-
if (err.code
|
|
5100
|
+
if (err.code === "ENOENT") {
|
|
5101
|
+
for (const location of aliases) index.hydrateDevice(deviceId, location.id, []);
|
|
5102
|
+
continue;
|
|
5103
|
+
}
|
|
5104
|
+
logger.warn("recorder: hydrateDevice walk failed for location", {
|
|
5079
5105
|
tags: { deviceId },
|
|
5080
5106
|
meta: {
|
|
5081
5107
|
locationIds: aliases.map((location) => location.id),
|
|
@@ -5103,6 +5129,7 @@ async function hydrateDeviceFromStorage(_api, index, deviceId, locations, logger
|
|
|
5103
5129
|
handled += 1;
|
|
5104
5130
|
if (handled % chunkSize === 0) await yieldBetween();
|
|
5105
5131
|
}
|
|
5132
|
+
for (const location of aliases) if (!pathsByLocation.has(location.id)) pathsByLocation.set(location.id, []);
|
|
5106
5133
|
let indexed = 0;
|
|
5107
5134
|
for (const [locationId, relPaths] of pathsByLocation) {
|
|
5108
5135
|
index.hydrateDevice(deviceId, locationId, relPaths);
|
|
@@ -6562,6 +6589,13 @@ var RecordingController = class {
|
|
|
6562
6589
|
* records again.
|
|
6563
6590
|
*/
|
|
6564
6591
|
unrecordableReported = /* @__PURE__ */ new Set();
|
|
6592
|
+
/**
|
|
6593
|
+
* Profiles the last attach skipped (stream not published / getStream threw).
|
|
6594
|
+
* `evaluateDevice` retries only these on the converge tick so a dead `low`
|
|
6595
|
+
* cannot keep `high` dark, without a `listAllProfileSlots` RPC on every
|
|
6596
|
+
* healthy device every 30s.
|
|
6597
|
+
*/
|
|
6598
|
+
skippedProfiles = /* @__PURE__ */ new Map();
|
|
6565
6599
|
restore = null;
|
|
6566
6600
|
/** Cheap, never-delayed: is any pinned writer producing nothing? */
|
|
6567
6601
|
livenessPass = null;
|
|
@@ -6768,7 +6802,10 @@ var RecordingController = class {
|
|
|
6768
6802
|
return;
|
|
6769
6803
|
}
|
|
6770
6804
|
this.unrecordableReported.delete(deviceId);
|
|
6771
|
-
if (this.active.has(deviceId))
|
|
6805
|
+
if (this.active.has(deviceId)) {
|
|
6806
|
+
if (this.skippedProfiles.has(deviceId)) await this.fillMissingProfiles(deviceId, config);
|
|
6807
|
+
return;
|
|
6808
|
+
}
|
|
6772
6809
|
if (this.attaching.has(deviceId)) return;
|
|
6773
6810
|
this.attaching.add(deviceId);
|
|
6774
6811
|
const attaching = this.performAttach(deviceId, config);
|
|
@@ -6819,8 +6856,12 @@ var RecordingController = class {
|
|
|
6819
6856
|
/**
|
|
6820
6857
|
* The attach body — extracted so the {@link attaching} in-flight guard in
|
|
6821
6858
|
* `evaluateDevice` wraps it cleanly. Resolves the profiles, spawns a
|
|
6822
|
-
* SegmentWriter + watcher per profile, and records the set in `active`.
|
|
6823
|
-
*
|
|
6859
|
+
* SegmentWriter + watcher per profile, and records the set in `active`.
|
|
6860
|
+
*
|
|
6861
|
+
* Per-profile: a throw from one slot is logged and skipped. Rolling back the
|
|
6862
|
+
* whole device when a single stream was dead (618 `derived` on `low`) left
|
|
6863
|
+
* ZERO footage for hours. If every profile fails, throw so the restore queue
|
|
6864
|
+
* retries the device.
|
|
6824
6865
|
*/
|
|
6825
6866
|
async performAttach(deviceId, config) {
|
|
6826
6867
|
this.restore?.add(deviceId, true);
|
|
@@ -6828,38 +6869,106 @@ var RecordingController = class {
|
|
|
6828
6869
|
if (profiles.length === 0) throw new Error(`recorder controller: no assigned broker sources for device ${deviceId}`);
|
|
6829
6870
|
const segmentSeconds = config.segmentSeconds ?? this.deps.segmentSeconds;
|
|
6830
6871
|
const recordings = [];
|
|
6831
|
-
|
|
6832
|
-
|
|
6872
|
+
const failed = [];
|
|
6873
|
+
for (const profile of profiles) try {
|
|
6874
|
+
recordings.push(await this.attachProfile(deviceId, profile, segmentSeconds));
|
|
6833
6875
|
} catch (err) {
|
|
6834
|
-
|
|
6835
|
-
|
|
6876
|
+
failed.push(profile);
|
|
6877
|
+
this.deps.logger.warn("recorder: profile attach failed — recording the remaining profiles", {
|
|
6878
|
+
tags: { deviceId },
|
|
6879
|
+
meta: {
|
|
6880
|
+
profile,
|
|
6881
|
+
error: errMsg(err)
|
|
6882
|
+
}
|
|
6883
|
+
});
|
|
6836
6884
|
}
|
|
6885
|
+
if (recordings.length === 0) throw new Error(`recorder controller: no profile attached for device ${deviceId}` + (failed.length > 0 ? ` (failed: ${failed.join(",")})` : ""));
|
|
6837
6886
|
if (this.paused) {
|
|
6838
6887
|
await Promise.all(recordings.map((recording) => this.teardownProfile(deviceId, recording)));
|
|
6839
6888
|
return;
|
|
6840
6889
|
}
|
|
6841
6890
|
this.active.set(deviceId, recordings);
|
|
6891
|
+
if (failed.length > 0) this.skippedProfiles.set(deviceId, new Set(failed));
|
|
6892
|
+
else this.skippedProfiles.delete(deviceId);
|
|
6842
6893
|
this.deps.logger.info("recorder pinned broker source(s) for recording", {
|
|
6843
6894
|
tags: { deviceId },
|
|
6844
6895
|
meta: {
|
|
6845
|
-
profiles,
|
|
6896
|
+
profiles: recordings.map((r) => r.profile),
|
|
6897
|
+
failedProfiles: failed,
|
|
6846
6898
|
segmentSeconds,
|
|
6847
6899
|
pipelineKeys: recordings.map((r) => r.pipelineKey)
|
|
6848
6900
|
}
|
|
6849
6901
|
});
|
|
6850
6902
|
}
|
|
6851
6903
|
/**
|
|
6904
|
+
* Retry profiles the last attach skipped, without tearing down writers that
|
|
6905
|
+
* are already producing. Called from `evaluateDevice` when the device is
|
|
6906
|
+
* already in `active` and {@link skippedProfiles} is non-empty. Failures stay
|
|
6907
|
+
* logged; the next converge tick retries.
|
|
6908
|
+
*/
|
|
6909
|
+
async fillMissingProfiles(deviceId, config) {
|
|
6910
|
+
if (this.attaching.has(deviceId)) return;
|
|
6911
|
+
const current = this.active.get(deviceId);
|
|
6912
|
+
const skipped = this.skippedProfiles.get(deviceId);
|
|
6913
|
+
if (!current || !skipped || skipped.size === 0) return;
|
|
6914
|
+
this.attaching.add(deviceId);
|
|
6915
|
+
const filling = this.runFillMissingProfiles(deviceId, config, current, skipped);
|
|
6916
|
+
this.attachingTasks.set(deviceId, filling);
|
|
6917
|
+
try {
|
|
6918
|
+
await filling;
|
|
6919
|
+
} finally {
|
|
6920
|
+
this.attachingTasks.delete(deviceId);
|
|
6921
|
+
this.attaching.delete(deviceId);
|
|
6922
|
+
}
|
|
6923
|
+
}
|
|
6924
|
+
async runFillMissingProfiles(deviceId, config, current, skipped) {
|
|
6925
|
+
const have = new Set(current.map((r) => r.profile));
|
|
6926
|
+
const segmentSeconds = config.segmentSeconds ?? this.deps.segmentSeconds;
|
|
6927
|
+
const added = [];
|
|
6928
|
+
for (const profile of [...skipped]) {
|
|
6929
|
+
if (have.has(profile)) {
|
|
6930
|
+
skipped.delete(profile);
|
|
6931
|
+
continue;
|
|
6932
|
+
}
|
|
6933
|
+
try {
|
|
6934
|
+
added.push(await this.attachProfile(deviceId, profile, segmentSeconds));
|
|
6935
|
+
skipped.delete(profile);
|
|
6936
|
+
} catch (err) {
|
|
6937
|
+
this.deps.logger.warn("recorder: profile attach retry failed — keeping the healthy profiles", {
|
|
6938
|
+
tags: { deviceId },
|
|
6939
|
+
meta: {
|
|
6940
|
+
profile,
|
|
6941
|
+
error: errMsg(err)
|
|
6942
|
+
}
|
|
6943
|
+
});
|
|
6944
|
+
}
|
|
6945
|
+
}
|
|
6946
|
+
if (skipped.size === 0) this.skippedProfiles.delete(deviceId);
|
|
6947
|
+
if (added.length === 0) return;
|
|
6948
|
+
const existing = this.active.get(deviceId);
|
|
6949
|
+
if (!existing || this.paused || this.stopped) {
|
|
6950
|
+
await Promise.all(added.map((recording) => this.teardownProfile(deviceId, recording)));
|
|
6951
|
+
return;
|
|
6952
|
+
}
|
|
6953
|
+
this.active.set(deviceId, [...existing, ...added]);
|
|
6954
|
+
this.deps.logger.info("recorder pinned additional broker source(s) for recording", {
|
|
6955
|
+
tags: { deviceId },
|
|
6956
|
+
meta: {
|
|
6957
|
+
profiles: added.map((r) => r.profile),
|
|
6958
|
+
pipelineKeys: added.map((r) => r.pipelineKey)
|
|
6959
|
+
}
|
|
6960
|
+
});
|
|
6961
|
+
}
|
|
6962
|
+
/**
|
|
6852
6963
|
* Pick which profiles to record. The broker enumerates assigned profile slots,
|
|
6853
6964
|
* deduped by physical source (`selectAssignedProfileSlots`) so the same camera
|
|
6854
|
-
* encoder is never recorded twice.
|
|
6855
|
-
*
|
|
6856
|
-
*
|
|
6965
|
+
* encoder is never recorded twice. Default is high+low among those slots
|
|
6966
|
+
* (`resolveRecordingProfiles`); an operator `override` DISABLES the
|
|
6967
|
+
* unselected profiles. A selection matching none of the assigned sources
|
|
6968
|
+
* falls back to the default/assigned set (minimum of 1 when anything is assigned).
|
|
6857
6969
|
*/
|
|
6858
6970
|
async resolveProfiles(deviceId, override) {
|
|
6859
|
-
|
|
6860
|
-
if (!override || override.length === 0) return assigned;
|
|
6861
|
-
const selected = assigned.filter((p) => override.includes(p));
|
|
6862
|
-
return selected.length > 0 ? selected : assigned;
|
|
6971
|
+
return resolveRecordingProfiles(selectAssignedProfileSlots(await this.deps.brokerCall(() => withDeadline(this.deps.api.streamBroker.listAllProfileSlots.query(void 0, nodePin(this.deps.ownerNodeId)), ATTACH_RPC_TIMEOUT_MS, "listAllProfileSlots")), deviceId).map((slot) => slot.profile), override);
|
|
6863
6972
|
}
|
|
6864
6973
|
/**
|
|
6865
6974
|
* Attach one (device, profile): acquire the broker source, mkdir the staging
|
|
@@ -6989,6 +7098,7 @@ var RecordingController = class {
|
|
|
6989
7098
|
const recordings = this.active.get(deviceId);
|
|
6990
7099
|
if (!recordings) return;
|
|
6991
7100
|
this.active.delete(deviceId);
|
|
7101
|
+
this.skippedProfiles.delete(deviceId);
|
|
6992
7102
|
for (const r of recordings) this.lastSegmentAt.delete(idleKey(deviceId, r.profile));
|
|
6993
7103
|
await Promise.all(recordings.map((r) => this.teardownProfile(deviceId, r)));
|
|
6994
7104
|
this.deps.logger.info("recorder unpinned broker source(s) — recording stopped", {
|
|
@@ -8605,6 +8715,9 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
8605
8715
|
refreshLocations: () => this.refreshLocations(),
|
|
8606
8716
|
capacity: (root) => this.locationCapacity(root),
|
|
8607
8717
|
hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
|
|
8718
|
+
reconcileHourLedger: async (deviceId) => {
|
|
8719
|
+
await this.segmentHours?.reconcileFromIndex(this.index, [deviceId], Date.now());
|
|
8720
|
+
},
|
|
8608
8721
|
calendar: this.calendar,
|
|
8609
8722
|
mfraFor: (deviceId, profile, startMs) => this.mfraTables.get(deviceId, profile, startMs),
|
|
8610
8723
|
hydrateWindow: (deviceId, fromMs, toMs) => hydrateWindowFromStorage(this.index, deviceId, fromMs, toMs, this.resolvedLocations, this.ctx.logger),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
3
|
-
const require_lazy_sharp = require("../lazy-sharp-
|
|
2
|
+
const require_worker_protocol = require("../worker-protocol-Bq28qobd.js");
|
|
3
|
+
const require_lazy_sharp = require("../lazy-sharp-C3zOnmiU.js");
|
|
4
4
|
//#region src/session-decode/color-conversion.ts
|
|
5
5
|
/**
|
|
6
6
|
* Explicit YUV→RGB colorspace/range resolution for the session-decode worker.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-
|
|
1
|
+
import { i as formatNativeLeaseKnobs, n as isWorkerRequest, o as resolveNativeLeaseKnobs, r as logLevelForLine } from "../worker-protocol-PzAKg8mC.mjs";
|
|
2
2
|
import { n as setSharpWarnSink, r as hostExternalEntryUrls, t as getSharp } from "../lazy-sharp-6oymT_yf.mjs";
|
|
3
3
|
//#region src/session-decode/color-conversion.ts
|
|
4
4
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-
|
|
1
|
+
import { a as e, c as t, d as n, f as r, i, l as a, n as o, o as s, p as c, r as l, s as u, t as d, u as f } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DsIi4G5Q.mjs";
|
|
2
2
|
import { a as p, i as m, n as h, o as g, r as _, t as v } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react__loadShare__.js-C9j-2lBe.mjs";
|
|
3
3
|
import { n as y, r as b, t as x } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-XO0-Pyu6.mjs";
|
|
4
|
-
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-
|
|
4
|
+
import { t as S } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dut56EeQ.mjs";
|
|
5
5
|
import { n as C, t as w } from "./_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-BO7TIbJV.mjs";
|
|
6
6
|
//#region ../../node_modules/lucide-react/dist/esm/shared/src/utils.js
|
|
7
7
|
var T = (e) => e.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(), E = (e) => e.replace(/^([A-Z])|[\s-_]+(\w)/g, (e, t, n) => n ? n.toUpperCase() : t.toLowerCase()), D = (e) => {
|
|
@@ -18,7 +18,7 @@ var e = {
|
|
|
18
18
|
},
|
|
19
19
|
"@camstack/types": {
|
|
20
20
|
name: "@camstack/types",
|
|
21
|
-
version: "1.2.
|
|
21
|
+
version: "1.2.84",
|
|
22
22
|
scope: ["default"],
|
|
23
23
|
loaded: !1,
|
|
24
24
|
from: "addon_stream_broker_widgets",
|
|
@@ -33,7 +33,7 @@ var e = {
|
|
|
33
33
|
},
|
|
34
34
|
"@camstack/ui-library": {
|
|
35
35
|
name: "@camstack/ui-library",
|
|
36
|
-
version: "1.2.
|
|
36
|
+
version: "1.2.58",
|
|
37
37
|
scope: ["default"],
|
|
38
38
|
loaded: !1,
|
|
39
39
|
from: "addon_stream_broker_widgets",
|