@camstack/addon-pipeline 1.2.330 → 1.2.331
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-B6gKGnfx.js → default-detection-model-D3SnO2hN.js} +1 -1
- package/dist/{default-detection-model-CPDT_eFT.mjs → default-detection-model-DELTCQgv.mjs} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-CuPEyZY2.mjs → dist-CNXhBzn6.mjs} +134 -15
- package/dist/{dist-CMeICL07.js → dist-DrUx3EGR.js} +134 -15
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-X_0Tka7-.js → node-B4qy3lnV.js} +1 -1
- package/dist/{node-USq5jYwq.mjs → node-BmxmlYLx.mjs} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-07XYts8V.mjs → process-memory-BGc5PzZI.mjs} +1 -1
- package/dist/{process-memory-o4LFWbBN.js → process-memory-CRtWJbjV.js} +1 -1
- package/dist/recorder/index.js +2 -2
- package/dist/recorder/index.mjs +2 -2
- package/dist/{segment-demux-js-FqaQjG7n.js → segment-demux-js-CpSgZmSR.js} +1 -1
- package/dist/{segment-demux-js-Bq8DrH0b.mjs → segment-demux-js-VqsLAgQ0.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-EYRUYv_r.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BnncdmtJ.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-2jlYV34n.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-Bt8aa0wk.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-iGdBpvGc.mjs → hostInit-CF_2Rlu0.mjs} +2 -2
- package/dist/stream-broker/index.js +712 -79
- package/dist/stream-broker/index.mjs +712 -79
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BnP7C_HO.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-CR0--kRv.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-DrUx3EGR.js");
|
|
7
|
+
const require_node = require("../node-B4qy3lnV.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-CpSgZmSR.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");
|
|
@@ -24422,7 +24422,9 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
24422
24422
|
* tier when loss persists — smaller packets survive lossy links better.
|
|
24423
24423
|
* Must stay below H264_REPACKETIZER_MTU (1100). */
|
|
24424
24424
|
static ADAPTIVE_LOW_MTU = 900;
|
|
24425
|
-
/** Broker source-RTP pre-buffer accessor + one-shot replay guard.
|
|
24425
|
+
/** Broker source-RTP pre-buffer accessor + one-shot replay guard. Replaced
|
|
24426
|
+
* by `switchLiveSource` when an adaptive tier change moves the track onto
|
|
24427
|
+
* another broker in place. */
|
|
24426
24428
|
rtpBootstrap;
|
|
24427
24429
|
rtpBootstrapDone = false;
|
|
24428
24430
|
/**
|
|
@@ -24517,6 +24519,46 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
24517
24519
|
reason
|
|
24518
24520
|
} });
|
|
24519
24521
|
}
|
|
24522
|
+
/**
|
|
24523
|
+
* Move the live track onto ANOTHER source of the same codec, in place — an
|
|
24524
|
+
* adaptive tier change on the session the viewer already has (D733).
|
|
24525
|
+
*
|
|
24526
|
+
* Everything `onSourceRestarted` does for a re-dial, because a different
|
|
24527
|
+
* rung IS a different RTP stream (new SSRC, new sequence/timestamp base, new
|
|
24528
|
+
* parameter sets), plus the two things only a different broker changes:
|
|
24529
|
+
* - the bootstrap accessor now reads the NEW broker's pre-buffer, so the
|
|
24530
|
+
* next forwarded packet replays its current GOP from the keyframe — the
|
|
24531
|
+
* decoder switches on an IDR instead of waiting for the camera's next one;
|
|
24532
|
+
* - the repacketizer is re-seeded with the new rung's SDP parameter sets,
|
|
24533
|
+
* for a camera that publishes them only in SDP.
|
|
24534
|
+
* The MTU returns to the default a freshly built session would have: the
|
|
24535
|
+
* reduced MTU belongs to the lowest rung's loss recovery, not to the session.
|
|
24536
|
+
*/
|
|
24537
|
+
switchLiveSource(next) {
|
|
24538
|
+
if (this.closed) return;
|
|
24539
|
+
this.rtpBootstrap = next.rtpBootstrap;
|
|
24540
|
+
this.activeRtpReplay?.abort();
|
|
24541
|
+
this.activeRtpReplay = null;
|
|
24542
|
+
this.h264Repacketizer = null;
|
|
24543
|
+
this.h265Repacketizer = null;
|
|
24544
|
+
this.sourceVideoSsrc = null;
|
|
24545
|
+
this.rtpRebasePending = true;
|
|
24546
|
+
this.rtpBootstrapDone = false;
|
|
24547
|
+
this.repacketizerMtu = AdaptiveSession.H264_REPACKETIZER_MTU;
|
|
24548
|
+
const ps = next.parameterSets ?? [];
|
|
24549
|
+
if (ps.length > 0) if (this.sourceCodec === "H265") this.seedH265CodecInfoFromSdp(ps);
|
|
24550
|
+
else this.seedH264CodecInfoFromSdp(ps);
|
|
24551
|
+
this.logger.info("live source switched in place — replaying the new source from its keyframe", {
|
|
24552
|
+
tags: { deviceId: this.deviceId },
|
|
24553
|
+
meta: {
|
|
24554
|
+
phase: "session",
|
|
24555
|
+
sessionId: this.sessionId,
|
|
24556
|
+
deviceId: this.deviceId,
|
|
24557
|
+
reason: next.reason,
|
|
24558
|
+
seededParamSets: ps.length
|
|
24559
|
+
}
|
|
24560
|
+
});
|
|
24561
|
+
}
|
|
24520
24562
|
/** Drop the repacketizer + bootstrap/sync state so a newly bound source
|
|
24521
24563
|
* stream re-seeds its SPS/PPS and replays instant-start cleanly. */
|
|
24522
24564
|
resetRepacketizer() {
|
|
@@ -27209,6 +27251,32 @@ var AdaptiveController = class {
|
|
|
27209
27251
|
onRtt(rttMs, now) {
|
|
27210
27252
|
this.rtt.observe(rttMs, now);
|
|
27211
27253
|
}
|
|
27254
|
+
/**
|
|
27255
|
+
* The session now serves `tier`, decided OUTSIDE this ladder's own tick
|
|
27256
|
+
* (D733): the viewer's downlink probe placed it there, or the rung the ladder
|
|
27257
|
+
* asked for is not served and the switch landed on the one that is.
|
|
27258
|
+
*
|
|
27259
|
+
* Before D733 both were a close-and-recreate re-offer, which built a NEW
|
|
27260
|
+
* controller on the rung the session landed on. The ladder ends up where that
|
|
27261
|
+
* new controller started: on `tier`, not on trial (a probe IS capacity
|
|
27262
|
+
* evidence; a fallback rung was not tried), with its healthy clock and change
|
|
27263
|
+
* blackout starting now, and with no promotion counted. The measured-failure
|
|
27264
|
+
* memory is kept: a rung this session already proved unusable stays backed off.
|
|
27265
|
+
*/
|
|
27266
|
+
settleOn(tier, now) {
|
|
27267
|
+
this.armed = true;
|
|
27268
|
+
this.hold = null;
|
|
27269
|
+
this.tier = tier;
|
|
27270
|
+
this.mtuReduced = false;
|
|
27271
|
+
this.transcodeEngaged = false;
|
|
27272
|
+
this.trial = null;
|
|
27273
|
+
this.tryCarry = {
|
|
27274
|
+
...this.tryCarry,
|
|
27275
|
+
trialFrom: null
|
|
27276
|
+
};
|
|
27277
|
+
this.commitChange(now);
|
|
27278
|
+
this.healthySince = now;
|
|
27279
|
+
}
|
|
27212
27280
|
/** Why the ladder did not climb on the last evaluation; null = at the top, or it just moved. */
|
|
27213
27281
|
get climbHold() {
|
|
27214
27282
|
return this.hold;
|
|
@@ -31231,6 +31299,8 @@ var TimelineSession = class {
|
|
|
31231
31299
|
* paused across that required quality transition. */
|
|
31232
31300
|
rate = 1;
|
|
31233
31301
|
liveProfileReported = false;
|
|
31302
|
+
/** The live rung after an in-place tier switch; undefined = `initialLiveProfile`. */
|
|
31303
|
+
liveProfile = void 0;
|
|
31234
31304
|
/**
|
|
31235
31305
|
* The last position the viewer was SHOWN (every `_emitPosition`). Handed to
|
|
31236
31306
|
* `seek` as `travelFromMs` so the seek travel animation works across feeder
|
|
@@ -31705,11 +31775,29 @@ var TimelineSession = class {
|
|
|
31705
31775
|
}));
|
|
31706
31776
|
}
|
|
31707
31777
|
emitLiveProfile() {
|
|
31708
|
-
|
|
31778
|
+
const profile = this.liveProfile ?? this.deps.initialLiveProfile;
|
|
31779
|
+
if (this.liveProfileReported || profile === void 0) return;
|
|
31709
31780
|
this.liveProfileReported = true;
|
|
31710
31781
|
this.deps.session.sendControl(serializeServerMessage({
|
|
31711
31782
|
t: "liveProfile",
|
|
31712
|
-
profile
|
|
31783
|
+
profile
|
|
31784
|
+
}));
|
|
31785
|
+
}
|
|
31786
|
+
/**
|
|
31787
|
+
* The live rung moved under this coordinator — an adaptive tier switch that
|
|
31788
|
+
* stayed on the session (D733). Before D733 a tier change built a new
|
|
31789
|
+
* coordinator, which reported its own `initialLiveProfile`; now the same one
|
|
31790
|
+
* outlives the switch and must say what the viewer is watching. Reported at
|
|
31791
|
+
* once when the viewer has already been told a profile, otherwise on its
|
|
31792
|
+
* first control message as before.
|
|
31793
|
+
*/
|
|
31794
|
+
setLiveProfile(profile) {
|
|
31795
|
+
if (this.liveProfile === profile) return;
|
|
31796
|
+
this.liveProfile = profile;
|
|
31797
|
+
if (!this.liveProfileReported) return;
|
|
31798
|
+
this.deps.session.sendControl(serializeServerMessage({
|
|
31799
|
+
t: "liveProfile",
|
|
31800
|
+
profile
|
|
31713
31801
|
}));
|
|
31714
31802
|
}
|
|
31715
31803
|
dispose() {
|
|
@@ -34632,6 +34720,98 @@ function preferLiveAdaptiveCandidates(candidates) {
|
|
|
34632
34720
|
return natives.length > 0 ? natives : [...candidates];
|
|
34633
34721
|
}
|
|
34634
34722
|
//#endregion
|
|
34723
|
+
//#region src/stream-broker/webrtc/in-place-tier-switch.ts
|
|
34724
|
+
/**
|
|
34725
|
+
* How long a switch waits for the new rendition's first switchable keyframe.
|
|
34726
|
+
* A warm broker commits on its next packet; a cold one needs its dial (2–3 s on
|
|
34727
|
+
* this hub) plus the camera's next IDR (≤ 4 s at the GOPs measured). 8 s covers
|
|
34728
|
+
* both; past it the old source has been shown the whole time, so nothing froze.
|
|
34729
|
+
*/
|
|
34730
|
+
var IN_PLACE_SWITCH_KEYFRAME_WAIT_MS = 8e3;
|
|
34731
|
+
/** Map a broker's codec string to the session codec it would negotiate; null = unknown. */
|
|
34732
|
+
function sessionCodecOf(brokerCodec) {
|
|
34733
|
+
if (brokerCodec === void 0 || brokerCodec === "") return null;
|
|
34734
|
+
const c = brokerCodec.toLowerCase();
|
|
34735
|
+
return c === "h265" || c === "hevc" ? "H265" : "H264";
|
|
34736
|
+
}
|
|
34737
|
+
/**
|
|
34738
|
+
* Whether the switch may stay on the session; the refusal names why not.
|
|
34739
|
+
* Facts not known yet (a cold target's codec or audio) do not refuse — the
|
|
34740
|
+
* judgement is repeated at commit time, when they are.
|
|
34741
|
+
*/
|
|
34742
|
+
function judgeInPlaceSwitch(session, target) {
|
|
34743
|
+
if (session.recorded) return "recorded";
|
|
34744
|
+
if (session.forceTranscodeDown) return "transcode-down";
|
|
34745
|
+
if (!session.useRtpRepacketizer) return "not-rtp-passthrough";
|
|
34746
|
+
if (session.needsTranscode) return "transcode";
|
|
34747
|
+
if (!target.tierMatches) return "tier-unavailable";
|
|
34748
|
+
if (!target.isRtp) return "target-not-rtp";
|
|
34749
|
+
if (target.codec !== null && target.codec !== session.sessionCodec) return "codec-differs";
|
|
34750
|
+
if (target.needsBaselineTranscode) return "target-needs-transcode";
|
|
34751
|
+
if (target.audioCodec !== void 0 && !sameAudioCodec(session.audioCodec, target.audioCodec)) return "audio-codec-differs";
|
|
34752
|
+
if (session.usesOpusEgress && !target.supportsOpusEgress) return "opus-egress-unsupported";
|
|
34753
|
+
return null;
|
|
34754
|
+
}
|
|
34755
|
+
function sameAudioCodec(a, b) {
|
|
34756
|
+
return (a ?? "").toLowerCase() === (b ?? "").toLowerCase();
|
|
34757
|
+
}
|
|
34758
|
+
/**
|
|
34759
|
+
* Subscribe `broker`'s RTP while the session's current source keeps feeding,
|
|
34760
|
+
* and commit when the new rendition can start on a keyframe.
|
|
34761
|
+
*
|
|
34762
|
+
* ONE subscription goes from pending to forwarding — never a second one added
|
|
34763
|
+
* during the delivery of the committing packet, which a broker iterating its
|
|
34764
|
+
* subscriber set would hand the same packet again (a duplicate behind the
|
|
34765
|
+
* replay). A warm-up consumer keeps a cold broker's dial up while pending and
|
|
34766
|
+
* is released only after the commit has attached the session's own consumers.
|
|
34767
|
+
*/
|
|
34768
|
+
function bindPendingSource(opts) {
|
|
34769
|
+
let state = "pending";
|
|
34770
|
+
let warm = opts.broker.onEncodedData(() => {}, {
|
|
34771
|
+
kind: "warmup",
|
|
34772
|
+
media: "both"
|
|
34773
|
+
});
|
|
34774
|
+
const releaseWarm = () => {
|
|
34775
|
+
warm?.();
|
|
34776
|
+
warm = null;
|
|
34777
|
+
};
|
|
34778
|
+
let unsubRtp = null;
|
|
34779
|
+
const timer = setTimeout(() => {
|
|
34780
|
+
if (state !== "pending") return;
|
|
34781
|
+
state = "gone";
|
|
34782
|
+
unsubRtp?.();
|
|
34783
|
+
releaseWarm();
|
|
34784
|
+
opts.onTimeout();
|
|
34785
|
+
}, opts.timeoutMs);
|
|
34786
|
+
unsubRtp = opts.broker.onVideoRtp((rtp) => {
|
|
34787
|
+
if (state === "gone") return;
|
|
34788
|
+
if (state === "committed") {
|
|
34789
|
+
opts.forward(rtp);
|
|
34790
|
+
return;
|
|
34791
|
+
}
|
|
34792
|
+
if (opts.broker.getRtpPreBuffer().length === 0) return;
|
|
34793
|
+
clearTimeout(timer);
|
|
34794
|
+
if (!opts.commit()) {
|
|
34795
|
+
state = "gone";
|
|
34796
|
+
unsubRtp?.();
|
|
34797
|
+
releaseWarm();
|
|
34798
|
+
return;
|
|
34799
|
+
}
|
|
34800
|
+
state = "committed";
|
|
34801
|
+
releaseWarm();
|
|
34802
|
+
opts.forward(rtp);
|
|
34803
|
+
}, opts.attribution);
|
|
34804
|
+
return {
|
|
34805
|
+
unsubscribe: () => {
|
|
34806
|
+
if (state === "pending") clearTimeout(timer);
|
|
34807
|
+
state = "gone";
|
|
34808
|
+
unsubRtp?.();
|
|
34809
|
+
releaseWarm();
|
|
34810
|
+
},
|
|
34811
|
+
committed: () => state === "committed"
|
|
34812
|
+
};
|
|
34813
|
+
}
|
|
34814
|
+
//#endregion
|
|
34635
34815
|
//#region src/stream-broker/webrtc/adaptive-tier.ts
|
|
34636
34816
|
var RANK = {
|
|
34637
34817
|
low: 1,
|
|
@@ -34811,6 +34991,107 @@ function adaptivePickLogLine(reason) {
|
|
|
34811
34991
|
if (reason === "default-low") return "Adaptive source selected (no client hints — smallest available)";
|
|
34812
34992
|
return "Adaptive source selected";
|
|
34813
34993
|
}
|
|
34994
|
+
/**
|
|
34995
|
+
* One broker audio packet onto a session's push source — the per-session codec
|
|
34996
|
+
* branch (mirrors the live/recorded split). The broker fans out BOTH a
|
|
34997
|
+
* PCMU/PCMA stream (always) and, when an Opus session is attached, a parallel
|
|
34998
|
+
* `codec:'opus'` stream. Forward only the stream matching THIS session's
|
|
34999
|
+
* negotiated codec; drop the other so a PCMU-fallback session never sees Opus
|
|
35000
|
+
* and vice-versa. Shared by the session's first broker and by every broker an
|
|
35001
|
+
* in-place tier switch moves it to (D733).
|
|
35002
|
+
*/
|
|
35003
|
+
function forwardBrokerAudio(packet, session, pushFrame) {
|
|
35004
|
+
const pktCodec = packet.codec?.toLowerCase();
|
|
35005
|
+
if (session?.isOpusAudio === true) {
|
|
35006
|
+
if (pktCodec !== "opus") return;
|
|
35007
|
+
pushFrame({
|
|
35008
|
+
type: "audio",
|
|
35009
|
+
frame: {
|
|
35010
|
+
data: packet.data,
|
|
35011
|
+
codec: "Opus",
|
|
35012
|
+
sampleRate: 48e3,
|
|
35013
|
+
channels: 2,
|
|
35014
|
+
timestampMicros: packet.pts * 1e3
|
|
35015
|
+
}
|
|
35016
|
+
});
|
|
35017
|
+
return;
|
|
35018
|
+
}
|
|
35019
|
+
if (pktCodec === "opus") return;
|
|
35020
|
+
pushFrame({
|
|
35021
|
+
type: "audio",
|
|
35022
|
+
frame: {
|
|
35023
|
+
data: packet.data,
|
|
35024
|
+
codec: pktCodec === "pcma" ? "Pcma" : "Pcmu",
|
|
35025
|
+
sampleRate: 8e3,
|
|
35026
|
+
channels: 1,
|
|
35027
|
+
timestampMicros: packet.pts * 1e3
|
|
35028
|
+
}
|
|
35029
|
+
});
|
|
35030
|
+
}
|
|
35031
|
+
/**
|
|
35032
|
+
* The `onVideoRtp` callback of an RTP-passthrough session: source RTP through
|
|
35033
|
+
* the codec repacketizer, unless a transcode owns the wire or recorded playback
|
|
35034
|
+
* owns the track. One implementation for the session's first broker and for
|
|
35035
|
+
* every broker an in-place tier switch moves it to (D733).
|
|
35036
|
+
*/
|
|
35037
|
+
function makeSourceRtpForwarder(input) {
|
|
35038
|
+
const { session, sessionLogger, sessionId, deviceId, brokerId, sessionCodec } = input;
|
|
35039
|
+
let firstRtpForwarded = false;
|
|
35040
|
+
let gateDropLogged = false;
|
|
35041
|
+
let forwardAfterGateLogged = false;
|
|
35042
|
+
return (rtpData) => {
|
|
35043
|
+
if (session.needsTranscode) return;
|
|
35044
|
+
if (session.isLiveFeedGated) {
|
|
35045
|
+
if (!gateDropLogged) {
|
|
35046
|
+
gateDropLogged = true;
|
|
35047
|
+
forwardAfterGateLogged = false;
|
|
35048
|
+
sessionLogger.info("recorded: live RTP GATED (dropping live video)", {
|
|
35049
|
+
tags: { deviceId },
|
|
35050
|
+
meta: {
|
|
35051
|
+
sessionId,
|
|
35052
|
+
deviceId
|
|
35053
|
+
}
|
|
35054
|
+
});
|
|
35055
|
+
}
|
|
35056
|
+
return;
|
|
35057
|
+
}
|
|
35058
|
+
if (gateDropLogged && !forwardAfterGateLogged) {
|
|
35059
|
+
forwardAfterGateLogged = true;
|
|
35060
|
+
gateDropLogged = false;
|
|
35061
|
+
sessionLogger.info("recorded: live RTP UNGATED (forwarding live video again)", {
|
|
35062
|
+
tags: { deviceId },
|
|
35063
|
+
meta: {
|
|
35064
|
+
sessionId,
|
|
35065
|
+
deviceId
|
|
35066
|
+
}
|
|
35067
|
+
});
|
|
35068
|
+
}
|
|
35069
|
+
if (!firstRtpForwarded) {
|
|
35070
|
+
firstRtpForwarded = true;
|
|
35071
|
+
sessionLogger.info("RTP session: first source RTP forwarded to repacketizer", {
|
|
35072
|
+
tags: { deviceId },
|
|
35073
|
+
meta: {
|
|
35074
|
+
sessionId,
|
|
35075
|
+
brokerId,
|
|
35076
|
+
codec: sessionCodec,
|
|
35077
|
+
bytes: rtpData.length
|
|
35078
|
+
}
|
|
35079
|
+
});
|
|
35080
|
+
}
|
|
35081
|
+
try {
|
|
35082
|
+
session.forwardSourceRtpVideo(rtpData);
|
|
35083
|
+
} catch (err) {
|
|
35084
|
+
sessionLogger.warn("forwardSourceRtpVideo threw", {
|
|
35085
|
+
tags: { deviceId },
|
|
35086
|
+
meta: {
|
|
35087
|
+
sessionId,
|
|
35088
|
+
codec: sessionCodec,
|
|
35089
|
+
error: require_dist.errMsg(err)
|
|
35090
|
+
}
|
|
35091
|
+
});
|
|
35092
|
+
}
|
|
35093
|
+
};
|
|
35094
|
+
}
|
|
34814
35095
|
/** Push-queue overflow purges are aggregated and reported at most this often. */
|
|
34815
35096
|
var FRAME_DROP_LOG_INTERVAL_MS = 3e4;
|
|
34816
35097
|
/**
|
|
@@ -35436,34 +35717,7 @@ var BrokerWebrtcServer = class {
|
|
|
35436
35717
|
timestampMicros: packet.pts * 1e3
|
|
35437
35718
|
}
|
|
35438
35719
|
});
|
|
35439
|
-
} else if (packet.type === "audio")
|
|
35440
|
-
const pktCodec = packet.codec?.toLowerCase();
|
|
35441
|
-
if (session?.isOpusAudio === true) {
|
|
35442
|
-
if (pktCodec !== "opus") return;
|
|
35443
|
-
pushFrame({
|
|
35444
|
-
type: "audio",
|
|
35445
|
-
frame: {
|
|
35446
|
-
data: packet.data,
|
|
35447
|
-
codec: "Opus",
|
|
35448
|
-
sampleRate: 48e3,
|
|
35449
|
-
channels: 2,
|
|
35450
|
-
timestampMicros: packet.pts * 1e3
|
|
35451
|
-
}
|
|
35452
|
-
});
|
|
35453
|
-
} else {
|
|
35454
|
-
if (pktCodec === "opus") return;
|
|
35455
|
-
pushFrame({
|
|
35456
|
-
type: "audio",
|
|
35457
|
-
frame: {
|
|
35458
|
-
data: packet.data,
|
|
35459
|
-
codec: pktCodec === "pcma" ? "Pcma" : "Pcmu",
|
|
35460
|
-
sampleRate: 8e3,
|
|
35461
|
-
channels: 1,
|
|
35462
|
-
timestampMicros: packet.pts * 1e3
|
|
35463
|
-
}
|
|
35464
|
-
});
|
|
35465
|
-
}
|
|
35466
|
-
}
|
|
35720
|
+
} else if (packet.type === "audio") forwardBrokerAudio(packet, session, pushFrame);
|
|
35467
35721
|
}, annexbAttribution);
|
|
35468
35722
|
warmUpUnsubscribe?.();
|
|
35469
35723
|
this.brokerLinger.cancel(brokerId);
|
|
@@ -35584,49 +35838,14 @@ var BrokerWebrtcServer = class {
|
|
|
35584
35838
|
}
|
|
35585
35839
|
});
|
|
35586
35840
|
}
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
|
|
35590
|
-
|
|
35591
|
-
|
|
35592
|
-
|
|
35593
|
-
|
|
35594
|
-
|
|
35595
|
-
forwardAfterGateLogged = false;
|
|
35596
|
-
sessionLogger.info("recorded: live RTP GATED (dropping live video)", { meta: {
|
|
35597
|
-
sessionId,
|
|
35598
|
-
deviceId
|
|
35599
|
-
} });
|
|
35600
|
-
}
|
|
35601
|
-
return;
|
|
35602
|
-
}
|
|
35603
|
-
if (gateDropLogged && !forwardAfterGateLogged) {
|
|
35604
|
-
forwardAfterGateLogged = true;
|
|
35605
|
-
gateDropLogged = false;
|
|
35606
|
-
sessionLogger.info("recorded: live RTP UNGATED (forwarding live video again)", { meta: {
|
|
35607
|
-
sessionId,
|
|
35608
|
-
deviceId
|
|
35609
|
-
} });
|
|
35610
|
-
}
|
|
35611
|
-
if (!firstRtpForwarded) {
|
|
35612
|
-
firstRtpForwarded = true;
|
|
35613
|
-
sessionLogger.info("RTP session: first source RTP forwarded to repacketizer", { meta: {
|
|
35614
|
-
sessionId,
|
|
35615
|
-
brokerId,
|
|
35616
|
-
codec: sessionCodec,
|
|
35617
|
-
bytes: rtpData.length
|
|
35618
|
-
} });
|
|
35619
|
-
}
|
|
35620
|
-
try {
|
|
35621
|
-
session.forwardSourceRtpVideo(rtpData);
|
|
35622
|
-
} catch (err) {
|
|
35623
|
-
sessionLogger.warn("forwardSourceRtpVideo threw", { meta: {
|
|
35624
|
-
sessionId,
|
|
35625
|
-
codec: sessionCodec,
|
|
35626
|
-
error: require_dist.errMsg(err)
|
|
35627
|
-
} });
|
|
35628
|
-
}
|
|
35629
|
-
}, rtpAttribution);
|
|
35841
|
+
unsubRtp = broker.onVideoRtp(makeSourceRtpForwarder({
|
|
35842
|
+
session,
|
|
35843
|
+
sessionLogger,
|
|
35844
|
+
sessionId,
|
|
35845
|
+
deviceId,
|
|
35846
|
+
brokerId,
|
|
35847
|
+
sessionCodec
|
|
35848
|
+
}), rtpAttribution);
|
|
35630
35849
|
}
|
|
35631
35850
|
const unsubSourceRestart = broker.onSourceRestart((reason) => {
|
|
35632
35851
|
sessionLogger.info("broker re-dialed the source — re-syncing session", { meta: {
|
|
@@ -35652,7 +35871,17 @@ var BrokerWebrtcServer = class {
|
|
|
35652
35871
|
unsubRtp,
|
|
35653
35872
|
unsubSdpParams,
|
|
35654
35873
|
unsubSourceRestart,
|
|
35655
|
-
closeSource
|
|
35874
|
+
closeSource,
|
|
35875
|
+
rebind: {
|
|
35876
|
+
deviceId,
|
|
35877
|
+
sessionCodec,
|
|
35878
|
+
useRtpRepacketizer,
|
|
35879
|
+
offeredProfileLevelIds,
|
|
35880
|
+
hints,
|
|
35881
|
+
sessionLogger,
|
|
35882
|
+
pushFrame,
|
|
35883
|
+
baseAttribution
|
|
35884
|
+
}
|
|
35656
35885
|
};
|
|
35657
35886
|
this.sessions.set(sessionId, entry);
|
|
35658
35887
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
@@ -36379,6 +36608,88 @@ var BrokerWebrtcServer = class {
|
|
|
36379
36608
|
});
|
|
36380
36609
|
controller.onRtt(rttMs, now);
|
|
36381
36610
|
}
|
|
36611
|
+
/**
|
|
36612
|
+
* The viewer's downlink probe for ONE session (D733) — the server half of the
|
|
36613
|
+
* probe climb. The player used to answer its own probe with a second session
|
|
36614
|
+
* (a make-before-break reconnect carrying `downlinkMbps`); it now hands the
|
|
36615
|
+
* measurement to the session it already has, and the broker places that
|
|
36616
|
+
* session on the rung the probe picks, in place when the codec allows.
|
|
36617
|
+
*
|
|
36618
|
+
* The client sends a MEASUREMENT, never a tier: the pick is the same
|
|
36619
|
+
* `pickAdaptiveTier` an offer's hints get, against the same viewport the
|
|
36620
|
+
* offer carried, so a tile still ceilings to `low`. A probe only ever
|
|
36621
|
+
* raises the rung — lowering it is the ladder's job, on delivery evidence.
|
|
36622
|
+
*/
|
|
36623
|
+
applyDownlinkProbe(sessionId, downlinkMbps) {
|
|
36624
|
+
const entry = this.sessions.get(sessionId);
|
|
36625
|
+
if (entry === void 0) return {
|
|
36626
|
+
outcome: "held",
|
|
36627
|
+
tier: null,
|
|
36628
|
+
reason: "no-session"
|
|
36629
|
+
};
|
|
36630
|
+
const deviceId = deviceIdFromBrokerId(entry.brokerId);
|
|
36631
|
+
const served = this.profileTierResolver?.(entry.brokerId) ?? null;
|
|
36632
|
+
const controller = entry.adaptiveController;
|
|
36633
|
+
const rebind = entry.rebind;
|
|
36634
|
+
const now = Date.now();
|
|
36635
|
+
const held = (reason) => {
|
|
36636
|
+
this.logger.info("adaptive: downlink probe holds the session where it is", {
|
|
36637
|
+
tags: { deviceId },
|
|
36638
|
+
meta: {
|
|
36639
|
+
sessionId,
|
|
36640
|
+
deviceId,
|
|
36641
|
+
downlinkMbps,
|
|
36642
|
+
tier: served,
|
|
36643
|
+
reason
|
|
36644
|
+
}
|
|
36645
|
+
});
|
|
36646
|
+
return {
|
|
36647
|
+
outcome: "held",
|
|
36648
|
+
tier: served,
|
|
36649
|
+
reason
|
|
36650
|
+
};
|
|
36651
|
+
};
|
|
36652
|
+
if (controller === void 0 || rebind === void 0) return held("not-adaptive");
|
|
36653
|
+
this.recordDeviceProbe(deviceId, probeFromHints(downlinkMbps, now));
|
|
36654
|
+
if (rebind.hints.prefersTier !== void 0) return held("pinned");
|
|
36655
|
+
if (entry.timeline?.currentMode === "recorded") return held("recorded");
|
|
36656
|
+
const pick = pickAdaptiveTier({
|
|
36657
|
+
available: preferLiveAdaptiveCandidates(this.deviceBrokerCandidates(deviceId)).map((c) => c.tier),
|
|
36658
|
+
downlinkMbps,
|
|
36659
|
+
viewportWidth: rebind.hints.viewportWidth,
|
|
36660
|
+
viewportHeight: rebind.hints.viewportHeight
|
|
36661
|
+
});
|
|
36662
|
+
const current = entry.pendingSwitch?.tier ?? controller.currentTier;
|
|
36663
|
+
if (require_dist.CAM_PROFILE_ORDER.indexOf(pick.tier) >= require_dist.CAM_PROFILE_ORDER.indexOf(current)) return held(pick.tier === current ? "already-there" : "probe-below-current");
|
|
36664
|
+
controller.settleOn(pick.tier, now);
|
|
36665
|
+
this.logger.info("adaptive: downlink probe places the session on a higher rung", {
|
|
36666
|
+
tags: { deviceId },
|
|
36667
|
+
meta: {
|
|
36668
|
+
sessionId,
|
|
36669
|
+
deviceId,
|
|
36670
|
+
downlinkMbps,
|
|
36671
|
+
from: current,
|
|
36672
|
+
tier: pick.tier,
|
|
36673
|
+
pick: pick.reason
|
|
36674
|
+
}
|
|
36675
|
+
});
|
|
36676
|
+
this.switchSessionTier(entry, deviceId, pick.tier, sessionId);
|
|
36677
|
+
return {
|
|
36678
|
+
outcome: "switching",
|
|
36679
|
+
tier: pick.tier,
|
|
36680
|
+
reason: pick.reason
|
|
36681
|
+
};
|
|
36682
|
+
}
|
|
36683
|
+
/** Every registered broker of `deviceId`, with the tier it currently serves. */
|
|
36684
|
+
deviceBrokerCandidates(deviceId) {
|
|
36685
|
+
const prefix = `${String(deviceId)}/`;
|
|
36686
|
+
const out = [];
|
|
36687
|
+
for (const bid of this.brokers.keys()) if (bid.startsWith(prefix)) out.push({
|
|
36688
|
+
brokerId: bid,
|
|
36689
|
+
tier: this.profileTierResolver?.(bid) ?? null
|
|
36690
|
+
});
|
|
36691
|
+
return out;
|
|
36692
|
+
}
|
|
36382
36693
|
/** Adaptive sessions of `deviceId` currently on this broker server. */
|
|
36383
36694
|
adaptiveSessionCount(deviceId) {
|
|
36384
36695
|
const prefix = `${String(deviceId)}/`;
|
|
@@ -36391,6 +36702,322 @@ var BrokerWebrtcServer = class {
|
|
|
36391
36702
|
this.deviceProbe.record(deviceId, probe);
|
|
36392
36703
|
}
|
|
36393
36704
|
/**
|
|
36705
|
+
* Move an adaptive session to `tier` — ON THE SESSION when it can (D733),
|
|
36706
|
+
* by the close-and-recreate re-offer when it cannot, and the refusal that
|
|
36707
|
+
* forced the re-offer is logged by name.
|
|
36708
|
+
*
|
|
36709
|
+
* A newer switch replaces one still waiting for its keyframe: the ladder's
|
|
36710
|
+
* latest verdict is the one that must land, and an abandoned wait leaves the
|
|
36711
|
+
* old source on the track (nothing froze).
|
|
36712
|
+
*/
|
|
36713
|
+
switchSessionTier(entry, deviceId, tier, sessionId, forceTranscodeDown = false, decision) {
|
|
36714
|
+
entry.pendingSwitch?.cancel();
|
|
36715
|
+
entry.pendingSwitch = void 0;
|
|
36716
|
+
const why = decision?.cause ?? "probe";
|
|
36717
|
+
if (forceTranscodeDown) {
|
|
36718
|
+
this.logInPlaceFallback(entry, deviceId, tier, sessionId, "transcode-down", why);
|
|
36719
|
+
this.scheduleTierReoffer(entry, deviceId, tier, sessionId, true, decision);
|
|
36720
|
+
return;
|
|
36721
|
+
}
|
|
36722
|
+
this.switchTierInPlace(entry, deviceId, tier, sessionId, why).then((outcome) => {
|
|
36723
|
+
if (outcome.kind !== "refused") return;
|
|
36724
|
+
if (this.sessions.get(sessionId) !== entry) return;
|
|
36725
|
+
this.logInPlaceFallback(entry, deviceId, tier, sessionId, outcome.reason, why);
|
|
36726
|
+
this.scheduleTierReoffer(entry, deviceId, tier, sessionId, false, decision);
|
|
36727
|
+
}).catch((err) => {
|
|
36728
|
+
this.logger.warn("adaptive: in-place tier switch threw — falling back to re-offer", {
|
|
36729
|
+
tags: { deviceId },
|
|
36730
|
+
meta: {
|
|
36731
|
+
sessionId,
|
|
36732
|
+
deviceId,
|
|
36733
|
+
tier,
|
|
36734
|
+
error: require_dist.errMsg(err)
|
|
36735
|
+
}
|
|
36736
|
+
});
|
|
36737
|
+
if (this.sessions.get(sessionId) !== entry) return;
|
|
36738
|
+
this.scheduleTierReoffer(entry, deviceId, tier, sessionId, false, decision);
|
|
36739
|
+
});
|
|
36740
|
+
}
|
|
36741
|
+
logInPlaceFallback(entry, deviceId, tier, sessionId, reason, why) {
|
|
36742
|
+
this.logger.info("adaptive: tier switch needs a re-offer — cannot stay on the session", {
|
|
36743
|
+
tags: { deviceId },
|
|
36744
|
+
meta: {
|
|
36745
|
+
sessionId,
|
|
36746
|
+
deviceId,
|
|
36747
|
+
tier,
|
|
36748
|
+
fromBrokerId: entry.brokerId,
|
|
36749
|
+
reason,
|
|
36750
|
+
cause: why
|
|
36751
|
+
}
|
|
36752
|
+
});
|
|
36753
|
+
}
|
|
36754
|
+
/** What the session is now, for `judgeInPlaceSwitch`. */
|
|
36755
|
+
inPlaceSessionShape(entry, rebind) {
|
|
36756
|
+
return {
|
|
36757
|
+
recorded: entry.timeline?.currentMode === "recorded",
|
|
36758
|
+
forceTranscodeDown: false,
|
|
36759
|
+
useRtpRepacketizer: rebind.useRtpRepacketizer,
|
|
36760
|
+
needsTranscode: entry.session.needsTranscode,
|
|
36761
|
+
sessionCodec: rebind.sessionCodec,
|
|
36762
|
+
audioCodec: this.brokers.get(entry.brokerId)?.getStats().audio?.codec ?? null,
|
|
36763
|
+
usesOpusEgress: entry.releaseOpusEgress !== void 0
|
|
36764
|
+
};
|
|
36765
|
+
}
|
|
36766
|
+
/** What the target rendition is, as far as it is known now. */
|
|
36767
|
+
inPlaceTargetShape(rebind, landed, target) {
|
|
36768
|
+
if (target === void 0) return {
|
|
36769
|
+
tierMatches: false,
|
|
36770
|
+
isRtp: false,
|
|
36771
|
+
codec: null,
|
|
36772
|
+
needsBaselineTranscode: false,
|
|
36773
|
+
audioCodec: void 0,
|
|
36774
|
+
supportsOpusEgress: false
|
|
36775
|
+
};
|
|
36776
|
+
const stats = target.getStats();
|
|
36777
|
+
const codec = sessionCodecOf(stats.codec);
|
|
36778
|
+
let needsBaselineTranscode = false;
|
|
36779
|
+
if (codec === "H264") {
|
|
36780
|
+
const profile = deriveH264ProfileLevelId(target.getSdpParameterSets(), target.getPreBuffer());
|
|
36781
|
+
needsBaselineTranscode = rebind.offeredProfileLevelIds !== void 0 ? resolveClientOfferTranscodeToBaseline("H264", profile, rebind.offeredProfileLevelIds) : resolveBrokerTranscodeToBaseline("H264", profile);
|
|
36782
|
+
}
|
|
36783
|
+
return {
|
|
36784
|
+
tierMatches: landed !== null,
|
|
36785
|
+
isRtp: target.isRtpSource(),
|
|
36786
|
+
codec,
|
|
36787
|
+
needsBaselineTranscode,
|
|
36788
|
+
audioCodec: codec === null ? void 0 : stats.audio?.codec ?? null,
|
|
36789
|
+
supportsOpusEgress: supportsOpusEgress(target)
|
|
36790
|
+
};
|
|
36791
|
+
}
|
|
36792
|
+
/**
|
|
36793
|
+
* The in-place half of a tier switch (D733): resolve the tier's broker,
|
|
36794
|
+
* judge whether the negotiated session carries it as is, subscribe it
|
|
36795
|
+
* make-before-break and commit on its first switchable keyframe.
|
|
36796
|
+
*/
|
|
36797
|
+
async switchTierInPlace(entry, deviceId, tier, sessionId, why) {
|
|
36798
|
+
const rebind = entry.rebind;
|
|
36799
|
+
if (rebind === void 0) return {
|
|
36800
|
+
kind: "refused",
|
|
36801
|
+
reason: "not-rtp-passthrough"
|
|
36802
|
+
};
|
|
36803
|
+
const early = judgeInPlaceSwitch(this.inPlaceSessionShape(entry, rebind), {
|
|
36804
|
+
tierMatches: true,
|
|
36805
|
+
isRtp: true,
|
|
36806
|
+
codec: null,
|
|
36807
|
+
needsBaselineTranscode: false,
|
|
36808
|
+
audioCodec: void 0,
|
|
36809
|
+
supportsOpusEgress: true
|
|
36810
|
+
});
|
|
36811
|
+
if (early !== null) return {
|
|
36812
|
+
kind: "refused",
|
|
36813
|
+
reason: early
|
|
36814
|
+
};
|
|
36815
|
+
try {
|
|
36816
|
+
await this.ensureAdaptiveTierBroker?.(deviceId, tier);
|
|
36817
|
+
} catch (err) {
|
|
36818
|
+
this.logger.warn("adaptive: could not prepare the tier broker for an in-place switch", {
|
|
36819
|
+
tags: { deviceId },
|
|
36820
|
+
meta: {
|
|
36821
|
+
sessionId,
|
|
36822
|
+
deviceId,
|
|
36823
|
+
tier,
|
|
36824
|
+
error: require_dist.errMsg(err)
|
|
36825
|
+
}
|
|
36826
|
+
});
|
|
36827
|
+
return {
|
|
36828
|
+
kind: "refused",
|
|
36829
|
+
reason: "tier-unavailable"
|
|
36830
|
+
};
|
|
36831
|
+
}
|
|
36832
|
+
if (this.sessions.get(sessionId) !== entry) return { kind: "abandoned" };
|
|
36833
|
+
const targetId = this.selectBestBroker(String(deviceId), { prefersTier: tier });
|
|
36834
|
+
const target = this.brokers.get(targetId);
|
|
36835
|
+
const landed = this.profileTierResolver?.(targetId) ?? null;
|
|
36836
|
+
if (target !== void 0 && targetId === entry.brokerId) {
|
|
36837
|
+
if (landed !== null) entry.adaptiveController?.settleOn(landed, Date.now());
|
|
36838
|
+
rebind.sessionLogger.info("adaptive: wanted tier is not served — the session stays on its rung", {
|
|
36839
|
+
tags: { deviceId },
|
|
36840
|
+
meta: {
|
|
36841
|
+
sessionId,
|
|
36842
|
+
deviceId,
|
|
36843
|
+
tier,
|
|
36844
|
+
landed,
|
|
36845
|
+
brokerId: targetId,
|
|
36846
|
+
cause: why
|
|
36847
|
+
}
|
|
36848
|
+
});
|
|
36849
|
+
return { kind: "committed" };
|
|
36850
|
+
}
|
|
36851
|
+
const judge = () => judgeInPlaceSwitch(this.inPlaceSessionShape(entry, rebind), this.inPlaceTargetShape(rebind, landed, target));
|
|
36852
|
+
const refusal = judge();
|
|
36853
|
+
if (refusal !== null || target === void 0) return {
|
|
36854
|
+
kind: "refused",
|
|
36855
|
+
reason: refusal ?? "tier-unavailable"
|
|
36856
|
+
};
|
|
36857
|
+
const fromBrokerId = entry.brokerId;
|
|
36858
|
+
const startedAt = Date.now();
|
|
36859
|
+
rebind.sessionLogger.info("adaptive: tier switch staying on the session — waiting for a keyframe", {
|
|
36860
|
+
tags: { deviceId },
|
|
36861
|
+
meta: {
|
|
36862
|
+
sessionId,
|
|
36863
|
+
deviceId,
|
|
36864
|
+
tier,
|
|
36865
|
+
fromBrokerId,
|
|
36866
|
+
toBrokerId: targetId,
|
|
36867
|
+
cause: why
|
|
36868
|
+
}
|
|
36869
|
+
});
|
|
36870
|
+
return new Promise((resolve) => {
|
|
36871
|
+
const forward = makeSourceRtpForwarder({
|
|
36872
|
+
session: entry.session,
|
|
36873
|
+
sessionLogger: rebind.sessionLogger,
|
|
36874
|
+
sessionId,
|
|
36875
|
+
deviceId,
|
|
36876
|
+
brokerId: targetId,
|
|
36877
|
+
sessionCodec: rebind.sessionCodec
|
|
36878
|
+
});
|
|
36879
|
+
const pending = bindPendingSource({
|
|
36880
|
+
broker: target,
|
|
36881
|
+
attribution: {
|
|
36882
|
+
...rebind.baseAttribution,
|
|
36883
|
+
media: "video",
|
|
36884
|
+
targetCodec: rebind.sessionCodec,
|
|
36885
|
+
transport: "repacketize"
|
|
36886
|
+
},
|
|
36887
|
+
timeoutMs: IN_PLACE_SWITCH_KEYFRAME_WAIT_MS,
|
|
36888
|
+
commit: () => {
|
|
36889
|
+
if (this.sessions.get(sessionId) !== entry || entry.pendingSwitch !== handle) {
|
|
36890
|
+
resolve({ kind: "abandoned" });
|
|
36891
|
+
return false;
|
|
36892
|
+
}
|
|
36893
|
+
entry.pendingSwitch = void 0;
|
|
36894
|
+
const late = judge();
|
|
36895
|
+
if (late !== null) {
|
|
36896
|
+
resolve({
|
|
36897
|
+
kind: "refused",
|
|
36898
|
+
reason: late
|
|
36899
|
+
});
|
|
36900
|
+
return false;
|
|
36901
|
+
}
|
|
36902
|
+
if (landed !== null && landed !== tier) entry.adaptiveController?.settleOn(landed, Date.now());
|
|
36903
|
+
this.commitInPlaceSwitch(entry, rebind, {
|
|
36904
|
+
sessionId,
|
|
36905
|
+
tier: landed ?? tier,
|
|
36906
|
+
targetId,
|
|
36907
|
+
target,
|
|
36908
|
+
unsubRtp: pending.unsubscribe,
|
|
36909
|
+
waitedMs: Date.now() - startedAt,
|
|
36910
|
+
cause: why
|
|
36911
|
+
});
|
|
36912
|
+
resolve({ kind: "committed" });
|
|
36913
|
+
return true;
|
|
36914
|
+
},
|
|
36915
|
+
forward,
|
|
36916
|
+
onTimeout: () => {
|
|
36917
|
+
if (entry.pendingSwitch === handle) entry.pendingSwitch = void 0;
|
|
36918
|
+
resolve({
|
|
36919
|
+
kind: "refused",
|
|
36920
|
+
reason: "target-no-keyframe"
|
|
36921
|
+
});
|
|
36922
|
+
}
|
|
36923
|
+
});
|
|
36924
|
+
const handle = {
|
|
36925
|
+
tier,
|
|
36926
|
+
cancel: () => {
|
|
36927
|
+
pending.unsubscribe();
|
|
36928
|
+
resolve({ kind: "abandoned" });
|
|
36929
|
+
}
|
|
36930
|
+
};
|
|
36931
|
+
entry.pendingSwitch = handle;
|
|
36932
|
+
});
|
|
36933
|
+
}
|
|
36934
|
+
/**
|
|
36935
|
+
* Swap every subscription of `entry` onto `target` in one synchronous step,
|
|
36936
|
+
* inside the delivery of the target's committing packet: nothing from the old
|
|
36937
|
+
* broker can reach the track after this returns, and the target's next
|
|
36938
|
+
* forward replays its GOP from the keyframe onto a rebased RTP stream.
|
|
36939
|
+
*/
|
|
36940
|
+
commitInPlaceSwitch(entry, rebind, sw) {
|
|
36941
|
+
const { session } = entry;
|
|
36942
|
+
const { deviceId, sessionLogger } = rebind;
|
|
36943
|
+
const old = {
|
|
36944
|
+
brokerId: entry.brokerId,
|
|
36945
|
+
unsubBroker: entry.unsubBroker,
|
|
36946
|
+
unsubRtp: entry.unsubRtp,
|
|
36947
|
+
unsubSdpParams: entry.unsubSdpParams,
|
|
36948
|
+
unsubSourceRestart: entry.unsubSourceRestart,
|
|
36949
|
+
releaseOpusEgress: entry.releaseOpusEgress
|
|
36950
|
+
};
|
|
36951
|
+
const target = sw.target;
|
|
36952
|
+
entry.unsubBroker = target.onEncodedData((packet) => {
|
|
36953
|
+
if (packet.type === "audio") forwardBrokerAudio(packet, session, rebind.pushFrame);
|
|
36954
|
+
}, {
|
|
36955
|
+
...rebind.baseAttribution,
|
|
36956
|
+
media: "audio",
|
|
36957
|
+
transport: "passthrough"
|
|
36958
|
+
});
|
|
36959
|
+
entry.unsubRtp = sw.unsubRtp;
|
|
36960
|
+
if (old.releaseOpusEgress !== void 0 && supportsOpusEgress(target)) {
|
|
36961
|
+
target.acquireOpusEgress();
|
|
36962
|
+
entry.releaseOpusEgress = () => target.releaseOpusEgress();
|
|
36963
|
+
} else entry.releaseOpusEgress = void 0;
|
|
36964
|
+
const parameterSets = target.getSdpParameterSets();
|
|
36965
|
+
session.switchLiveSource({
|
|
36966
|
+
rtpBootstrap: () => target.getRtpPreBuffer(),
|
|
36967
|
+
parameterSets,
|
|
36968
|
+
reason: `tier ${sw.tier} (${sw.cause})`
|
|
36969
|
+
});
|
|
36970
|
+
entry.unsubSdpParams = parameterSets !== null && parameterSets.length > 0 ? null : target.onSdpParameterSets((ps) => {
|
|
36971
|
+
if (rebind.sessionCodec === "H265") session.seedH265CodecInfoFromSdp(ps);
|
|
36972
|
+
else session.seedH264CodecInfoFromSdp(ps);
|
|
36973
|
+
});
|
|
36974
|
+
entry.unsubSourceRestart = target.onSourceRestart((reason) => {
|
|
36975
|
+
sessionLogger.info("broker re-dialed the source — re-syncing session", {
|
|
36976
|
+
tags: { deviceId },
|
|
36977
|
+
meta: {
|
|
36978
|
+
sessionId: sw.sessionId,
|
|
36979
|
+
brokerId: sw.targetId,
|
|
36980
|
+
deviceId,
|
|
36981
|
+
reason
|
|
36982
|
+
}
|
|
36983
|
+
});
|
|
36984
|
+
session.onSourceRestarted(reason);
|
|
36985
|
+
});
|
|
36986
|
+
entry.brokerId = sw.targetId;
|
|
36987
|
+
old.unsubRtp?.();
|
|
36988
|
+
old.unsubBroker();
|
|
36989
|
+
old.unsubSdpParams?.();
|
|
36990
|
+
old.unsubSourceRestart();
|
|
36991
|
+
old.releaseOpusEgress?.();
|
|
36992
|
+
this.brokerLinger.cancel(sw.targetId);
|
|
36993
|
+
let oldStillServed = false;
|
|
36994
|
+
for (const other of this.sessions.values()) if (other.brokerId === old.brokerId) {
|
|
36995
|
+
oldStillServed = true;
|
|
36996
|
+
break;
|
|
36997
|
+
}
|
|
36998
|
+
if (!oldStillServed && !this.stopped) this.brokerLinger.hold(old.brokerId);
|
|
36999
|
+
const pair = session.readNominatedPair();
|
|
37000
|
+
if (pair !== null) {
|
|
37001
|
+
const remoteAddr = pair.remoteType === "host" ? pair.remoteAddr : `${pair.remoteAddr} (${pair.remoteType})`;
|
|
37002
|
+
target.updateConsumerAttributionBySession(sw.sessionId, { remoteAddr });
|
|
37003
|
+
}
|
|
37004
|
+
entry.timeline?.setLiveProfile(sw.tier);
|
|
37005
|
+
sessionLogger.info("adaptive: tier switched in place — same session, same connection", {
|
|
37006
|
+
tags: { deviceId },
|
|
37007
|
+
meta: {
|
|
37008
|
+
sessionId: sw.sessionId,
|
|
37009
|
+
deviceId,
|
|
37010
|
+
tier: sw.tier,
|
|
37011
|
+
fromBrokerId: old.brokerId,
|
|
37012
|
+
toBrokerId: sw.targetId,
|
|
37013
|
+
codec: rebind.sessionCodec,
|
|
37014
|
+
waitedMs: sw.waitedMs,
|
|
37015
|
+
cause: sw.cause,
|
|
37016
|
+
replayPackets: target.getRtpPreBuffer().length
|
|
37017
|
+
}
|
|
37018
|
+
});
|
|
37019
|
+
}
|
|
37020
|
+
/**
|
|
36394
37021
|
* Schedule an adaptive tier switch via a close-and-recreate re-offer.
|
|
36395
37022
|
*
|
|
36396
37023
|
* Records the controller's desired tier (+ forced transcode-down) in
|
|
@@ -36403,7 +37030,7 @@ var BrokerWebrtcServer = class {
|
|
|
36403
37030
|
* consumes the recorded intent to pin the recreated session to `tier` and
|
|
36404
37031
|
* apply the reduced transcode profile when `forceTranscodeDown` is set.
|
|
36405
37032
|
*/
|
|
36406
|
-
|
|
37033
|
+
scheduleTierReoffer(entry, deviceId, tier, sessionId, forceTranscodeDown = false, decision) {
|
|
36407
37034
|
const prev = this.adaptiveIntent.get(sessionId);
|
|
36408
37035
|
const failures = new Map(entry.adaptiveController?.failureCounts ?? prev?.failures);
|
|
36409
37036
|
const upgradeCount = entry.adaptiveController?.successfulUpgradeCount ?? prev?.upgradeCount ?? 0;
|
|
@@ -36451,6 +37078,8 @@ var BrokerWebrtcServer = class {
|
|
|
36451
37078
|
const entry = this.sessions.get(sessionId);
|
|
36452
37079
|
if (!entry) return;
|
|
36453
37080
|
this.sessions.delete(sessionId);
|
|
37081
|
+
entry.pendingSwitch?.cancel();
|
|
37082
|
+
entry.pendingSwitch = void 0;
|
|
36454
37083
|
let brokerStillServed = false;
|
|
36455
37084
|
for (const other of this.sessions.values()) if (other.brokerId === entry.brokerId) {
|
|
36456
37085
|
brokerStillServed = true;
|
|
@@ -37110,6 +37739,10 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
|
|
|
37110
37739
|
async getSessionState(input) {
|
|
37111
37740
|
return this.webrtcServer.getSessionRenegotiationState(input.sessionId);
|
|
37112
37741
|
}
|
|
37742
|
+
/** The viewer's downlink probe for one session (D733) — see the cap's docblock. */
|
|
37743
|
+
async applyDownlinkProbe(input) {
|
|
37744
|
+
return this.webrtcServer.applyDownlinkProbe(input.sessionId, input.downlinkMbps);
|
|
37745
|
+
}
|
|
37113
37746
|
/**
|
|
37114
37747
|
* The SERVER half of the stream-debug overlay. Thin facade — the gathering
|
|
37115
37748
|
* lives in the WebRTC server and the shaping in the pure
|