@camstack/addon-pipeline 1.2.268 → 1.2.269
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-CKNqjjRy.js → default-detection-model-BfMsW_XK.js} +1 -1
- package/dist/{default-detection-model--x95ODoz.mjs → default-detection-model-pXamampo.mjs} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-C7CvyImc.js → dist-CkNVb7Zh.js} +187 -11
- package/dist/{dist-Dm1V0nJX.mjs → dist-Cqf-U6EN.mjs} +187 -11
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-CjN6Vze9.js → node-CqC8R84-.js} +116 -2
- package/dist/{node-B5haKIVa.mjs → node-jLmcbaar.mjs} +111 -3
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-BMCVwJcT.mjs → process-memory-B1YHg1_g.mjs} +1 -1
- package/dist/{process-memory-CaFOThD9.js → process-memory-D7LlWXty.js} +1 -1
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/{segment-demux-js-BaWzAeJu.mjs → segment-demux-js-B2TLcR_g.mjs} +1 -1
- package/dist/{segment-demux-js-bfaGjVqq.js → segment-demux-js-Bu-_Q7Um.js} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-6gUUOaeo.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbUvu4Jn.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DJVznXRG.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-CeR_IxxU.mjs +26 -0
- 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-DBu-k8Ps.mjs → hostInit-9ZN1ghaZ.mjs} +2 -2
- package/dist/stream-broker/index.js +397 -60
- package/dist/stream-broker/index.mjs +397 -60
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/python/inference_pool.py +84 -2
- package/python/test_inference_pool_ov_threads.py +112 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B5hze8qG.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DTzfOQZq.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-CkNVb7Zh.js");
|
|
7
|
+
const require_node = require("../node-CqC8R84-.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-Bu-_Q7Um.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");
|
|
@@ -495,6 +495,8 @@ function logBannerArgs(level) {
|
|
|
495
495
|
}
|
|
496
496
|
//#endregion
|
|
497
497
|
//#region src/shared/ffmpeg-decode-args.ts
|
|
498
|
+
/** Default DRM render node used for the VA-API / QSV hardware decode path. */
|
|
499
|
+
var DEFAULT_VAAPI_RENDER_DEVICE = "/dev/dri/renderD128";
|
|
498
500
|
/**
|
|
499
501
|
* Canonical decode-hwaccel preference order for the ffmpeg-subprocess decoder.
|
|
500
502
|
*
|
|
@@ -5281,6 +5283,26 @@ function decideAudioPlaneAction(observation, state, nowMs) {
|
|
|
5281
5283
|
* "Device creation failed: -12 … Cannot allocate memory". Every egress child
|
|
5282
5284
|
* therefore spends ~3 s dying before its own software retry rescues it.
|
|
5283
5285
|
*
|
|
5286
|
+
* ## CORRECTION (2026-09-18): that `-12` was not a resource condition
|
|
5287
|
+
*
|
|
5288
|
+
* The two runners were not competing for a device — they were running
|
|
5289
|
+
* DIFFERENT BINARIES. The decoder addon reached a capable ffmpeg; the broker
|
|
5290
|
+
* reached the downloaded static build, which has no VAAPI compiled in at all
|
|
5291
|
+
* (`hwaccels: vdpau`). `-12` is what fftools prints when
|
|
5292
|
+
* `av_hwdevice_ctx_alloc` returns NULL because the type is not in the build:
|
|
5293
|
+
* the NAME parses, so the request looks valid and the device simply cannot
|
|
5294
|
+
* exist. It READS as a shortage and is a property of the binary (D536).
|
|
5295
|
+
*
|
|
5296
|
+
* That is why it "recurred forever on a hub where it has never once
|
|
5297
|
+
* succeeded": there was nothing to recover. The fix is upstream of this file —
|
|
5298
|
+
* `TranscodePipelineManager` now asks the binary (`ffmpeg -hwaccels`) and
|
|
5299
|
+
* refuses a backend it does not carry, so this quarantine never sees that
|
|
5300
|
+
* class of failure again.
|
|
5301
|
+
*
|
|
5302
|
+
* Everything below still stands for what remains: a backend the binary HAS and
|
|
5303
|
+
* the device still refuses. That one is a genuine runtime condition and can
|
|
5304
|
+
* clear, which is exactly why this is a quarantine and not a tombstone.
|
|
5305
|
+
*
|
|
5284
5306
|
* That was already reported — `EgressTranscodeManager.onDecodeHwAccelFailed`
|
|
5285
5307
|
* poisoned the broker's 60 s `egressHwAccelCache`. The memo is the wrong shape
|
|
5286
5308
|
* for the verdict: it exists to avoid a per-session cross-process
|
|
@@ -5292,10 +5314,11 @@ function decideAudioPlaneAction(observation, state, nowMs) {
|
|
|
5292
5314
|
*
|
|
5293
5315
|
* ## Why this is a quarantine and not a tombstone
|
|
5294
5316
|
*
|
|
5295
|
-
* A permanent verdict is wrong for the failure
|
|
5296
|
-
*
|
|
5297
|
-
* can clear (the operator gives the container more of
|
|
5298
|
-
* process releases it).
|
|
5317
|
+
* A permanent verdict is wrong for the failure this file is now left with:
|
|
5318
|
+
* a device that exists in the build and still refuses is a RESOURCE condition,
|
|
5319
|
+
* and a resource condition can clear (the operator gives the container more of
|
|
5320
|
+
* the device, another process releases it). The build-property case — the one
|
|
5321
|
+
* that produced the measurements above — no longer reaches here at all. A process that decided "never again" at 03:00 would
|
|
5299
5322
|
* still be decoding in software at noon with a working accelerator, and nothing
|
|
5300
5323
|
* would ever say so.
|
|
5301
5324
|
*
|
|
@@ -5433,36 +5456,54 @@ function classifyDecodeHwAccel(setting) {
|
|
|
5433
5456
|
function toAudioSidecarCodec(audio) {
|
|
5434
5457
|
return audio === "aac" || audio === "opus" || audio === "pcmu" ? audio : null;
|
|
5435
5458
|
}
|
|
5436
|
-
var KNOWN_ENCODERS = new Set([
|
|
5437
|
-
"libx264",
|
|
5438
|
-
"libx265",
|
|
5439
|
-
"h264_videotoolbox",
|
|
5440
|
-
"hevc_videotoolbox",
|
|
5441
|
-
"h264_vaapi",
|
|
5442
|
-
"hevc_vaapi",
|
|
5443
|
-
"h264_qsv",
|
|
5444
|
-
"hevc_qsv",
|
|
5445
|
-
"h264_nvenc",
|
|
5446
|
-
"hevc_nvenc",
|
|
5447
|
-
"h264_amf",
|
|
5448
|
-
"hevc_amf"
|
|
5449
|
-
]);
|
|
5450
|
-
/** Type guard — an encoder id the shared builder knows how to emit. */
|
|
5451
|
-
function isKnownEncoder(value) {
|
|
5452
|
-
return KNOWN_ENCODERS.has(value);
|
|
5453
|
-
}
|
|
5454
5459
|
/**
|
|
5455
|
-
*
|
|
5456
|
-
*
|
|
5457
|
-
*
|
|
5458
|
-
*
|
|
5459
|
-
*
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5460
|
+
* Choose the video encoder — separately from the DECODE preference, which is
|
|
5461
|
+
* the whole point.
|
|
5462
|
+
*
|
|
5463
|
+
* {@link pickVideoEncoder} folded the two together: `hwAccel: 'copy'` meant
|
|
5464
|
+
* "software everything". But `copy` is this fleet's decode DEFAULT for a real
|
|
5465
|
+
* reason — hardware decode hard-fails on a fragile HEVC source — and it was
|
|
5466
|
+
* quietly deciding the encoder as well. On a hub with a working Intel iGPU
|
|
5467
|
+
* that put every egress and derived transcode on `libx264`, spending cores
|
|
5468
|
+
* this box does not have, while the GPU idled. A software decode can feed a
|
|
5469
|
+
* hardware encoder (`hwupload` is exactly that), and the encode is where the
|
|
5470
|
+
* CPU goes.
|
|
5471
|
+
*
|
|
5472
|
+
* Three things can send it back to the CPU, and each is NAMED so the caller
|
|
5473
|
+
* can say which — a silent software transcode is the thing this exists to end:
|
|
5474
|
+
*
|
|
5475
|
+
* - `operator-forced-software` — someone asked for `none`. Honoured.
|
|
5476
|
+
* - `no-hardware-on-node` — the hwaccel resolver offers nothing.
|
|
5477
|
+
* - `binary-lacks-encoder` — the node has the hardware and THIS ffmpeg cannot
|
|
5478
|
+
* use it. That is a build property, not a resource condition (D536).
|
|
5479
|
+
*
|
|
5480
|
+
* An UNKNOWN encoder list (`null`, a probe that failed) is not a "no": we
|
|
5481
|
+
* could not ask, so the hardware encoder is still attempted and ffmpeg's own
|
|
5482
|
+
* failure is the answer. Reading "unknown" as "unsupported" would keep a
|
|
5483
|
+
* capable node in software forever on one bad probe.
|
|
5484
|
+
*/
|
|
5485
|
+
function chooseVideoEncoder(input) {
|
|
5486
|
+
const software = input.target === "H264" ? "libx264" : "libx265";
|
|
5487
|
+
const fallback = (reason) => ({
|
|
5488
|
+
encoder: software,
|
|
5489
|
+
hwEncodeDevice: null,
|
|
5490
|
+
softwareReason: reason
|
|
5491
|
+
});
|
|
5492
|
+
if (input.hwAccelSetting === "none") return fallback("operator-forced-software");
|
|
5493
|
+
const backend = input.backend === null ? null : input.backend.toLowerCase();
|
|
5494
|
+
if (backend === null) return fallback("no-hardware-on-node");
|
|
5495
|
+
const ids = ENCODER_IDS_BY_BACKEND[backend];
|
|
5496
|
+
if (!ids) return fallback("no-hardware-on-node");
|
|
5497
|
+
const encoder = input.target === "H264" ? ids.h264 : ids.h265;
|
|
5498
|
+
if (input.binaryEncoders !== null && !input.binaryEncoders.has(encoder)) return fallback("binary-lacks-encoder");
|
|
5499
|
+
return {
|
|
5500
|
+
encoder,
|
|
5501
|
+
hwEncodeDevice: backend === "vaapi" ? {
|
|
5502
|
+
kind: "vaapi",
|
|
5503
|
+
renderNode: input.renderNode
|
|
5504
|
+
} : null,
|
|
5505
|
+
softwareReason: null
|
|
5506
|
+
};
|
|
5466
5507
|
}
|
|
5467
5508
|
/**
|
|
5468
5509
|
* The h264/hevc hardware ENCODER ids for a decode-hwaccel backend name — a
|
|
@@ -5495,24 +5536,6 @@ var ENCODER_IDS_BY_BACKEND = {
|
|
|
5495
5536
|
}
|
|
5496
5537
|
};
|
|
5497
5538
|
/**
|
|
5498
|
-
* Build the {@link HardwareEncoders} default pair for THIS node's decode
|
|
5499
|
-
* backend, resolved LOCALLY from `ctx.kernel.hwaccel` — WITHOUT probing
|
|
5500
|
-
* `ffmpeg -encoders` through the `platform-probe` singleton, whose per-node
|
|
5501
|
-
* `nodePin` does not survive a forked agent child's link chain (it answers with
|
|
5502
|
-
* the HUB's encoders). Returns `null` for software / an unmapped backend.
|
|
5503
|
-
*/
|
|
5504
|
-
function localHardwareEncoders(backend) {
|
|
5505
|
-
if (backend === null) return null;
|
|
5506
|
-
const ids = ENCODER_IDS_BY_BACKEND[backend.toLowerCase()];
|
|
5507
|
-
if (!ids) return null;
|
|
5508
|
-
return {
|
|
5509
|
-
encoders: [],
|
|
5510
|
-
defaultH264: ids.h264,
|
|
5511
|
-
defaultH265: ids.h265,
|
|
5512
|
-
probedAt: Date.now()
|
|
5513
|
-
};
|
|
5514
|
-
}
|
|
5515
|
-
/**
|
|
5516
5539
|
* Translate the broker's transcode request into the shared
|
|
5517
5540
|
* {@link FfmpegInvocation}. Emits no arguments itself — `buildFfmpegArgs` from
|
|
5518
5541
|
* `@camstack/types` does that, and it is the only thing that may.
|
|
@@ -5526,6 +5549,7 @@ function brokerTranscodeInvocation(inv) {
|
|
|
5526
5549
|
return {
|
|
5527
5550
|
logLevel: "warning",
|
|
5528
5551
|
decodeHwAccel: inv.decodeHwAccel,
|
|
5552
|
+
...inv.hwEncodeDevice ? { hwEncodeDevice: inv.hwEncodeDevice } : {},
|
|
5529
5553
|
input: {
|
|
5530
5554
|
url: inv.sourceUrl,
|
|
5531
5555
|
rtspTransport: "tcp"
|
|
@@ -16280,6 +16304,9 @@ var TranscodePipelineManager = class {
|
|
|
16280
16304
|
/** In-flight transcode starts, keyed by pipeline key — single-flight the
|
|
16281
16305
|
* async egress start so concurrent acquires share one egress. */
|
|
16282
16306
|
pending = /* @__PURE__ */ new Map();
|
|
16307
|
+
/** What the ffmpeg at a path can do, asked once. `null` = could not ask. */
|
|
16308
|
+
binaryCapabilities = /* @__PURE__ */ new Map();
|
|
16309
|
+
probeFfmpeg;
|
|
16283
16310
|
logger;
|
|
16284
16311
|
hwaccel;
|
|
16285
16312
|
cameraStreamLookup;
|
|
@@ -16299,6 +16326,7 @@ var TranscodePipelineManager = class {
|
|
|
16299
16326
|
this.resolveProfileSource = opts.resolveProfileSource ?? null;
|
|
16300
16327
|
this.releaseGraceMs = opts.releaseGraceMs ?? 5e3;
|
|
16301
16328
|
this.ffmpegConfig = opts.ffmpegConfig ?? (() => DEFAULT_FFMPEG_CONFIG);
|
|
16329
|
+
this.probeFfmpeg = opts.probeFfmpeg ?? require_node.probeFfmpegBinary;
|
|
16302
16330
|
this.resolveStreamHwAccel = opts.resolveStreamHwAccel ?? null;
|
|
16303
16331
|
this.spawnFn = opts.spawnFn ?? node_child_process.spawn;
|
|
16304
16332
|
this.acquireChannel = opts.acquireChannel ?? null;
|
|
@@ -16371,8 +16399,33 @@ var TranscodePipelineManager = class {
|
|
|
16371
16399
|
if (!this.acquireChannel) throw new Error("getStreamWithCodec: transcode egress not wired (acquireChannel missing)");
|
|
16372
16400
|
const cfg = this.ffmpegConfig();
|
|
16373
16401
|
const effectiveHwAccel = this.effectiveHwAccelSetting(opts.deviceId, opts.camStreamId, cfg.hwAccel);
|
|
16374
|
-
const
|
|
16375
|
-
|
|
16402
|
+
const choice = chooseVideoEncoder({
|
|
16403
|
+
target: opts.targetCodec,
|
|
16404
|
+
backend: await this.resolveHardwareBackend(),
|
|
16405
|
+
binaryEncoders: await this.resolveBinaryEncoders(cfg.binaryPath),
|
|
16406
|
+
hwAccelSetting: effectiveHwAccel,
|
|
16407
|
+
renderNode: DEFAULT_VAAPI_RENDER_DEVICE
|
|
16408
|
+
});
|
|
16409
|
+
const encoder = choice.encoder;
|
|
16410
|
+
if (choice.softwareReason !== null) this.logger.error("transcode is running on the CPU — a software encode is work the hardware was supposed to do", {
|
|
16411
|
+
tags: { deviceId: opts.deviceId },
|
|
16412
|
+
meta: {
|
|
16413
|
+
reason: choice.softwareReason,
|
|
16414
|
+
encoder,
|
|
16415
|
+
camStreamId: opts.camStreamId,
|
|
16416
|
+
targetCodec: opts.targetCodec,
|
|
16417
|
+
hwAccelSetting: effectiveHwAccel,
|
|
16418
|
+
ffmpeg: cfg.binaryPath
|
|
16419
|
+
}
|
|
16420
|
+
});
|
|
16421
|
+
else this.logger.info("transcode encodes on the GPU", {
|
|
16422
|
+
tags: { deviceId: opts.deviceId },
|
|
16423
|
+
meta: {
|
|
16424
|
+
encoder,
|
|
16425
|
+
camStreamId: opts.camStreamId,
|
|
16426
|
+
device: choice.hwEncodeDevice?.renderNode
|
|
16427
|
+
}
|
|
16428
|
+
});
|
|
16376
16429
|
const decodeHwAccel = await this.resolveDecodeHwAccel(effectiveHwAccel);
|
|
16377
16430
|
const sourceUrl = this.resolveSourceUrl(opts.deviceId, opts.camStreamId);
|
|
16378
16431
|
const brokerId = `transcode:${opts.deviceId}:${(0, node_crypto.randomUUID)().slice(0, 8)}`;
|
|
@@ -16389,6 +16442,7 @@ var TranscodePipelineManager = class {
|
|
|
16389
16442
|
sourceUrl,
|
|
16390
16443
|
targetCodec: opts.targetCodec,
|
|
16391
16444
|
encoder,
|
|
16445
|
+
hwEncodeDevice: choice.hwEncodeDevice,
|
|
16392
16446
|
decodeHwAccel,
|
|
16393
16447
|
scale: opts.scale,
|
|
16394
16448
|
audio: opts.audio,
|
|
@@ -16519,16 +16573,59 @@ var TranscodePipelineManager = class {
|
|
|
16519
16573
|
* ranked above qsv (qsv is broken on the Intel stack) so an Intel node
|
|
16520
16574
|
* transcodes with `h264_vaapi`.
|
|
16521
16575
|
*/
|
|
16522
|
-
|
|
16576
|
+
/**
|
|
16577
|
+
* The node's hardware backend name (`vaapi`, `qsv`, `videotoolbox`, …), or
|
|
16578
|
+
* `null`. Resolved LOCALLY from `ctx.kernel.hwaccel` for the reason the
|
|
16579
|
+
* sibling method's docblock gives: the `platform-probe` singleton answers
|
|
16580
|
+
* with the HUB's hardware on a forked agent child.
|
|
16581
|
+
*/
|
|
16582
|
+
async resolveHardwareBackend() {
|
|
16523
16583
|
if (!this.hwaccel) return null;
|
|
16524
16584
|
try {
|
|
16525
|
-
return
|
|
16585
|
+
return rankDecodeHwAccels((await this.hwaccel.resolve(null)).preferred, [])[0] ?? null;
|
|
16526
16586
|
} catch (err) {
|
|
16527
|
-
this.logger.warn("local hardware
|
|
16587
|
+
this.logger.warn("local hardware resolve failed — the encoder falls back to software", { meta: { error: require_dist.errMsg(err) } });
|
|
16528
16588
|
return null;
|
|
16529
16589
|
}
|
|
16530
16590
|
}
|
|
16531
16591
|
/**
|
|
16592
|
+
* What THIS ffmpeg can encode with, asked of the binary once and latched.
|
|
16593
|
+
*
|
|
16594
|
+
* `null` means we could not ask, which is not the same as "it cannot" — see
|
|
16595
|
+
* {@link chooseVideoEncoder}. Keyed by binary PATH, so an operator who
|
|
16596
|
+
* repoints `binaryPath` gets a fresh answer without a restart.
|
|
16597
|
+
*/
|
|
16598
|
+
async resolveBinaryEncoders(binaryPath) {
|
|
16599
|
+
const caps = await this.resolveBinaryCapabilities(binaryPath);
|
|
16600
|
+
return caps === null || caps.encoders.size === 0 ? null : caps.encoders;
|
|
16601
|
+
}
|
|
16602
|
+
/**
|
|
16603
|
+
* What the ffmpeg at this path reports about itself, asked once and latched
|
|
16604
|
+
* per PATH — so an operator who repoints `binaryPath` gets a fresh answer
|
|
16605
|
+
* without a restart.
|
|
16606
|
+
*
|
|
16607
|
+
* A probe that answers nothing is `null`: "we could not ask" must never be
|
|
16608
|
+
* read as "it cannot", or one bad probe pins a capable node in software.
|
|
16609
|
+
*/
|
|
16610
|
+
async resolveBinaryCapabilities(binaryPath) {
|
|
16611
|
+
const cached = this.binaryCapabilities.get(binaryPath);
|
|
16612
|
+
if (cached !== void 0) return await cached;
|
|
16613
|
+
const probing = this.probeFfmpeg(binaryPath).then((caps) => {
|
|
16614
|
+
if (caps.encoders.size === 0 && caps.hwaccels.size === 0) {
|
|
16615
|
+
this.logger.warn("ffmpeg answered nothing about itself — hardware is attempted anyway, and its own failure is the answer", { meta: { ffmpeg: binaryPath } });
|
|
16616
|
+
return null;
|
|
16617
|
+
}
|
|
16618
|
+
this.logger.info("ffmpeg capabilities probed for transcoding", { meta: {
|
|
16619
|
+
ffmpeg: binaryPath,
|
|
16620
|
+
version: caps.version,
|
|
16621
|
+
hwaccels: [...caps.hwaccels].join(",") || "none"
|
|
16622
|
+
} });
|
|
16623
|
+
return caps;
|
|
16624
|
+
});
|
|
16625
|
+
this.binaryCapabilities.set(binaryPath, probing);
|
|
16626
|
+
return await probing;
|
|
16627
|
+
}
|
|
16628
|
+
/**
|
|
16532
16629
|
* Resolve the effective decode `hwaccel` setting for a (deviceId, camStreamId)
|
|
16533
16630
|
* source. Precedence: per-source-stream override > global `ffmpeg.hwAccel`.
|
|
16534
16631
|
* The global default itself is `'copy'` (software) out of the box.
|
|
@@ -16562,7 +16659,18 @@ var TranscodePipelineManager = class {
|
|
|
16562
16659
|
async resolveSpecificDecodeHwAccel(prefer) {
|
|
16563
16660
|
if (!this.hwaccel) return null;
|
|
16564
16661
|
try {
|
|
16565
|
-
|
|
16662
|
+
const backend = rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
|
|
16663
|
+
if (backend === null) return null;
|
|
16664
|
+
const caps = await this.resolveBinaryCapabilities(this.ffmpegConfig().binaryPath);
|
|
16665
|
+
if (caps !== null && !caps.hwaccels.has(backend)) {
|
|
16666
|
+
this.logger.warn("this ffmpeg has no such decode backend compiled in — decoding in software instead of dying on a device that cannot exist", { meta: {
|
|
16667
|
+
backend,
|
|
16668
|
+
ffmpeg: this.ffmpegConfig().binaryPath,
|
|
16669
|
+
hwaccels: [...caps.hwaccels].join(",") || "none"
|
|
16670
|
+
} });
|
|
16671
|
+
return null;
|
|
16672
|
+
}
|
|
16673
|
+
return backend;
|
|
16566
16674
|
} catch (err) {
|
|
16567
16675
|
this.logger.warn("local decode hw-accel resolve failed — software decode fallback", { meta: { error: require_dist.errMsg(err) } });
|
|
16568
16676
|
return null;
|
|
@@ -18128,6 +18236,20 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18128
18236
|
if (!parsed) return null;
|
|
18129
18237
|
return findProfileForStream(this.assignments.get(parsed.deviceId)?.map ?? {}, parsed.camStreamId);
|
|
18130
18238
|
}, this.remoteBrokerTiers));
|
|
18239
|
+
server.setStreamOriginResolver((brokerId) => {
|
|
18240
|
+
if (this.remoteBrokerAdapters.has(brokerId)) return {
|
|
18241
|
+
ingestNodeId: this.cachedIngestOwnerNodeId,
|
|
18242
|
+
ingestIsRemote: true
|
|
18243
|
+
};
|
|
18244
|
+
if (this.localNodeId === null) return {
|
|
18245
|
+
ingestNodeId: null,
|
|
18246
|
+
ingestIsRemote: null
|
|
18247
|
+
};
|
|
18248
|
+
return {
|
|
18249
|
+
ingestNodeId: this.localNodeId,
|
|
18250
|
+
ingestIsRemote: false
|
|
18251
|
+
};
|
|
18252
|
+
});
|
|
18131
18253
|
server.setEgressTranscodeProvider({
|
|
18132
18254
|
acquire: (request) => this.acquireEgressTranscode(request),
|
|
18133
18255
|
release: (input) => this.releaseEgressTranscode(input)
|
|
@@ -23442,6 +23564,18 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
23442
23564
|
transcodeToBaseline: this.transcodeToBaseline
|
|
23443
23565
|
});
|
|
23444
23566
|
}
|
|
23567
|
+
/**
|
|
23568
|
+
* The codec off the SOURCE. Read-only, for the stream-debug overlay: an
|
|
23569
|
+
* operator staring at a frozen picture needs to know whether this hub is
|
|
23570
|
+
* re-encoding for them, and `needsTranscode` alone does not say WHAT from.
|
|
23571
|
+
*/
|
|
23572
|
+
get debugSourceCodec() {
|
|
23573
|
+
return this.sourceCodec;
|
|
23574
|
+
}
|
|
23575
|
+
/** The codec this client's SDP answer settled on. See `debugSourceCodec`. */
|
|
23576
|
+
get debugNegotiatedCodec() {
|
|
23577
|
+
return this.negotiatedCodec;
|
|
23578
|
+
}
|
|
23445
23579
|
_firstKeyFrame;
|
|
23446
23580
|
/**
|
|
23447
23581
|
* Last seen SPS and PPS NALs. Many cameras send SPS/PPS only once
|
|
@@ -26246,6 +26380,85 @@ var AdaptiveController = class {
|
|
|
26246
26380
|
this.healthySince = now;
|
|
26247
26381
|
}
|
|
26248
26382
|
};
|
|
26383
|
+
//#endregion
|
|
26384
|
+
//#region src/stream-broker/webrtc/session-debug.ts
|
|
26385
|
+
/** `<deviceId>/<camStreamId>` → the camStreamId, or null when it is not one. */
|
|
26386
|
+
function camStreamIdOf(brokerId) {
|
|
26387
|
+
const slash = brokerId.indexOf("/");
|
|
26388
|
+
if (slash <= 0) return null;
|
|
26389
|
+
const tail = brokerId.slice(slash + 1);
|
|
26390
|
+
return tail.length > 0 ? tail : null;
|
|
26391
|
+
}
|
|
26392
|
+
/**
|
|
26393
|
+
* Native (the camera's own encode) vs derived (a rung this hub BUILT).
|
|
26394
|
+
* `unknown` when the brokerId does not parse — a guess here would tell an
|
|
26395
|
+
* operator the hub is transcoding when it is not, or the reverse.
|
|
26396
|
+
*/
|
|
26397
|
+
function sourceKindOf(camStreamId) {
|
|
26398
|
+
if (camStreamId === null) return "unknown";
|
|
26399
|
+
return camStreamId.startsWith("derived:") ? "derived" : "native";
|
|
26400
|
+
}
|
|
26401
|
+
var DEAD = {
|
|
26402
|
+
alive: false,
|
|
26403
|
+
brokerId: null,
|
|
26404
|
+
camStreamId: null,
|
|
26405
|
+
sourceKind: "unknown",
|
|
26406
|
+
adaptive: false,
|
|
26407
|
+
servedTier: null,
|
|
26408
|
+
ingestNodeId: null,
|
|
26409
|
+
ingestIsRemote: null,
|
|
26410
|
+
sourceCodec: null,
|
|
26411
|
+
negotiatedCodec: null,
|
|
26412
|
+
egressLeg: "unknown",
|
|
26413
|
+
brokerStatus: null,
|
|
26414
|
+
brokerCodec: null,
|
|
26415
|
+
brokerBitrateKbps: null,
|
|
26416
|
+
brokerInputFps: null,
|
|
26417
|
+
brokerUptimeMs: null,
|
|
26418
|
+
rtpPacketsSent: null,
|
|
26419
|
+
trackMode: null,
|
|
26420
|
+
lastDecision: null,
|
|
26421
|
+
tierFailures: [],
|
|
26422
|
+
upgradeCount: null
|
|
26423
|
+
};
|
|
26424
|
+
function projectSessionDebug(input) {
|
|
26425
|
+
const { session } = input;
|
|
26426
|
+
if (session === null) return {
|
|
26427
|
+
...DEAD,
|
|
26428
|
+
sessionId: input.sessionId,
|
|
26429
|
+
observedAt: input.observedAt
|
|
26430
|
+
};
|
|
26431
|
+
const camStreamId = camStreamIdOf(session.brokerId);
|
|
26432
|
+
const stats = input.brokerStats;
|
|
26433
|
+
return {
|
|
26434
|
+
sessionId: input.sessionId,
|
|
26435
|
+
alive: true,
|
|
26436
|
+
observedAt: input.observedAt,
|
|
26437
|
+
brokerId: session.brokerId,
|
|
26438
|
+
camStreamId,
|
|
26439
|
+
sourceKind: sourceKindOf(camStreamId),
|
|
26440
|
+
adaptive: session.adaptive,
|
|
26441
|
+
servedTier: input.servedTier,
|
|
26442
|
+
ingestNodeId: input.origin?.ingestNodeId ?? null,
|
|
26443
|
+
ingestIsRemote: input.origin?.ingestIsRemote ?? null,
|
|
26444
|
+
sourceCodec: session.sourceCodec,
|
|
26445
|
+
negotiatedCodec: session.negotiatedCodec,
|
|
26446
|
+
egressLeg: session.needsTranscode ? "transcode" : "copy",
|
|
26447
|
+
brokerStatus: stats?.status ?? null,
|
|
26448
|
+
brokerCodec: stats?.codec !== void 0 && stats.codec.length > 0 ? stats.codec : null,
|
|
26449
|
+
brokerBitrateKbps: stats?.bitrateKbps ?? null,
|
|
26450
|
+
brokerInputFps: stats?.inputFps ?? null,
|
|
26451
|
+
brokerUptimeMs: stats?.uptimeMs ?? null,
|
|
26452
|
+
rtpPacketsSent: session.rtpPacketsSent,
|
|
26453
|
+
trackMode: session.trackMode,
|
|
26454
|
+
lastDecision: input.ladder?.lastDecision ?? null,
|
|
26455
|
+
tierFailures: (input.ladder?.failures ?? []).map(([tier, failures]) => ({
|
|
26456
|
+
tier,
|
|
26457
|
+
failures
|
|
26458
|
+
})),
|
|
26459
|
+
upgradeCount: input.ladder?.upgradeCount ?? null
|
|
26460
|
+
};
|
|
26461
|
+
}
|
|
26249
26462
|
var PLAYBACK_PROFILE_FALLBACK_ORDER = [
|
|
26250
26463
|
"high",
|
|
26251
26464
|
"mid",
|
|
@@ -30605,6 +30818,29 @@ var BrokerWebrtcServer = class {
|
|
|
30605
30818
|
* close.
|
|
30606
30819
|
*/
|
|
30607
30820
|
adaptiveIntent = /* @__PURE__ */ new Map();
|
|
30821
|
+
/**
|
|
30822
|
+
* sessionId → the ladder's LAST move, and the numbers it rested on.
|
|
30823
|
+
*
|
|
30824
|
+
* Kept on the SERVER and not on the `SessionEntry` for the same reason
|
|
30825
|
+
* `adaptiveIntent` is: a tier switch is a close-and-recreate re-offer, so
|
|
30826
|
+
* the entry that made the decision is gone by the time anybody asks — which
|
|
30827
|
+
* is exactly the moment an operator opens the debug overlay. Cleared on
|
|
30828
|
+
* close, in `cleanupSession`.
|
|
30829
|
+
*
|
|
30830
|
+
* A snapshot, deliberately, not a re-read of the controller: the decision
|
|
30831
|
+
* was made on a window that has since moved on, and a recomputed "current"
|
|
30832
|
+
* loss ratio answers a different question than "what made it move".
|
|
30833
|
+
* Absent until the ladder actually moves — a ladder that has never moved
|
|
30834
|
+
* reports `null`, which is NOT the same fact as a `healthy` move with a
|
|
30835
|
+
* 0 loss ratio (D393).
|
|
30836
|
+
*/
|
|
30837
|
+
lastLadderDecision = /* @__PURE__ */ new Map();
|
|
30838
|
+
/**
|
|
30839
|
+
* brokerId → where its media is ingested. Injected by the manager, which
|
|
30840
|
+
* owns the ownership map; unset (or a null answer) means UNRESOLVED, and
|
|
30841
|
+
* the overlay says so rather than naming the local node.
|
|
30842
|
+
*/
|
|
30843
|
+
streamOriginResolver = null;
|
|
30608
30844
|
stopped = false;
|
|
30609
30845
|
constructor(options) {
|
|
30610
30846
|
this.logger = options.logger;
|
|
@@ -31555,6 +31791,7 @@ var BrokerWebrtcServer = class {
|
|
|
31555
31791
|
closes.push(entry.session.close().catch(() => {}));
|
|
31556
31792
|
}
|
|
31557
31793
|
await Promise.all(closes);
|
|
31794
|
+
this.lastLadderDecision.clear();
|
|
31558
31795
|
this.brokers.clear();
|
|
31559
31796
|
this.logger.info("WebRTC server stopped");
|
|
31560
31797
|
}
|
|
@@ -31570,6 +31807,92 @@ var BrokerWebrtcServer = class {
|
|
|
31570
31807
|
alive: entry !== void 0
|
|
31571
31808
|
};
|
|
31572
31809
|
}
|
|
31810
|
+
/** Injected by the manager: brokerId → where its media is ingested. */
|
|
31811
|
+
setStreamOriginResolver(resolver) {
|
|
31812
|
+
this.streamOriginResolver = resolver;
|
|
31813
|
+
}
|
|
31814
|
+
/**
|
|
31815
|
+
* The SERVER half of the stream-debug overlay for one session — the rung
|
|
31816
|
+
* `auto` actually resolved to, why the ladder last moved, where the media
|
|
31817
|
+
* is ingested, and whether the egress leg copies or re-encodes.
|
|
31818
|
+
*
|
|
31819
|
+
* Read-only and allocation-light: polled at ~1 Hz by ONE overlay while it
|
|
31820
|
+
* is open. Shaping lives in the pure `projectSessionDebug`; this method
|
|
31821
|
+
* only gathers.
|
|
31822
|
+
*/
|
|
31823
|
+
getSessionDebug(sessionId, now) {
|
|
31824
|
+
const entry = this.sessions.get(sessionId);
|
|
31825
|
+
if (entry === void 0) return projectSessionDebug({
|
|
31826
|
+
sessionId,
|
|
31827
|
+
observedAt: now,
|
|
31828
|
+
session: null,
|
|
31829
|
+
servedTier: null,
|
|
31830
|
+
origin: null,
|
|
31831
|
+
brokerStats: null,
|
|
31832
|
+
ladder: null
|
|
31833
|
+
});
|
|
31834
|
+
const controller = entry.adaptiveController;
|
|
31835
|
+
const decision = this.lastLadderDecision.get(sessionId) ?? null;
|
|
31836
|
+
const ladder = controller !== void 0 ? {
|
|
31837
|
+
lastDecision: decision,
|
|
31838
|
+
failures: [...controller.failureCounts],
|
|
31839
|
+
upgradeCount: controller.successfulUpgradeCount
|
|
31840
|
+
} : decision !== null ? {
|
|
31841
|
+
lastDecision: decision,
|
|
31842
|
+
failures: [],
|
|
31843
|
+
upgradeCount: 0
|
|
31844
|
+
} : null;
|
|
31845
|
+
let brokerStats = null;
|
|
31846
|
+
try {
|
|
31847
|
+
brokerStats = this.brokers.get(entry.brokerId)?.getStats() ?? null;
|
|
31848
|
+
} catch {
|
|
31849
|
+
brokerStats = null;
|
|
31850
|
+
}
|
|
31851
|
+
return projectSessionDebug({
|
|
31852
|
+
sessionId,
|
|
31853
|
+
observedAt: now,
|
|
31854
|
+
session: {
|
|
31855
|
+
brokerId: entry.brokerId,
|
|
31856
|
+
adaptive: controller !== void 0,
|
|
31857
|
+
sourceCodec: entry.session.debugSourceCodec,
|
|
31858
|
+
negotiatedCodec: entry.session.debugNegotiatedCodec,
|
|
31859
|
+
needsTranscode: entry.session.needsTranscode,
|
|
31860
|
+
rtpPacketsSent: entry.session.rtpPacketsSentTotal,
|
|
31861
|
+
trackMode: entry.timeline?.currentMode ?? "live"
|
|
31862
|
+
},
|
|
31863
|
+
servedTier: this.profileTierResolver?.(entry.brokerId) ?? null,
|
|
31864
|
+
origin: this.streamOriginResolver?.(entry.brokerId) ?? null,
|
|
31865
|
+
brokerStats,
|
|
31866
|
+
ladder
|
|
31867
|
+
});
|
|
31868
|
+
}
|
|
31869
|
+
/**
|
|
31870
|
+
* Bound the ladder-decision memory. Entries survive a session teardown on
|
|
31871
|
+
* purpose (the re-offer keeps the id), so nothing else would ever evict the
|
|
31872
|
+
* one left behind by a client that disconnected mid-switch. Insertion order
|
|
31873
|
+
* is eviction order.
|
|
31874
|
+
*/
|
|
31875
|
+
pruneLadderDecisions() {
|
|
31876
|
+
const MAX_REMEMBERED_DECISIONS = 64;
|
|
31877
|
+
while (this.lastLadderDecision.size > MAX_REMEMBERED_DECISIONS) {
|
|
31878
|
+
const oldest = this.lastLadderDecision.keys().next();
|
|
31879
|
+
if (oldest.done === true) break;
|
|
31880
|
+
this.lastLadderDecision.delete(oldest.value);
|
|
31881
|
+
}
|
|
31882
|
+
}
|
|
31883
|
+
/** Snapshot the ladder's move so the overlay can name it later. */
|
|
31884
|
+
recordLadderDecision(sessionId, decision, tier, forcedTranscodeDown) {
|
|
31885
|
+
this.lastLadderDecision.set(sessionId, {
|
|
31886
|
+
cause: decision.cause,
|
|
31887
|
+
tier,
|
|
31888
|
+
forcedTranscodeDown,
|
|
31889
|
+
lossRatio: decision.lossRatio,
|
|
31890
|
+
clientKbps: decision.stall.clientKbps,
|
|
31891
|
+
stalledForMs: decision.stall.stalledForMs,
|
|
31892
|
+
serverSending: decision.stall.serverSending,
|
|
31893
|
+
at: Date.now()
|
|
31894
|
+
});
|
|
31895
|
+
}
|
|
31573
31896
|
/**
|
|
31574
31897
|
* Wire an {@link AdaptiveController} to a freshly-created adaptive session.
|
|
31575
31898
|
* The controller is fed loss from two sources — RTCP Receiver Reports
|
|
@@ -31594,9 +31917,11 @@ var BrokerWebrtcServer = class {
|
|
|
31594
31917
|
priorUpgradeCount,
|
|
31595
31918
|
config: ADAPTIVE_CONFIG,
|
|
31596
31919
|
onSwitchTier: (tier, decision) => {
|
|
31920
|
+
this.recordLadderDecision(sessionId, decision, tier, false);
|
|
31597
31921
|
this.switchSessionTier(entry, deviceId, tier, sessionId, false, decision);
|
|
31598
31922
|
},
|
|
31599
31923
|
onReduceMtu: (decision) => {
|
|
31924
|
+
this.recordLadderDecision(sessionId, decision, null, false);
|
|
31600
31925
|
entry.session.setRepacketizerMtu(AdaptiveSession.ADAPTIVE_LOW_MTU);
|
|
31601
31926
|
this.logger.info("adaptive: reducing repacketizer MTU", {
|
|
31602
31927
|
tags: { deviceId },
|
|
@@ -31613,6 +31938,7 @@ var BrokerWebrtcServer = class {
|
|
|
31613
31938
|
});
|
|
31614
31939
|
},
|
|
31615
31940
|
onTranscodeDown: (decision) => {
|
|
31941
|
+
this.recordLadderDecision(sessionId, decision, "low", true);
|
|
31616
31942
|
this.switchSessionTier(entry, deviceId, "low", sessionId, true, decision);
|
|
31617
31943
|
}
|
|
31618
31944
|
});
|
|
@@ -31718,6 +32044,7 @@ var BrokerWebrtcServer = class {
|
|
|
31718
32044
|
}
|
|
31719
32045
|
if (!brokerStillServed && !this.stopped) this.brokerLinger.hold(entry.brokerId);
|
|
31720
32046
|
this.adaptiveIntent.delete(sessionId);
|
|
32047
|
+
this.pruneLadderDecisions();
|
|
31721
32048
|
if (entry.adaptiveTimer) clearInterval(entry.adaptiveTimer);
|
|
31722
32049
|
if (entry.attributionTimer) clearTimeout(entry.attributionTimer);
|
|
31723
32050
|
entry.attributionTagged = true;
|
|
@@ -32365,6 +32692,16 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
|
|
|
32365
32692
|
async getSessionState(input) {
|
|
32366
32693
|
return this.webrtcServer.getSessionRenegotiationState(input.sessionId);
|
|
32367
32694
|
}
|
|
32695
|
+
/**
|
|
32696
|
+
* The SERVER half of the stream-debug overlay. Thin facade — the gathering
|
|
32697
|
+
* lives in the WebRTC server and the shaping in the pure
|
|
32698
|
+
* `projectSessionDebug`. `deviceId` is not used to look the session up
|
|
32699
|
+
* (session ids are already unique); it is part of the device-scoped cap's
|
|
32700
|
+
* signature and identifies the camera for the route.
|
|
32701
|
+
*/
|
|
32702
|
+
async getSessionDebug(input) {
|
|
32703
|
+
return this.webrtcServer.getSessionDebug(input.sessionId, Date.now());
|
|
32704
|
+
}
|
|
32368
32705
|
};
|
|
32369
32706
|
//#endregion
|
|
32370
32707
|
//#region src/stream-broker/event-loop-stall-monitor.ts
|