@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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $t as
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node
|
|
2
|
+
import { $t as CAM_PROFILE_ORDER, B as cameraStreamsCapability, Bt as streamBrokerCapability, Cn as number, D as RATE_CONTROL_RELAXED, Dn as string, Dt as rankStreams, En as record, F as addonWidgetsSourceCapability, G as declareLogChannel, Gt as errMsg, Jt as buildFfmpegArgs$1, Kt as AUDIO_PRESETS, M as RingBuffer, Nt as resolveEgressDecodeHwAccel, O as RATE_CONTROL_TIGHT, On as union, Q as egressTransportFromRequest, Qt as BaseAddon, Sn as literal, U as createHwAccelCache, W as createLogChannelsProvider, Wt as webrtcSessionCapability, Xt as isSoftwareDecode, Yt as invocationFromEncodeProfile, Z as egressTranscodeSharingKey, Zt as BOOT_RECOVERY_BACKOFF_MS, _ as EncodeProfileSchema, _n as _enum, b as G711_SCALE_CORRECTION_DB, bn as discriminatedUnion, dn as makeSourceBrokerId, dt as logChannelsCapability, en as CamProfileSchema, fn as nodePin, i as BatteryStatusSchema, in as createEvent, kn as EventCategory, ln as isEvent, mn as parseProfileBrokerId, nn as DeviceType, pt as maskUrlCredentials, q as deriveBatteryPresence, sn as expandAudioChunkToF32le, tn as DeviceFeature, un as makeProfileBrokerId, ut as loadContributionCapability, vn as array, wn as object, yn as boolean, z as batteryCapability } from "../dist-C1pCVJk7.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node--UY0iepi.mjs";
|
|
4
4
|
import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
|
|
5
5
|
import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
|
|
6
6
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
|
|
7
|
-
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
7
|
+
import { i as require_mp4box_all, n as demuxSegmentPreferJs, r as runFfmpegOnce, t as demuxSegmentFullJs } from "../segment-demux-js-B0_aAzN0.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -26566,6 +26566,21 @@ var ControlMessageSchema = discriminatedUnion("t", [
|
|
|
26566
26566
|
epoch: number(),
|
|
26567
26567
|
profile: string()
|
|
26568
26568
|
}),
|
|
26569
|
+
object({
|
|
26570
|
+
t: literal("playClip"),
|
|
26571
|
+
deviceId: number(),
|
|
26572
|
+
clipId: string().min(1),
|
|
26573
|
+
offsetMs: number().min(0),
|
|
26574
|
+
/**
|
|
26575
|
+
* Which TWIN of the clip — the vendor's own quality pair, on the one
|
|
26576
|
+
* quality scale the system has. `auto` is deliberately not in
|
|
26577
|
+
* `CamProfileSchema`'s domain here for the same reason
|
|
26578
|
+
* `videoclips.getClipPlayback` refuses it: a stored file has no broker
|
|
26579
|
+
* session to resolve an adaptive tier against. Absent ⇒ the provider's own
|
|
26580
|
+
* default; never invented here.
|
|
26581
|
+
*/
|
|
26582
|
+
profile: CamProfileSchema.optional()
|
|
26583
|
+
}),
|
|
26569
26584
|
object({
|
|
26570
26585
|
t: literal("seek"),
|
|
26571
26586
|
epoch: number()
|
|
@@ -29885,6 +29900,18 @@ var TimelineSession = class {
|
|
|
29885
29900
|
* old recorded position is not where the viewer is coming from.
|
|
29886
29901
|
*/
|
|
29887
29902
|
lastPositionMs = null;
|
|
29903
|
+
/**
|
|
29904
|
+
* The clip the pointer is on, or `null` when the recorded source is the
|
|
29905
|
+
* recorder's own footage.
|
|
29906
|
+
*
|
|
29907
|
+
* It is deliberately NOT a third `mode`. Clip playback IS recorded mode with
|
|
29908
|
+
* a different byte source, so `currentMode` still reads `recorded` and every
|
|
29909
|
+
* invariant built on it holds unchanged — the adaptive ladder still suspends
|
|
29910
|
+
* itself while the pointer is off live, and `goLive` still leaves in one
|
|
29911
|
+
* dispatch (D570's go-live terminator: a mode `GO_LIVE` cannot leave is a
|
|
29912
|
+
* loop, and the cheapest way not to have one is not to add the mode).
|
|
29913
|
+
*/
|
|
29914
|
+
clip = null;
|
|
29888
29915
|
constructor(deps) {
|
|
29889
29916
|
this.deps = deps;
|
|
29890
29917
|
deps.session.setLiveFeedGate(false);
|
|
@@ -29903,13 +29930,26 @@ var TimelineSession = class {
|
|
|
29903
29930
|
this.emitLiveProfile();
|
|
29904
29931
|
switch (msg.t) {
|
|
29905
29932
|
case "playRecorded":
|
|
29933
|
+
this.leaveClip();
|
|
29906
29934
|
this.requestedProfile = msg.profile;
|
|
29907
29935
|
await this.playAt(msg.epoch, msg.profile, { travelFromMs: this.lastPositionMs });
|
|
29908
29936
|
break;
|
|
29937
|
+
case "playClip":
|
|
29938
|
+
await this.playClip(msg.deviceId, msg.clipId, msg.profile ?? null, msg.offsetMs);
|
|
29939
|
+
break;
|
|
29909
29940
|
case "seek":
|
|
29941
|
+
if (this.clip) {
|
|
29942
|
+
await this.feeder?.seek(msg.epoch);
|
|
29943
|
+
break;
|
|
29944
|
+
}
|
|
29910
29945
|
await this.playAt(msg.epoch, this.requestedProfile, { travelFromMs: this.lastPositionMs });
|
|
29911
29946
|
break;
|
|
29912
29947
|
case "scrubCommit":
|
|
29948
|
+
if (this.clip) {
|
|
29949
|
+
if (msg.dragId !== void 0) await this.feeder?.seek(msg.epoch, { dragId: msg.dragId });
|
|
29950
|
+
else await this.feeder?.seek(msg.epoch);
|
|
29951
|
+
break;
|
|
29952
|
+
}
|
|
29913
29953
|
if (msg.dragId !== void 0) await this.playAt(msg.epoch, this.requestedProfile, { dragId: msg.dragId });
|
|
29914
29954
|
else await this.playAt(msg.epoch, this.requestedProfile);
|
|
29915
29955
|
break;
|
|
@@ -29935,6 +29975,14 @@ var TimelineSession = class {
|
|
|
29935
29975
|
if (this.mode === "recorded") this.feeder?.stepFrame(msg.dir);
|
|
29936
29976
|
break;
|
|
29937
29977
|
case "setProfile":
|
|
29978
|
+
if (this.clip) {
|
|
29979
|
+
this.deps.onLog?.("clip playback: setProfile ignored — a clip file has no recorded tier", {
|
|
29980
|
+
deviceId: this.clip.deviceId,
|
|
29981
|
+
clipId: this.clip.clipId,
|
|
29982
|
+
requested: msg.profile
|
|
29983
|
+
});
|
|
29984
|
+
break;
|
|
29985
|
+
}
|
|
29938
29986
|
this.requestedProfile = msg.profile;
|
|
29939
29987
|
if (msg.profile === this.profile) break;
|
|
29940
29988
|
this.profile = msg.profile;
|
|
@@ -29953,12 +30001,52 @@ var TimelineSession = class {
|
|
|
29953
30001
|
this.feeder?.dispose();
|
|
29954
30002
|
this.deps.session.setLiveFeedGate(true);
|
|
29955
30003
|
this.mode = "recorded";
|
|
29956
|
-
const
|
|
30004
|
+
const clip = this.clip;
|
|
30005
|
+
const makeClip = this.deps.makeClipFeeder;
|
|
30006
|
+
const feeder = clip && makeClip ? makeClip({ handle: clip }) : this.deps.makeFeeder(this.profile);
|
|
29957
30007
|
if (this.rate !== 1) feeder.setRate(this.rate);
|
|
29958
30008
|
this.feeder = feeder;
|
|
29959
30009
|
return feeder;
|
|
29960
30010
|
}
|
|
29961
30011
|
/**
|
|
30012
|
+
* Put the pointer on a clip file: same track, same feeder, same control
|
|
30013
|
+
* protocol, on the clip's own axis.
|
|
30014
|
+
*
|
|
30015
|
+
* A session with no clip byte source REFUSES by name — it never gates the
|
|
30016
|
+
* live feed, never builds a feeder and never leaves the operator on a frozen
|
|
30017
|
+
* frame with no explanation (D391: the branch that accepts work and produces
|
|
30018
|
+
* nothing is the same branch as the one that drops it).
|
|
30019
|
+
*/
|
|
30020
|
+
async playClip(deviceId, clipId, profile, offsetMs) {
|
|
30021
|
+
if (!this.deps.makeClipFeeder) {
|
|
30022
|
+
this.deps.onLog?.("clip playback: refused — no clip byte source wired on this session (videoclips.readClipBytes absent)", {
|
|
30023
|
+
deviceId,
|
|
30024
|
+
clipId,
|
|
30025
|
+
profile,
|
|
30026
|
+
offsetMs
|
|
30027
|
+
});
|
|
30028
|
+
this._emitState("error");
|
|
30029
|
+
return;
|
|
30030
|
+
}
|
|
30031
|
+
this.clip = {
|
|
30032
|
+
deviceId,
|
|
30033
|
+
clipId,
|
|
30034
|
+
profile
|
|
30035
|
+
};
|
|
30036
|
+
this.lastPositionMs = null;
|
|
30037
|
+
await this.enterRecorded().seek(offsetMs);
|
|
30038
|
+
}
|
|
30039
|
+
/** Drop the clip pointer. Every path back to the recorder's own footage — a
|
|
30040
|
+
* `playRecorded` and `goLive` — goes through here, so there is exactly one
|
|
30041
|
+
* place that can forget to. */
|
|
30042
|
+
leaveClip() {
|
|
30043
|
+
if (!this.clip) return;
|
|
30044
|
+
this.clip = null;
|
|
30045
|
+
this.feeder?.dispose();
|
|
30046
|
+
this.feeder = null;
|
|
30047
|
+
this.mode = "live";
|
|
30048
|
+
}
|
|
30049
|
+
/**
|
|
29962
30050
|
* Play recorded footage at `epochMs`. If `locate` is wired and the requested
|
|
29963
30051
|
* profile has no segment, try high → mid → low so events-mode cameras that
|
|
29964
30052
|
* only kept one tier still play.
|
|
@@ -30049,6 +30137,7 @@ var TimelineSession = class {
|
|
|
30049
30137
|
}));
|
|
30050
30138
|
}
|
|
30051
30139
|
goLive() {
|
|
30140
|
+
this.clip = null;
|
|
30052
30141
|
this.feeder?.dispose();
|
|
30053
30142
|
this.feeder = null;
|
|
30054
30143
|
this.lastPositionMs = null;
|
|
@@ -30092,6 +30181,331 @@ var TimelineSession = class {
|
|
|
30092
30181
|
}
|
|
30093
30182
|
};
|
|
30094
30183
|
//#endregion
|
|
30184
|
+
//#region src/stream-broker/recorded/clip-mp4-index.ts
|
|
30185
|
+
/**
|
|
30186
|
+
* The sample index for a CLIP FILE, so a clip can be another position of the
|
|
30187
|
+
* recorded source pointer (D575).
|
|
30188
|
+
*
|
|
30189
|
+
* `RecordedFeeder` asks its byte source two questions — "is there a segment at
|
|
30190
|
+
* `epochMs`?" and "give me its bytes" — and the recorder answers both from the
|
|
30191
|
+
* segment's own `mfra`. A clip is a progressive `+faststart` MP4: it has a
|
|
30192
|
+
* `moov` at the FRONT and no `mfra` at all, so the time→sample map has to be
|
|
30193
|
+
* read out of `stbl` instead. That is what this module does, and it is the only
|
|
30194
|
+
* place in the clip path that parses MP4 structure.
|
|
30195
|
+
*
|
|
30196
|
+
* **It is a TIME index, not a byte index, and that is deliberate.** The
|
|
30197
|
+
* offsets in a progressive `moov` (`stco`/`stsc`) are absolute to the WHOLE
|
|
30198
|
+
* file, so a GOP-ranged slice of one is not a standalone-demuxable MP4 — making
|
|
30199
|
+
* it one means rewriting `stbl`, i.e. a remux. A clip arrives whole and
|
|
30200
|
+
* already compressed (one bounded by-handle fetch, D9/D18), so the file IS its
|
|
30201
|
+
* byte range, exactly as `RecordedFeederDeps.readGop` says of the playback
|
|
30202
|
+
* path. Parsing offsets nobody reads would be a leftover describing a design
|
|
30203
|
+
* this slice does not have.
|
|
30204
|
+
*
|
|
30205
|
+
* What the index IS for:
|
|
30206
|
+
* 1. **The gate.** A file with no usable sample table cannot join the recorded
|
|
30207
|
+
* path, and is refused BY NAME rather than served as a zero-length
|
|
30208
|
+
* timeline. Measured 2026-09-20: an HKSV clip has `mvhd.duration = 0`,
|
|
30209
|
+
* `mdhd.duration = 0`, no `sidx` and no `mfra` — ffmpeg writes the trailer
|
|
30210
|
+
* only on close and the tee cuts like a pipe.
|
|
30211
|
+
* 2. **The file-relative timeline.** `durationMs` is the segment span the
|
|
30212
|
+
* feeder plays and the bound `ended` is decided on; `firstSyncMs` is where a
|
|
30213
|
+
* seek before the first keyframe snaps to.
|
|
30214
|
+
*/
|
|
30215
|
+
var FOURCC_LENGTH = 4;
|
|
30216
|
+
var BOX_HEADER_LENGTH = 8;
|
|
30217
|
+
var LARGE_SIZE_HEADER_LENGTH = 16;
|
|
30218
|
+
/**
|
|
30219
|
+
* Walk the direct children of `[start, end)`. Returns `null` when the region is
|
|
30220
|
+
* structurally impossible (a size that does not fit), which the caller turns
|
|
30221
|
+
* into `malformed` — never a throw, because these bytes come off a camera.
|
|
30222
|
+
*/
|
|
30223
|
+
function childBoxes(bytes, start, end) {
|
|
30224
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
30225
|
+
const out = [];
|
|
30226
|
+
let off = start;
|
|
30227
|
+
while (off + BOX_HEADER_LENGTH <= end) {
|
|
30228
|
+
const declared = view.getUint32(off);
|
|
30229
|
+
let type = "";
|
|
30230
|
+
for (let i = 0; i < FOURCC_LENGTH; i++) type += String.fromCharCode(bytes[off + 4 + i] ?? 0);
|
|
30231
|
+
let size = declared;
|
|
30232
|
+
let contentStart = off + BOX_HEADER_LENGTH;
|
|
30233
|
+
if (declared === 1) {
|
|
30234
|
+
if (off + LARGE_SIZE_HEADER_LENGTH > end) return null;
|
|
30235
|
+
const hi = view.getUint32(off + 8);
|
|
30236
|
+
const lo = view.getUint32(off + 12);
|
|
30237
|
+
size = hi * 4294967296 + lo;
|
|
30238
|
+
contentStart = off + LARGE_SIZE_HEADER_LENGTH;
|
|
30239
|
+
} else if (declared === 0) size = end - off;
|
|
30240
|
+
if (size < contentStart - off) return null;
|
|
30241
|
+
const boxEnd = off + size;
|
|
30242
|
+
if (boxEnd > end) return null;
|
|
30243
|
+
out.push({
|
|
30244
|
+
type,
|
|
30245
|
+
start: off,
|
|
30246
|
+
end: boxEnd,
|
|
30247
|
+
contentStart
|
|
30248
|
+
});
|
|
30249
|
+
off = boxEnd;
|
|
30250
|
+
}
|
|
30251
|
+
return out;
|
|
30252
|
+
}
|
|
30253
|
+
function findBox(boxes, type) {
|
|
30254
|
+
for (const b of boxes) if (b.type === type) return b;
|
|
30255
|
+
return null;
|
|
30256
|
+
}
|
|
30257
|
+
function readSttsRuns(bytes, box) {
|
|
30258
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
30259
|
+
const base = box.contentStart + 4;
|
|
30260
|
+
if (base + 4 > box.end) return null;
|
|
30261
|
+
const entries = view.getUint32(base);
|
|
30262
|
+
if (base + 4 + entries * 8 > box.end) return null;
|
|
30263
|
+
const runs = [];
|
|
30264
|
+
for (let i = 0; i < entries; i++) {
|
|
30265
|
+
const at = base + 4 + i * 8;
|
|
30266
|
+
runs.push({
|
|
30267
|
+
count: view.getUint32(at),
|
|
30268
|
+
delta: view.getUint32(at + 4)
|
|
30269
|
+
});
|
|
30270
|
+
}
|
|
30271
|
+
return runs;
|
|
30272
|
+
}
|
|
30273
|
+
function readSyncSampleNumbers(bytes, box) {
|
|
30274
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
30275
|
+
const base = box.contentStart + 4;
|
|
30276
|
+
if (base + 4 > box.end) return null;
|
|
30277
|
+
const entries = view.getUint32(base);
|
|
30278
|
+
if (base + 4 + entries * 4 > box.end) return null;
|
|
30279
|
+
const out = /* @__PURE__ */ new Set();
|
|
30280
|
+
for (let i = 0; i < entries; i++) out.add(view.getUint32(base + 4 + i * 4));
|
|
30281
|
+
return out;
|
|
30282
|
+
}
|
|
30283
|
+
function mediaTimescale(bytes, mdhd) {
|
|
30284
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
30285
|
+
const version = bytes[mdhd.contentStart] ?? 0;
|
|
30286
|
+
const at = mdhd.contentStart + 4 + (version === 1 ? 16 : 8);
|
|
30287
|
+
if (at + 4 > mdhd.end) return null;
|
|
30288
|
+
const ts = view.getUint32(at);
|
|
30289
|
+
return ts > 0 ? ts : null;
|
|
30290
|
+
}
|
|
30291
|
+
function handlerType(bytes, hdlr) {
|
|
30292
|
+
const at = hdlr.contentStart + 8;
|
|
30293
|
+
if (at + FOURCC_LENGTH > hdlr.end) return "";
|
|
30294
|
+
let out = "";
|
|
30295
|
+
for (let i = 0; i < FOURCC_LENGTH; i++) out += String.fromCharCode(bytes[at + i] ?? 0);
|
|
30296
|
+
return out;
|
|
30297
|
+
}
|
|
30298
|
+
/** The video `trak` of a `moov`, or `null` when it has none. */
|
|
30299
|
+
function videoTrack(bytes, moov) {
|
|
30300
|
+
const children = childBoxes(bytes, moov.contentStart, moov.end);
|
|
30301
|
+
if (!children) return null;
|
|
30302
|
+
for (const trak of children) {
|
|
30303
|
+
if (trak.type !== "trak") continue;
|
|
30304
|
+
const trakKids = childBoxes(bytes, trak.contentStart, trak.end);
|
|
30305
|
+
const mdia = trakKids ? findBox(trakKids, "mdia") : null;
|
|
30306
|
+
if (!mdia) continue;
|
|
30307
|
+
const mdiaKids = childBoxes(bytes, mdia.contentStart, mdia.end);
|
|
30308
|
+
const hdlr = mdiaKids ? findBox(mdiaKids, "hdlr") : null;
|
|
30309
|
+
if (hdlr && handlerType(bytes, hdlr) === "vide") return mdia;
|
|
30310
|
+
}
|
|
30311
|
+
return null;
|
|
30312
|
+
}
|
|
30313
|
+
/**
|
|
30314
|
+
* Read a clip file's time→sample map. Never throws: every structural failure
|
|
30315
|
+
* comes back as a named refusal so the caller can say WHY a clip will not play.
|
|
30316
|
+
*/
|
|
30317
|
+
function indexClipMp4(bytes) {
|
|
30318
|
+
const top = childBoxes(bytes, 0, bytes.length);
|
|
30319
|
+
if (!top) return {
|
|
30320
|
+
kind: "refused",
|
|
30321
|
+
reason: "malformed"
|
|
30322
|
+
};
|
|
30323
|
+
const moov = findBox(top, "moov");
|
|
30324
|
+
if (!moov) return {
|
|
30325
|
+
kind: "refused",
|
|
30326
|
+
reason: "no-moov"
|
|
30327
|
+
};
|
|
30328
|
+
const mdia = videoTrack(bytes, moov);
|
|
30329
|
+
if (!mdia) return {
|
|
30330
|
+
kind: "refused",
|
|
30331
|
+
reason: "no-video-track"
|
|
30332
|
+
};
|
|
30333
|
+
const mdiaKids = childBoxes(bytes, mdia.contentStart, mdia.end);
|
|
30334
|
+
if (!mdiaKids) return {
|
|
30335
|
+
kind: "refused",
|
|
30336
|
+
reason: "malformed"
|
|
30337
|
+
};
|
|
30338
|
+
const mdhd = findBox(mdiaKids, "mdhd");
|
|
30339
|
+
const minf = findBox(mdiaKids, "minf");
|
|
30340
|
+
if (!mdhd || !minf) return {
|
|
30341
|
+
kind: "refused",
|
|
30342
|
+
reason: "malformed"
|
|
30343
|
+
};
|
|
30344
|
+
const timescale = mediaTimescale(bytes, mdhd);
|
|
30345
|
+
if (timescale === null) return {
|
|
30346
|
+
kind: "refused",
|
|
30347
|
+
reason: "malformed"
|
|
30348
|
+
};
|
|
30349
|
+
const minfKids = childBoxes(bytes, minf.contentStart, minf.end);
|
|
30350
|
+
const stbl = minfKids ? findBox(minfKids, "stbl") : null;
|
|
30351
|
+
if (!stbl) return {
|
|
30352
|
+
kind: "refused",
|
|
30353
|
+
reason: "no-samples"
|
|
30354
|
+
};
|
|
30355
|
+
const stblKids = childBoxes(bytes, stbl.contentStart, stbl.end);
|
|
30356
|
+
if (!stblKids) return {
|
|
30357
|
+
kind: "refused",
|
|
30358
|
+
reason: "malformed"
|
|
30359
|
+
};
|
|
30360
|
+
const sttsBox = findBox(stblKids, "stts");
|
|
30361
|
+
if (!sttsBox) return {
|
|
30362
|
+
kind: "refused",
|
|
30363
|
+
reason: "no-samples"
|
|
30364
|
+
};
|
|
30365
|
+
const runs = readSttsRuns(bytes, sttsBox);
|
|
30366
|
+
if (!runs) return {
|
|
30367
|
+
kind: "refused",
|
|
30368
|
+
reason: "malformed"
|
|
30369
|
+
};
|
|
30370
|
+
const stssBox = findBox(stblKids, "stss");
|
|
30371
|
+
const syncNumbers = stssBox === null ? null : readSyncSampleNumbers(bytes, stssBox);
|
|
30372
|
+
if (stssBox !== null && syncNumbers === null) return {
|
|
30373
|
+
kind: "refused",
|
|
30374
|
+
reason: "malformed"
|
|
30375
|
+
};
|
|
30376
|
+
const samples = [];
|
|
30377
|
+
let ticks = 0;
|
|
30378
|
+
let syncSampleCount = 0;
|
|
30379
|
+
let firstSyncMs = null;
|
|
30380
|
+
for (const run of runs) for (let i = 0; i < run.count; i++) {
|
|
30381
|
+
const sampleNumber = samples.length + 1;
|
|
30382
|
+
const sync = syncNumbers === null ? true : syncNumbers.has(sampleNumber);
|
|
30383
|
+
const startMs = ticks * 1e3 / timescale;
|
|
30384
|
+
const durMs = run.delta * 1e3 / timescale;
|
|
30385
|
+
if (sync) {
|
|
30386
|
+
syncSampleCount++;
|
|
30387
|
+
if (firstSyncMs === null) firstSyncMs = startMs;
|
|
30388
|
+
}
|
|
30389
|
+
samples.push({
|
|
30390
|
+
startMs,
|
|
30391
|
+
durMs,
|
|
30392
|
+
sync
|
|
30393
|
+
});
|
|
30394
|
+
ticks += run.delta;
|
|
30395
|
+
}
|
|
30396
|
+
if (samples.length === 0 || ticks === 0) return {
|
|
30397
|
+
kind: "refused",
|
|
30398
|
+
reason: "no-samples"
|
|
30399
|
+
};
|
|
30400
|
+
return {
|
|
30401
|
+
kind: "index",
|
|
30402
|
+
durationMs: ticks * 1e3 / timescale,
|
|
30403
|
+
firstSyncMs: firstSyncMs ?? 0,
|
|
30404
|
+
syncSampleCount,
|
|
30405
|
+
samples
|
|
30406
|
+
};
|
|
30407
|
+
}
|
|
30408
|
+
//#endregion
|
|
30409
|
+
//#region src/stream-broker/recorded/clip-file-source.ts
|
|
30410
|
+
var ClipSourceError = class extends Error {
|
|
30411
|
+
reason;
|
|
30412
|
+
constructor(reason, message) {
|
|
30413
|
+
super(message);
|
|
30414
|
+
this.reason = reason;
|
|
30415
|
+
this.name = "ClipSourceError";
|
|
30416
|
+
}
|
|
30417
|
+
};
|
|
30418
|
+
function createClipFileSource(deps) {
|
|
30419
|
+
const maxBytes = deps.maxBytes ?? 67108864;
|
|
30420
|
+
const { deviceId, clipId, profile } = deps.handle;
|
|
30421
|
+
let resolved = null;
|
|
30422
|
+
let inFlight = null;
|
|
30423
|
+
/**
|
|
30424
|
+
* A refusal that can never change for this file — it is unindexable, or it
|
|
30425
|
+
* is over the bound. Cached so a drag does not re-fetch a 35 MB file per
|
|
30426
|
+
* pointer move. A FETCH failure is deliberately NOT cached: a sleeping
|
|
30427
|
+
* battery camera is a transient no, and the operator's retry must reach it.
|
|
30428
|
+
*/
|
|
30429
|
+
let terminal = null;
|
|
30430
|
+
const refuse = (reason, message, data) => {
|
|
30431
|
+
const err = new ClipSourceError(reason, message);
|
|
30432
|
+
deps.onLog?.(message, {
|
|
30433
|
+
deviceId,
|
|
30434
|
+
clipId,
|
|
30435
|
+
profile,
|
|
30436
|
+
reason,
|
|
30437
|
+
...data
|
|
30438
|
+
});
|
|
30439
|
+
return err;
|
|
30440
|
+
};
|
|
30441
|
+
const load = async () => {
|
|
30442
|
+
let bytes;
|
|
30443
|
+
try {
|
|
30444
|
+
bytes = await deps.read({
|
|
30445
|
+
deviceId,
|
|
30446
|
+
clipId,
|
|
30447
|
+
profile
|
|
30448
|
+
});
|
|
30449
|
+
} catch (err) {
|
|
30450
|
+
throw refuse("fetch-failed", "clip playback: byte fetch failed", { error: err instanceof Error ? err.message : String(err) });
|
|
30451
|
+
}
|
|
30452
|
+
if (bytes.length > maxBytes) {
|
|
30453
|
+
terminal = refuse("too-large", "clip playback: clip exceeds the broker byte bound", {
|
|
30454
|
+
bytes: bytes.length,
|
|
30455
|
+
maxBytes
|
|
30456
|
+
});
|
|
30457
|
+
throw terminal;
|
|
30458
|
+
}
|
|
30459
|
+
const index = indexClipMp4(bytes);
|
|
30460
|
+
if (index.kind === "refused") {
|
|
30461
|
+
terminal = refuse(index.reason, "clip playback: clip cannot be indexed — it will not play in the recorded path", { bytes: bytes.length });
|
|
30462
|
+
throw terminal;
|
|
30463
|
+
}
|
|
30464
|
+
return {
|
|
30465
|
+
bytes,
|
|
30466
|
+
index
|
|
30467
|
+
};
|
|
30468
|
+
};
|
|
30469
|
+
const ensure = async () => {
|
|
30470
|
+
if (terminal) throw terminal;
|
|
30471
|
+
if (resolved) return resolved;
|
|
30472
|
+
if (!inFlight) inFlight = load().then((r) => {
|
|
30473
|
+
resolved = r;
|
|
30474
|
+
return r;
|
|
30475
|
+
}).finally(() => {
|
|
30476
|
+
inFlight = null;
|
|
30477
|
+
});
|
|
30478
|
+
return inFlight;
|
|
30479
|
+
};
|
|
30480
|
+
return {
|
|
30481
|
+
async locate(args) {
|
|
30482
|
+
const { index, bytes } = await ensure();
|
|
30483
|
+
if (args.epochMs >= index.durationMs) return {
|
|
30484
|
+
kind: "gap",
|
|
30485
|
+
nearestEdgeMs: null,
|
|
30486
|
+
prevEndMs: index.durationMs
|
|
30487
|
+
};
|
|
30488
|
+
if (args.epochMs < index.firstSyncMs) return {
|
|
30489
|
+
kind: "gap",
|
|
30490
|
+
nearestEdgeMs: index.firstSyncMs,
|
|
30491
|
+
prevEndMs: null
|
|
30492
|
+
};
|
|
30493
|
+
return {
|
|
30494
|
+
kind: "segment",
|
|
30495
|
+
startMs: 0,
|
|
30496
|
+
durMs: index.durationMs,
|
|
30497
|
+
bytes: bytes.length
|
|
30498
|
+
};
|
|
30499
|
+
},
|
|
30500
|
+
async readBytes() {
|
|
30501
|
+
return (await ensure()).bytes;
|
|
30502
|
+
},
|
|
30503
|
+
async durationMs() {
|
|
30504
|
+
return (await ensure()).index.durationMs;
|
|
30505
|
+
}
|
|
30506
|
+
};
|
|
30507
|
+
}
|
|
30508
|
+
//#endregion
|
|
30095
30509
|
//#region src/stream-broker/recorded/demux-off-thread.ts
|
|
30096
30510
|
/**
|
|
30097
30511
|
* Off-thread JS demux client — mp4box parse on a `worker_threads` Worker
|
|
@@ -30837,6 +31251,7 @@ var BrokerWebrtcServer = class {
|
|
|
30837
31251
|
locateRecordedSegment;
|
|
30838
31252
|
readRecordedSegmentBytes;
|
|
30839
31253
|
readRecordedGopBytes;
|
|
31254
|
+
readClipBytes;
|
|
30840
31255
|
resolveFfmpegPath;
|
|
30841
31256
|
/**
|
|
30842
31257
|
* THE ffmpeg primitive, for the transcode leg. `null` until the manager wires
|
|
@@ -30929,6 +31344,7 @@ var BrokerWebrtcServer = class {
|
|
|
30929
31344
|
this.locateRecordedSegment = options.locateRecordedSegment;
|
|
30930
31345
|
this.readRecordedSegmentBytes = options.readRecordedSegmentBytes;
|
|
30931
31346
|
this.readRecordedGopBytes = options.readRecordedGopBytes;
|
|
31347
|
+
this.readClipBytes = options.readClipBytes;
|
|
30932
31348
|
this.resolveFfmpegPath = options.resolveFfmpegPath;
|
|
30933
31349
|
}
|
|
30934
31350
|
registerBroker(brokerId, broker) {
|
|
@@ -31648,7 +32064,96 @@ var BrokerWebrtcServer = class {
|
|
|
31648
32064
|
bytes: res.data
|
|
31649
32065
|
};
|
|
31650
32066
|
} : void 0;
|
|
32067
|
+
const readClip = this.readClipBytes;
|
|
31651
32068
|
let timeline;
|
|
32069
|
+
/**
|
|
32070
|
+
* ONE feeder builder for BOTH sources (D575). `clip === null` is the
|
|
32071
|
+
* recorder's own footage; a clip swaps only the two byte closures —
|
|
32072
|
+
* everything else (demux, sink, audio codec branch, state/position/log
|
|
32073
|
+
* seams) is identical, which is the whole claim the clip path makes.
|
|
32074
|
+
*/
|
|
32075
|
+
const buildFeeder = (profile, clip) => {
|
|
32076
|
+
const opusSession = session.isOpusAudio;
|
|
32077
|
+
const audioSink = opusSession ? {
|
|
32078
|
+
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
32079
|
+
pushAudioOpus: (opus, ts) => session.writeRecordedAudioOpus(Buffer.from(opus), ts)
|
|
32080
|
+
} : {
|
|
32081
|
+
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
32082
|
+
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
32083
|
+
};
|
|
32084
|
+
const demuxAudio = opusSession ? async (b) => {
|
|
32085
|
+
return (await demuxSegmentAudioOpusPreferJs(b, {
|
|
32086
|
+
ffmpegPath: resolveFfmpeg?.(),
|
|
32087
|
+
onFfmpegFallback: (reason) => {
|
|
32088
|
+
if (audioOpusFallbackLogged) return;
|
|
32089
|
+
audioOpusFallbackLogged = true;
|
|
32090
|
+
sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
|
|
32091
|
+
sessionId,
|
|
32092
|
+
deviceId,
|
|
32093
|
+
profile,
|
|
32094
|
+
reason
|
|
32095
|
+
} });
|
|
32096
|
+
}
|
|
32097
|
+
})).map((au) => ({
|
|
32098
|
+
data: au.data,
|
|
32099
|
+
ptsMs: au.ptsMs
|
|
32100
|
+
}));
|
|
32101
|
+
} : (b) => demuxSegmentToPcmu(b, resolveFfmpeg?.());
|
|
32102
|
+
const clipSource = clip && readClip ? createClipFileSource({
|
|
32103
|
+
handle: clip,
|
|
32104
|
+
read: readClip,
|
|
32105
|
+
onLog: (msg, data) => sessionLogger.warn(msg, {
|
|
32106
|
+
tags: { deviceId: clip.deviceId },
|
|
32107
|
+
meta: {
|
|
32108
|
+
sessionId,
|
|
32109
|
+
...data
|
|
32110
|
+
}
|
|
32111
|
+
})
|
|
32112
|
+
}) : null;
|
|
32113
|
+
return new RecordedFeeder({
|
|
32114
|
+
deviceId: clip ? clip.deviceId : deviceId,
|
|
32115
|
+
profile,
|
|
32116
|
+
locate: clipSource ? clipSource.locate : locateRecorded,
|
|
32117
|
+
readBytes: clipSource ? clipSource.readBytes : readRecorded,
|
|
32118
|
+
...clipSource ? {} : {
|
|
32119
|
+
readGop,
|
|
32120
|
+
fragmentCache: scrubCache
|
|
32121
|
+
},
|
|
32122
|
+
demux: demuxVideo,
|
|
32123
|
+
demuxTally,
|
|
32124
|
+
demuxAudio,
|
|
32125
|
+
audioClockRateHz: opusSession ? 48e3 : 8e3,
|
|
32126
|
+
sink: audioSink,
|
|
32127
|
+
onState: (t) => {
|
|
32128
|
+
sessionLogger.info("recorded: feeder state", {
|
|
32129
|
+
tags: { deviceId: clip ? clip.deviceId : deviceId },
|
|
32130
|
+
meta: {
|
|
32131
|
+
sessionId,
|
|
32132
|
+
deviceId,
|
|
32133
|
+
profile,
|
|
32134
|
+
clipId: clip?.clipId,
|
|
32135
|
+
from: t.from,
|
|
32136
|
+
to: t.to,
|
|
32137
|
+
reason: t.reason,
|
|
32138
|
+
seekToken: t.seekToken,
|
|
32139
|
+
dragId: t.dragId
|
|
32140
|
+
}
|
|
32141
|
+
});
|
|
32142
|
+
timeline._emitState(t.to);
|
|
32143
|
+
},
|
|
32144
|
+
onPosition: (epochMs, pushSeq) => timeline._emitPosition(epochMs, pushSeq),
|
|
32145
|
+
onLog: (msg, data) => sessionLogger.info(msg, {
|
|
32146
|
+
tags: { deviceId: clip ? clip.deviceId : deviceId },
|
|
32147
|
+
meta: {
|
|
32148
|
+
sessionId,
|
|
32149
|
+
deviceId,
|
|
32150
|
+
profile,
|
|
32151
|
+
clipId: clip?.clipId,
|
|
32152
|
+
...data
|
|
32153
|
+
}
|
|
32154
|
+
})
|
|
32155
|
+
});
|
|
32156
|
+
};
|
|
31652
32157
|
timeline = new TimelineSession({
|
|
31653
32158
|
session,
|
|
31654
32159
|
deviceId,
|
|
@@ -31679,71 +32184,15 @@ var BrokerWebrtcServer = class {
|
|
|
31679
32184
|
} });
|
|
31680
32185
|
});
|
|
31681
32186
|
} : void 0,
|
|
31682
|
-
makeFeeder: (profile) =>
|
|
31683
|
-
|
|
31684
|
-
|
|
31685
|
-
|
|
31686
|
-
|
|
31687
|
-
|
|
31688
|
-
|
|
31689
|
-
|
|
31690
|
-
|
|
31691
|
-
demux: demuxVideo,
|
|
31692
|
-
demuxTally,
|
|
31693
|
-
demuxAudio: opusSession ? async (b) => {
|
|
31694
|
-
return (await demuxSegmentAudioOpusPreferJs(b, {
|
|
31695
|
-
ffmpegPath: resolveFfmpeg?.(),
|
|
31696
|
-
onFfmpegFallback: (reason) => {
|
|
31697
|
-
if (audioOpusFallbackLogged) return;
|
|
31698
|
-
audioOpusFallbackLogged = true;
|
|
31699
|
-
sessionLogger.warn("recorded audio: JS Opus demux missed — ffmpeg AAC→Opus fallback (once/session)", { meta: {
|
|
31700
|
-
sessionId,
|
|
31701
|
-
deviceId,
|
|
31702
|
-
profile,
|
|
31703
|
-
reason
|
|
31704
|
-
} });
|
|
31705
|
-
}
|
|
31706
|
-
})).map((au) => ({
|
|
31707
|
-
data: au.data,
|
|
31708
|
-
ptsMs: au.ptsMs
|
|
31709
|
-
}));
|
|
31710
|
-
} : (b) => demuxSegmentToPcmu(b, resolveFfmpeg?.()),
|
|
31711
|
-
audioClockRateHz: opusSession ? 48e3 : 8e3,
|
|
31712
|
-
sink: opusSession ? {
|
|
31713
|
-
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
31714
|
-
pushAudioOpus: (opus, ts) => session.writeRecordedAudioOpus(Buffer.from(opus), ts)
|
|
31715
|
-
} : {
|
|
31716
|
-
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
31717
|
-
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
31718
|
-
},
|
|
31719
|
-
onState: (t) => {
|
|
31720
|
-
sessionLogger.info("recorded: feeder state", {
|
|
31721
|
-
tags: { deviceId },
|
|
31722
|
-
meta: {
|
|
31723
|
-
sessionId,
|
|
31724
|
-
deviceId,
|
|
31725
|
-
profile,
|
|
31726
|
-
from: t.from,
|
|
31727
|
-
to: t.to,
|
|
31728
|
-
reason: t.reason,
|
|
31729
|
-
seekToken: t.seekToken,
|
|
31730
|
-
dragId: t.dragId
|
|
31731
|
-
}
|
|
31732
|
-
});
|
|
31733
|
-
timeline._emitState(t.to);
|
|
31734
|
-
},
|
|
31735
|
-
onPosition: (epochMs, pushSeq) => timeline._emitPosition(epochMs, pushSeq),
|
|
31736
|
-
onLog: (msg, data) => sessionLogger.info(msg, {
|
|
31737
|
-
tags: { deviceId },
|
|
31738
|
-
meta: {
|
|
31739
|
-
sessionId,
|
|
31740
|
-
deviceId,
|
|
31741
|
-
profile,
|
|
31742
|
-
...data
|
|
31743
|
-
}
|
|
31744
|
-
})
|
|
31745
|
-
});
|
|
31746
|
-
}
|
|
32187
|
+
makeFeeder: (profile) => buildFeeder(profile, null),
|
|
32188
|
+
...readClip ? { makeClipFeeder: ({ handle }) => buildFeeder(handle.profile ?? "clip", handle) } : {},
|
|
32189
|
+
onLog: (msg, data) => sessionLogger.warn(msg, {
|
|
32190
|
+
tags: { deviceId },
|
|
32191
|
+
meta: {
|
|
32192
|
+
sessionId,
|
|
32193
|
+
...data
|
|
32194
|
+
}
|
|
32195
|
+
})
|
|
31747
32196
|
});
|
|
31748
32197
|
entry.timeline = timeline;
|
|
31749
32198
|
sessionLogger.info("recorded: TimelineSession wired (control accessors present)", { meta: {
|
|
@@ -33246,6 +33695,14 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
33246
33695
|
},
|
|
33247
33696
|
locateRecordedSegment: (args) => this.ctx.api.recording.locateSegment.query(args),
|
|
33248
33697
|
readRecordedSegmentBytes: async (args) => (await this.ctx.api.recording.readSegmentBytes.query(args)).data,
|
|
33698
|
+
readClipBytes: async (args) => {
|
|
33699
|
+
const res = await this.ctx.api.videoclips.readClipBytes.query({
|
|
33700
|
+
deviceId: args.deviceId,
|
|
33701
|
+
clipId: args.clipId,
|
|
33702
|
+
...args.profile !== null ? { profile: args.profile } : {}
|
|
33703
|
+
});
|
|
33704
|
+
return new Uint8Array(Buffer.from(res.base64, "base64"));
|
|
33705
|
+
},
|
|
33249
33706
|
readRecordedGopBytes: async (args) => {
|
|
33250
33707
|
try {
|
|
33251
33708
|
return await this.ctx.api.recording.readGopBytes.query({
|