@camstack/addon-pipeline 1.2.274 → 1.2.276
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-CAWhHQo2.js → default-detection-model-6lCvep7n.js} +1 -1
- package/dist/{default-detection-model-BXwE-Dvo.mjs → default-detection-model-dQSwHHfx.mjs} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-g5MfpiBh.mjs → dist-C1pCVJk7.mjs} +440 -32
- package/dist/{dist-k6Vu-AhZ.js → dist-NYjn7hkE.js} +469 -31
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-BXsHQH5h.mjs → node--UY0iepi.mjs} +1 -1
- package/dist/{node--u_jUObS.js → node-CVYY8a4y.js} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-Gl3EowL-.mjs → process-memory-BQOfL3Ig.mjs} +1 -1
- package/dist/{process-memory-DN7opum2.js → process-memory-DDvuSEMJ.js} +1 -1
- package/dist/recorder/index.js +572 -40
- package/dist/recorder/index.mjs +572 -40
- package/dist/{segment-demux-js-sYP_jndp.mjs → segment-demux-js-B0_aAzN0.mjs} +1 -1
- package/dist/{segment-demux-js-DHX8o8pI.js → segment-demux-js-DVupBBc4.js} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CEy8IryH.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D0A4Ra4h.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BV2tQ1w3.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-MU3_q1MX.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BnLVuoDg.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-UKNN4qes.mjs → hostInit-C8TnCvWj.mjs} +2 -2
- package/dist/stream-broker/index.js +526 -69
- package/dist/stream-broker/index.mjs +526 -69
- package/dist/stream-broker/remoteEntry.js +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-BlXyfeXU.mjs +0 -26
|
@@ -3,12 +3,12 @@ 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-
|
|
7
|
-
const require_node = require("../node
|
|
6
|
+
const require_dist = require("../dist-NYjn7hkE.js");
|
|
7
|
+
const require_node = require("../node-CVYY8a4y.js");
|
|
8
8
|
const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
|
|
9
9
|
const require_remote_restream = require("../remote-restream-CO36Sr30.js");
|
|
10
10
|
const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
|
|
11
|
-
const require_segment_demux_js = require("../segment-demux-js-
|
|
11
|
+
const require_segment_demux_js = require("../segment-demux-js-DVupBBc4.js");
|
|
12
12
|
let node_os = require("node:os");
|
|
13
13
|
node_os = require_chunk.__toESM(node_os);
|
|
14
14
|
let node_crypto = require("node:crypto");
|
|
@@ -26570,6 +26570,21 @@ var ControlMessageSchema = require_dist.discriminatedUnion("t", [
|
|
|
26570
26570
|
epoch: require_dist.number(),
|
|
26571
26571
|
profile: require_dist.string()
|
|
26572
26572
|
}),
|
|
26573
|
+
require_dist.object({
|
|
26574
|
+
t: require_dist.literal("playClip"),
|
|
26575
|
+
deviceId: require_dist.number(),
|
|
26576
|
+
clipId: require_dist.string().min(1),
|
|
26577
|
+
offsetMs: require_dist.number().min(0),
|
|
26578
|
+
/**
|
|
26579
|
+
* Which TWIN of the clip — the vendor's own quality pair, on the one
|
|
26580
|
+
* quality scale the system has. `auto` is deliberately not in
|
|
26581
|
+
* `CamProfileSchema`'s domain here for the same reason
|
|
26582
|
+
* `videoclips.getClipPlayback` refuses it: a stored file has no broker
|
|
26583
|
+
* session to resolve an adaptive tier against. Absent ⇒ the provider's own
|
|
26584
|
+
* default; never invented here.
|
|
26585
|
+
*/
|
|
26586
|
+
profile: require_dist.CamProfileSchema.optional()
|
|
26587
|
+
}),
|
|
26573
26588
|
require_dist.object({
|
|
26574
26589
|
t: require_dist.literal("seek"),
|
|
26575
26590
|
epoch: require_dist.number()
|
|
@@ -29889,6 +29904,18 @@ var TimelineSession = class {
|
|
|
29889
29904
|
* old recorded position is not where the viewer is coming from.
|
|
29890
29905
|
*/
|
|
29891
29906
|
lastPositionMs = null;
|
|
29907
|
+
/**
|
|
29908
|
+
* The clip the pointer is on, or `null` when the recorded source is the
|
|
29909
|
+
* recorder's own footage.
|
|
29910
|
+
*
|
|
29911
|
+
* It is deliberately NOT a third `mode`. Clip playback IS recorded mode with
|
|
29912
|
+
* a different byte source, so `currentMode` still reads `recorded` and every
|
|
29913
|
+
* invariant built on it holds unchanged — the adaptive ladder still suspends
|
|
29914
|
+
* itself while the pointer is off live, and `goLive` still leaves in one
|
|
29915
|
+
* dispatch (D570's go-live terminator: a mode `GO_LIVE` cannot leave is a
|
|
29916
|
+
* loop, and the cheapest way not to have one is not to add the mode).
|
|
29917
|
+
*/
|
|
29918
|
+
clip = null;
|
|
29892
29919
|
constructor(deps) {
|
|
29893
29920
|
this.deps = deps;
|
|
29894
29921
|
deps.session.setLiveFeedGate(false);
|
|
@@ -29907,13 +29934,26 @@ var TimelineSession = class {
|
|
|
29907
29934
|
this.emitLiveProfile();
|
|
29908
29935
|
switch (msg.t) {
|
|
29909
29936
|
case "playRecorded":
|
|
29937
|
+
this.leaveClip();
|
|
29910
29938
|
this.requestedProfile = msg.profile;
|
|
29911
29939
|
await this.playAt(msg.epoch, msg.profile, { travelFromMs: this.lastPositionMs });
|
|
29912
29940
|
break;
|
|
29941
|
+
case "playClip":
|
|
29942
|
+
await this.playClip(msg.deviceId, msg.clipId, msg.profile ?? null, msg.offsetMs);
|
|
29943
|
+
break;
|
|
29913
29944
|
case "seek":
|
|
29945
|
+
if (this.clip) {
|
|
29946
|
+
await this.feeder?.seek(msg.epoch);
|
|
29947
|
+
break;
|
|
29948
|
+
}
|
|
29914
29949
|
await this.playAt(msg.epoch, this.requestedProfile, { travelFromMs: this.lastPositionMs });
|
|
29915
29950
|
break;
|
|
29916
29951
|
case "scrubCommit":
|
|
29952
|
+
if (this.clip) {
|
|
29953
|
+
if (msg.dragId !== void 0) await this.feeder?.seek(msg.epoch, { dragId: msg.dragId });
|
|
29954
|
+
else await this.feeder?.seek(msg.epoch);
|
|
29955
|
+
break;
|
|
29956
|
+
}
|
|
29917
29957
|
if (msg.dragId !== void 0) await this.playAt(msg.epoch, this.requestedProfile, { dragId: msg.dragId });
|
|
29918
29958
|
else await this.playAt(msg.epoch, this.requestedProfile);
|
|
29919
29959
|
break;
|
|
@@ -29939,6 +29979,14 @@ var TimelineSession = class {
|
|
|
29939
29979
|
if (this.mode === "recorded") this.feeder?.stepFrame(msg.dir);
|
|
29940
29980
|
break;
|
|
29941
29981
|
case "setProfile":
|
|
29982
|
+
if (this.clip) {
|
|
29983
|
+
this.deps.onLog?.("clip playback: setProfile ignored — a clip file has no recorded tier", {
|
|
29984
|
+
deviceId: this.clip.deviceId,
|
|
29985
|
+
clipId: this.clip.clipId,
|
|
29986
|
+
requested: msg.profile
|
|
29987
|
+
});
|
|
29988
|
+
break;
|
|
29989
|
+
}
|
|
29942
29990
|
this.requestedProfile = msg.profile;
|
|
29943
29991
|
if (msg.profile === this.profile) break;
|
|
29944
29992
|
this.profile = msg.profile;
|
|
@@ -29957,12 +30005,52 @@ var TimelineSession = class {
|
|
|
29957
30005
|
this.feeder?.dispose();
|
|
29958
30006
|
this.deps.session.setLiveFeedGate(true);
|
|
29959
30007
|
this.mode = "recorded";
|
|
29960
|
-
const
|
|
30008
|
+
const clip = this.clip;
|
|
30009
|
+
const makeClip = this.deps.makeClipFeeder;
|
|
30010
|
+
const feeder = clip && makeClip ? makeClip({ handle: clip }) : this.deps.makeFeeder(this.profile);
|
|
29961
30011
|
if (this.rate !== 1) feeder.setRate(this.rate);
|
|
29962
30012
|
this.feeder = feeder;
|
|
29963
30013
|
return feeder;
|
|
29964
30014
|
}
|
|
29965
30015
|
/**
|
|
30016
|
+
* Put the pointer on a clip file: same track, same feeder, same control
|
|
30017
|
+
* protocol, on the clip's own axis.
|
|
30018
|
+
*
|
|
30019
|
+
* A session with no clip byte source REFUSES by name — it never gates the
|
|
30020
|
+
* live feed, never builds a feeder and never leaves the operator on a frozen
|
|
30021
|
+
* frame with no explanation (D391: the branch that accepts work and produces
|
|
30022
|
+
* nothing is the same branch as the one that drops it).
|
|
30023
|
+
*/
|
|
30024
|
+
async playClip(deviceId, clipId, profile, offsetMs) {
|
|
30025
|
+
if (!this.deps.makeClipFeeder) {
|
|
30026
|
+
this.deps.onLog?.("clip playback: refused — no clip byte source wired on this session (videoclips.readClipBytes absent)", {
|
|
30027
|
+
deviceId,
|
|
30028
|
+
clipId,
|
|
30029
|
+
profile,
|
|
30030
|
+
offsetMs
|
|
30031
|
+
});
|
|
30032
|
+
this._emitState("error");
|
|
30033
|
+
return;
|
|
30034
|
+
}
|
|
30035
|
+
this.clip = {
|
|
30036
|
+
deviceId,
|
|
30037
|
+
clipId,
|
|
30038
|
+
profile
|
|
30039
|
+
};
|
|
30040
|
+
this.lastPositionMs = null;
|
|
30041
|
+
await this.enterRecorded().seek(offsetMs);
|
|
30042
|
+
}
|
|
30043
|
+
/** Drop the clip pointer. Every path back to the recorder's own footage — a
|
|
30044
|
+
* `playRecorded` and `goLive` — goes through here, so there is exactly one
|
|
30045
|
+
* place that can forget to. */
|
|
30046
|
+
leaveClip() {
|
|
30047
|
+
if (!this.clip) return;
|
|
30048
|
+
this.clip = null;
|
|
30049
|
+
this.feeder?.dispose();
|
|
30050
|
+
this.feeder = null;
|
|
30051
|
+
this.mode = "live";
|
|
30052
|
+
}
|
|
30053
|
+
/**
|
|
29966
30054
|
* Play recorded footage at `epochMs`. If `locate` is wired and the requested
|
|
29967
30055
|
* profile has no segment, try high → mid → low so events-mode cameras that
|
|
29968
30056
|
* only kept one tier still play.
|
|
@@ -30053,6 +30141,7 @@ var TimelineSession = class {
|
|
|
30053
30141
|
}));
|
|
30054
30142
|
}
|
|
30055
30143
|
goLive() {
|
|
30144
|
+
this.clip = null;
|
|
30056
30145
|
this.feeder?.dispose();
|
|
30057
30146
|
this.feeder = null;
|
|
30058
30147
|
this.lastPositionMs = null;
|
|
@@ -30096,6 +30185,331 @@ var TimelineSession = class {
|
|
|
30096
30185
|
}
|
|
30097
30186
|
};
|
|
30098
30187
|
//#endregion
|
|
30188
|
+
//#region src/stream-broker/recorded/clip-mp4-index.ts
|
|
30189
|
+
/**
|
|
30190
|
+
* The sample index for a CLIP FILE, so a clip can be another position of the
|
|
30191
|
+
* recorded source pointer (D575).
|
|
30192
|
+
*
|
|
30193
|
+
* `RecordedFeeder` asks its byte source two questions — "is there a segment at
|
|
30194
|
+
* `epochMs`?" and "give me its bytes" — and the recorder answers both from the
|
|
30195
|
+
* segment's own `mfra`. A clip is a progressive `+faststart` MP4: it has a
|
|
30196
|
+
* `moov` at the FRONT and no `mfra` at all, so the time→sample map has to be
|
|
30197
|
+
* read out of `stbl` instead. That is what this module does, and it is the only
|
|
30198
|
+
* place in the clip path that parses MP4 structure.
|
|
30199
|
+
*
|
|
30200
|
+
* **It is a TIME index, not a byte index, and that is deliberate.** The
|
|
30201
|
+
* offsets in a progressive `moov` (`stco`/`stsc`) are absolute to the WHOLE
|
|
30202
|
+
* file, so a GOP-ranged slice of one is not a standalone-demuxable MP4 — making
|
|
30203
|
+
* it one means rewriting `stbl`, i.e. a remux. A clip arrives whole and
|
|
30204
|
+
* already compressed (one bounded by-handle fetch, D9/D18), so the file IS its
|
|
30205
|
+
* byte range, exactly as `RecordedFeederDeps.readGop` says of the playback
|
|
30206
|
+
* path. Parsing offsets nobody reads would be a leftover describing a design
|
|
30207
|
+
* this slice does not have.
|
|
30208
|
+
*
|
|
30209
|
+
* What the index IS for:
|
|
30210
|
+
* 1. **The gate.** A file with no usable sample table cannot join the recorded
|
|
30211
|
+
* path, and is refused BY NAME rather than served as a zero-length
|
|
30212
|
+
* timeline. Measured 2026-09-20: an HKSV clip has `mvhd.duration = 0`,
|
|
30213
|
+
* `mdhd.duration = 0`, no `sidx` and no `mfra` — ffmpeg writes the trailer
|
|
30214
|
+
* only on close and the tee cuts like a pipe.
|
|
30215
|
+
* 2. **The file-relative timeline.** `durationMs` is the segment span the
|
|
30216
|
+
* feeder plays and the bound `ended` is decided on; `firstSyncMs` is where a
|
|
30217
|
+
* seek before the first keyframe snaps to.
|
|
30218
|
+
*/
|
|
30219
|
+
var FOURCC_LENGTH = 4;
|
|
30220
|
+
var BOX_HEADER_LENGTH = 8;
|
|
30221
|
+
var LARGE_SIZE_HEADER_LENGTH = 16;
|
|
30222
|
+
/**
|
|
30223
|
+
* Walk the direct children of `[start, end)`. Returns `null` when the region is
|
|
30224
|
+
* structurally impossible (a size that does not fit), which the caller turns
|
|
30225
|
+
* into `malformed` — never a throw, because these bytes come off a camera.
|
|
30226
|
+
*/
|
|
30227
|
+
function childBoxes(bytes, start, end) {
|
|
30228
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
30229
|
+
const out = [];
|
|
30230
|
+
let off = start;
|
|
30231
|
+
while (off + BOX_HEADER_LENGTH <= end) {
|
|
30232
|
+
const declared = view.getUint32(off);
|
|
30233
|
+
let type = "";
|
|
30234
|
+
for (let i = 0; i < FOURCC_LENGTH; i++) type += String.fromCharCode(bytes[off + 4 + i] ?? 0);
|
|
30235
|
+
let size = declared;
|
|
30236
|
+
let contentStart = off + BOX_HEADER_LENGTH;
|
|
30237
|
+
if (declared === 1) {
|
|
30238
|
+
if (off + LARGE_SIZE_HEADER_LENGTH > end) return null;
|
|
30239
|
+
const hi = view.getUint32(off + 8);
|
|
30240
|
+
const lo = view.getUint32(off + 12);
|
|
30241
|
+
size = hi * 4294967296 + lo;
|
|
30242
|
+
contentStart = off + LARGE_SIZE_HEADER_LENGTH;
|
|
30243
|
+
} else if (declared === 0) size = end - off;
|
|
30244
|
+
if (size < contentStart - off) return null;
|
|
30245
|
+
const boxEnd = off + size;
|
|
30246
|
+
if (boxEnd > end) return null;
|
|
30247
|
+
out.push({
|
|
30248
|
+
type,
|
|
30249
|
+
start: off,
|
|
30250
|
+
end: boxEnd,
|
|
30251
|
+
contentStart
|
|
30252
|
+
});
|
|
30253
|
+
off = boxEnd;
|
|
30254
|
+
}
|
|
30255
|
+
return out;
|
|
30256
|
+
}
|
|
30257
|
+
function findBox(boxes, type) {
|
|
30258
|
+
for (const b of boxes) if (b.type === type) return b;
|
|
30259
|
+
return null;
|
|
30260
|
+
}
|
|
30261
|
+
function readSttsRuns(bytes, box) {
|
|
30262
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
30263
|
+
const base = box.contentStart + 4;
|
|
30264
|
+
if (base + 4 > box.end) return null;
|
|
30265
|
+
const entries = view.getUint32(base);
|
|
30266
|
+
if (base + 4 + entries * 8 > box.end) return null;
|
|
30267
|
+
const runs = [];
|
|
30268
|
+
for (let i = 0; i < entries; i++) {
|
|
30269
|
+
const at = base + 4 + i * 8;
|
|
30270
|
+
runs.push({
|
|
30271
|
+
count: view.getUint32(at),
|
|
30272
|
+
delta: view.getUint32(at + 4)
|
|
30273
|
+
});
|
|
30274
|
+
}
|
|
30275
|
+
return runs;
|
|
30276
|
+
}
|
|
30277
|
+
function readSyncSampleNumbers(bytes, box) {
|
|
30278
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
30279
|
+
const base = box.contentStart + 4;
|
|
30280
|
+
if (base + 4 > box.end) return null;
|
|
30281
|
+
const entries = view.getUint32(base);
|
|
30282
|
+
if (base + 4 + entries * 4 > box.end) return null;
|
|
30283
|
+
const out = /* @__PURE__ */ new Set();
|
|
30284
|
+
for (let i = 0; i < entries; i++) out.add(view.getUint32(base + 4 + i * 4));
|
|
30285
|
+
return out;
|
|
30286
|
+
}
|
|
30287
|
+
function mediaTimescale(bytes, mdhd) {
|
|
30288
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
30289
|
+
const version = bytes[mdhd.contentStart] ?? 0;
|
|
30290
|
+
const at = mdhd.contentStart + 4 + (version === 1 ? 16 : 8);
|
|
30291
|
+
if (at + 4 > mdhd.end) return null;
|
|
30292
|
+
const ts = view.getUint32(at);
|
|
30293
|
+
return ts > 0 ? ts : null;
|
|
30294
|
+
}
|
|
30295
|
+
function handlerType(bytes, hdlr) {
|
|
30296
|
+
const at = hdlr.contentStart + 8;
|
|
30297
|
+
if (at + FOURCC_LENGTH > hdlr.end) return "";
|
|
30298
|
+
let out = "";
|
|
30299
|
+
for (let i = 0; i < FOURCC_LENGTH; i++) out += String.fromCharCode(bytes[at + i] ?? 0);
|
|
30300
|
+
return out;
|
|
30301
|
+
}
|
|
30302
|
+
/** The video `trak` of a `moov`, or `null` when it has none. */
|
|
30303
|
+
function videoTrack(bytes, moov) {
|
|
30304
|
+
const children = childBoxes(bytes, moov.contentStart, moov.end);
|
|
30305
|
+
if (!children) return null;
|
|
30306
|
+
for (const trak of children) {
|
|
30307
|
+
if (trak.type !== "trak") continue;
|
|
30308
|
+
const trakKids = childBoxes(bytes, trak.contentStart, trak.end);
|
|
30309
|
+
const mdia = trakKids ? findBox(trakKids, "mdia") : null;
|
|
30310
|
+
if (!mdia) continue;
|
|
30311
|
+
const mdiaKids = childBoxes(bytes, mdia.contentStart, mdia.end);
|
|
30312
|
+
const hdlr = mdiaKids ? findBox(mdiaKids, "hdlr") : null;
|
|
30313
|
+
if (hdlr && handlerType(bytes, hdlr) === "vide") return mdia;
|
|
30314
|
+
}
|
|
30315
|
+
return null;
|
|
30316
|
+
}
|
|
30317
|
+
/**
|
|
30318
|
+
* Read a clip file's time→sample map. Never throws: every structural failure
|
|
30319
|
+
* comes back as a named refusal so the caller can say WHY a clip will not play.
|
|
30320
|
+
*/
|
|
30321
|
+
function indexClipMp4(bytes) {
|
|
30322
|
+
const top = childBoxes(bytes, 0, bytes.length);
|
|
30323
|
+
if (!top) return {
|
|
30324
|
+
kind: "refused",
|
|
30325
|
+
reason: "malformed"
|
|
30326
|
+
};
|
|
30327
|
+
const moov = findBox(top, "moov");
|
|
30328
|
+
if (!moov) return {
|
|
30329
|
+
kind: "refused",
|
|
30330
|
+
reason: "no-moov"
|
|
30331
|
+
};
|
|
30332
|
+
const mdia = videoTrack(bytes, moov);
|
|
30333
|
+
if (!mdia) return {
|
|
30334
|
+
kind: "refused",
|
|
30335
|
+
reason: "no-video-track"
|
|
30336
|
+
};
|
|
30337
|
+
const mdiaKids = childBoxes(bytes, mdia.contentStart, mdia.end);
|
|
30338
|
+
if (!mdiaKids) return {
|
|
30339
|
+
kind: "refused",
|
|
30340
|
+
reason: "malformed"
|
|
30341
|
+
};
|
|
30342
|
+
const mdhd = findBox(mdiaKids, "mdhd");
|
|
30343
|
+
const minf = findBox(mdiaKids, "minf");
|
|
30344
|
+
if (!mdhd || !minf) return {
|
|
30345
|
+
kind: "refused",
|
|
30346
|
+
reason: "malformed"
|
|
30347
|
+
};
|
|
30348
|
+
const timescale = mediaTimescale(bytes, mdhd);
|
|
30349
|
+
if (timescale === null) return {
|
|
30350
|
+
kind: "refused",
|
|
30351
|
+
reason: "malformed"
|
|
30352
|
+
};
|
|
30353
|
+
const minfKids = childBoxes(bytes, minf.contentStart, minf.end);
|
|
30354
|
+
const stbl = minfKids ? findBox(minfKids, "stbl") : null;
|
|
30355
|
+
if (!stbl) return {
|
|
30356
|
+
kind: "refused",
|
|
30357
|
+
reason: "no-samples"
|
|
30358
|
+
};
|
|
30359
|
+
const stblKids = childBoxes(bytes, stbl.contentStart, stbl.end);
|
|
30360
|
+
if (!stblKids) return {
|
|
30361
|
+
kind: "refused",
|
|
30362
|
+
reason: "malformed"
|
|
30363
|
+
};
|
|
30364
|
+
const sttsBox = findBox(stblKids, "stts");
|
|
30365
|
+
if (!sttsBox) return {
|
|
30366
|
+
kind: "refused",
|
|
30367
|
+
reason: "no-samples"
|
|
30368
|
+
};
|
|
30369
|
+
const runs = readSttsRuns(bytes, sttsBox);
|
|
30370
|
+
if (!runs) return {
|
|
30371
|
+
kind: "refused",
|
|
30372
|
+
reason: "malformed"
|
|
30373
|
+
};
|
|
30374
|
+
const stssBox = findBox(stblKids, "stss");
|
|
30375
|
+
const syncNumbers = stssBox === null ? null : readSyncSampleNumbers(bytes, stssBox);
|
|
30376
|
+
if (stssBox !== null && syncNumbers === null) return {
|
|
30377
|
+
kind: "refused",
|
|
30378
|
+
reason: "malformed"
|
|
30379
|
+
};
|
|
30380
|
+
const samples = [];
|
|
30381
|
+
let ticks = 0;
|
|
30382
|
+
let syncSampleCount = 0;
|
|
30383
|
+
let firstSyncMs = null;
|
|
30384
|
+
for (const run of runs) for (let i = 0; i < run.count; i++) {
|
|
30385
|
+
const sampleNumber = samples.length + 1;
|
|
30386
|
+
const sync = syncNumbers === null ? true : syncNumbers.has(sampleNumber);
|
|
30387
|
+
const startMs = ticks * 1e3 / timescale;
|
|
30388
|
+
const durMs = run.delta * 1e3 / timescale;
|
|
30389
|
+
if (sync) {
|
|
30390
|
+
syncSampleCount++;
|
|
30391
|
+
if (firstSyncMs === null) firstSyncMs = startMs;
|
|
30392
|
+
}
|
|
30393
|
+
samples.push({
|
|
30394
|
+
startMs,
|
|
30395
|
+
durMs,
|
|
30396
|
+
sync
|
|
30397
|
+
});
|
|
30398
|
+
ticks += run.delta;
|
|
30399
|
+
}
|
|
30400
|
+
if (samples.length === 0 || ticks === 0) return {
|
|
30401
|
+
kind: "refused",
|
|
30402
|
+
reason: "no-samples"
|
|
30403
|
+
};
|
|
30404
|
+
return {
|
|
30405
|
+
kind: "index",
|
|
30406
|
+
durationMs: ticks * 1e3 / timescale,
|
|
30407
|
+
firstSyncMs: firstSyncMs ?? 0,
|
|
30408
|
+
syncSampleCount,
|
|
30409
|
+
samples
|
|
30410
|
+
};
|
|
30411
|
+
}
|
|
30412
|
+
//#endregion
|
|
30413
|
+
//#region src/stream-broker/recorded/clip-file-source.ts
|
|
30414
|
+
var ClipSourceError = class extends Error {
|
|
30415
|
+
reason;
|
|
30416
|
+
constructor(reason, message) {
|
|
30417
|
+
super(message);
|
|
30418
|
+
this.reason = reason;
|
|
30419
|
+
this.name = "ClipSourceError";
|
|
30420
|
+
}
|
|
30421
|
+
};
|
|
30422
|
+
function createClipFileSource(deps) {
|
|
30423
|
+
const maxBytes = deps.maxBytes ?? 67108864;
|
|
30424
|
+
const { deviceId, clipId, profile } = deps.handle;
|
|
30425
|
+
let resolved = null;
|
|
30426
|
+
let inFlight = null;
|
|
30427
|
+
/**
|
|
30428
|
+
* A refusal that can never change for this file — it is unindexable, or it
|
|
30429
|
+
* is over the bound. Cached so a drag does not re-fetch a 35 MB file per
|
|
30430
|
+
* pointer move. A FETCH failure is deliberately NOT cached: a sleeping
|
|
30431
|
+
* battery camera is a transient no, and the operator's retry must reach it.
|
|
30432
|
+
*/
|
|
30433
|
+
let terminal = null;
|
|
30434
|
+
const refuse = (reason, message, data) => {
|
|
30435
|
+
const err = new ClipSourceError(reason, message);
|
|
30436
|
+
deps.onLog?.(message, {
|
|
30437
|
+
deviceId,
|
|
30438
|
+
clipId,
|
|
30439
|
+
profile,
|
|
30440
|
+
reason,
|
|
30441
|
+
...data
|
|
30442
|
+
});
|
|
30443
|
+
return err;
|
|
30444
|
+
};
|
|
30445
|
+
const load = async () => {
|
|
30446
|
+
let bytes;
|
|
30447
|
+
try {
|
|
30448
|
+
bytes = await deps.read({
|
|
30449
|
+
deviceId,
|
|
30450
|
+
clipId,
|
|
30451
|
+
profile
|
|
30452
|
+
});
|
|
30453
|
+
} catch (err) {
|
|
30454
|
+
throw refuse("fetch-failed", "clip playback: byte fetch failed", { error: err instanceof Error ? err.message : String(err) });
|
|
30455
|
+
}
|
|
30456
|
+
if (bytes.length > maxBytes) {
|
|
30457
|
+
terminal = refuse("too-large", "clip playback: clip exceeds the broker byte bound", {
|
|
30458
|
+
bytes: bytes.length,
|
|
30459
|
+
maxBytes
|
|
30460
|
+
});
|
|
30461
|
+
throw terminal;
|
|
30462
|
+
}
|
|
30463
|
+
const index = indexClipMp4(bytes);
|
|
30464
|
+
if (index.kind === "refused") {
|
|
30465
|
+
terminal = refuse(index.reason, "clip playback: clip cannot be indexed — it will not play in the recorded path", { bytes: bytes.length });
|
|
30466
|
+
throw terminal;
|
|
30467
|
+
}
|
|
30468
|
+
return {
|
|
30469
|
+
bytes,
|
|
30470
|
+
index
|
|
30471
|
+
};
|
|
30472
|
+
};
|
|
30473
|
+
const ensure = async () => {
|
|
30474
|
+
if (terminal) throw terminal;
|
|
30475
|
+
if (resolved) return resolved;
|
|
30476
|
+
if (!inFlight) inFlight = load().then((r) => {
|
|
30477
|
+
resolved = r;
|
|
30478
|
+
return r;
|
|
30479
|
+
}).finally(() => {
|
|
30480
|
+
inFlight = null;
|
|
30481
|
+
});
|
|
30482
|
+
return inFlight;
|
|
30483
|
+
};
|
|
30484
|
+
return {
|
|
30485
|
+
async locate(args) {
|
|
30486
|
+
const { index, bytes } = await ensure();
|
|
30487
|
+
if (args.epochMs >= index.durationMs) return {
|
|
30488
|
+
kind: "gap",
|
|
30489
|
+
nearestEdgeMs: null,
|
|
30490
|
+
prevEndMs: index.durationMs
|
|
30491
|
+
};
|
|
30492
|
+
if (args.epochMs < index.firstSyncMs) return {
|
|
30493
|
+
kind: "gap",
|
|
30494
|
+
nearestEdgeMs: index.firstSyncMs,
|
|
30495
|
+
prevEndMs: null
|
|
30496
|
+
};
|
|
30497
|
+
return {
|
|
30498
|
+
kind: "segment",
|
|
30499
|
+
startMs: 0,
|
|
30500
|
+
durMs: index.durationMs,
|
|
30501
|
+
bytes: bytes.length
|
|
30502
|
+
};
|
|
30503
|
+
},
|
|
30504
|
+
async readBytes() {
|
|
30505
|
+
return (await ensure()).bytes;
|
|
30506
|
+
},
|
|
30507
|
+
async durationMs() {
|
|
30508
|
+
return (await ensure()).index.durationMs;
|
|
30509
|
+
}
|
|
30510
|
+
};
|
|
30511
|
+
}
|
|
30512
|
+
//#endregion
|
|
30099
30513
|
//#region src/stream-broker/recorded/demux-off-thread.ts
|
|
30100
30514
|
/**
|
|
30101
30515
|
* Off-thread JS demux client — mp4box parse on a `worker_threads` Worker
|
|
@@ -30841,6 +31255,7 @@ var BrokerWebrtcServer = class {
|
|
|
30841
31255
|
locateRecordedSegment;
|
|
30842
31256
|
readRecordedSegmentBytes;
|
|
30843
31257
|
readRecordedGopBytes;
|
|
31258
|
+
readClipBytes;
|
|
30844
31259
|
resolveFfmpegPath;
|
|
30845
31260
|
/**
|
|
30846
31261
|
* THE ffmpeg primitive, for the transcode leg. `null` until the manager wires
|
|
@@ -30933,6 +31348,7 @@ var BrokerWebrtcServer = class {
|
|
|
30933
31348
|
this.locateRecordedSegment = options.locateRecordedSegment;
|
|
30934
31349
|
this.readRecordedSegmentBytes = options.readRecordedSegmentBytes;
|
|
30935
31350
|
this.readRecordedGopBytes = options.readRecordedGopBytes;
|
|
31351
|
+
this.readClipBytes = options.readClipBytes;
|
|
30936
31352
|
this.resolveFfmpegPath = options.resolveFfmpegPath;
|
|
30937
31353
|
}
|
|
30938
31354
|
registerBroker(brokerId, broker) {
|
|
@@ -31652,7 +32068,96 @@ var BrokerWebrtcServer = class {
|
|
|
31652
32068
|
bytes: res.data
|
|
31653
32069
|
};
|
|
31654
32070
|
} : void 0;
|
|
32071
|
+
const readClip = this.readClipBytes;
|
|
31655
32072
|
let timeline;
|
|
32073
|
+
/**
|
|
32074
|
+
* ONE feeder builder for BOTH sources (D575). `clip === null` is the
|
|
32075
|
+
* recorder's own footage; a clip swaps only the two byte closures —
|
|
32076
|
+
* everything else (demux, sink, audio codec branch, state/position/log
|
|
32077
|
+
* seams) is identical, which is the whole claim the clip path makes.
|
|
32078
|
+
*/
|
|
32079
|
+
const buildFeeder = (profile, clip) => {
|
|
32080
|
+
const opusSession = session.isOpusAudio;
|
|
32081
|
+
const audioSink = opusSession ? {
|
|
32082
|
+
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
32083
|
+
pushAudioOpus: (opus, ts) => session.writeRecordedAudioOpus(Buffer.from(opus), ts)
|
|
32084
|
+
} : {
|
|
32085
|
+
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
32086
|
+
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
32087
|
+
};
|
|
32088
|
+
const demuxAudio = opusSession ? async (b) => {
|
|
32089
|
+
return (await demuxSegmentAudioOpusPreferJs(b, {
|
|
32090
|
+
ffmpegPath: resolveFfmpeg?.(),
|
|
32091
|
+
onFfmpegFallback: (reason) => {
|
|
32092
|
+
if (audioOpusFallbackLogged) return;
|
|
32093
|
+
audioOpusFallbackLogged = true;
|
|
32094
|
+
sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
|
|
32095
|
+
sessionId,
|
|
32096
|
+
deviceId,
|
|
32097
|
+
profile,
|
|
32098
|
+
reason
|
|
32099
|
+
} });
|
|
32100
|
+
}
|
|
32101
|
+
})).map((au) => ({
|
|
32102
|
+
data: au.data,
|
|
32103
|
+
ptsMs: au.ptsMs
|
|
32104
|
+
}));
|
|
32105
|
+
} : (b) => demuxSegmentToPcmu(b, resolveFfmpeg?.());
|
|
32106
|
+
const clipSource = clip && readClip ? createClipFileSource({
|
|
32107
|
+
handle: clip,
|
|
32108
|
+
read: readClip,
|
|
32109
|
+
onLog: (msg, data) => sessionLogger.warn(msg, {
|
|
32110
|
+
tags: { deviceId: clip.deviceId },
|
|
32111
|
+
meta: {
|
|
32112
|
+
sessionId,
|
|
32113
|
+
...data
|
|
32114
|
+
}
|
|
32115
|
+
})
|
|
32116
|
+
}) : null;
|
|
32117
|
+
return new RecordedFeeder({
|
|
32118
|
+
deviceId: clip ? clip.deviceId : deviceId,
|
|
32119
|
+
profile,
|
|
32120
|
+
locate: clipSource ? clipSource.locate : locateRecorded,
|
|
32121
|
+
readBytes: clipSource ? clipSource.readBytes : readRecorded,
|
|
32122
|
+
...clipSource ? {} : {
|
|
32123
|
+
readGop,
|
|
32124
|
+
fragmentCache: scrubCache
|
|
32125
|
+
},
|
|
32126
|
+
demux: demuxVideo,
|
|
32127
|
+
demuxTally,
|
|
32128
|
+
demuxAudio,
|
|
32129
|
+
audioClockRateHz: opusSession ? 48e3 : 8e3,
|
|
32130
|
+
sink: audioSink,
|
|
32131
|
+
onState: (t) => {
|
|
32132
|
+
sessionLogger.info("recorded: feeder state", {
|
|
32133
|
+
tags: { deviceId: clip ? clip.deviceId : deviceId },
|
|
32134
|
+
meta: {
|
|
32135
|
+
sessionId,
|
|
32136
|
+
deviceId,
|
|
32137
|
+
profile,
|
|
32138
|
+
clipId: clip?.clipId,
|
|
32139
|
+
from: t.from,
|
|
32140
|
+
to: t.to,
|
|
32141
|
+
reason: t.reason,
|
|
32142
|
+
seekToken: t.seekToken,
|
|
32143
|
+
dragId: t.dragId
|
|
32144
|
+
}
|
|
32145
|
+
});
|
|
32146
|
+
timeline._emitState(t.to);
|
|
32147
|
+
},
|
|
32148
|
+
onPosition: (epochMs, pushSeq) => timeline._emitPosition(epochMs, pushSeq),
|
|
32149
|
+
onLog: (msg, data) => sessionLogger.info(msg, {
|
|
32150
|
+
tags: { deviceId: clip ? clip.deviceId : deviceId },
|
|
32151
|
+
meta: {
|
|
32152
|
+
sessionId,
|
|
32153
|
+
deviceId,
|
|
32154
|
+
profile,
|
|
32155
|
+
clipId: clip?.clipId,
|
|
32156
|
+
...data
|
|
32157
|
+
}
|
|
32158
|
+
})
|
|
32159
|
+
});
|
|
32160
|
+
};
|
|
31656
32161
|
timeline = new TimelineSession({
|
|
31657
32162
|
session,
|
|
31658
32163
|
deviceId,
|
|
@@ -31683,71 +32188,15 @@ var BrokerWebrtcServer = class {
|
|
|
31683
32188
|
} });
|
|
31684
32189
|
});
|
|
31685
32190
|
} : void 0,
|
|
31686
|
-
makeFeeder: (profile) =>
|
|
31687
|
-
|
|
31688
|
-
|
|
31689
|
-
|
|
31690
|
-
|
|
31691
|
-
|
|
31692
|
-
|
|
31693
|
-
|
|
31694
|
-
|
|
31695
|
-
demux: demuxVideo,
|
|
31696
|
-
demuxTally,
|
|
31697
|
-
demuxAudio: opusSession ? async (b) => {
|
|
31698
|
-
return (await demuxSegmentAudioOpusPreferJs(b, {
|
|
31699
|
-
ffmpegPath: resolveFfmpeg?.(),
|
|
31700
|
-
onFfmpegFallback: (reason) => {
|
|
31701
|
-
if (audioOpusFallbackLogged) return;
|
|
31702
|
-
audioOpusFallbackLogged = true;
|
|
31703
|
-
sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
|
|
31704
|
-
sessionId,
|
|
31705
|
-
deviceId,
|
|
31706
|
-
profile,
|
|
31707
|
-
reason
|
|
31708
|
-
} });
|
|
31709
|
-
}
|
|
31710
|
-
})).map((au) => ({
|
|
31711
|
-
data: au.data,
|
|
31712
|
-
ptsMs: au.ptsMs
|
|
31713
|
-
}));
|
|
31714
|
-
} : (b) => demuxSegmentToPcmu(b, resolveFfmpeg?.()),
|
|
31715
|
-
audioClockRateHz: opusSession ? 48e3 : 8e3,
|
|
31716
|
-
sink: opusSession ? {
|
|
31717
|
-
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
31718
|
-
pushAudioOpus: (opus, ts) => session.writeRecordedAudioOpus(Buffer.from(opus), ts)
|
|
31719
|
-
} : {
|
|
31720
|
-
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
31721
|
-
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
31722
|
-
},
|
|
31723
|
-
onState: (t) => {
|
|
31724
|
-
sessionLogger.info("recorded: feeder state", {
|
|
31725
|
-
tags: { deviceId },
|
|
31726
|
-
meta: {
|
|
31727
|
-
sessionId,
|
|
31728
|
-
deviceId,
|
|
31729
|
-
profile,
|
|
31730
|
-
from: t.from,
|
|
31731
|
-
to: t.to,
|
|
31732
|
-
reason: t.reason,
|
|
31733
|
-
seekToken: t.seekToken,
|
|
31734
|
-
dragId: t.dragId
|
|
31735
|
-
}
|
|
31736
|
-
});
|
|
31737
|
-
timeline._emitState(t.to);
|
|
31738
|
-
},
|
|
31739
|
-
onPosition: (epochMs, pushSeq) => timeline._emitPosition(epochMs, pushSeq),
|
|
31740
|
-
onLog: (msg, data) => sessionLogger.info(msg, {
|
|
31741
|
-
tags: { deviceId },
|
|
31742
|
-
meta: {
|
|
31743
|
-
sessionId,
|
|
31744
|
-
deviceId,
|
|
31745
|
-
profile,
|
|
31746
|
-
...data
|
|
31747
|
-
}
|
|
31748
|
-
})
|
|
31749
|
-
});
|
|
31750
|
-
}
|
|
32191
|
+
makeFeeder: (profile) => buildFeeder(profile, null),
|
|
32192
|
+
...readClip ? { makeClipFeeder: ({ handle }) => buildFeeder(handle.profile ?? "clip", handle) } : {},
|
|
32193
|
+
onLog: (msg, data) => sessionLogger.warn(msg, {
|
|
32194
|
+
tags: { deviceId },
|
|
32195
|
+
meta: {
|
|
32196
|
+
sessionId,
|
|
32197
|
+
...data
|
|
32198
|
+
}
|
|
32199
|
+
})
|
|
31751
32200
|
});
|
|
31752
32201
|
entry.timeline = timeline;
|
|
31753
32202
|
sessionLogger.info("recorded: TimelineSession wired (control accessors present)", { meta: {
|
|
@@ -33250,6 +33699,14 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
33250
33699
|
},
|
|
33251
33700
|
locateRecordedSegment: (args) => this.ctx.api.recording.locateSegment.query(args),
|
|
33252
33701
|
readRecordedSegmentBytes: async (args) => (await this.ctx.api.recording.readSegmentBytes.query(args)).data,
|
|
33702
|
+
readClipBytes: async (args) => {
|
|
33703
|
+
const res = await this.ctx.api.videoclips.readClipBytes.query({
|
|
33704
|
+
deviceId: args.deviceId,
|
|
33705
|
+
clipId: args.clipId,
|
|
33706
|
+
...args.profile !== null ? { profile: args.profile } : {}
|
|
33707
|
+
});
|
|
33708
|
+
return new Uint8Array(Buffer.from(res.base64, "base64"));
|
|
33709
|
+
},
|
|
33253
33710
|
readRecordedGopBytes: async (args) => {
|
|
33254
33711
|
try {
|
|
33255
33712
|
return await this.ctx.api.recording.readGopBytes.query({
|