@camstack/addon-pipeline 1.1.19 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/audio-codec-ffmpeg/index.js +1125 -0
- package/dist/audio-codec-ffmpeg/index.mjs +1107 -0
- package/dist/decoder-ffmpeg/index.js +543 -288
- package/dist/decoder-ffmpeg/index.mjs +542 -287
- package/dist/detection-pipeline/index.js +1 -1
- package/dist/detection-pipeline/index.mjs +1 -1
- package/dist/{dist-BgoBMCez.js → dist-BiP1gPeY.js} +20 -1
- package/dist/{dist-7Yx2dmuV.mjs → dist-tzhTTRq4.mjs} +20 -1
- package/dist/{ffmpeg-args-common-c3p-nWtU.mjs → frame-dropper-CwkBTPGV.mjs} +22 -22
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +1 -1
- package/dist/pipeline-runner/index.mjs +1 -1
- package/dist/recorder/index.js +24 -4
- package/dist/recorder/index.mjs +24 -4
- package/dist/stream-broker/_stub.js +35 -35
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-w2pP0MYO.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D08qJTw_.mjs} +3 -3
- package/dist/stream-broker/{hostInit-BUEbBinp.mjs → hostInit-CGnMNtPl.mjs} +3 -3
- package/dist/stream-broker/index.js +487 -458
- package/dist/stream-broker/index.mjs +477 -448
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CYmHXacX.js → MaskShapeCanvas-DI4BY7W2-CW4Qu4V6.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-B-BkPQvX.js → MotionZonesSettings-NcxxQN8r-CHJ39tfi.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DC3Jw85p.js → PrivacyMaskSettings-APgPLF7p-dInJ7kcz.js} +1 -1
- package/embed-dist/assets/{index-B0IQgci0.js → index-CKpVWKSG.js} +4 -4
- package/embed-dist/index.html +1 -1
- package/package.json +10 -10
- package/dist/audio-codec-nodeav/index.js +0 -310
- package/dist/audio-codec-nodeav/index.mjs +0 -305
- package/dist/codec-runtime-BOk-13PN.js +0 -202
- package/dist/codec-runtime-BsqlEjPi.mjs +0 -197
- package/dist/{ffmpeg-args-common-CASoNt42.js → frame-dropper-7RTo_YyG.js} +21 -21
|
@@ -3,9 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BnQ_awK4.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const
|
|
8
|
-
const require_codec_runtime = require("../codec-runtime-BOk-13PN.js");
|
|
6
|
+
const require_dist = require("../dist-BiP1gPeY.js");
|
|
7
|
+
const require_frame_dropper = require("../frame-dropper-7RTo_YyG.js");
|
|
9
8
|
let node_fs = require("node:fs");
|
|
10
9
|
node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
|
|
11
10
|
let node_path = require("node:path");
|
|
@@ -353,6 +352,11 @@ var FrameHandlePlane = class {
|
|
|
353
352
|
localNodeId;
|
|
354
353
|
subscriptions = /* @__PURE__ */ new Map();
|
|
355
354
|
sessions = /* @__PURE__ */ new Map();
|
|
355
|
+
/** In-flight `ensureSession` creation per format. Coalesces concurrent
|
|
356
|
+
* same-format callers onto ONE decoder session — without it two callers
|
|
357
|
+
* both pass the `sessions.get` check, both `createSession` (two ffmpeg),
|
|
358
|
+
* and the second registration orphans the first with no `destroySession`. */
|
|
359
|
+
sessionCreating = /* @__PURE__ */ new Map();
|
|
356
360
|
disposed = false;
|
|
357
361
|
/** Re-entrancy guard for `armPendingSessions` — collapses the per-packet
|
|
358
362
|
* `pushPacket` calls into a single in-flight deferred-arm pass. */
|
|
@@ -627,15 +631,29 @@ var FrameHandlePlane = class {
|
|
|
627
631
|
existing.subscriberIds.add(subscriptionId);
|
|
628
632
|
return;
|
|
629
633
|
}
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
format
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
634
|
+
const inflight = this.sessionCreating.get(format);
|
|
635
|
+
if (inflight) {
|
|
636
|
+
await inflight;
|
|
637
|
+
this.sessions.get(format)?.subscriberIds.add(subscriptionId);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const creation = (async () => {
|
|
641
|
+
const subscriberIds = new Set([subscriptionId]);
|
|
642
|
+
for (const subscription of this.subscriptions.values()) if (subscription.format === format) subscriberIds.add(subscription.id);
|
|
643
|
+
const session = {
|
|
644
|
+
format,
|
|
645
|
+
proxy: null,
|
|
646
|
+
nodeId: null,
|
|
647
|
+
subscriberIds
|
|
648
|
+
};
|
|
649
|
+
if (await this.startSessionDecoder(session)) this.sessions.set(format, session);
|
|
650
|
+
})();
|
|
651
|
+
this.sessionCreating.set(format, creation);
|
|
652
|
+
try {
|
|
653
|
+
await creation;
|
|
654
|
+
} finally {
|
|
655
|
+
this.sessionCreating.delete(format);
|
|
656
|
+
}
|
|
639
657
|
}
|
|
640
658
|
/**
|
|
641
659
|
* Create the `frameSink: 'shm'` decoder for a `FormatSession` and start
|
|
@@ -6137,6 +6155,10 @@ var RtspSession = class {
|
|
|
6137
6155
|
lastRtpAt = 0;
|
|
6138
6156
|
/** Total video RTP bytes written to the socket. */
|
|
6139
6157
|
bytesSent = 0;
|
|
6158
|
+
/** Client `User-Agent` (from the first request that carries one). Lets the
|
|
6159
|
+
* broker panel identify a consumer by purpose — e.g. the recorder sets
|
|
6160
|
+
* `-user_agent recorder` on its RTSP pull — instead of a bare address. */
|
|
6161
|
+
userAgent = null;
|
|
6140
6162
|
/** When true, this session receives video-only (no audio RTP). */
|
|
6141
6163
|
muted;
|
|
6142
6164
|
constructor(socket, sdp, onClose, muted) {
|
|
@@ -6180,6 +6202,7 @@ var RtspSession = class {
|
|
|
6180
6202
|
return {
|
|
6181
6203
|
sessionId: this.sessionId,
|
|
6182
6204
|
remoteAddr: `${remoteIp}:${remotePort}`,
|
|
6205
|
+
userAgent: this.userAgent,
|
|
6183
6206
|
playing: this.isPlaying(),
|
|
6184
6207
|
muted: this.muted,
|
|
6185
6208
|
connectedAt: this.connectedAt,
|
|
@@ -6246,7 +6269,10 @@ var RtspSession = class {
|
|
|
6246
6269
|
const requestText = this.buffer.slice(0, endIdx);
|
|
6247
6270
|
this.buffer = this.buffer.slice(endIdx + 4);
|
|
6248
6271
|
const request = this.parseRequest(requestText);
|
|
6249
|
-
if (request)
|
|
6272
|
+
if (request) {
|
|
6273
|
+
if (this.userAgent === null) this.userAgent = request.headers.get("user-agent") ?? null;
|
|
6274
|
+
this.handleRequest(request);
|
|
6275
|
+
}
|
|
6250
6276
|
}
|
|
6251
6277
|
}
|
|
6252
6278
|
parseRequest(text) {
|
|
@@ -6495,6 +6521,7 @@ var RtspListenServer = class RtspListenServer {
|
|
|
6495
6521
|
releaseOnce();
|
|
6496
6522
|
}, isMuted);
|
|
6497
6523
|
restreamer.addSession(session);
|
|
6524
|
+
releaseOnce();
|
|
6498
6525
|
socket.unshift(Buffer.from(requestBuffer, "ascii"));
|
|
6499
6526
|
}
|
|
6500
6527
|
/**
|
|
@@ -6697,23 +6724,23 @@ function pickVideoEncoder(target, encoders, hwAccel) {
|
|
|
6697
6724
|
/** Audio encoder args for a transcode target (preset values over the shared low-level builder). */
|
|
6698
6725
|
function pickAudioEncoderArgs(audio) {
|
|
6699
6726
|
switch (audio) {
|
|
6700
|
-
case "aac": return
|
|
6727
|
+
case "aac": return require_frame_dropper.audioEncoderArgs("aac", {
|
|
6701
6728
|
bitrateKbps: 128,
|
|
6702
6729
|
sampleRateHz: 48e3,
|
|
6703
6730
|
channels: 2
|
|
6704
6731
|
});
|
|
6705
|
-
case "opus": return
|
|
6732
|
+
case "opus": return require_frame_dropper.audioEncoderArgs("opus", {
|
|
6706
6733
|
bitrateKbps: 64,
|
|
6707
6734
|
sampleRateHz: 48e3,
|
|
6708
6735
|
channels: 2
|
|
6709
6736
|
});
|
|
6710
|
-
case "pcmu": return
|
|
6737
|
+
case "pcmu": return require_frame_dropper.audioEncoderArgs("pcmu", {
|
|
6711
6738
|
sampleRateHz: 8e3,
|
|
6712
6739
|
channels: 1
|
|
6713
6740
|
});
|
|
6714
|
-
case "copy": return
|
|
6741
|
+
case "copy": return require_frame_dropper.audioEncoderArgs("copy");
|
|
6715
6742
|
case "none": return ["-an"];
|
|
6716
|
-
default: return
|
|
6743
|
+
default: return require_frame_dropper.audioEncoderArgs("aac", { bitrateKbps: 128 });
|
|
6717
6744
|
}
|
|
6718
6745
|
}
|
|
6719
6746
|
/**
|
|
@@ -6778,7 +6805,7 @@ function buildFfmpegArgs$1(inv) {
|
|
|
6778
6805
|
const threadArgs = inv.threadCount > 0 ? ["-threads", String(inv.threadCount)] : [];
|
|
6779
6806
|
const audioArgs = inv.sink.kind === "stdout" && (inv.sink.container === "h264" || inv.sink.container === "hevc") ? ["-an"] : pickAudioEncoderArgs(inv.audio);
|
|
6780
6807
|
return [
|
|
6781
|
-
...
|
|
6808
|
+
...require_frame_dropper.logBannerArgs("warning"),
|
|
6782
6809
|
...decodeArgs,
|
|
6783
6810
|
"-rtsp_transport",
|
|
6784
6811
|
"tcp",
|
|
@@ -11579,7 +11606,7 @@ function planAudioSilencePadPackets(input) {
|
|
|
11579
11606
|
* audio plane honours the profile literally.
|
|
11580
11607
|
*/
|
|
11581
11608
|
function buildTranscodeFfmpegArgs(inputFormat, profile, decodeHwAccel, sourceUrl) {
|
|
11582
|
-
const args = [...
|
|
11609
|
+
const args = [...require_frame_dropper.logBannerArgs("error")];
|
|
11583
11610
|
if (profile.inputArgs?.length) args.push(...profile.inputArgs);
|
|
11584
11611
|
if (decodeHwAccel && decodeHwAccel !== "none") args.push("-hwaccel", decodeHwAccel);
|
|
11585
11612
|
args.push("-fflags", "+discardcorrupt", "-rtsp_transport", "tcp", "-i", sourceUrl);
|
|
@@ -11601,7 +11628,7 @@ function buildTranscodeFfmpegArgs(inputFormat, profile, decodeHwAccel, sourceUrl
|
|
|
11601
11628
|
if (profile.audio === "passthrough") args.push("-an");
|
|
11602
11629
|
else {
|
|
11603
11630
|
const a = profile.audio;
|
|
11604
|
-
args.push(...
|
|
11631
|
+
args.push(...require_frame_dropper.audioEncoderArgs(a.codec, {
|
|
11605
11632
|
bitrateKbps: a.bitrateKbps,
|
|
11606
11633
|
sampleRateHz: a.sampleRateHz,
|
|
11607
11634
|
channels: a.channels
|
|
@@ -14302,475 +14329,468 @@ var TimelineSession = class {
|
|
|
14302
14329
|
}
|
|
14303
14330
|
};
|
|
14304
14331
|
//#endregion
|
|
14305
|
-
//#region src/stream-broker/recorded/
|
|
14332
|
+
//#region src/stream-broker/recorded/ffmpeg-run-once.ts
|
|
14306
14333
|
/**
|
|
14307
|
-
*
|
|
14308
|
-
*
|
|
14309
|
-
*
|
|
14334
|
+
* ffmpeg-run-once — spawn ffmpeg for ONE batch conversion: feed an in-memory
|
|
14335
|
+
* input buffer on stdin, collect the whole stdout, resolve when the process
|
|
14336
|
+
* exits. Used by the recorded-playback demuxers (`segment-demux`,
|
|
14337
|
+
* `segment-audio`) which turn a single self-contained `.m4s` fragment into
|
|
14338
|
+
* Annex-B / µ-law bytes in a crash-isolated subprocess (replacing the old
|
|
14339
|
+
* in-process `node-av` binding).
|
|
14310
14340
|
*
|
|
14311
|
-
*
|
|
14312
|
-
*
|
|
14313
|
-
*
|
|
14341
|
+
* Unlike the live streaming sessions (`decoder-ffmpeg`, `audio-codec-ffmpeg`)
|
|
14342
|
+
* this is fire-once: there is no persistent stdin feed. stdin is written and
|
|
14343
|
+
* closed up-front so ffmpeg reads the whole fragment, transcodes, flushes, and
|
|
14344
|
+
* exits; we buffer stdout and resolve with it.
|
|
14345
|
+
*
|
|
14346
|
+
* The kill sequence copies the CORRECTED gating from `decoder-ffmpeg`/
|
|
14347
|
+
* `audio-codec-ffmpeg`: SIGTERM then escalated SIGKILL gated on
|
|
14348
|
+
* `exitCode === null && signalCode === null` (NOT on `child.killed`, which Node
|
|
14349
|
+
* sets true after ANY kill() so `!child.killed` never fires → leaked ffmpeg).
|
|
14314
14350
|
*/
|
|
14315
|
-
|
|
14316
|
-
|
|
14317
|
-
|
|
14318
|
-
|
|
14319
|
-
|
|
14320
|
-
if (codecId === hevcId) return {
|
|
14321
|
-
bsf: "hevc_mp4toannexb",
|
|
14322
|
-
codec: "H265"
|
|
14323
|
-
};
|
|
14324
|
-
throw new Error(`segment-demux: unsupported video codec id ${codecId} (expected H264=${h264Id} or HEVC=${hevcId})`);
|
|
14325
|
-
}
|
|
14351
|
+
/** Hard ceiling for one fragment conversion; a ~4 s GOP transcodes in well
|
|
14352
|
+
* under this even on a slow agent. Prevents a wedged ffmpeg from leaking. */
|
|
14353
|
+
var RUN_TIMEOUT_MS = 8e3;
|
|
14354
|
+
/** Grace period between SIGTERM and the escalated SIGKILL. */
|
|
14355
|
+
var KILL_GRACE_MS = 500;
|
|
14326
14356
|
/**
|
|
14327
|
-
* `
|
|
14328
|
-
*
|
|
14329
|
-
*
|
|
14357
|
+
* Run ffmpeg once over `input` and resolve with the collected stdout bytes.
|
|
14358
|
+
*
|
|
14359
|
+
* @throws If ffmpeg cannot be spawned, times out, or exits non-zero (unless
|
|
14360
|
+
* `tolerateNonZeroExit` is set).
|
|
14330
14361
|
*/
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
|
|
14334
|
-
|
|
14335
|
-
|
|
14336
|
-
|
|
14337
|
-
|
|
14338
|
-
|
|
14339
|
-
|
|
14340
|
-
|
|
14341
|
-
|
|
14342
|
-
|
|
14362
|
+
function runFfmpegOnce(options) {
|
|
14363
|
+
const { ffmpegPath, args, input, tolerateNonZeroExit = false } = options;
|
|
14364
|
+
return new Promise((resolve, reject) => {
|
|
14365
|
+
const chunks = [];
|
|
14366
|
+
const stderrTail = [];
|
|
14367
|
+
let settled = false;
|
|
14368
|
+
const child = (() => {
|
|
14369
|
+
try {
|
|
14370
|
+
return (0, node_child_process.spawn)(ffmpegPath, [...args]);
|
|
14371
|
+
} catch (err) {
|
|
14372
|
+
reject(/* @__PURE__ */ new Error(`ffmpeg spawn failed: ${require_dist.errMsg(err)}`));
|
|
14373
|
+
return null;
|
|
14374
|
+
}
|
|
14375
|
+
})();
|
|
14376
|
+
if (child === null) return;
|
|
14377
|
+
const timer = setTimeout(() => {
|
|
14378
|
+
if (settled) return;
|
|
14379
|
+
settle(() => reject(/* @__PURE__ */ new Error("ffmpeg-run-once: timed out")));
|
|
14380
|
+
kill();
|
|
14381
|
+
}, RUN_TIMEOUT_MS);
|
|
14382
|
+
timer.unref?.();
|
|
14383
|
+
const settle = (fn) => {
|
|
14384
|
+
if (settled) return;
|
|
14385
|
+
settled = true;
|
|
14386
|
+
clearTimeout(timer);
|
|
14387
|
+
fn();
|
|
14388
|
+
};
|
|
14389
|
+
const kill = () => {
|
|
14390
|
+
try {
|
|
14391
|
+
child.kill("SIGTERM");
|
|
14392
|
+
} catch {}
|
|
14393
|
+
setTimeout(() => {
|
|
14394
|
+
if (child.exitCode === null && child.signalCode === null) try {
|
|
14395
|
+
child.kill("SIGKILL");
|
|
14396
|
+
} catch {}
|
|
14397
|
+
}, KILL_GRACE_MS).unref?.();
|
|
14398
|
+
};
|
|
14399
|
+
child.stdin?.on("error", () => {});
|
|
14400
|
+
child.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
14401
|
+
child.stderr?.on("data", (data) => {
|
|
14402
|
+
const line = data.toString().trim();
|
|
14403
|
+
if (line) {
|
|
14404
|
+
stderrTail.push(line);
|
|
14405
|
+
if (stderrTail.length > 8) stderrTail.shift();
|
|
14406
|
+
}
|
|
14407
|
+
});
|
|
14408
|
+
child.on("error", (err) => {
|
|
14409
|
+
settle(() => reject(/* @__PURE__ */ new Error(`ffmpeg process error: ${err.message}`)));
|
|
14410
|
+
});
|
|
14411
|
+
child.on("exit", (code, signal) => {
|
|
14412
|
+
const stdout = Buffer.concat(chunks);
|
|
14413
|
+
if (code === 0 || tolerateNonZeroExit) {
|
|
14414
|
+
settle(() => resolve(stdout));
|
|
14415
|
+
return;
|
|
14416
|
+
}
|
|
14417
|
+
settle(() => reject(/* @__PURE__ */ new Error(`ffmpeg exited code=${code} signal=${signal}: ${stderrTail.join(" | ") || "(no stderr)"}`)));
|
|
14418
|
+
});
|
|
14419
|
+
try {
|
|
14420
|
+
child.stdin?.write(Buffer.from(input.buffer, input.byteOffset, input.byteLength));
|
|
14421
|
+
child.stdin?.end();
|
|
14422
|
+
} catch {}
|
|
14423
|
+
});
|
|
14343
14424
|
}
|
|
14425
|
+
//#endregion
|
|
14426
|
+
//#region src/stream-broker/recorded/ts-annexb-demux.ts
|
|
14344
14427
|
/**
|
|
14345
|
-
*
|
|
14346
|
-
* access units.
|
|
14428
|
+
* ts-annexb-demux — pure MPEG-TS → ordered Annex-B access units.
|
|
14347
14429
|
*
|
|
14348
|
-
*
|
|
14349
|
-
*
|
|
14350
|
-
*
|
|
14351
|
-
*
|
|
14352
|
-
* the underlying `node-av` packets are freed before this resolves, so the
|
|
14353
|
-
* result is safe to retain. Throws on a non-H.264/H.265 video codec.
|
|
14430
|
+
* Companion to {@link demuxSegmentToAnnexB}: `segment-demux` runs one `.m4s`
|
|
14431
|
+
* fragment through `ffmpeg -c:v copy -f mpegts` (crash-isolated subprocess,
|
|
14432
|
+
* replacing the old in-process `node-av` demux) and hands the resulting
|
|
14433
|
+
* transport stream here to be split back into per-frame access units.
|
|
14354
14434
|
*
|
|
14355
|
-
*
|
|
14356
|
-
*
|
|
14435
|
+
* Why MPEG-TS as the intermediate container:
|
|
14436
|
+
* - it preserves EXACTLY the two things the recorded-playback feeder needs
|
|
14437
|
+
* that a raw Annex-B elementary stream (`-f h264/hevc`) throws away — the
|
|
14438
|
+
* per-frame presentation timestamp (PES `PTS`, 90 kHz) and the frame
|
|
14439
|
+
* boundaries (one video PES per access unit, marked by `payload_unit_start`);
|
|
14440
|
+
* - the ffmpeg mpegts muxer FUSES the parameter sets onto every random-access
|
|
14441
|
+
* point exactly like the `h264/hevc_mp4toannexb` bitstream filter did — the
|
|
14442
|
+
* IDR PES carries `AUD` + `VPS/SPS/PPS` (H.265) or `AUD` + `SPS/PPS`
|
|
14443
|
+
* (H.264) ahead of the IDR slice — so no explicit BSF is needed and the
|
|
14444
|
+
* downstream pusher's "one AU per frame, params on keyframes" contract holds;
|
|
14445
|
+
* - the codec is read straight from the PMT `stream_type` (`0x1B` → H.264,
|
|
14446
|
+
* `0x24` → H.265), so no separate probe pass is required.
|
|
14447
|
+
*
|
|
14448
|
+
* This module is intentionally pure (a `Uint8Array` in, access units out) so it
|
|
14449
|
+
* unit-tests against a committed `.ts` fixture with no ffmpeg/native binding.
|
|
14357
14450
|
*/
|
|
14358
|
-
|
|
14359
|
-
|
|
14360
|
-
|
|
14361
|
-
|
|
14362
|
-
|
|
14363
|
-
|
|
14364
|
-
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14370
|
-
|
|
14371
|
-
|
|
14372
|
-
|
|
14373
|
-
|
|
14374
|
-
|
|
14375
|
-
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
14451
|
+
/** MPEG-TS transport packet size. */
|
|
14452
|
+
var TS_PACKET_SIZE = 188;
|
|
14453
|
+
/** Transport packet sync byte. */
|
|
14454
|
+
var TS_SYNC_BYTE = 71;
|
|
14455
|
+
/** The PAT always lives on PID 0. */
|
|
14456
|
+
var PAT_PID = 0;
|
|
14457
|
+
/** PMT `stream_type` for H.264 (AVC) video. */
|
|
14458
|
+
var STREAM_TYPE_H264 = 27;
|
|
14459
|
+
/** PMT `stream_type` for H.265 (HEVC) video. */
|
|
14460
|
+
var STREAM_TYPE_H265 = 36;
|
|
14461
|
+
/** Map a PMT `stream_type` to the AccessUnit codec tag, or null if not video. */
|
|
14462
|
+
function codecForStreamType(streamType) {
|
|
14463
|
+
if (streamType === STREAM_TYPE_H264) return "H264";
|
|
14464
|
+
if (streamType === STREAM_TYPE_H265) return "H265";
|
|
14465
|
+
return null;
|
|
14466
|
+
}
|
|
14467
|
+
/**
|
|
14468
|
+
* Detect whether an Annex-B access unit is a keyframe by scanning its NAL units:
|
|
14469
|
+
* H.264 IDR (type 5) or H.265 IRAP (types 16–21). Mirrors
|
|
14470
|
+
* `AnnexBDeframer.isKeyframeNal`; params/AUD NALs ahead of the slice are ignored.
|
|
14471
|
+
*/
|
|
14472
|
+
function isKeyframeAccessUnit(au, codec) {
|
|
14473
|
+
for (const nalStart of iterateNalStarts(au)) {
|
|
14474
|
+
const header = au[nalStart];
|
|
14475
|
+
if (header === void 0) continue;
|
|
14476
|
+
if (codec === "H264") {
|
|
14477
|
+
if ((header & 31) === 5) return true;
|
|
14478
|
+
} else {
|
|
14479
|
+
const type = header >> 1 & 63;
|
|
14480
|
+
if (type >= 16 && type <= 21) return true;
|
|
14481
|
+
}
|
|
14482
|
+
}
|
|
14483
|
+
return false;
|
|
14484
|
+
}
|
|
14485
|
+
/**
|
|
14486
|
+
* Yield the byte offset of the FIRST byte of each NAL unit (i.e. just past its
|
|
14487
|
+
* Annex-B start code, `00 00 01` or `00 00 00 01`) in `buf`.
|
|
14488
|
+
*/
|
|
14489
|
+
function* iterateNalStarts(buf) {
|
|
14490
|
+
const len = buf.length;
|
|
14491
|
+
let i = 0;
|
|
14492
|
+
while (i + 2 < len) {
|
|
14493
|
+
if (buf[i] === 0 && buf[i + 1] === 0) {
|
|
14494
|
+
if (buf[i + 2] === 1) {
|
|
14495
|
+
yield i + 3;
|
|
14496
|
+
i += 3;
|
|
14497
|
+
continue;
|
|
14498
|
+
}
|
|
14499
|
+
if (buf[i + 2] === 0 && buf[i + 3] === 1) {
|
|
14500
|
+
yield i + 4;
|
|
14501
|
+
i += 4;
|
|
14502
|
+
continue;
|
|
14388
14503
|
}
|
|
14389
14504
|
}
|
|
14390
|
-
|
|
14391
|
-
}
|
|
14392
|
-
|
|
14393
|
-
|
|
14505
|
+
i++;
|
|
14506
|
+
}
|
|
14507
|
+
}
|
|
14508
|
+
/**
|
|
14509
|
+
* Read the 33-bit PES `PTS` (or DTS) from the 5 bytes at `off` and return it in
|
|
14510
|
+
* 90 kHz ticks. The layout interleaves marker bits every byte
|
|
14511
|
+
* (`0010 PPP1 PPPPPPPP PPPPPPP1 PPPPPPPP PPPPPPP1`).
|
|
14512
|
+
*/
|
|
14513
|
+
function readPts33(buf, off) {
|
|
14514
|
+
const b0 = buf[off] ?? 0;
|
|
14515
|
+
const b1 = buf[off + 1] ?? 0;
|
|
14516
|
+
const b2 = buf[off + 2] ?? 0;
|
|
14517
|
+
const b3 = buf[off + 3] ?? 0;
|
|
14518
|
+
const b4 = buf[off + 4] ?? 0;
|
|
14519
|
+
const hi = b0 >> 1 & 7;
|
|
14520
|
+
const mid = b1 << 7 | b2 >> 1;
|
|
14521
|
+
const lo = b3 << 7 | b4 >> 1;
|
|
14522
|
+
return hi * 1073741824 + mid * 32768 + lo;
|
|
14523
|
+
}
|
|
14524
|
+
/** Locate the payload offset within a TS packet, skipping any adaptation field. */
|
|
14525
|
+
function payloadOffset(pkt, base) {
|
|
14526
|
+
const adaptationFieldControl = pkt[base + 3] >> 4 & 3;
|
|
14527
|
+
let off = base + 4;
|
|
14528
|
+
if (adaptationFieldControl & 2) off += 1 + pkt[off];
|
|
14529
|
+
return off;
|
|
14530
|
+
}
|
|
14531
|
+
/** Whether a TS packet's adaptation_field_control indicates a payload is present. */
|
|
14532
|
+
function hasPayload(pkt, base) {
|
|
14533
|
+
const afc = pkt[base + 3] >> 4 & 3;
|
|
14534
|
+
return afc === 1 || afc === 3;
|
|
14535
|
+
}
|
|
14536
|
+
/** Parse the PAT payload → the first program's PMT PID. */
|
|
14537
|
+
function parsePatForPmtPid(payload) {
|
|
14538
|
+
const p = 1 + payload[0] + 8;
|
|
14539
|
+
if (p + 3 >= payload.length) return null;
|
|
14540
|
+
return (payload[p + 2] & 31) << 8 | payload[p + 3];
|
|
14541
|
+
}
|
|
14542
|
+
/** Parse the PMT payload → the first H.264/H.265 elementary stream's PID + codec. */
|
|
14543
|
+
function parsePmt(payload) {
|
|
14544
|
+
const p = 1 + payload[0];
|
|
14545
|
+
const sectionLength = (payload[p + 1] & 15) << 8 | payload[p + 2];
|
|
14546
|
+
const programInfoLength = (payload[p + 10] & 15) << 8 | payload[p + 11];
|
|
14547
|
+
let es = p + 12 + programInfoLength;
|
|
14548
|
+
const sectionEnd = p + 3 + sectionLength - 4;
|
|
14549
|
+
while (es + 4 < sectionEnd && es + 4 < payload.length) {
|
|
14550
|
+
const streamType = payload[es];
|
|
14551
|
+
const elementaryPid = (payload[es + 1] & 31) << 8 | payload[es + 2];
|
|
14552
|
+
const esInfoLength = (payload[es + 3] & 15) << 8 | payload[es + 4];
|
|
14553
|
+
const codec = codecForStreamType(streamType);
|
|
14554
|
+
if (codec !== null) return {
|
|
14555
|
+
videoPid: elementaryPid,
|
|
14556
|
+
codec
|
|
14557
|
+
};
|
|
14558
|
+
es += 5 + esInfoLength;
|
|
14394
14559
|
}
|
|
14560
|
+
return null;
|
|
14561
|
+
}
|
|
14562
|
+
/** Split one reassembled PES packet into its Annex-B elementary-stream payload
|
|
14563
|
+
* and 90 kHz PTS (null when the PES carries no PTS flag). */
|
|
14564
|
+
function splitPes(pes) {
|
|
14565
|
+
if (pes.length < 9 || pes[0] !== 0 || pes[1] !== 0 || pes[2] !== 1) return null;
|
|
14566
|
+
const ptsDtsFlags = pes[7] >> 6 & 3;
|
|
14567
|
+
const headerDataLength = pes[8];
|
|
14568
|
+
return {
|
|
14569
|
+
es: pes.subarray(9 + headerDataLength),
|
|
14570
|
+
pts: ptsDtsFlags & 2 ? readPts33(pes, 9) : null
|
|
14571
|
+
};
|
|
14572
|
+
}
|
|
14573
|
+
/**
|
|
14574
|
+
* Demux an MPEG-TS byte stream (as produced by `ffmpeg -c:v copy -f mpegts`)
|
|
14575
|
+
* into ordered Annex-B access units, one per video PES.
|
|
14576
|
+
*
|
|
14577
|
+
* @param ts - The full transport stream.
|
|
14578
|
+
* @returns The detected codec and its access units in transport (decode) order.
|
|
14579
|
+
* @throws If no H.264/H.265 video elementary stream is present.
|
|
14580
|
+
*/
|
|
14581
|
+
function demuxMpegTsToAnnexB(ts) {
|
|
14582
|
+
let pmtPid = null;
|
|
14583
|
+
let pmt = null;
|
|
14584
|
+
for (let i = 0; i + TS_PACKET_SIZE <= ts.length; i += TS_PACKET_SIZE) {
|
|
14585
|
+
const pkt = ts.subarray(i, i + TS_PACKET_SIZE);
|
|
14586
|
+
if (pkt[0] !== TS_SYNC_BYTE) continue;
|
|
14587
|
+
const pid = (pkt[1] & 31) << 8 | pkt[2];
|
|
14588
|
+
if (!((pkt[1] & 64) !== 0) || !hasPayload(pkt, 0)) continue;
|
|
14589
|
+
const payload = pkt.subarray(payloadOffset(pkt, 0));
|
|
14590
|
+
if (pid === PAT_PID) pmtPid ??= parsePatForPmtPid(payload);
|
|
14591
|
+
else if (pmtPid !== null && pid === pmtPid && pmt === null) pmt = parsePmt(payload);
|
|
14592
|
+
}
|
|
14593
|
+
if (pmt === null) throw new Error("ts-annexb-demux: no H.264/H.265 video stream found in transport stream");
|
|
14594
|
+
const { videoPid, codec } = pmt;
|
|
14595
|
+
const accessUnits = [];
|
|
14596
|
+
let current = null;
|
|
14597
|
+
const finalize = () => {
|
|
14598
|
+
if (current === null) return;
|
|
14599
|
+
const pes = current.length === 1 ? current[0] : concat(current);
|
|
14600
|
+
current = null;
|
|
14601
|
+
const split = splitPes(pes);
|
|
14602
|
+
if (split === null || split.es.length === 0) return;
|
|
14603
|
+
const data = new Uint8Array(split.es.length);
|
|
14604
|
+
data.set(split.es);
|
|
14605
|
+
accessUnits.push({
|
|
14606
|
+
data,
|
|
14607
|
+
ptsMs: split.pts === null ? 0 : Math.round(split.pts / 90),
|
|
14608
|
+
keyframe: isKeyframeAccessUnit(data, codec)
|
|
14609
|
+
});
|
|
14610
|
+
};
|
|
14611
|
+
for (let i = 0; i + TS_PACKET_SIZE <= ts.length; i += TS_PACKET_SIZE) {
|
|
14612
|
+
const pkt = ts.subarray(i, i + TS_PACKET_SIZE);
|
|
14613
|
+
if (pkt[0] !== TS_SYNC_BYTE) continue;
|
|
14614
|
+
if (((pkt[1] & 31) << 8 | pkt[2]) !== videoPid) continue;
|
|
14615
|
+
if (!hasPayload(pkt, 0)) continue;
|
|
14616
|
+
const payloadStart = (pkt[1] & 64) !== 0;
|
|
14617
|
+
const payload = pkt.subarray(payloadOffset(pkt, 0));
|
|
14618
|
+
if (payloadStart) {
|
|
14619
|
+
finalize();
|
|
14620
|
+
current = [Uint8Array.from(payload)];
|
|
14621
|
+
} else if (current !== null) current.push(Uint8Array.from(payload));
|
|
14622
|
+
}
|
|
14623
|
+
finalize();
|
|
14624
|
+
return {
|
|
14625
|
+
codec,
|
|
14626
|
+
accessUnits
|
|
14627
|
+
};
|
|
14628
|
+
}
|
|
14629
|
+
/** Concatenate TS payload fragments into one contiguous PES buffer. */
|
|
14630
|
+
function concat(parts) {
|
|
14631
|
+
let total = 0;
|
|
14632
|
+
for (const p of parts) total += p.length;
|
|
14633
|
+
const out = new Uint8Array(total);
|
|
14634
|
+
let off = 0;
|
|
14635
|
+
for (const p of parts) {
|
|
14636
|
+
out.set(p, off);
|
|
14637
|
+
off += p.length;
|
|
14638
|
+
}
|
|
14639
|
+
return out;
|
|
14395
14640
|
}
|
|
14396
14641
|
//#endregion
|
|
14397
|
-
//#region src/stream-broker/recorded/
|
|
14642
|
+
//#region src/stream-broker/recorded/segment-demux.ts
|
|
14398
14643
|
/**
|
|
14399
|
-
*
|
|
14644
|
+
* segment-demux — turn one self-contained `.m4s` fragment (a Buffer) into
|
|
14645
|
+
* ordered Annex-B access units. Codec-agnostic: handles BOTH H.264 and H.265
|
|
14646
|
+
* recordings (the recorder is `-c:v copy` passthrough, so an H.264 camera
|
|
14647
|
+
* produces H.264 recordings and an H.265 camera produces H.265 recordings).
|
|
14400
14648
|
*
|
|
14401
|
-
* The
|
|
14402
|
-
*
|
|
14403
|
-
*
|
|
14649
|
+
* The recorder writes self-contained fragments: each `.m4s` carries its own
|
|
14650
|
+
* `moov` (and thus its own `avcC`/`hvcC` parameter sets) plus exactly one
|
|
14651
|
+
* IDR-led GOP (~4s). To replay one over a push pipeline we need the bytes as
|
|
14652
|
+
* Annex-B access units with parameter sets prepended to every IDR (SPS/PPS for
|
|
14653
|
+
* H.264; VPS/SPS/PPS for H.265) and each unit tagged with its source PTS.
|
|
14404
14654
|
*
|
|
14405
|
-
*
|
|
14655
|
+
* Implementation: run the fragment through `ffmpeg -c:v copy -f mpegts` (a
|
|
14656
|
+
* crash-isolated subprocess — this replaced the old in-process `node-av`
|
|
14657
|
+
* demux + `h264/hevc_mp4toannexb` BSF, which could SIGBUS the whole broker on
|
|
14658
|
+
* a fragile stream), then split the transport stream back into access units
|
|
14659
|
+
* with {@link demuxMpegTsToAnnexB}. The mpegts muxer fuses the parameter sets
|
|
14660
|
+
* onto every IDR exactly like the mp4toannexb BSF did, preserves the per-frame
|
|
14661
|
+
* PTS (PES 90 kHz), and exposes the codec via the PMT `stream_type` — so no
|
|
14662
|
+
* explicit bitstream filter and no separate probe pass are needed. See
|
|
14663
|
+
* `ts-annexb-demux.ts` for the container-choice rationale.
|
|
14406
14664
|
*/
|
|
14407
|
-
|
|
14408
|
-
|
|
14409
|
-
|
|
14410
|
-
|
|
14411
|
-
|
|
14412
|
-
|
|
14413
|
-
|
|
14414
|
-
|
|
14415
|
-
|
|
14416
|
-
|
|
14417
|
-
|
|
14418
|
-
|
|
14419
|
-
|
|
14420
|
-
|
|
14421
|
-
|
|
14422
|
-
|
|
14423
|
-
|
|
14424
|
-
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
4,
|
|
14428
|
-
4,
|
|
14429
|
-
4,
|
|
14430
|
-
4,
|
|
14431
|
-
4,
|
|
14432
|
-
4,
|
|
14433
|
-
4,
|
|
14434
|
-
4,
|
|
14435
|
-
4,
|
|
14436
|
-
4,
|
|
14437
|
-
4,
|
|
14438
|
-
4,
|
|
14439
|
-
4,
|
|
14440
|
-
4,
|
|
14441
|
-
4,
|
|
14442
|
-
5,
|
|
14443
|
-
5,
|
|
14444
|
-
5,
|
|
14445
|
-
5,
|
|
14446
|
-
5,
|
|
14447
|
-
5,
|
|
14448
|
-
5,
|
|
14449
|
-
5,
|
|
14450
|
-
5,
|
|
14451
|
-
5,
|
|
14452
|
-
5,
|
|
14453
|
-
5,
|
|
14454
|
-
5,
|
|
14455
|
-
5,
|
|
14456
|
-
5,
|
|
14457
|
-
5,
|
|
14458
|
-
5,
|
|
14459
|
-
5,
|
|
14460
|
-
5,
|
|
14461
|
-
5,
|
|
14462
|
-
5,
|
|
14463
|
-
5,
|
|
14464
|
-
5,
|
|
14465
|
-
5,
|
|
14466
|
-
5,
|
|
14467
|
-
5,
|
|
14468
|
-
5,
|
|
14469
|
-
5,
|
|
14470
|
-
5,
|
|
14471
|
-
5,
|
|
14472
|
-
5,
|
|
14473
|
-
5,
|
|
14474
|
-
6,
|
|
14475
|
-
6,
|
|
14476
|
-
6,
|
|
14477
|
-
6,
|
|
14478
|
-
6,
|
|
14479
|
-
6,
|
|
14480
|
-
6,
|
|
14481
|
-
6,
|
|
14482
|
-
6,
|
|
14483
|
-
6,
|
|
14484
|
-
6,
|
|
14485
|
-
6,
|
|
14486
|
-
6,
|
|
14487
|
-
6,
|
|
14488
|
-
6,
|
|
14489
|
-
6,
|
|
14490
|
-
6,
|
|
14491
|
-
6,
|
|
14492
|
-
6,
|
|
14493
|
-
6,
|
|
14494
|
-
6,
|
|
14495
|
-
6,
|
|
14496
|
-
6,
|
|
14497
|
-
6,
|
|
14498
|
-
6,
|
|
14499
|
-
6,
|
|
14500
|
-
6,
|
|
14501
|
-
6,
|
|
14502
|
-
6,
|
|
14503
|
-
6,
|
|
14504
|
-
6,
|
|
14505
|
-
6,
|
|
14506
|
-
6,
|
|
14507
|
-
6,
|
|
14508
|
-
6,
|
|
14509
|
-
6,
|
|
14510
|
-
6,
|
|
14511
|
-
6,
|
|
14512
|
-
6,
|
|
14513
|
-
6,
|
|
14514
|
-
6,
|
|
14515
|
-
6,
|
|
14516
|
-
6,
|
|
14517
|
-
6,
|
|
14518
|
-
6,
|
|
14519
|
-
6,
|
|
14520
|
-
6,
|
|
14521
|
-
6,
|
|
14522
|
-
6,
|
|
14523
|
-
6,
|
|
14524
|
-
6,
|
|
14525
|
-
6,
|
|
14526
|
-
6,
|
|
14527
|
-
6,
|
|
14528
|
-
6,
|
|
14529
|
-
6,
|
|
14530
|
-
6,
|
|
14531
|
-
6,
|
|
14532
|
-
6,
|
|
14533
|
-
6,
|
|
14534
|
-
6,
|
|
14535
|
-
6,
|
|
14536
|
-
6,
|
|
14537
|
-
6,
|
|
14538
|
-
7,
|
|
14539
|
-
7,
|
|
14540
|
-
7,
|
|
14541
|
-
7,
|
|
14542
|
-
7,
|
|
14543
|
-
7,
|
|
14544
|
-
7,
|
|
14545
|
-
7,
|
|
14546
|
-
7,
|
|
14547
|
-
7,
|
|
14548
|
-
7,
|
|
14549
|
-
7,
|
|
14550
|
-
7,
|
|
14551
|
-
7,
|
|
14552
|
-
7,
|
|
14553
|
-
7,
|
|
14554
|
-
7,
|
|
14555
|
-
7,
|
|
14556
|
-
7,
|
|
14557
|
-
7,
|
|
14558
|
-
7,
|
|
14559
|
-
7,
|
|
14560
|
-
7,
|
|
14561
|
-
7,
|
|
14562
|
-
7,
|
|
14563
|
-
7,
|
|
14564
|
-
7,
|
|
14565
|
-
7,
|
|
14566
|
-
7,
|
|
14567
|
-
7,
|
|
14568
|
-
7,
|
|
14569
|
-
7,
|
|
14570
|
-
7,
|
|
14571
|
-
7,
|
|
14572
|
-
7,
|
|
14573
|
-
7,
|
|
14574
|
-
7,
|
|
14575
|
-
7,
|
|
14576
|
-
7,
|
|
14577
|
-
7,
|
|
14578
|
-
7,
|
|
14579
|
-
7,
|
|
14580
|
-
7,
|
|
14581
|
-
7,
|
|
14582
|
-
7,
|
|
14583
|
-
7,
|
|
14584
|
-
7,
|
|
14585
|
-
7,
|
|
14586
|
-
7,
|
|
14587
|
-
7,
|
|
14588
|
-
7,
|
|
14589
|
-
7,
|
|
14590
|
-
7,
|
|
14591
|
-
7,
|
|
14592
|
-
7,
|
|
14593
|
-
7,
|
|
14594
|
-
7,
|
|
14595
|
-
7,
|
|
14596
|
-
7,
|
|
14597
|
-
7,
|
|
14598
|
-
7,
|
|
14599
|
-
7,
|
|
14600
|
-
7,
|
|
14601
|
-
7,
|
|
14602
|
-
7,
|
|
14603
|
-
7,
|
|
14604
|
-
7,
|
|
14605
|
-
7,
|
|
14606
|
-
7,
|
|
14607
|
-
7,
|
|
14608
|
-
7,
|
|
14609
|
-
7,
|
|
14610
|
-
7,
|
|
14611
|
-
7,
|
|
14612
|
-
7,
|
|
14613
|
-
7,
|
|
14614
|
-
7,
|
|
14615
|
-
7,
|
|
14616
|
-
7,
|
|
14617
|
-
7,
|
|
14618
|
-
7,
|
|
14619
|
-
7,
|
|
14620
|
-
7,
|
|
14621
|
-
7,
|
|
14622
|
-
7,
|
|
14623
|
-
7,
|
|
14624
|
-
7,
|
|
14625
|
-
7,
|
|
14626
|
-
7,
|
|
14627
|
-
7,
|
|
14628
|
-
7,
|
|
14629
|
-
7,
|
|
14630
|
-
7,
|
|
14631
|
-
7,
|
|
14632
|
-
7,
|
|
14633
|
-
7,
|
|
14634
|
-
7,
|
|
14635
|
-
7,
|
|
14636
|
-
7,
|
|
14637
|
-
7,
|
|
14638
|
-
7,
|
|
14639
|
-
7,
|
|
14640
|
-
7,
|
|
14641
|
-
7,
|
|
14642
|
-
7,
|
|
14643
|
-
7,
|
|
14644
|
-
7,
|
|
14645
|
-
7,
|
|
14646
|
-
7,
|
|
14647
|
-
7,
|
|
14648
|
-
7,
|
|
14649
|
-
7,
|
|
14650
|
-
7,
|
|
14651
|
-
7,
|
|
14652
|
-
7,
|
|
14653
|
-
7,
|
|
14654
|
-
7,
|
|
14655
|
-
7,
|
|
14656
|
-
7,
|
|
14657
|
-
7,
|
|
14658
|
-
7,
|
|
14659
|
-
7,
|
|
14660
|
-
7,
|
|
14661
|
-
7,
|
|
14662
|
-
7,
|
|
14663
|
-
7,
|
|
14664
|
-
7,
|
|
14665
|
-
7
|
|
14665
|
+
/**
|
|
14666
|
+
* ffmpeg arguments to remux one `.m4s` fragment (read from stdin) into an
|
|
14667
|
+
* Annex-B MPEG-TS on stdout. `-c:v copy` = no re-encode; the mpegts muxer does
|
|
14668
|
+
* the mp4→annexb conversion and fuses parameter sets onto every IDR. `-an`
|
|
14669
|
+
* drops audio (handled separately by `segment-audio`). Exported for the spec.
|
|
14670
|
+
*/
|
|
14671
|
+
var SEGMENT_DEMUX_FFMPEG_ARGS = [
|
|
14672
|
+
"-hide_banner",
|
|
14673
|
+
"-loglevel",
|
|
14674
|
+
"error",
|
|
14675
|
+
"-i",
|
|
14676
|
+
"pipe:0",
|
|
14677
|
+
"-map",
|
|
14678
|
+
"0:v:0",
|
|
14679
|
+
"-c:v",
|
|
14680
|
+
"copy",
|
|
14681
|
+
"-an",
|
|
14682
|
+
"-f",
|
|
14683
|
+
"mpegts",
|
|
14684
|
+
"pipe:1"
|
|
14666
14685
|
];
|
|
14667
|
-
/** Compand one signed 16-bit linear PCM sample to an 8-bit µ-law byte. */
|
|
14668
|
-
function linearToMuLaw(sample) {
|
|
14669
|
-
let s = sample;
|
|
14670
|
-
const sign = s >> 8 & 128;
|
|
14671
|
-
if (sign !== 0) s = -s;
|
|
14672
|
-
if (s > CLIP) s = CLIP;
|
|
14673
|
-
s += BIAS;
|
|
14674
|
-
const exponent = EXP_LUT[s >> 7 & 255];
|
|
14675
|
-
const mantissa = s >> exponent + 3 & 15;
|
|
14676
|
-
return ~(sign | exponent << 4 | mantissa) & 255;
|
|
14677
|
-
}
|
|
14678
14686
|
/**
|
|
14679
|
-
*
|
|
14680
|
-
*
|
|
14687
|
+
* Demux a self-contained H.264 or H.265 `.m4s` fragment into ordered Annex-B
|
|
14688
|
+
* access units.
|
|
14689
|
+
*
|
|
14690
|
+
* The video codec is detected from the transport stream's PMT `stream_type`;
|
|
14691
|
+
* the mpegts muxer fuses parameter sets onto every IDR (SPS/PPS for H.264,
|
|
14692
|
+
* VPS/SPS/PPS for H.265). Each returned {@link AccessUnit} owns a detached copy
|
|
14693
|
+
* of the bytes. Throws on a non-H.264/H.265 stream or a failed conversion.
|
|
14694
|
+
*
|
|
14695
|
+
* @param bytes - The full `.m4s` fragment (one moov + one IDR-led GOP).
|
|
14696
|
+
* @param ffmpegPath - ffmpeg binary path (defaults to PATH `ffmpeg`).
|
|
14697
|
+
* @returns Access units in source presentation order, each tagged with its codec.
|
|
14681
14698
|
*/
|
|
14682
|
-
function
|
|
14683
|
-
const
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14699
|
+
async function demuxSegmentToAnnexB(bytes, ffmpegPath = "ffmpeg") {
|
|
14700
|
+
const { codec, accessUnits } = demuxMpegTsToAnnexB(await runFfmpegOnce({
|
|
14701
|
+
ffmpegPath,
|
|
14702
|
+
args: SEGMENT_DEMUX_FFMPEG_ARGS,
|
|
14703
|
+
input: bytes
|
|
14704
|
+
}));
|
|
14705
|
+
return accessUnits.map((au) => ({
|
|
14706
|
+
data: au.data,
|
|
14707
|
+
ptsMs: au.ptsMs,
|
|
14708
|
+
keyframe: au.keyframe,
|
|
14709
|
+
codec
|
|
14710
|
+
}));
|
|
14688
14711
|
}
|
|
14689
14712
|
//#endregion
|
|
14690
14713
|
//#region src/stream-broker/recorded/segment-audio.ts
|
|
14691
14714
|
/**
|
|
14692
|
-
* segment-audio — turn one `.m4s` fragment's
|
|
14715
|
+
* segment-audio — turn one `.m4s` fragment's audio into 8 kHz µ-law (PCMU)
|
|
14693
14716
|
* access units, matching the WebRTC audio sender's negotiated codec
|
|
14694
14717
|
* (`audio/PCMU`, 8 kHz, PT 0 — see session.ts). Recorded audio is stored as AAC
|
|
14695
|
-
* (the recorder transcodes every camera's audio to AAC); WebRTC speaks PCMU
|
|
14696
|
-
*
|
|
14697
|
-
*
|
|
14718
|
+
* (the recorder transcodes every camera's audio to AAC); WebRTC speaks PCMU.
|
|
14719
|
+
*
|
|
14720
|
+
* Implementation: run the fragment through
|
|
14721
|
+
* `ffmpeg -vn -ar 8000 -ac 1 -c:a pcm_mulaw -f mulaw` (a crash-isolated
|
|
14722
|
+
* subprocess — this replaced the old in-process `node-av` `DecodeRuntime`
|
|
14723
|
+
* decode + hand-rolled µ-law companding). ffmpeg decodes the AAC, resamples to
|
|
14724
|
+
* 8 kHz mono, and companded µ-law bytes come out on stdout; we slice them into
|
|
14725
|
+
* fixed 20 ms (160-sample) PCMU frames here.
|
|
14698
14726
|
*
|
|
14699
|
-
* Output: 20 ms
|
|
14700
|
-
*
|
|
14727
|
+
* Output: 20 ms PCMU frames with segment-relative `ptsMs`, ready to push on the
|
|
14728
|
+
* session's audio sender alongside the recorded video.
|
|
14701
14729
|
*
|
|
14702
|
-
* Best-effort by contract: returns [] when the fragment has no usable audio
|
|
14703
|
-
*
|
|
14730
|
+
* Best-effort by contract: returns [] when the fragment has no usable audio
|
|
14731
|
+
* (ffmpeg exits non-zero → tolerated, empty output), and the caller swallows
|
|
14732
|
+
* throws — recorded audio must NEVER block recorded video.
|
|
14704
14733
|
*/
|
|
14705
14734
|
/** PCMU target: 8 kHz mono, 160 samples (20 ms) per RTP frame. */
|
|
14706
|
-
var TARGET_RATE = 8e3;
|
|
14707
14735
|
var FRAME_SAMPLES = 160;
|
|
14708
14736
|
var FRAME_MS = 20;
|
|
14709
|
-
/**
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14737
|
+
/**
|
|
14738
|
+
* ffmpeg arguments to decode one `.m4s` fragment's audio (read from stdin) into
|
|
14739
|
+
* raw 8 kHz mono µ-law bytes on stdout. `-vn` drops video (handled by
|
|
14740
|
+
* `segment-demux`); `-c:a pcm_mulaw -f mulaw` companded output = one byte per
|
|
14741
|
+
* 8 kHz sample. Exported for the spec.
|
|
14742
|
+
*/
|
|
14743
|
+
var SEGMENT_AUDIO_FFMPEG_ARGS = [
|
|
14744
|
+
"-hide_banner",
|
|
14745
|
+
"-loglevel",
|
|
14746
|
+
"error",
|
|
14747
|
+
"-i",
|
|
14748
|
+
"pipe:0",
|
|
14749
|
+
"-vn",
|
|
14750
|
+
"-map",
|
|
14751
|
+
"0:a:0",
|
|
14752
|
+
"-ar",
|
|
14753
|
+
"8000",
|
|
14754
|
+
"-ac",
|
|
14755
|
+
"1",
|
|
14756
|
+
"-c:a",
|
|
14757
|
+
"pcm_mulaw",
|
|
14758
|
+
"-f",
|
|
14759
|
+
"mulaw",
|
|
14760
|
+
"pipe:1"
|
|
14761
|
+
];
|
|
14762
|
+
/**
|
|
14763
|
+
* Slice a contiguous 8 kHz µ-law byte stream (one byte per sample) into fixed
|
|
14764
|
+
* 160-sample (20 ms) PCMU frames, dropping any trailing partial frame. `ptsMs`
|
|
14765
|
+
* is derived from the running frame index — audio is continuous 8 kHz, and each
|
|
14766
|
+
* recorded segment re-anchors its own timeline downstream, so the frames start
|
|
14767
|
+
* at 0 (segment-relative). Pure, so it unit-tests without ffmpeg.
|
|
14768
|
+
*/
|
|
14769
|
+
function sliceMuLawToPcmuFrames(mulaw) {
|
|
14770
|
+
const out = [];
|
|
14771
|
+
for (let off = 0, idx = 0; off + FRAME_SAMPLES <= mulaw.length; off += FRAME_SAMPLES, idx++) out.push({
|
|
14772
|
+
data: mulaw.slice(off, off + FRAME_SAMPLES),
|
|
14773
|
+
ptsMs: idx * FRAME_MS
|
|
14774
|
+
});
|
|
14775
|
+
return out;
|
|
14717
14776
|
}
|
|
14718
14777
|
/**
|
|
14719
|
-
*
|
|
14720
|
-
* when there is no usable audio stream (
|
|
14721
|
-
* non-
|
|
14778
|
+
* Decode + resample one fragment's audio into PCMU access units. Returns []
|
|
14779
|
+
* when there is no usable audio stream (video-only fragment, or a codec ffmpeg
|
|
14780
|
+
* can't decode) — ffmpeg exits non-zero, which is tolerated as empty output.
|
|
14781
|
+
*
|
|
14782
|
+
* @param bytes - The full `.m4s` fragment.
|
|
14783
|
+
* @param ffmpegPath - ffmpeg binary path (defaults to PATH `ffmpeg`).
|
|
14722
14784
|
*/
|
|
14723
|
-
async function demuxSegmentToPcmu(bytes) {
|
|
14724
|
-
const
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
const cp = stream.codecpar;
|
|
14733
|
-
const codecName = cp.codecId === C.AV_CODEC_ID_AAC ? "aac" : cp.codecId === C.AV_CODEC_ID_AAC_LATM ? "aac_latm" : null;
|
|
14734
|
-
if (codecName === null) return [];
|
|
14735
|
-
const sourceChannels = cp.channelLayout?.nbChannels ?? cp.channels ?? 1;
|
|
14736
|
-
decode = require_codec_runtime.DecodeRuntime.create({
|
|
14737
|
-
codec: codecName,
|
|
14738
|
-
sourceSampleRate: cp.sampleRate,
|
|
14739
|
-
sourceChannels,
|
|
14740
|
-
extraData: cp.extradata ? new Uint8Array(cp.extradata) : void 0,
|
|
14741
|
-
targetSampleRate: TARGET_RATE,
|
|
14742
|
-
targetChannels: 1,
|
|
14743
|
-
targetFormat: "s16le"
|
|
14744
|
-
});
|
|
14745
|
-
let baseMs = null;
|
|
14746
|
-
const muLaw = [];
|
|
14747
|
-
for await (const packet of demuxer.packets(stream.index)) {
|
|
14748
|
-
if (packet === null) break;
|
|
14749
|
-
try {
|
|
14750
|
-
const data = packet.data;
|
|
14751
|
-
if (data && data.length > 0) {
|
|
14752
|
-
if (baseMs === null && packet.pts !== AV_NOPTS_VALUE) baseMs = ticksToMs(packet.pts, stream.timeBase);
|
|
14753
|
-
for (const pcm of decode.decode(data)) {
|
|
14754
|
-
const enc = encodeMuLaw(pcm.data);
|
|
14755
|
-
for (let i = 0; i < enc.length; i++) muLaw.push(enc[i]);
|
|
14756
|
-
}
|
|
14757
|
-
}
|
|
14758
|
-
} finally {
|
|
14759
|
-
packet.free();
|
|
14760
|
-
}
|
|
14761
|
-
}
|
|
14762
|
-
if (muLaw.length === 0) return [];
|
|
14763
|
-
const base = baseMs ?? 0;
|
|
14764
|
-
const out = [];
|
|
14765
|
-
for (let off = 0, idx = 0; off + FRAME_SAMPLES <= muLaw.length; off += FRAME_SAMPLES, idx++) out.push({
|
|
14766
|
-
data: Uint8Array.from(muLaw.slice(off, off + FRAME_SAMPLES)),
|
|
14767
|
-
ptsMs: base + idx * FRAME_MS
|
|
14768
|
-
});
|
|
14769
|
-
return out;
|
|
14770
|
-
} finally {
|
|
14771
|
-
decode?.close();
|
|
14772
|
-
await demuxer.close();
|
|
14773
|
-
}
|
|
14785
|
+
async function demuxSegmentToPcmu(bytes, ffmpegPath = "ffmpeg") {
|
|
14786
|
+
const mulaw = await runFfmpegOnce({
|
|
14787
|
+
ffmpegPath,
|
|
14788
|
+
args: SEGMENT_AUDIO_FFMPEG_ARGS,
|
|
14789
|
+
input: bytes,
|
|
14790
|
+
tolerateNonZeroExit: true
|
|
14791
|
+
});
|
|
14792
|
+
if (mulaw.length === 0) return [];
|
|
14793
|
+
return sliceMuLawToPcmuFrames(mulaw);
|
|
14774
14794
|
}
|
|
14775
14795
|
//#endregion
|
|
14776
14796
|
//#region src/stream-broker/webrtc/broker-webrtc-server.ts
|
|
@@ -14955,6 +14975,7 @@ var BrokerWebrtcServer = class {
|
|
|
14955
14975
|
resolveTranscodeDecodeHwAccelFn;
|
|
14956
14976
|
locateRecordedSegment;
|
|
14957
14977
|
readRecordedSegmentBytes;
|
|
14978
|
+
resolveFfmpegPath;
|
|
14958
14979
|
/** Cached decode-hwaccel backend for transcode feeds (resolved once). */
|
|
14959
14980
|
transcodeDecodeHwAccel = void 0;
|
|
14960
14981
|
/** brokerId → IStreamBroker reference. Set by the broker manager on registration. */
|
|
@@ -14998,6 +15019,7 @@ var BrokerWebrtcServer = class {
|
|
|
14998
15019
|
this.resolveTranscodeDecodeHwAccelFn = options.resolveTranscodeDecodeHwAccel;
|
|
14999
15020
|
this.locateRecordedSegment = options.locateRecordedSegment;
|
|
15000
15021
|
this.readRecordedSegmentBytes = options.readRecordedSegmentBytes;
|
|
15022
|
+
this.resolveFfmpegPath = options.resolveFfmpegPath;
|
|
15001
15023
|
}
|
|
15002
15024
|
/**
|
|
15003
15025
|
* Resolve (once, cached) the decode-hwaccel backend for transcode feeds.
|
|
@@ -15513,6 +15535,7 @@ var BrokerWebrtcServer = class {
|
|
|
15513
15535
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
15514
15536
|
const locateRecorded = this.locateRecordedSegment;
|
|
15515
15537
|
const readRecorded = this.readRecordedSegmentBytes;
|
|
15538
|
+
const resolveFfmpeg = this.resolveFfmpegPath;
|
|
15516
15539
|
if (locateRecorded && readRecorded) {
|
|
15517
15540
|
let timeline;
|
|
15518
15541
|
timeline = new TimelineSession({
|
|
@@ -15523,8 +15546,8 @@ var BrokerWebrtcServer = class {
|
|
|
15523
15546
|
profile,
|
|
15524
15547
|
locate: locateRecorded,
|
|
15525
15548
|
readBytes: readRecorded,
|
|
15526
|
-
demux: demuxSegmentToAnnexB,
|
|
15527
|
-
demuxAudio: demuxSegmentToPcmu,
|
|
15549
|
+
demux: (b) => demuxSegmentToAnnexB(b, resolveFfmpeg?.()),
|
|
15550
|
+
demuxAudio: (b) => demuxSegmentToPcmu(b, resolveFfmpeg?.()),
|
|
15528
15551
|
sink: {
|
|
15529
15552
|
pushAccessUnit: (data, ts, codec) => session.pushRecordedAccessUnit(data, ts, codec),
|
|
15530
15553
|
pushAudioPcmu: (pcmu, ts) => session.writeRecordedAudio(Buffer.from(pcmu), ts)
|
|
@@ -16285,6 +16308,10 @@ var BROKER_METRICS_HEARTBEAT_MS = 3e4;
|
|
|
16285
16308
|
var PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS = 6e4;
|
|
16286
16309
|
var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
16287
16310
|
brokerManager = null;
|
|
16311
|
+
/** ffmpeg binary path from the `ffmpeg` config section, mirrored here so the
|
|
16312
|
+
* recorded-playback demuxers (segment-demux/segment-audio) spawn the same
|
|
16313
|
+
* configured binary as the transcode egress. Kept in sync by loadFfmpegConfig. */
|
|
16314
|
+
ffmpegBinaryPath = "ffmpeg";
|
|
16288
16315
|
metricsSnapshotTimer = null;
|
|
16289
16316
|
catalogReconcileTimer = null;
|
|
16290
16317
|
/** Kept so onShutdown can close every live PeerConnection — releasing
|
|
@@ -16345,6 +16372,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
16345
16372
|
hwAccel,
|
|
16346
16373
|
threadCount
|
|
16347
16374
|
});
|
|
16375
|
+
this.ffmpegBinaryPath = binaryPath;
|
|
16348
16376
|
this.ctx.logger.info("Loaded ffmpeg config", { meta: {
|
|
16349
16377
|
binaryPath,
|
|
16350
16378
|
hwAccel,
|
|
@@ -16607,7 +16635,8 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
16607
16635
|
},
|
|
16608
16636
|
resolveTranscodeDecodeHwAccel: () => this.resolveTranscodeDecodeHwAccel(),
|
|
16609
16637
|
locateRecordedSegment: (args) => this.ctx.api.recording.locateSegment.query(args),
|
|
16610
|
-
readRecordedSegmentBytes: async (args) => (await this.ctx.api.recording.readSegmentBytes.query(args)).data
|
|
16638
|
+
readRecordedSegmentBytes: async (args) => (await this.ctx.api.recording.readSegmentBytes.query(args)).data,
|
|
16639
|
+
resolveFfmpegPath: () => this.ffmpegBinaryPath
|
|
16611
16640
|
});
|
|
16612
16641
|
this.brokerManager.setWebrtcServer(webrtcServer);
|
|
16613
16642
|
this.webrtcServer = webrtcServer;
|
|
@@ -16918,7 +16947,7 @@ function codecToInputFormat(codec) {
|
|
|
16918
16947
|
function buildFfmpegArgs(config) {
|
|
16919
16948
|
const inputFormat = codecToInputFormat(config.codec);
|
|
16920
16949
|
const args = [
|
|
16921
|
-
...
|
|
16950
|
+
...require_frame_dropper.logBannerArgs("error"),
|
|
16922
16951
|
"-fflags",
|
|
16923
16952
|
"+nobuffer+flush_packets",
|
|
16924
16953
|
"-flags",
|
|
@@ -16955,7 +16984,7 @@ var FfmpegDecoderSession = class {
|
|
|
16955
16984
|
constructor(config, logger = noopLogger) {
|
|
16956
16985
|
this.config = { ...config };
|
|
16957
16986
|
this.logger = logger;
|
|
16958
|
-
this.frameDropper = new
|
|
16987
|
+
this.frameDropper = new require_frame_dropper.FrameDropper(config.maxFps);
|
|
16959
16988
|
this.spawnFfmpeg();
|
|
16960
16989
|
}
|
|
16961
16990
|
spawnFfmpeg() {
|