@camstack/addon-pipeline 1.2.268 → 1.2.270
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-DZlxPoTV.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-DFerKhvZ.mjs} +2 -2
- package/dist/stream-broker/index.js +481 -63
- package/dist/stream-broker/index.mjs +481 -63
- 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"
|
|
@@ -13064,6 +13088,15 @@ var StreamBroker = class StreamBroker {
|
|
|
13064
13088
|
* H.264 video is 30fps → 80pps minimum on a working stream) while a
|
|
13065
13089
|
* real stall is detected before Chrome's ~15s ICE-disconnect timer
|
|
13066
13090
|
* has any chance to fire. */
|
|
13091
|
+
/** How many times a resume re-asks the registry before it stops asking. */
|
|
13092
|
+
static RESUME_RETRY_MAX = 5;
|
|
13093
|
+
/** Spacing of those asks — long enough for a catalog reconcile to land. */
|
|
13094
|
+
static RESUME_RETRY_DELAY_MS = 3e3;
|
|
13095
|
+
resumeRetryTimer;
|
|
13096
|
+
resumeRetryAttempts = 0;
|
|
13097
|
+
resumeRetryExhausted = false;
|
|
13098
|
+
/** One line per suspension when packets arrive with no watchdog armed. */
|
|
13099
|
+
suspendedActivityNoted = false;
|
|
13067
13100
|
static RTP_ACTIVITY_TIMEOUT_MS = 8e3;
|
|
13068
13101
|
/**
|
|
13069
13102
|
* How often the single sweep timer checks {@link lastRtpActivityAt}.
|
|
@@ -13574,6 +13607,54 @@ var StreamBroker = class StreamBroker {
|
|
|
13574
13607
|
this.scheduleReconnect();
|
|
13575
13608
|
}
|
|
13576
13609
|
/**
|
|
13610
|
+
* Ask again for a source the registry did not have yet — bounded.
|
|
13611
|
+
*
|
|
13612
|
+
* Demand exists and nothing can be dialled, so waiting for the NEXT demand
|
|
13613
|
+
* edge is waiting for something that may never come: the consumer is already
|
|
13614
|
+
* attached. The retry is what turns "nothing happened" back into either a
|
|
13615
|
+
* stream or a stated failure, and the bound is what stops a camera that
|
|
13616
|
+
* genuinely publishes nothing from spinning for the life of the process.
|
|
13617
|
+
*/
|
|
13618
|
+
scheduleResumeRetry() {
|
|
13619
|
+
if (this.resumeRetryTimer !== void 0) return;
|
|
13620
|
+
if (this.resumeRetryAttempts >= StreamBroker.RESUME_RETRY_MAX) {
|
|
13621
|
+
if (!this.resumeRetryExhausted) {
|
|
13622
|
+
this.resumeRetryExhausted = true;
|
|
13623
|
+
this.logger?.warn("demand is waiting and this broker still has no source — giving up until something changes", { meta: {
|
|
13624
|
+
brokerId: this.deviceId,
|
|
13625
|
+
attempts: this.resumeRetryAttempts,
|
|
13626
|
+
...this.numericDeviceId === null ? {} : { deviceId: this.numericDeviceId }
|
|
13627
|
+
} });
|
|
13628
|
+
}
|
|
13629
|
+
return;
|
|
13630
|
+
}
|
|
13631
|
+
this.resumeRetryAttempts += 1;
|
|
13632
|
+
this.logger?.warn("demand detected but no source could be resolved — retrying", {
|
|
13633
|
+
...this.numericDeviceId === null ? {} : { tags: { deviceId: this.numericDeviceId } },
|
|
13634
|
+
meta: {
|
|
13635
|
+
brokerId: this.deviceId,
|
|
13636
|
+
attempt: this.resumeRetryAttempts,
|
|
13637
|
+
ofMax: StreamBroker.RESUME_RETRY_MAX
|
|
13638
|
+
}
|
|
13639
|
+
});
|
|
13640
|
+
this.resumeRetryTimer = setTimeout(() => {
|
|
13641
|
+
this.resumeRetryTimer = void 0;
|
|
13642
|
+
if (this.manualStop || this.stopping || !this._suspended) return;
|
|
13643
|
+
if (!this.hasLiveIntent()) return;
|
|
13644
|
+
this.checkDemand();
|
|
13645
|
+
}, StreamBroker.RESUME_RETRY_DELAY_MS);
|
|
13646
|
+
this.resumeRetryTimer.unref?.();
|
|
13647
|
+
}
|
|
13648
|
+
/** A resume that SUCCEEDED clears the episode, counter included. */
|
|
13649
|
+
cancelResumeRetry() {
|
|
13650
|
+
if (this.resumeRetryTimer !== void 0) {
|
|
13651
|
+
clearTimeout(this.resumeRetryTimer);
|
|
13652
|
+
this.resumeRetryTimer = void 0;
|
|
13653
|
+
}
|
|
13654
|
+
this.resumeRetryAttempts = 0;
|
|
13655
|
+
this.resumeRetryExhausted = false;
|
|
13656
|
+
}
|
|
13657
|
+
/**
|
|
13577
13658
|
* Resolve the source to use for the next dial attempt. Prefers the
|
|
13578
13659
|
* manager-supplied resolver — re-derives from the cam-stream
|
|
13579
13660
|
* registry, so a re-publish that updated the URL is honoured on the
|
|
@@ -14082,6 +14163,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14082
14163
|
if (this._suspended) {
|
|
14083
14164
|
const fresh = this.resolveCurrentSource();
|
|
14084
14165
|
if (fresh) {
|
|
14166
|
+
this.cancelResumeRetry();
|
|
14085
14167
|
this._suspended = false;
|
|
14086
14168
|
this.logger?.info("demand detected — resuming stream");
|
|
14087
14169
|
this.start(fresh).catch((err) => {
|
|
@@ -14089,7 +14171,7 @@ var StreamBroker = class StreamBroker {
|
|
|
14089
14171
|
this._suspended = true;
|
|
14090
14172
|
this._status = "idle";
|
|
14091
14173
|
});
|
|
14092
|
-
}
|
|
14174
|
+
} else this.scheduleResumeRetry();
|
|
14093
14175
|
}
|
|
14094
14176
|
} else if (!this._suspended) {
|
|
14095
14177
|
if (!this.suspendTimer) this.suspendTimer = setTimeout(() => {
|
|
@@ -15789,7 +15871,20 @@ var StreamBroker = class StreamBroker {
|
|
|
15789
15871
|
* operations on the highest-frequency path in the busiest process.
|
|
15790
15872
|
*/
|
|
15791
15873
|
kickRtpActivityWatchdog() {
|
|
15792
|
-
if (this.manualStop || this.stopping || this._suspended)
|
|
15874
|
+
if (this.manualStop || this.stopping || this._suspended) {
|
|
15875
|
+
if (this._suspended && !this.suspendedActivityNoted) {
|
|
15876
|
+
this.suspendedActivityNoted = true;
|
|
15877
|
+
this.logger?.warn("source RTP is arriving while this broker is suspended — the stall watchdog is NOT armed for it", {
|
|
15878
|
+
...this.numericDeviceId === null ? {} : { tags: { deviceId: this.numericDeviceId } },
|
|
15879
|
+
meta: {
|
|
15880
|
+
brokerId: this.deviceId,
|
|
15881
|
+
status: this._status
|
|
15882
|
+
}
|
|
15883
|
+
});
|
|
15884
|
+
}
|
|
15885
|
+
return;
|
|
15886
|
+
}
|
|
15887
|
+
this.suspendedActivityNoted = false;
|
|
15793
15888
|
this.lastRtpActivityAt = Date.now();
|
|
15794
15889
|
if (this.rtpActivitySweep !== void 0) return;
|
|
15795
15890
|
this.rtpActivitySweep = setInterval(() => {
|
|
@@ -15829,7 +15924,17 @@ var StreamBroker = class StreamBroker {
|
|
|
15829
15924
|
*/
|
|
15830
15925
|
onRtpActivityWatchdogExpired() {
|
|
15831
15926
|
this.rtpActivityWatchdogFiredCount++;
|
|
15832
|
-
if (this.manualStop || this.stopping || this._suspended)
|
|
15927
|
+
if (this.manualStop || this.stopping || this._suspended) {
|
|
15928
|
+
if (this._suspended) this.logger?.warn("source RTP went silent on a suspended broker — the watchdog is disarmed and nothing will reconnect it", {
|
|
15929
|
+
...this.numericDeviceId === null ? {} : { tags: { deviceId: this.numericDeviceId } },
|
|
15930
|
+
meta: {
|
|
15931
|
+
brokerId: this.deviceId,
|
|
15932
|
+
timeoutMs: StreamBroker.RTP_ACTIVITY_TIMEOUT_MS,
|
|
15933
|
+
firedCount: this.rtpActivityWatchdogFiredCount
|
|
15934
|
+
}
|
|
15935
|
+
});
|
|
15936
|
+
return;
|
|
15937
|
+
}
|
|
15833
15938
|
const meta = {
|
|
15834
15939
|
timeoutMs: StreamBroker.RTP_ACTIVITY_TIMEOUT_MS,
|
|
15835
15940
|
...this.sourceMetaForLog()
|
|
@@ -16280,6 +16385,9 @@ var TranscodePipelineManager = class {
|
|
|
16280
16385
|
/** In-flight transcode starts, keyed by pipeline key — single-flight the
|
|
16281
16386
|
* async egress start so concurrent acquires share one egress. */
|
|
16282
16387
|
pending = /* @__PURE__ */ new Map();
|
|
16388
|
+
/** What the ffmpeg at a path can do, asked once. `null` = could not ask. */
|
|
16389
|
+
binaryCapabilities = /* @__PURE__ */ new Map();
|
|
16390
|
+
probeFfmpeg;
|
|
16283
16391
|
logger;
|
|
16284
16392
|
hwaccel;
|
|
16285
16393
|
cameraStreamLookup;
|
|
@@ -16299,6 +16407,7 @@ var TranscodePipelineManager = class {
|
|
|
16299
16407
|
this.resolveProfileSource = opts.resolveProfileSource ?? null;
|
|
16300
16408
|
this.releaseGraceMs = opts.releaseGraceMs ?? 5e3;
|
|
16301
16409
|
this.ffmpegConfig = opts.ffmpegConfig ?? (() => DEFAULT_FFMPEG_CONFIG);
|
|
16410
|
+
this.probeFfmpeg = opts.probeFfmpeg ?? require_node.probeFfmpegBinary;
|
|
16302
16411
|
this.resolveStreamHwAccel = opts.resolveStreamHwAccel ?? null;
|
|
16303
16412
|
this.spawnFn = opts.spawnFn ?? node_child_process.spawn;
|
|
16304
16413
|
this.acquireChannel = opts.acquireChannel ?? null;
|
|
@@ -16371,8 +16480,33 @@ var TranscodePipelineManager = class {
|
|
|
16371
16480
|
if (!this.acquireChannel) throw new Error("getStreamWithCodec: transcode egress not wired (acquireChannel missing)");
|
|
16372
16481
|
const cfg = this.ffmpegConfig();
|
|
16373
16482
|
const effectiveHwAccel = this.effectiveHwAccelSetting(opts.deviceId, opts.camStreamId, cfg.hwAccel);
|
|
16374
|
-
const
|
|
16375
|
-
|
|
16483
|
+
const choice = chooseVideoEncoder({
|
|
16484
|
+
target: opts.targetCodec,
|
|
16485
|
+
backend: await this.resolveHardwareBackend(),
|
|
16486
|
+
binaryEncoders: await this.resolveBinaryEncoders(cfg.binaryPath),
|
|
16487
|
+
hwAccelSetting: effectiveHwAccel,
|
|
16488
|
+
renderNode: DEFAULT_VAAPI_RENDER_DEVICE
|
|
16489
|
+
});
|
|
16490
|
+
const encoder = choice.encoder;
|
|
16491
|
+
if (choice.softwareReason !== null) this.logger.error("transcode is running on the CPU — a software encode is work the hardware was supposed to do", {
|
|
16492
|
+
tags: { deviceId: opts.deviceId },
|
|
16493
|
+
meta: {
|
|
16494
|
+
reason: choice.softwareReason,
|
|
16495
|
+
encoder,
|
|
16496
|
+
camStreamId: opts.camStreamId,
|
|
16497
|
+
targetCodec: opts.targetCodec,
|
|
16498
|
+
hwAccelSetting: effectiveHwAccel,
|
|
16499
|
+
ffmpeg: cfg.binaryPath
|
|
16500
|
+
}
|
|
16501
|
+
});
|
|
16502
|
+
else this.logger.info("transcode encodes on the GPU", {
|
|
16503
|
+
tags: { deviceId: opts.deviceId },
|
|
16504
|
+
meta: {
|
|
16505
|
+
encoder,
|
|
16506
|
+
camStreamId: opts.camStreamId,
|
|
16507
|
+
device: choice.hwEncodeDevice?.renderNode
|
|
16508
|
+
}
|
|
16509
|
+
});
|
|
16376
16510
|
const decodeHwAccel = await this.resolveDecodeHwAccel(effectiveHwAccel);
|
|
16377
16511
|
const sourceUrl = this.resolveSourceUrl(opts.deviceId, opts.camStreamId);
|
|
16378
16512
|
const brokerId = `transcode:${opts.deviceId}:${(0, node_crypto.randomUUID)().slice(0, 8)}`;
|
|
@@ -16389,6 +16523,7 @@ var TranscodePipelineManager = class {
|
|
|
16389
16523
|
sourceUrl,
|
|
16390
16524
|
targetCodec: opts.targetCodec,
|
|
16391
16525
|
encoder,
|
|
16526
|
+
hwEncodeDevice: choice.hwEncodeDevice,
|
|
16392
16527
|
decodeHwAccel,
|
|
16393
16528
|
scale: opts.scale,
|
|
16394
16529
|
audio: opts.audio,
|
|
@@ -16519,16 +16654,59 @@ var TranscodePipelineManager = class {
|
|
|
16519
16654
|
* ranked above qsv (qsv is broken on the Intel stack) so an Intel node
|
|
16520
16655
|
* transcodes with `h264_vaapi`.
|
|
16521
16656
|
*/
|
|
16522
|
-
|
|
16657
|
+
/**
|
|
16658
|
+
* The node's hardware backend name (`vaapi`, `qsv`, `videotoolbox`, …), or
|
|
16659
|
+
* `null`. Resolved LOCALLY from `ctx.kernel.hwaccel` for the reason the
|
|
16660
|
+
* sibling method's docblock gives: the `platform-probe` singleton answers
|
|
16661
|
+
* with the HUB's hardware on a forked agent child.
|
|
16662
|
+
*/
|
|
16663
|
+
async resolveHardwareBackend() {
|
|
16523
16664
|
if (!this.hwaccel) return null;
|
|
16524
16665
|
try {
|
|
16525
|
-
return
|
|
16666
|
+
return rankDecodeHwAccels((await this.hwaccel.resolve(null)).preferred, [])[0] ?? null;
|
|
16526
16667
|
} catch (err) {
|
|
16527
|
-
this.logger.warn("local hardware
|
|
16668
|
+
this.logger.warn("local hardware resolve failed — the encoder falls back to software", { meta: { error: require_dist.errMsg(err) } });
|
|
16528
16669
|
return null;
|
|
16529
16670
|
}
|
|
16530
16671
|
}
|
|
16531
16672
|
/**
|
|
16673
|
+
* What THIS ffmpeg can encode with, asked of the binary once and latched.
|
|
16674
|
+
*
|
|
16675
|
+
* `null` means we could not ask, which is not the same as "it cannot" — see
|
|
16676
|
+
* {@link chooseVideoEncoder}. Keyed by binary PATH, so an operator who
|
|
16677
|
+
* repoints `binaryPath` gets a fresh answer without a restart.
|
|
16678
|
+
*/
|
|
16679
|
+
async resolveBinaryEncoders(binaryPath) {
|
|
16680
|
+
const caps = await this.resolveBinaryCapabilities(binaryPath);
|
|
16681
|
+
return caps === null || caps.encoders.size === 0 ? null : caps.encoders;
|
|
16682
|
+
}
|
|
16683
|
+
/**
|
|
16684
|
+
* What the ffmpeg at this path reports about itself, asked once and latched
|
|
16685
|
+
* per PATH — so an operator who repoints `binaryPath` gets a fresh answer
|
|
16686
|
+
* without a restart.
|
|
16687
|
+
*
|
|
16688
|
+
* A probe that answers nothing is `null`: "we could not ask" must never be
|
|
16689
|
+
* read as "it cannot", or one bad probe pins a capable node in software.
|
|
16690
|
+
*/
|
|
16691
|
+
async resolveBinaryCapabilities(binaryPath) {
|
|
16692
|
+
const cached = this.binaryCapabilities.get(binaryPath);
|
|
16693
|
+
if (cached !== void 0) return await cached;
|
|
16694
|
+
const probing = this.probeFfmpeg(binaryPath).then((caps) => {
|
|
16695
|
+
if (caps.encoders.size === 0 && caps.hwaccels.size === 0) {
|
|
16696
|
+
this.logger.warn("ffmpeg answered nothing about itself — hardware is attempted anyway, and its own failure is the answer", { meta: { ffmpeg: binaryPath } });
|
|
16697
|
+
return null;
|
|
16698
|
+
}
|
|
16699
|
+
this.logger.info("ffmpeg capabilities probed for transcoding", { meta: {
|
|
16700
|
+
ffmpeg: binaryPath,
|
|
16701
|
+
version: caps.version,
|
|
16702
|
+
hwaccels: [...caps.hwaccels].join(",") || "none"
|
|
16703
|
+
} });
|
|
16704
|
+
return caps;
|
|
16705
|
+
});
|
|
16706
|
+
this.binaryCapabilities.set(binaryPath, probing);
|
|
16707
|
+
return await probing;
|
|
16708
|
+
}
|
|
16709
|
+
/**
|
|
16532
16710
|
* Resolve the effective decode `hwaccel` setting for a (deviceId, camStreamId)
|
|
16533
16711
|
* source. Precedence: per-source-stream override > global `ffmpeg.hwAccel`.
|
|
16534
16712
|
* The global default itself is `'copy'` (software) out of the box.
|
|
@@ -16562,7 +16740,18 @@ var TranscodePipelineManager = class {
|
|
|
16562
16740
|
async resolveSpecificDecodeHwAccel(prefer) {
|
|
16563
16741
|
if (!this.hwaccel) return null;
|
|
16564
16742
|
try {
|
|
16565
|
-
|
|
16743
|
+
const backend = rankDecodeHwAccels((await this.hwaccel.resolve(prefer)).preferred, [])[0] ?? null;
|
|
16744
|
+
if (backend === null) return null;
|
|
16745
|
+
const caps = await this.resolveBinaryCapabilities(this.ffmpegConfig().binaryPath);
|
|
16746
|
+
if (caps !== null && !caps.hwaccels.has(backend)) {
|
|
16747
|
+
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: {
|
|
16748
|
+
backend,
|
|
16749
|
+
ffmpeg: this.ffmpegConfig().binaryPath,
|
|
16750
|
+
hwaccels: [...caps.hwaccels].join(",") || "none"
|
|
16751
|
+
} });
|
|
16752
|
+
return null;
|
|
16753
|
+
}
|
|
16754
|
+
return backend;
|
|
16566
16755
|
} catch (err) {
|
|
16567
16756
|
this.logger.warn("local decode hw-accel resolve failed — software decode fallback", { meta: { error: require_dist.errMsg(err) } });
|
|
16568
16757
|
return null;
|
|
@@ -18128,6 +18317,20 @@ var StreamBrokerManager = class StreamBrokerManager {
|
|
|
18128
18317
|
if (!parsed) return null;
|
|
18129
18318
|
return findProfileForStream(this.assignments.get(parsed.deviceId)?.map ?? {}, parsed.camStreamId);
|
|
18130
18319
|
}, this.remoteBrokerTiers));
|
|
18320
|
+
server.setStreamOriginResolver((brokerId) => {
|
|
18321
|
+
if (this.remoteBrokerAdapters.has(brokerId)) return {
|
|
18322
|
+
ingestNodeId: this.cachedIngestOwnerNodeId,
|
|
18323
|
+
ingestIsRemote: true
|
|
18324
|
+
};
|
|
18325
|
+
if (this.localNodeId === null) return {
|
|
18326
|
+
ingestNodeId: null,
|
|
18327
|
+
ingestIsRemote: null
|
|
18328
|
+
};
|
|
18329
|
+
return {
|
|
18330
|
+
ingestNodeId: this.localNodeId,
|
|
18331
|
+
ingestIsRemote: false
|
|
18332
|
+
};
|
|
18333
|
+
});
|
|
18131
18334
|
server.setEgressTranscodeProvider({
|
|
18132
18335
|
acquire: (request) => this.acquireEgressTranscode(request),
|
|
18133
18336
|
release: (input) => this.releaseEgressTranscode(input)
|
|
@@ -23442,6 +23645,18 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
23442
23645
|
transcodeToBaseline: this.transcodeToBaseline
|
|
23443
23646
|
});
|
|
23444
23647
|
}
|
|
23648
|
+
/**
|
|
23649
|
+
* The codec off the SOURCE. Read-only, for the stream-debug overlay: an
|
|
23650
|
+
* operator staring at a frozen picture needs to know whether this hub is
|
|
23651
|
+
* re-encoding for them, and `needsTranscode` alone does not say WHAT from.
|
|
23652
|
+
*/
|
|
23653
|
+
get debugSourceCodec() {
|
|
23654
|
+
return this.sourceCodec;
|
|
23655
|
+
}
|
|
23656
|
+
/** The codec this client's SDP answer settled on. See `debugSourceCodec`. */
|
|
23657
|
+
get debugNegotiatedCodec() {
|
|
23658
|
+
return this.negotiatedCodec;
|
|
23659
|
+
}
|
|
23445
23660
|
_firstKeyFrame;
|
|
23446
23661
|
/**
|
|
23447
23662
|
* Last seen SPS and PPS NALs. Many cameras send SPS/PPS only once
|
|
@@ -26246,6 +26461,85 @@ var AdaptiveController = class {
|
|
|
26246
26461
|
this.healthySince = now;
|
|
26247
26462
|
}
|
|
26248
26463
|
};
|
|
26464
|
+
//#endregion
|
|
26465
|
+
//#region src/stream-broker/webrtc/session-debug.ts
|
|
26466
|
+
/** `<deviceId>/<camStreamId>` → the camStreamId, or null when it is not one. */
|
|
26467
|
+
function camStreamIdOf(brokerId) {
|
|
26468
|
+
const slash = brokerId.indexOf("/");
|
|
26469
|
+
if (slash <= 0) return null;
|
|
26470
|
+
const tail = brokerId.slice(slash + 1);
|
|
26471
|
+
return tail.length > 0 ? tail : null;
|
|
26472
|
+
}
|
|
26473
|
+
/**
|
|
26474
|
+
* Native (the camera's own encode) vs derived (a rung this hub BUILT).
|
|
26475
|
+
* `unknown` when the brokerId does not parse — a guess here would tell an
|
|
26476
|
+
* operator the hub is transcoding when it is not, or the reverse.
|
|
26477
|
+
*/
|
|
26478
|
+
function sourceKindOf(camStreamId) {
|
|
26479
|
+
if (camStreamId === null) return "unknown";
|
|
26480
|
+
return camStreamId.startsWith("derived:") ? "derived" : "native";
|
|
26481
|
+
}
|
|
26482
|
+
var DEAD = {
|
|
26483
|
+
alive: false,
|
|
26484
|
+
brokerId: null,
|
|
26485
|
+
camStreamId: null,
|
|
26486
|
+
sourceKind: "unknown",
|
|
26487
|
+
adaptive: false,
|
|
26488
|
+
servedTier: null,
|
|
26489
|
+
ingestNodeId: null,
|
|
26490
|
+
ingestIsRemote: null,
|
|
26491
|
+
sourceCodec: null,
|
|
26492
|
+
negotiatedCodec: null,
|
|
26493
|
+
egressLeg: "unknown",
|
|
26494
|
+
brokerStatus: null,
|
|
26495
|
+
brokerCodec: null,
|
|
26496
|
+
brokerBitrateKbps: null,
|
|
26497
|
+
brokerInputFps: null,
|
|
26498
|
+
brokerUptimeMs: null,
|
|
26499
|
+
rtpPacketsSent: null,
|
|
26500
|
+
trackMode: null,
|
|
26501
|
+
lastDecision: null,
|
|
26502
|
+
tierFailures: [],
|
|
26503
|
+
upgradeCount: null
|
|
26504
|
+
};
|
|
26505
|
+
function projectSessionDebug(input) {
|
|
26506
|
+
const { session } = input;
|
|
26507
|
+
if (session === null) return {
|
|
26508
|
+
...DEAD,
|
|
26509
|
+
sessionId: input.sessionId,
|
|
26510
|
+
observedAt: input.observedAt
|
|
26511
|
+
};
|
|
26512
|
+
const camStreamId = camStreamIdOf(session.brokerId);
|
|
26513
|
+
const stats = input.brokerStats;
|
|
26514
|
+
return {
|
|
26515
|
+
sessionId: input.sessionId,
|
|
26516
|
+
alive: true,
|
|
26517
|
+
observedAt: input.observedAt,
|
|
26518
|
+
brokerId: session.brokerId,
|
|
26519
|
+
camStreamId,
|
|
26520
|
+
sourceKind: sourceKindOf(camStreamId),
|
|
26521
|
+
adaptive: session.adaptive,
|
|
26522
|
+
servedTier: input.servedTier,
|
|
26523
|
+
ingestNodeId: input.origin?.ingestNodeId ?? null,
|
|
26524
|
+
ingestIsRemote: input.origin?.ingestIsRemote ?? null,
|
|
26525
|
+
sourceCodec: session.sourceCodec,
|
|
26526
|
+
negotiatedCodec: session.negotiatedCodec,
|
|
26527
|
+
egressLeg: session.needsTranscode ? "transcode" : "copy",
|
|
26528
|
+
brokerStatus: stats?.status ?? null,
|
|
26529
|
+
brokerCodec: stats?.codec !== void 0 && stats.codec.length > 0 ? stats.codec : null,
|
|
26530
|
+
brokerBitrateKbps: stats?.bitrateKbps ?? null,
|
|
26531
|
+
brokerInputFps: stats?.inputFps ?? null,
|
|
26532
|
+
brokerUptimeMs: stats?.uptimeMs ?? null,
|
|
26533
|
+
rtpPacketsSent: session.rtpPacketsSent,
|
|
26534
|
+
trackMode: session.trackMode,
|
|
26535
|
+
lastDecision: input.ladder?.lastDecision ?? null,
|
|
26536
|
+
tierFailures: (input.ladder?.failures ?? []).map(([tier, failures]) => ({
|
|
26537
|
+
tier,
|
|
26538
|
+
failures
|
|
26539
|
+
})),
|
|
26540
|
+
upgradeCount: input.ladder?.upgradeCount ?? null
|
|
26541
|
+
};
|
|
26542
|
+
}
|
|
26249
26543
|
var PLAYBACK_PROFILE_FALLBACK_ORDER = [
|
|
26250
26544
|
"high",
|
|
26251
26545
|
"mid",
|
|
@@ -30605,6 +30899,29 @@ var BrokerWebrtcServer = class {
|
|
|
30605
30899
|
* close.
|
|
30606
30900
|
*/
|
|
30607
30901
|
adaptiveIntent = /* @__PURE__ */ new Map();
|
|
30902
|
+
/**
|
|
30903
|
+
* sessionId → the ladder's LAST move, and the numbers it rested on.
|
|
30904
|
+
*
|
|
30905
|
+
* Kept on the SERVER and not on the `SessionEntry` for the same reason
|
|
30906
|
+
* `adaptiveIntent` is: a tier switch is a close-and-recreate re-offer, so
|
|
30907
|
+
* the entry that made the decision is gone by the time anybody asks — which
|
|
30908
|
+
* is exactly the moment an operator opens the debug overlay. Cleared on
|
|
30909
|
+
* close, in `cleanupSession`.
|
|
30910
|
+
*
|
|
30911
|
+
* A snapshot, deliberately, not a re-read of the controller: the decision
|
|
30912
|
+
* was made on a window that has since moved on, and a recomputed "current"
|
|
30913
|
+
* loss ratio answers a different question than "what made it move".
|
|
30914
|
+
* Absent until the ladder actually moves — a ladder that has never moved
|
|
30915
|
+
* reports `null`, which is NOT the same fact as a `healthy` move with a
|
|
30916
|
+
* 0 loss ratio (D393).
|
|
30917
|
+
*/
|
|
30918
|
+
lastLadderDecision = /* @__PURE__ */ new Map();
|
|
30919
|
+
/**
|
|
30920
|
+
* brokerId → where its media is ingested. Injected by the manager, which
|
|
30921
|
+
* owns the ownership map; unset (or a null answer) means UNRESOLVED, and
|
|
30922
|
+
* the overlay says so rather than naming the local node.
|
|
30923
|
+
*/
|
|
30924
|
+
streamOriginResolver = null;
|
|
30608
30925
|
stopped = false;
|
|
30609
30926
|
constructor(options) {
|
|
30610
30927
|
this.logger = options.logger;
|
|
@@ -31555,6 +31872,7 @@ var BrokerWebrtcServer = class {
|
|
|
31555
31872
|
closes.push(entry.session.close().catch(() => {}));
|
|
31556
31873
|
}
|
|
31557
31874
|
await Promise.all(closes);
|
|
31875
|
+
this.lastLadderDecision.clear();
|
|
31558
31876
|
this.brokers.clear();
|
|
31559
31877
|
this.logger.info("WebRTC server stopped");
|
|
31560
31878
|
}
|
|
@@ -31570,6 +31888,92 @@ var BrokerWebrtcServer = class {
|
|
|
31570
31888
|
alive: entry !== void 0
|
|
31571
31889
|
};
|
|
31572
31890
|
}
|
|
31891
|
+
/** Injected by the manager: brokerId → where its media is ingested. */
|
|
31892
|
+
setStreamOriginResolver(resolver) {
|
|
31893
|
+
this.streamOriginResolver = resolver;
|
|
31894
|
+
}
|
|
31895
|
+
/**
|
|
31896
|
+
* The SERVER half of the stream-debug overlay for one session — the rung
|
|
31897
|
+
* `auto` actually resolved to, why the ladder last moved, where the media
|
|
31898
|
+
* is ingested, and whether the egress leg copies or re-encodes.
|
|
31899
|
+
*
|
|
31900
|
+
* Read-only and allocation-light: polled at ~1 Hz by ONE overlay while it
|
|
31901
|
+
* is open. Shaping lives in the pure `projectSessionDebug`; this method
|
|
31902
|
+
* only gathers.
|
|
31903
|
+
*/
|
|
31904
|
+
getSessionDebug(sessionId, now) {
|
|
31905
|
+
const entry = this.sessions.get(sessionId);
|
|
31906
|
+
if (entry === void 0) return projectSessionDebug({
|
|
31907
|
+
sessionId,
|
|
31908
|
+
observedAt: now,
|
|
31909
|
+
session: null,
|
|
31910
|
+
servedTier: null,
|
|
31911
|
+
origin: null,
|
|
31912
|
+
brokerStats: null,
|
|
31913
|
+
ladder: null
|
|
31914
|
+
});
|
|
31915
|
+
const controller = entry.adaptiveController;
|
|
31916
|
+
const decision = this.lastLadderDecision.get(sessionId) ?? null;
|
|
31917
|
+
const ladder = controller !== void 0 ? {
|
|
31918
|
+
lastDecision: decision,
|
|
31919
|
+
failures: [...controller.failureCounts],
|
|
31920
|
+
upgradeCount: controller.successfulUpgradeCount
|
|
31921
|
+
} : decision !== null ? {
|
|
31922
|
+
lastDecision: decision,
|
|
31923
|
+
failures: [],
|
|
31924
|
+
upgradeCount: 0
|
|
31925
|
+
} : null;
|
|
31926
|
+
let brokerStats = null;
|
|
31927
|
+
try {
|
|
31928
|
+
brokerStats = this.brokers.get(entry.brokerId)?.getStats() ?? null;
|
|
31929
|
+
} catch {
|
|
31930
|
+
brokerStats = null;
|
|
31931
|
+
}
|
|
31932
|
+
return projectSessionDebug({
|
|
31933
|
+
sessionId,
|
|
31934
|
+
observedAt: now,
|
|
31935
|
+
session: {
|
|
31936
|
+
brokerId: entry.brokerId,
|
|
31937
|
+
adaptive: controller !== void 0,
|
|
31938
|
+
sourceCodec: entry.session.debugSourceCodec,
|
|
31939
|
+
negotiatedCodec: entry.session.debugNegotiatedCodec,
|
|
31940
|
+
needsTranscode: entry.session.needsTranscode,
|
|
31941
|
+
rtpPacketsSent: entry.session.rtpPacketsSentTotal,
|
|
31942
|
+
trackMode: entry.timeline?.currentMode ?? "live"
|
|
31943
|
+
},
|
|
31944
|
+
servedTier: this.profileTierResolver?.(entry.brokerId) ?? null,
|
|
31945
|
+
origin: this.streamOriginResolver?.(entry.brokerId) ?? null,
|
|
31946
|
+
brokerStats,
|
|
31947
|
+
ladder
|
|
31948
|
+
});
|
|
31949
|
+
}
|
|
31950
|
+
/**
|
|
31951
|
+
* Bound the ladder-decision memory. Entries survive a session teardown on
|
|
31952
|
+
* purpose (the re-offer keeps the id), so nothing else would ever evict the
|
|
31953
|
+
* one left behind by a client that disconnected mid-switch. Insertion order
|
|
31954
|
+
* is eviction order.
|
|
31955
|
+
*/
|
|
31956
|
+
pruneLadderDecisions() {
|
|
31957
|
+
const MAX_REMEMBERED_DECISIONS = 64;
|
|
31958
|
+
while (this.lastLadderDecision.size > MAX_REMEMBERED_DECISIONS) {
|
|
31959
|
+
const oldest = this.lastLadderDecision.keys().next();
|
|
31960
|
+
if (oldest.done === true) break;
|
|
31961
|
+
this.lastLadderDecision.delete(oldest.value);
|
|
31962
|
+
}
|
|
31963
|
+
}
|
|
31964
|
+
/** Snapshot the ladder's move so the overlay can name it later. */
|
|
31965
|
+
recordLadderDecision(sessionId, decision, tier, forcedTranscodeDown) {
|
|
31966
|
+
this.lastLadderDecision.set(sessionId, {
|
|
31967
|
+
cause: decision.cause,
|
|
31968
|
+
tier,
|
|
31969
|
+
forcedTranscodeDown,
|
|
31970
|
+
lossRatio: decision.lossRatio,
|
|
31971
|
+
clientKbps: decision.stall.clientKbps,
|
|
31972
|
+
stalledForMs: decision.stall.stalledForMs,
|
|
31973
|
+
serverSending: decision.stall.serverSending,
|
|
31974
|
+
at: Date.now()
|
|
31975
|
+
});
|
|
31976
|
+
}
|
|
31573
31977
|
/**
|
|
31574
31978
|
* Wire an {@link AdaptiveController} to a freshly-created adaptive session.
|
|
31575
31979
|
* The controller is fed loss from two sources — RTCP Receiver Reports
|
|
@@ -31594,9 +31998,11 @@ var BrokerWebrtcServer = class {
|
|
|
31594
31998
|
priorUpgradeCount,
|
|
31595
31999
|
config: ADAPTIVE_CONFIG,
|
|
31596
32000
|
onSwitchTier: (tier, decision) => {
|
|
32001
|
+
this.recordLadderDecision(sessionId, decision, tier, false);
|
|
31597
32002
|
this.switchSessionTier(entry, deviceId, tier, sessionId, false, decision);
|
|
31598
32003
|
},
|
|
31599
32004
|
onReduceMtu: (decision) => {
|
|
32005
|
+
this.recordLadderDecision(sessionId, decision, null, false);
|
|
31600
32006
|
entry.session.setRepacketizerMtu(AdaptiveSession.ADAPTIVE_LOW_MTU);
|
|
31601
32007
|
this.logger.info("adaptive: reducing repacketizer MTU", {
|
|
31602
32008
|
tags: { deviceId },
|
|
@@ -31613,6 +32019,7 @@ var BrokerWebrtcServer = class {
|
|
|
31613
32019
|
});
|
|
31614
32020
|
},
|
|
31615
32021
|
onTranscodeDown: (decision) => {
|
|
32022
|
+
this.recordLadderDecision(sessionId, decision, "low", true);
|
|
31616
32023
|
this.switchSessionTier(entry, deviceId, "low", sessionId, true, decision);
|
|
31617
32024
|
}
|
|
31618
32025
|
});
|
|
@@ -31718,6 +32125,7 @@ var BrokerWebrtcServer = class {
|
|
|
31718
32125
|
}
|
|
31719
32126
|
if (!brokerStillServed && !this.stopped) this.brokerLinger.hold(entry.brokerId);
|
|
31720
32127
|
this.adaptiveIntent.delete(sessionId);
|
|
32128
|
+
this.pruneLadderDecisions();
|
|
31721
32129
|
if (entry.adaptiveTimer) clearInterval(entry.adaptiveTimer);
|
|
31722
32130
|
if (entry.attributionTimer) clearTimeout(entry.attributionTimer);
|
|
31723
32131
|
entry.attributionTagged = true;
|
|
@@ -32365,6 +32773,16 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
|
|
|
32365
32773
|
async getSessionState(input) {
|
|
32366
32774
|
return this.webrtcServer.getSessionRenegotiationState(input.sessionId);
|
|
32367
32775
|
}
|
|
32776
|
+
/**
|
|
32777
|
+
* The SERVER half of the stream-debug overlay. Thin facade — the gathering
|
|
32778
|
+
* lives in the WebRTC server and the shaping in the pure
|
|
32779
|
+
* `projectSessionDebug`. `deviceId` is not used to look the session up
|
|
32780
|
+
* (session ids are already unique); it is part of the device-scoped cap's
|
|
32781
|
+
* signature and identifies the camera for the route.
|
|
32782
|
+
*/
|
|
32783
|
+
async getSessionDebug(input) {
|
|
32784
|
+
return this.webrtcServer.getSessionDebug(input.sessionId, Date.now());
|
|
32785
|
+
}
|
|
32368
32786
|
};
|
|
32369
32787
|
//#endregion
|
|
32370
32788
|
//#region src/stream-broker/event-loop-stall-monitor.ts
|