@camstack/addon-pipeline 1.2.215 → 1.2.218
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +2 -2
- package/dist/audio-analyzer/index.mjs +2 -2
- package/dist/{default-detection-model-C2vYSX3O.mjs → default-detection-model-AJyhoTLF.mjs} +1 -1
- package/dist/{default-detection-model-f0PImZnh.js → default-detection-model-Bf45EQ98.js} +1 -1
- package/dist/detection-pipeline/index.js +5 -5
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-DoHcimdu.js → dist-BbvtiBNc.js} +278 -20
- package/dist/{dist-DttqKvsQ.mjs → dist-Dl6GntEh.mjs} +278 -20
- package/dist/{lazy-sharp-BeeQGCUu.js → lazy-sharp-L9Lcr62v.js} +1 -1
- package/dist/motion-wasm/index.js +2 -2
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-NtM7__8q.mjs → node-CEyw52D2.mjs} +1 -1
- package/dist/{node-6uF72wri.js → node-CbCVHEvJ.js} +1 -1
- package/dist/pipeline-runner/index.js +546 -25
- package/dist/pipeline-runner/index.mjs +545 -24
- package/dist/{process-memory-BRdyl5fE.js → process-memory-DBxdiCSF.js} +1 -1
- package/dist/{process-memory-CddpCDWL.mjs → process-memory-Du00R6-e.mjs} +1 -1
- package/dist/recorder/index.js +238 -52
- package/dist/recorder/index.mjs +238 -52
- package/dist/{segment-demux-js-CsFtPVzH.mjs → segment-demux-js-B278aBtB.mjs} +1 -1
- package/dist/{segment-demux-js-CkJPdyDH.js → segment-demux-js-D7bjzY8M.js} +1 -1
- package/dist/session-decode/decode-worker-child.js +58 -45
- package/dist/session-decode/decode-worker-child.mjs +57 -44
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-ZIja2AOe.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BnmbfnAd.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-yJ2CyixO.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-Uxkwkgy5.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DICVQkNi.mjs} +1 -1
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-DpatMS4g.mjs → hostInit-MIKQqs86.mjs} +3 -3
- package/dist/stream-broker/index.js +20 -21
- package/dist/stream-broker/index.mjs +20 -21
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-BKwkLZWT.mjs → worker-protocol-BEbD5adq.mjs} +1 -1
- package/dist/{worker-protocol-BWO2WNjw.js → worker-protocol-C4rh5uiS.js} +1 -1
- package/package.json +1 -1
- package/python/__pycache__/inference_pool.cpython-312.pyc +0 -0
- package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
- package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_deadline.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_layout.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_layout.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_memstats.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_ov_ppp.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_preprocess.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_preprocess.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_reconfigure.cpython-314.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-312.pyc +0 -0
- package/python/__pycache__/test_inference_pool_static_batch.cpython-314.pyc +0 -0
- package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/__init__.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/clip.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/plate_slots.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/plate_slots.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/rfdetr.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/rfdetr.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/ssd.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_clip.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/test_ctc.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_plate_slots.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_rfdetr.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_rfdetr.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_saliency.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_softmax.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_softmax.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolo_nms.cpython-39.pyc +0 -0
- package/python/postprocessors/__pycache__/test_yolonas.cpython-312-pytest-9.1.1.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-39.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-314.pyc +0 -0
- package/python/postprocessors/__pycache__/yolonas.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolonas.cpython-314.pyc +0 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-0MF-KL1v.mjs +0 -26
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { At as storageEvictableCapability, Ct as recordingSignalCapability, D as RecordingSignalStatusSchema, E as RecordingConfigSchema, Et as resolveLocationMode, Gt as DeviceFeature, H as deriveBatteryPresence, It as errMsg, Jt as hydrateSchema, Kt as DeviceType, Nt as stripRetiredBandPreBufferSec, Ot as resolveRecordingProfiles, P as batteryCapability, Q as evictionPolicyOfLocation, Qt as nodePin, R as customAction, St as recordingExportCapability, T as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as defineCustomActions, W as deriveRecordingMode, Y as evaluateSensorEdge, Z as evictionPolicyForMode, an as boolean, ct as mayReadLocation, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, dn as preprocess, dt as modeMayWrite, et as gbToBytes, fn as record, g as ExportRecordSchema, hn as EventCategory, i as BatteryStatusSchema, in as array, it as loadContributionCapability, jt as storageOccupancyCapability, ln as number, lt as mayWriteToLocation, nn as sleep$1, pn as string, rn as _enum, rt as isFirstLevelMacroClass, tn as selectAssignedProfileSlots, un as object, ut as migrateLegacyDeviceSignalConfig, x as OpsLogEntrySchema, xt as recordingCapability } from "../dist-
|
|
2
|
-
import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-
|
|
1
|
+
import { At as storageEvictableCapability, Ct as recordingSignalCapability, D as RecordingSignalStatusSchema, E as RecordingConfigSchema, Et as resolveLocationMode, Gt as DeviceFeature, H as deriveBatteryPresence, It as errMsg, Jt as hydrateSchema, Kt as DeviceType, Nt as stripRetiredBandPreBufferSec, Ot as resolveRecordingProfiles, P as batteryCapability, Q as evictionPolicyOfLocation, Qt as nodePin, R as customAction, St as recordingExportCapability, T as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as defineCustomActions, W as deriveRecordingMode, Y as evaluateSensorEdge, Z as evictionPolicyForMode, an as boolean, ct as mayReadLocation, d as DEFAULT_EVENTS_BAND_BUFFER_SEC, dn as preprocess, dt as modeMayWrite, et as gbToBytes, fn as record, g as ExportRecordSchema, hn as EventCategory, i as BatteryStatusSchema, in as array, it as loadContributionCapability, jt as storageOccupancyCapability, ln as number, lt as mayWriteToLocation, nn as sleep$1, pn as string, rn as _enum, rt as isFirstLevelMacroClass, tn as selectAssignedProfileSlots, un as object, ut as migrateLegacyDeviceSignalConfig, x as OpsLogEntrySchema, xt as recordingCapability } from "../dist-Dl6GntEh.mjs";
|
|
2
|
+
import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-CEyw52D2.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { c as PREBUFFER_MAX_MS, r as RECORDING_PRE_ROLL_REQUEST_MS, s as withRecordingIntent } from "../restream-intent-DWe2kInY.mjs";
|
|
5
5
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
|
|
@@ -1328,27 +1328,6 @@ var RecordingIndex = class {
|
|
|
1328
1328
|
return [...out.values()];
|
|
1329
1329
|
}
|
|
1330
1330
|
/**
|
|
1331
|
-
* WHICH devices hold footage on any of `locationIds` — the census, without
|
|
1332
|
-
* the archive.
|
|
1333
|
-
*
|
|
1334
|
-
* `getStorageUsage` used to answer this with `segmentsOnLocation(id)` per
|
|
1335
|
-
* location: a full multi-device COPY of every row on the disk plus a sort of
|
|
1336
|
-
* it (294 k rows on the live hub), to read one number off each row and throw
|
|
1337
|
-
* the array away. This visits each device only until its first matching row.
|
|
1338
|
-
* Same membership rule as before — a device is listed when at least one of
|
|
1339
|
-
* its rows sits on one of the given locations, so footage stranded on a
|
|
1340
|
-
* location id that no longer resolves stays invisible here exactly as it was.
|
|
1341
|
-
* @residency archive-shaped
|
|
1342
|
-
*/
|
|
1343
|
-
deviceIdsOnLocations(locationIds) {
|
|
1344
|
-
const out = /* @__PURE__ */ new Set();
|
|
1345
|
-
for (const [deviceId, m] of this.byDevice) for (const s of m.values()) if (locationIds.has(s.locationId)) {
|
|
1346
|
-
out.add(deviceId);
|
|
1347
|
-
break;
|
|
1348
|
-
}
|
|
1349
|
-
return out;
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
1331
|
* Every RESIDENT segment on a storage location, across devices, oldest-first.
|
|
1353
1332
|
*
|
|
1354
1333
|
* ## The one that bit (D295)
|
|
@@ -3833,6 +3812,40 @@ async function segmentsOfHour(root, deviceId, profile, hourStartMs) {
|
|
|
3833
3812
|
* the directory route serve the profile the client asked for when it exists,
|
|
3834
3813
|
* and fall back to the availability pick when it does not.
|
|
3835
3814
|
*/
|
|
3815
|
+
/**
|
|
3816
|
+
* EVERY profile this camera holds footage in, across every root.
|
|
3817
|
+
*
|
|
3818
|
+
* {@link availabilityProfile} deliberately returns ONE — the timeline is a
|
|
3819
|
+
* single bar. That is right for the bar and wrong for the ANSWER: on
|
|
3820
|
+
* 2026-09-10 `getAvailability` reported twenty `high` ranges for camera 615
|
|
3821
|
+
* and the conclusion drawn was that this deployment records no `low` at all,
|
|
3822
|
+
* while `/media-fast/recordingsLow/615/low/` held 14 GB on a different
|
|
3823
|
+
* location (D433).
|
|
3824
|
+
*
|
|
3825
|
+
* Unions across roots for exactly that reason: `high` and `low` do not share a
|
|
3826
|
+
* location here, so a per-root answer sees one of them.
|
|
3827
|
+
*
|
|
3828
|
+
* Non-emptiness, not existence — the same rule {@link availabilityProfile}
|
|
3829
|
+
* keeps, so a directory left behind by a config change is not offered. And
|
|
3830
|
+
* only {@link KNOWN_PROFILES}: the camera dir also hosts `events/` and the
|
|
3831
|
+
* retired `strips/` trees, which a walker that trusts readdir would present as
|
|
3832
|
+
* playable.
|
|
3833
|
+
*
|
|
3834
|
+
* `list` is injectable for the test; production passes the module's own
|
|
3835
|
+
* directory read.
|
|
3836
|
+
*/
|
|
3837
|
+
async function profilesWithFootage(roots, deviceId, list = listDir) {
|
|
3838
|
+
const found = /* @__PURE__ */ new Set();
|
|
3839
|
+
for (const root of roots) {
|
|
3840
|
+
const entries = await list(path.join(root, String(deviceId)));
|
|
3841
|
+
for (const entry of entries) {
|
|
3842
|
+
if (!KNOWN_PROFILES$1.has(entry)) continue;
|
|
3843
|
+
if (found.has(entry)) continue;
|
|
3844
|
+
if ((await list(path.join(root, String(deviceId), entry))).length > 0) found.add(entry);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return [...found].sort();
|
|
3848
|
+
}
|
|
3836
3849
|
async function profileHasFootage(root, deviceId, profile) {
|
|
3837
3850
|
return (await listDir(path.join(root, String(deviceId), profile))).length > 0;
|
|
3838
3851
|
}
|
|
@@ -3954,6 +3967,8 @@ var CalendarIndex = class {
|
|
|
3954
3967
|
segsReadAt = /* @__PURE__ */ new Map();
|
|
3955
3968
|
/** The profile availability reads from, resolved once per device. */
|
|
3956
3969
|
profileFor = /* @__PURE__ */ new Map();
|
|
3970
|
+
profilesFor = /* @__PURE__ */ new Map();
|
|
3971
|
+
profilesInFlight = /* @__PURE__ */ new Map();
|
|
3957
3972
|
/** `deviceId:profile` → whether the device records that profile, resolved
|
|
3958
3973
|
* once. Same stability trade as {@link profileFor}: a config change that
|
|
3959
3974
|
* adds a profile mid-session is picked up on restart, not on a timer. */
|
|
@@ -4236,6 +4251,31 @@ var CalendarIndex = class {
|
|
|
4236
4251
|
* reads at a time and they ALL ask for the profile before the first answer
|
|
4237
4252
|
* lands, so a cache alone let the pick run eight times. Parallelism found it.
|
|
4238
4253
|
*/
|
|
4254
|
+
/**
|
|
4255
|
+
* Every profile this camera holds footage in, across every root — cached
|
|
4256
|
+
* like the pick, because it is the same shallow read (D433).
|
|
4257
|
+
*
|
|
4258
|
+
* {@link availabilityProfileFor} answers with ONE; this says what the one
|
|
4259
|
+
* was chosen FROM, so a caller cannot mistake the timeline's single bar for
|
|
4260
|
+
* the whole archive.
|
|
4261
|
+
*/
|
|
4262
|
+
async profilesWithFootageFor(deviceId) {
|
|
4263
|
+
const cached = this.profilesFor.get(deviceId);
|
|
4264
|
+
if (cached !== void 0) return cached;
|
|
4265
|
+
const running = this.profilesInFlight.get(deviceId);
|
|
4266
|
+
if (running) return running;
|
|
4267
|
+
const resolve = (async () => {
|
|
4268
|
+
const found = await profilesWithFootage(this.deps.roots(), deviceId);
|
|
4269
|
+
this.profilesFor.set(deviceId, found);
|
|
4270
|
+
return found;
|
|
4271
|
+
})();
|
|
4272
|
+
this.profilesInFlight.set(deviceId, resolve);
|
|
4273
|
+
try {
|
|
4274
|
+
return await resolve;
|
|
4275
|
+
} finally {
|
|
4276
|
+
if (this.profilesInFlight.get(deviceId) === resolve) this.profilesInFlight.delete(deviceId);
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4239
4279
|
async availabilityProfileFor(deviceId) {
|
|
4240
4280
|
const cached = this.profileFor.get(deviceId);
|
|
4241
4281
|
if (cached !== void 0) return cached;
|
|
@@ -5879,6 +5919,8 @@ var TIME_RE = /time=(\d+):(\d+):(\d+(?:\.\d+)?)/;
|
|
|
5879
5919
|
* whatever ffmpeg build is installed.
|
|
5880
5920
|
*/
|
|
5881
5921
|
var EXPORT_MIN_PLAUSIBLE_BYTES = 1024;
|
|
5922
|
+
/** ffmpeg's own announcement of that pass — the only signal it gives. */
|
|
5923
|
+
var FASTSTART_RE = /Starting second pass: moving the moov atom/i;
|
|
5882
5924
|
/** Expected OUTPUT duration (seconds) for progress scaling. */
|
|
5883
5925
|
function expectedOutSeconds(rec) {
|
|
5884
5926
|
const rangeSec = Math.max(.001, (rec.toMs - rec.fromMs) / 1e3);
|
|
@@ -6188,6 +6230,7 @@ var ExportEngine = class {
|
|
|
6188
6230
|
args
|
|
6189
6231
|
} });
|
|
6190
6232
|
const stallMs = this.deps.stallMs ?? 9e4;
|
|
6233
|
+
const faststartStallMs = this.deps.faststartStallMs ?? 9e5;
|
|
6191
6234
|
return new Promise((resolve) => {
|
|
6192
6235
|
const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
|
|
6193
6236
|
this.active.set(id, proc);
|
|
@@ -6199,14 +6242,16 @@ var ExportEngine = class {
|
|
|
6199
6242
|
stallTimer = null;
|
|
6200
6243
|
}
|
|
6201
6244
|
};
|
|
6245
|
+
let budgetMs = stallMs;
|
|
6202
6246
|
const armStall = () => {
|
|
6203
6247
|
clearStall();
|
|
6248
|
+
const armedFor = budgetMs;
|
|
6204
6249
|
stallTimer = setTimeout(() => {
|
|
6205
|
-
stderrTail.push(`ffmpeg stalled: no progress for ${
|
|
6250
|
+
stderrTail.push(`ffmpeg stalled: no progress for ${armedFor}ms`);
|
|
6206
6251
|
try {
|
|
6207
6252
|
proc.kill("SIGKILL");
|
|
6208
6253
|
} catch {}
|
|
6209
|
-
},
|
|
6254
|
+
}, armedFor);
|
|
6210
6255
|
};
|
|
6211
6256
|
armStall();
|
|
6212
6257
|
proc.stderr?.on("data", (chunk) => {
|
|
@@ -6217,6 +6262,10 @@ var ExportEngine = class {
|
|
|
6217
6262
|
stderrTail.push(trimmed);
|
|
6218
6263
|
if (stderrTail.length > STDERR_TAIL_LINES$1) stderrTail.shift();
|
|
6219
6264
|
}
|
|
6265
|
+
if (FASTSTART_RE.test(line)) {
|
|
6266
|
+
budgetMs = faststartStallMs;
|
|
6267
|
+
armStall();
|
|
6268
|
+
}
|
|
6220
6269
|
const m = TIME_RE.exec(line);
|
|
6221
6270
|
if (m) {
|
|
6222
6271
|
armStall();
|
|
@@ -6860,6 +6909,81 @@ function buildRecordingDeviceSchema() {
|
|
|
6860
6909
|
}]
|
|
6861
6910
|
}] };
|
|
6862
6911
|
}
|
|
6912
|
+
//#endregion
|
|
6913
|
+
//#region src/recorder/addon/device-storage-usage.ts
|
|
6914
|
+
/**
|
|
6915
|
+
* Gap between the per-camera sum and the per-volume total that is worth
|
|
6916
|
+
* saying out loud, as a share of the volume total.
|
|
6917
|
+
*
|
|
6918
|
+
* The two are aggregations of the SAME hour rows over the same location set —
|
|
6919
|
+
* one folded per camera, one summed in SQL — so in a healthy recorder they
|
|
6920
|
+
* agree to the byte. A few tenths of a percent is a write landing between the
|
|
6921
|
+
* two reads; 97% (60.5 GB against 1.9 TB) is the defect this file closes,
|
|
6922
|
+
* rebuilt somewhere new.
|
|
6923
|
+
*/
|
|
6924
|
+
var USAGE_MISMATCH_SHARE = .02;
|
|
6925
|
+
/** Floor under {@link USAGE_MISMATCH_SHARE} so a nearly empty disk does not
|
|
6926
|
+
* warn on a single in-flight segment. */
|
|
6927
|
+
var USAGE_MISMATCH_FLOOR_BYTES = 1e9;
|
|
6928
|
+
/**
|
|
6929
|
+
* One row per camera the ARCHIVE holds footage for on `locationIds`, biggest
|
|
6930
|
+
* first (deviceId breaks a tie, so two consecutive calls compare).
|
|
6931
|
+
*
|
|
6932
|
+
* A node with no ledger REFUSES (D295 rule 3): empty rows, `read:
|
|
6933
|
+
* 'unavailable'`, and a line saying so. Falling back to the index would answer
|
|
6934
|
+
* the same question with the wrong quantity and look exactly like a correct
|
|
6935
|
+
* answer, which is how this shipped in the first place.
|
|
6936
|
+
*/
|
|
6937
|
+
function buildDeviceUsage(input) {
|
|
6938
|
+
if (input.archive === null) {
|
|
6939
|
+
input.logger.warn("recorder: no hour ledger on this node — the per-camera storage rows are UNANSWERED, not empty", { meta: { locations: [...input.locationIds].toSorted().join(",") } });
|
|
6940
|
+
return {
|
|
6941
|
+
devices: [],
|
|
6942
|
+
totalUsedBytes: 0,
|
|
6943
|
+
read: "unavailable"
|
|
6944
|
+
};
|
|
6945
|
+
}
|
|
6946
|
+
const devices = [];
|
|
6947
|
+
let totalUsedBytes = 0;
|
|
6948
|
+
for (const [deviceId, archived] of input.archive) {
|
|
6949
|
+
const resident = input.resident(deviceId);
|
|
6950
|
+
totalUsedBytes += archived.bytes;
|
|
6951
|
+
devices.push({
|
|
6952
|
+
deviceId,
|
|
6953
|
+
usedBytes: archived.bytes,
|
|
6954
|
+
oldestMs: archived.oldestMs,
|
|
6955
|
+
segments: archived.segments,
|
|
6956
|
+
residentBytes: resident.bytes,
|
|
6957
|
+
residentOldestMs: resident.oldestMs
|
|
6958
|
+
});
|
|
6959
|
+
}
|
|
6960
|
+
return {
|
|
6961
|
+
devices: devices.toSorted((a, b) => b.usedBytes - a.usedBytes || a.deviceId - b.deviceId),
|
|
6962
|
+
totalUsedBytes,
|
|
6963
|
+
read: "archive"
|
|
6964
|
+
};
|
|
6965
|
+
}
|
|
6966
|
+
/**
|
|
6967
|
+
* Say it when the per-camera sum and the per-volume total disagree.
|
|
6968
|
+
*
|
|
6969
|
+
* The "I saw N of M" rule of D295, applied to a REPORT: these two numbers are
|
|
6970
|
+
* the same rows counted two ways, so a gap means one of the two reads saw a
|
|
6971
|
+
* fraction of the archive — and the whole point of D418 is that a page which
|
|
6972
|
+
* shows both without comparing them lets a 97% gap read as a fact about the
|
|
6973
|
+
* disks.
|
|
6974
|
+
*/
|
|
6975
|
+
function warnOnUsageMismatch(logger, deviceBytes, volumeBytes, read) {
|
|
6976
|
+
if (read !== "archive") return;
|
|
6977
|
+
if (volumeBytes === 0) return;
|
|
6978
|
+
const gap = Math.abs(volumeBytes - deviceBytes);
|
|
6979
|
+
if (gap <= USAGE_MISMATCH_FLOOR_BYTES) return;
|
|
6980
|
+
if (gap <= volumeBytes * USAGE_MISMATCH_SHARE) return;
|
|
6981
|
+
logger.warn("recorder: the per-camera storage sum disagrees with the per-volume total — one of the two reads saw a fraction of the archive", { meta: {
|
|
6982
|
+
deviceBytes,
|
|
6983
|
+
volumeBytes,
|
|
6984
|
+
gap
|
|
6985
|
+
} });
|
|
6986
|
+
}
|
|
6863
6987
|
/** The placement-store key for one (camera, profile). Mirrors `assignmentKey`
|
|
6864
6988
|
* in `placement-store.ts`; kept local so this module stays dependency-free. */
|
|
6865
6989
|
function sourceKey(source) {
|
|
@@ -7319,13 +7443,15 @@ function buildRecordingProvider(deps) {
|
|
|
7319
7443
|
* and by the batch alike — the batch collapses the TRANSPORT, and sharing the
|
|
7320
7444
|
* function is what makes that literally true instead of aspirational.
|
|
7321
7445
|
*/
|
|
7322
|
-
const availabilityFor = async (deviceId, fromMs, toMs) => {
|
|
7446
|
+
const availabilityFor = async (deviceId, fromMs, toMs, requestedProfile) => {
|
|
7323
7447
|
const calendar = deps.calendar;
|
|
7324
7448
|
if (calendar) {
|
|
7325
|
-
const
|
|
7449
|
+
const profilesWithFootage = await calendar.profilesWithFootageFor(deviceId);
|
|
7450
|
+
const profile = await calendar.servedProfileFor(deviceId, requestedProfile);
|
|
7326
7451
|
if (profile === null) return {
|
|
7327
7452
|
deviceId,
|
|
7328
|
-
ranges: []
|
|
7453
|
+
ranges: [],
|
|
7454
|
+
profilesWithFootage: [...profilesWithFootage]
|
|
7329
7455
|
};
|
|
7330
7456
|
const ranges = [];
|
|
7331
7457
|
for (const r of await calendar.rangesIn(deviceId, profile, fromMs, toMs)) ranges.push({
|
|
@@ -7335,16 +7461,20 @@ function buildRecordingProvider(deps) {
|
|
|
7335
7461
|
});
|
|
7336
7462
|
return {
|
|
7337
7463
|
deviceId,
|
|
7338
|
-
ranges
|
|
7464
|
+
ranges,
|
|
7465
|
+
profilesWithFootage: [...profilesWithFootage]
|
|
7339
7466
|
};
|
|
7340
7467
|
}
|
|
7468
|
+
const profiles = new Set(deps.index.segments(deviceId).map((s) => s.profile));
|
|
7469
|
+
const asked = requestedProfile !== void 0 && profiles.has(requestedProfile);
|
|
7341
7470
|
return {
|
|
7342
7471
|
deviceId,
|
|
7343
|
-
ranges: [...
|
|
7472
|
+
ranges: [...profiles].filter((p) => !asked || p === requestedProfile).flatMap((profile) => deps.index.ranges(deviceId, profile, fromMs, toMs, RANGE_MERGE_GAP_MS).map((r) => ({
|
|
7344
7473
|
profile,
|
|
7345
7474
|
startMs: r.startMs,
|
|
7346
7475
|
endMs: r.endMs
|
|
7347
|
-
})))
|
|
7476
|
+
}))),
|
|
7477
|
+
profilesWithFootage: [...profiles].sort()
|
|
7348
7478
|
};
|
|
7349
7479
|
};
|
|
7350
7480
|
/** The per-camera day read. Same rule as {@link availabilityFor}: one body. */
|
|
@@ -7374,7 +7504,7 @@ function buildRecordingProvider(deps) {
|
|
|
7374
7504
|
getStatus: async ({ deviceId }) => {
|
|
7375
7505
|
return statusFor(deps, deviceId, await loadDeviceConfig(deps.configStore, deviceId));
|
|
7376
7506
|
},
|
|
7377
|
-
getAvailability: ({ deviceId, fromMs, toMs }) => availabilityFor(deviceId, fromMs, toMs),
|
|
7507
|
+
getAvailability: ({ deviceId, fromMs, toMs, profile }) => availabilityFor(deviceId, fromMs, toMs, profile),
|
|
7378
7508
|
/**
|
|
7379
7509
|
* `getAvailability` for a SET of cameras. Same body per camera, run
|
|
7380
7510
|
* concurrently; only the transport collapses.
|
|
@@ -7388,11 +7518,12 @@ function buildRecordingProvider(deps) {
|
|
|
7388
7518
|
refuseOverCap("getAvailabilityBatch", deviceIds.length);
|
|
7389
7519
|
return Promise.all(deviceIds.map(async (deviceId) => {
|
|
7390
7520
|
try {
|
|
7391
|
-
const { ranges } = await availabilityFor(deviceId, fromMs, toMs);
|
|
7521
|
+
const { ranges, profilesWithFootage } = await availabilityFor(deviceId, fromMs, toMs);
|
|
7392
7522
|
return {
|
|
7393
7523
|
deviceId,
|
|
7394
7524
|
read: "read",
|
|
7395
|
-
ranges
|
|
7525
|
+
ranges,
|
|
7526
|
+
profilesWithFootage
|
|
7396
7527
|
};
|
|
7397
7528
|
} catch (err) {
|
|
7398
7529
|
deps.logger.warn("recording availability could not be read", {
|
|
@@ -7406,7 +7537,8 @@ function buildRecordingProvider(deps) {
|
|
|
7406
7537
|
return {
|
|
7407
7538
|
deviceId,
|
|
7408
7539
|
read: "unreadable",
|
|
7409
|
-
ranges: []
|
|
7540
|
+
ranges: [],
|
|
7541
|
+
profilesWithFootage: []
|
|
7410
7542
|
};
|
|
7411
7543
|
}
|
|
7412
7544
|
}));
|
|
@@ -7447,18 +7579,15 @@ function buildRecordingProvider(deps) {
|
|
|
7447
7579
|
getPlaybackManifest: ({ deviceId, fromMs, toMs }) => buildPlaybackManifest(deps, codecCache, deviceId, fromMs, toMs),
|
|
7448
7580
|
getStorageUsage: async () => {
|
|
7449
7581
|
const locations = deps.locations();
|
|
7450
|
-
const
|
|
7451
|
-
const
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
oldestMs: accounting.oldestMs
|
|
7460
|
-
});
|
|
7461
|
-
}
|
|
7582
|
+
const locationIds = new Set(locations.map((l) => l.id));
|
|
7583
|
+
const deviceUsage = buildDeviceUsage({
|
|
7584
|
+
locationIds,
|
|
7585
|
+
archive: deps.archiveDeviceUsage?.(locationIds) ?? null,
|
|
7586
|
+
resident: (deviceId) => deps.index.accounting(deviceId),
|
|
7587
|
+
logger: deps.logger
|
|
7588
|
+
});
|
|
7589
|
+
const devices = deviceUsage.devices;
|
|
7590
|
+
const totalUsedBytes = deviceUsage.totalUsedBytes;
|
|
7462
7591
|
const byPhysical = /* @__PURE__ */ new Map();
|
|
7463
7592
|
for (const loc of locations) {
|
|
7464
7593
|
const key = physicalRootKey(loc.root);
|
|
@@ -7487,11 +7616,12 @@ function buildRecordingProvider(deps) {
|
|
|
7487
7616
|
totalBytes: cap.totalBytes
|
|
7488
7617
|
};
|
|
7489
7618
|
}));
|
|
7490
|
-
|
|
7619
|
+
warnOnUsageMismatch(deps.logger, totalUsedBytes, locationUsages.reduce((sum, l) => sum + l.usedBytes, 0), deviceUsage.read);
|
|
7491
7620
|
return {
|
|
7492
7621
|
nodeId: deps.nodeId,
|
|
7493
7622
|
totalUsedBytes,
|
|
7494
|
-
devices,
|
|
7623
|
+
devices: [...devices],
|
|
7624
|
+
devicesRead: deviceUsage.read,
|
|
7495
7625
|
locations: locationUsages
|
|
7496
7626
|
};
|
|
7497
7627
|
},
|
|
@@ -8385,6 +8515,14 @@ var DurableLedger = class DurableLedger {
|
|
|
8385
8515
|
//#endregion
|
|
8386
8516
|
//#region src/recorder/addon/segment-hour-ledger.ts
|
|
8387
8517
|
var HOUR_MS$3 = 36e5;
|
|
8518
|
+
/** `Math.min` where a missing left operand means "nothing yet", not `NaN`. */
|
|
8519
|
+
function minOf(a, b) {
|
|
8520
|
+
return a === null ? b : Math.min(a, b);
|
|
8521
|
+
}
|
|
8522
|
+
/** `Math.max` with the same rule as {@link minOf}. */
|
|
8523
|
+
function maxOf(a, b) {
|
|
8524
|
+
return a === null ? b : Math.max(a, b);
|
|
8525
|
+
}
|
|
8388
8526
|
var macrotask$1 = () => new Promise((resolve) => {
|
|
8389
8527
|
setImmediate(resolve);
|
|
8390
8528
|
});
|
|
@@ -9035,7 +9173,7 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9035
9173
|
*
|
|
9036
9174
|
* The reader repairs the durable index it just reconstructed, so the next
|
|
9037
9175
|
* cold read of this hour is a point read instead of another `readdir`
|
|
9038
|
-
* (`docs/design/2026-08-29-
|
|
9176
|
+
* (`docs/design/2026-08-29-reference-nvr-recording-model.md` §4.2). Two
|
|
9039
9177
|
* constraints, both enforced by the caller and re-stated here because
|
|
9040
9178
|
* getting either wrong turns a READ into durable damage: the hour must be
|
|
9041
9179
|
* CLOSED (a live hour's directory races the writer, so a walk of it is a
|
|
@@ -9268,6 +9406,53 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
9268
9406
|
for (const row of this.ledger.snapshot()) if (locationIds.has(row.locationId)) out.add(row.deviceId);
|
|
9269
9407
|
return out;
|
|
9270
9408
|
}
|
|
9409
|
+
/**
|
|
9410
|
+
* Bytes / segments / oldest / newest per CAMERA on these locations — the
|
|
9411
|
+
* per-camera half of the storage page, answered by the ARCHIVE (D418).
|
|
9412
|
+
*
|
|
9413
|
+
* ## Why the T1 fold and not `census({ deviceId })` per camera
|
|
9414
|
+
*
|
|
9415
|
+
* `census` is the right authority for a SCOPE, and it cannot express this
|
|
9416
|
+
* scope: {@link SegmentHourLedger.predicate} takes ONE `locationId`, so
|
|
9417
|
+
* "camera 615 across the three resolvable volumes" is three aggregates, and
|
|
9418
|
+
* `census({ deviceId })` alone would silently count footage stranded on a
|
|
9419
|
+
* location id that no longer resolves — bytes no volume row on the same page
|
|
9420
|
+
* accounts for. Per camera per volume that is `cameras × volumes` SQL
|
|
9421
|
+
* aggregates for one operator click, and it grows with the fleet.
|
|
9422
|
+
*
|
|
9423
|
+
* The T1 hour summary answers all of them in ONE pass with no I/O, exactly
|
|
9424
|
+
* as {@link devicesWithFootage} and {@link profilesWithFootage} already do,
|
|
9425
|
+
* and it is the same rows `accountingForLocations` sums in SQL — the mirror
|
|
9426
|
+
* is write-behind, so it is never BEHIND the store. The cost is one pass
|
|
9427
|
+
* over the hour rows the mirror holds — O(writers × profiles × hours), ~19 k
|
|
9428
|
+
* on the live hub (see {@link REPAIR_MAX_ROWS}) against 98 445 segments,
|
|
9429
|
+
* i.e. it does NOT scale with the segment count — and it is capped for good
|
|
9430
|
+
* by {@link SEGMENT_HOUR_SUMMARY_LIMIT}, the mirror's own ceiling.
|
|
9431
|
+
*
|
|
9432
|
+
* ## What an empty answer means
|
|
9433
|
+
*
|
|
9434
|
+
* The same thing it means for {@link devicesWithFootage}: "the mirror knows
|
|
9435
|
+
* no hour here" — which is also what a ledger whose load failed reports. The
|
|
9436
|
+
* caller must not render that as "no camera has footage"; `getStorageUsage`
|
|
9437
|
+
* cross-checks the per-camera sum against the volume aggregate and SAYS so
|
|
9438
|
+
* when the two disagree.
|
|
9439
|
+
*/
|
|
9440
|
+
usageByDevice(locationIds) {
|
|
9441
|
+
const out = /* @__PURE__ */ new Map();
|
|
9442
|
+
if (locationIds.size === 0) return out;
|
|
9443
|
+
for (const row of this.ledger.snapshot()) {
|
|
9444
|
+
if (!locationIds.has(row.locationId)) continue;
|
|
9445
|
+
const empty = row.segments === 0;
|
|
9446
|
+
const prev = out.get(row.deviceId);
|
|
9447
|
+
out.set(row.deviceId, {
|
|
9448
|
+
bytes: (prev?.bytes ?? 0) + row.bytes,
|
|
9449
|
+
segments: (prev?.segments ?? 0) + row.segments,
|
|
9450
|
+
oldestMs: empty ? prev?.oldestMs ?? null : minOf(prev?.oldestMs ?? null, row.minStartMs),
|
|
9451
|
+
newestMs: empty ? prev?.newestMs ?? null : maxOf(prev?.newestMs ?? null, row.maxStartMs)
|
|
9452
|
+
});
|
|
9453
|
+
}
|
|
9454
|
+
return out;
|
|
9455
|
+
}
|
|
9271
9456
|
async oldestSegmentsScoped(locationIds, deviceIds, targetBytes) {
|
|
9272
9457
|
const ids = [...locationIds];
|
|
9273
9458
|
if (ids.length === 0 || targetBytes <= 0) return {
|
|
@@ -14527,7 +14712,7 @@ var RecordingController = class {
|
|
|
14527
14712
|
/**
|
|
14528
14713
|
* The ADAPTIVE attach bound. A transient `spawn ffmpeg` failure (EAGAIN) is
|
|
14529
14714
|
* the host saying it cannot fork right now, and the answer is to attach
|
|
14530
|
-
* FEWER cameras at once — not to serialise them, which
|
|
14715
|
+
* FEWER cameras at once — not to serialise them, which removed
|
|
14531
14716
|
* for good reason. Every bounded-parallel attach in this file reads it.
|
|
14532
14717
|
*/
|
|
14533
14718
|
spawnPressure;
|
|
@@ -16032,7 +16217,7 @@ function selectRedundantSegments(segs) {
|
|
|
16032
16217
|
* The pruner that used to remove it, `runRedundancyJanitor`, was reachable ONLY
|
|
16033
16218
|
* from `runFullArchiveWalk`, which was reachable only from
|
|
16034
16219
|
* `scheduleDeferredFullWalk`, which had no caller at all — so on 2026-08-27
|
|
16035
|
-
*
|
|
16220
|
+
* all three were deleted together as dead code. That was
|
|
16036
16221
|
* correct about the walk and left the system with **no duplicate pruner
|
|
16037
16222
|
* whatsoever**. The precedent for letting this class of waste run unattended is
|
|
16038
16223
|
* the 179 GB of stranded staging segments in `staging-reconcile.ts`.
|
|
@@ -17443,6 +17628,7 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
17443
17628
|
refreshLocations: () => this.refreshLocations(),
|
|
17444
17629
|
capacity: (root) => this.locationCapacity(root),
|
|
17445
17630
|
archiveAccounting: (locationIds) => this.segmentHours?.accountingForLocations(locationIds) ?? Promise.resolve(EMPTY_ARCHIVE_ACCOUNTING),
|
|
17631
|
+
archiveDeviceUsage: (locationIds) => this.segmentHours?.usageByDevice(locationIds) ?? null,
|
|
17446
17632
|
archive: () => this.segmentHours,
|
|
17447
17633
|
ledgerWalk: this.ledgerWalkReconciler(),
|
|
17448
17634
|
hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { It as errMsg } from "./dist-
|
|
2
|
+
import { It as errMsg } from "./dist-Dl6GntEh.mjs";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
//#region ../../node_modules/mp4box/dist/mp4box.all.js
|
|
5
5
|
var require_mp4box_all = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_dist = require("./dist-
|
|
1
|
+
const require_dist = require("./dist-BbvtiBNc.js");
|
|
2
2
|
let node_child_process = require("node:child_process");
|
|
3
3
|
//#region ../../node_modules/mp4box/dist/mp4box.all.js
|
|
4
4
|
var require_mp4box_all = /* @__PURE__ */ require_dist.__commonJSMin(((exports) => {
|
|
@@ -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-C4rh5uiS.js");
|
|
3
|
+
const require_lazy_sharp = require("../lazy-sharp-L9Lcr62v.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.
|
|
@@ -323,44 +323,6 @@ var FrameSlot = class {
|
|
|
323
323
|
}
|
|
324
324
|
};
|
|
325
325
|
//#endregion
|
|
326
|
-
//#region src/session-decode/media-clock.ts
|
|
327
|
-
/**
|
|
328
|
-
* Convert a container pts to milliseconds.
|
|
329
|
-
*
|
|
330
|
-
* Returns `null` — never a fabricated `0` — for a time base that cannot
|
|
331
|
-
* describe a duration. A zero here would place every frame of the session at
|
|
332
|
-
* the anchor, i.e. a whole burst of detections sharing one instant, which is
|
|
333
|
-
* exactly the kind of plausible-looking wrong timestamp this module exists to
|
|
334
|
-
* refuse. The caller degrades to the wall clock instead.
|
|
335
|
-
*/
|
|
336
|
-
function ptsToMs(pts, timeBase) {
|
|
337
|
-
if (!Number.isFinite(timeBase.num) || !Number.isFinite(timeBase.den)) return null;
|
|
338
|
-
if (timeBase.num <= 0 || timeBase.den <= 0) return null;
|
|
339
|
-
return Number(pts) * timeBase.num * 1e3 / timeBase.den;
|
|
340
|
-
}
|
|
341
|
-
/** Anchor the session's media clock on its first decoded frame. */
|
|
342
|
-
function anchorMediaClock(input) {
|
|
343
|
-
const preRoll = Number.isFinite(input.preRollMs) && input.preRollMs > 0 ? input.preRollMs : 0;
|
|
344
|
-
return {
|
|
345
|
-
firstPtsMs: input.firstPtsMs,
|
|
346
|
-
wallAtFirstPts: input.nowWall - preRoll
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* The wall clock at which the frame with `ptsMs` was shot.
|
|
351
|
-
*
|
|
352
|
-
* Clamped to `nowWall`: a container whose pts runs past the anchor (a clock
|
|
353
|
-
* jump, a source whose pts is not wall-clock-shaped, a re-dial that reset it)
|
|
354
|
-
* must never produce a track that starts in the FUTURE. A future `firstSeen`
|
|
355
|
-
* sorts a track ahead of everything, and no later pass can detect that it is
|
|
356
|
-
* wrong. Clamping loses precision on a pathological source and keeps the
|
|
357
|
-
* ordering property every consumer relies on.
|
|
358
|
-
*/
|
|
359
|
-
function mediaWallClockOf(anchor, ptsMs, nowWall) {
|
|
360
|
-
const shotAt = anchor.wallAtFirstPts + (ptsMs - anchor.firstPtsMs);
|
|
361
|
-
return shotAt > nowWall ? nowWall : shotAt;
|
|
362
|
-
}
|
|
363
|
-
//#endregion
|
|
364
326
|
//#region src/session-decode/lease-activity-gate.ts
|
|
365
327
|
/**
|
|
366
328
|
* Demand-driven gate for the native-frame lease capture (see
|
|
@@ -545,6 +507,56 @@ var LeaseAdmissionLedger = class {
|
|
|
545
507
|
}
|
|
546
508
|
};
|
|
547
509
|
//#endregion
|
|
510
|
+
//#region src/session-decode/media-clock.ts
|
|
511
|
+
/**
|
|
512
|
+
* Convert a container pts to milliseconds.
|
|
513
|
+
*
|
|
514
|
+
* Returns `null` — never a fabricated `0` — for a time base that cannot
|
|
515
|
+
* describe a duration. A zero here would place every frame of the session at
|
|
516
|
+
* the anchor, i.e. a whole burst of detections sharing one instant, which is
|
|
517
|
+
* exactly the kind of plausible-looking wrong timestamp this module exists to
|
|
518
|
+
* refuse. The caller degrades to the wall clock instead.
|
|
519
|
+
*/
|
|
520
|
+
function ptsToMs(pts, timeBase) {
|
|
521
|
+
if (!Number.isFinite(timeBase.num) || !Number.isFinite(timeBase.den)) return null;
|
|
522
|
+
if (timeBase.num <= 0 || timeBase.den <= 0) return null;
|
|
523
|
+
return Number(pts) * timeBase.num * 1e3 / timeBase.den;
|
|
524
|
+
}
|
|
525
|
+
/** Anchor the session's media clock on its first decoded frame. */
|
|
526
|
+
function anchorMediaClock(input) {
|
|
527
|
+
const preRoll = Number.isFinite(input.preRollMs) && input.preRollMs > 0 ? input.preRollMs : 0;
|
|
528
|
+
return {
|
|
529
|
+
firstPtsMs: input.firstPtsMs,
|
|
530
|
+
wallAtFirstPts: input.nowWall - preRoll
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* The wall clock at which the frame with `ptsMs` was shot.
|
|
535
|
+
*
|
|
536
|
+
* Clamped to `nowWall`: a container whose pts runs past the anchor (a clock
|
|
537
|
+
* jump, a source whose pts is not wall-clock-shaped, a re-dial that reset it)
|
|
538
|
+
* must never produce a track that starts in the FUTURE. A future `firstSeen`
|
|
539
|
+
* sorts a track ahead of everything, and no later pass can detect that it is
|
|
540
|
+
* wrong. Clamping loses precision on a pathological source and keeps the
|
|
541
|
+
* ordering property every consumer relies on.
|
|
542
|
+
*/
|
|
543
|
+
function mediaWallClockOf(anchor, ptsMs, nowWall) {
|
|
544
|
+
const shotAt = anchor.wallAtFirstPts + (ptsMs - anchor.firstPtsMs);
|
|
545
|
+
return shotAt > nowWall ? nowWall : shotAt;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* The anchor for a frame of dial `dialSeq`: the current one while the dial is
|
|
549
|
+
* the same, a fresh one — anchored on THIS frame — the moment the dial changes.
|
|
550
|
+
* Pure; the caller keeps the returned value and passes it back next frame.
|
|
551
|
+
*/
|
|
552
|
+
function mediaClockForDial(current, dialSeq, input) {
|
|
553
|
+
if (current !== null && current.dialSeq === dialSeq) return current;
|
|
554
|
+
return {
|
|
555
|
+
dialSeq,
|
|
556
|
+
anchor: anchorMediaClock(input)
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
548
560
|
//#region src/session-decode/native-frame-ring.ts
|
|
549
561
|
/**
|
|
550
562
|
* A hard-capped, insertion-ordered map from worker frameId → retained frame.
|
|
@@ -2136,7 +2148,7 @@ var DecodeWorkerChild = class {
|
|
|
2136
2148
|
lastColorProvenanceKeyByPath = /* @__PURE__ */ new Map();
|
|
2137
2149
|
/** Per-frame color provenance logging (opt-in; the transition marker is always on). */
|
|
2138
2150
|
colorDebug = process.env["CAMSTACK_DECODE_COLOR_DEBUG"] === "1";
|
|
2139
|
-
/**
|
|
2151
|
+
/** Throughput counters (plain integers, no per-frame allocation). */
|
|
2140
2152
|
framesDecoded = 0;
|
|
2141
2153
|
/** Releases that found a held frame / that found nothing to release. */
|
|
2142
2154
|
leaseReleased = 0;
|
|
@@ -2202,6 +2214,7 @@ var DecodeWorkerChild = class {
|
|
|
2202
2214
|
* frame arrives, or forever on a source whose pts cannot be read — in which
|
|
2203
2215
|
* case every frame falls back to the wall clock, exactly as before.
|
|
2204
2216
|
*/
|
|
2217
|
+
/** Anchored per DIAL (see `mediaClockForDial`): a re-dial is a new pts epoch. */
|
|
2205
2218
|
mediaClock = null;
|
|
2206
2219
|
/** History this dial asked the broker for; `0` ⇒ there is no burst. */
|
|
2207
2220
|
preRollMs = 0;
|
|
@@ -2217,7 +2230,7 @@ var DecodeWorkerChild = class {
|
|
|
2217
2230
|
/**
|
|
2218
2231
|
* The latest-wins decoded-frame slot + reserved-for-`toBuffer` holder +
|
|
2219
2232
|
* pending-pull flag — extracted to `FrameSlot` (Epic C P2 Task 2 / I2) so
|
|
2220
|
-
* the reserve-on-pull invariant
|
|
2233
|
+
* the reserve-on-pull invariant is unit-tested without
|
|
2221
2234
|
* node-av. See that file's doc comment for the full semantics.
|
|
2222
2235
|
*/
|
|
2223
2236
|
frames = new FrameSlot({ retainSuperseded: (frameId, frame) => this.captureNativeFrame(frameId, frame) });
|
|
@@ -3225,16 +3238,16 @@ var DecodeWorkerChild = class {
|
|
|
3225
3238
|
mediaWallClockOfFrame(frame, now) {
|
|
3226
3239
|
const ptsMs = this.frameMediaPtsMs(frame);
|
|
3227
3240
|
if (ptsMs === null) return now;
|
|
3228
|
-
if (
|
|
3241
|
+
if (this.mediaClock === null || this.mediaClock.dialSeq !== this.dialSeq) {
|
|
3229
3242
|
const servedPreRollMs = 0;
|
|
3230
3243
|
if (this.preRollMs > 0) this.emitStderr(`[decode] pre-roll requested ${this.preRollMs}ms but the SERVED span does not reach this worker — burst suppressed, clock anchored at now (D411 follow-up)`);
|
|
3231
|
-
this.mediaClock =
|
|
3244
|
+
this.mediaClock = mediaClockForDial(this.mediaClock, this.dialSeq, {
|
|
3232
3245
|
firstPtsMs: ptsMs,
|
|
3233
3246
|
preRollMs: servedPreRollMs,
|
|
3234
3247
|
nowWall: now
|
|
3235
3248
|
});
|
|
3236
3249
|
}
|
|
3237
|
-
return mediaWallClockOf(this.mediaClock, ptsMs, now);
|
|
3250
|
+
return mediaWallClockOf(this.mediaClock.anchor, ptsMs, now);
|
|
3238
3251
|
}
|
|
3239
3252
|
/** This frame's pts in ms, or `null` when the source cannot express one. */
|
|
3240
3253
|
frameMediaPtsMs(frame) {
|