@camstack/addon-pipeline 1.2.329 → 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-ChH7Y219.js → default-detection-model-D3SnO2hN.js} +39 -67
- package/dist/{default-detection-model-j0nBpRoB.mjs → default-detection-model-DELTCQgv.mjs} +39 -67
- package/dist/detection-pipeline/index.js +197 -31
- package/dist/detection-pipeline/index.mjs +197 -31
- package/dist/{dist-0HoA3wAi.mjs → dist-CNXhBzn6.mjs} +847 -439
- package/dist/{dist-BlIWhjnu.js → dist-DrUx3EGR.js} +892 -466
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-BPpVMnZy.js → node-B4qy3lnV.js} +1 -1
- package/dist/{node-HOMWRPzW.mjs → node-BmxmlYLx.mjs} +1 -1
- package/dist/pipeline-runner/index.js +355 -104
- package/dist/pipeline-runner/index.mjs +355 -104
- package/dist/{process-memory-DuF8pa2w.mjs → process-memory-BGc5PzZI.mjs} +1 -1
- package/dist/{process-memory-BW06vOIQ.js → process-memory-CRtWJbjV.js} +1 -1
- package/dist/recorder/index.js +7 -7
- package/dist/recorder/index.mjs +7 -7
- package/dist/{segment-demux-js-Dl52Zz8w.js → segment-demux-js-CpSgZmSR.js} +2 -2
- package/dist/{segment-demux-js-BU2MXpln.mjs → segment-demux-js-VqsLAgQ0.mjs} +2 -2
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-vNfS65In.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-Ciacp783.mjs → hostInit-CF_2Rlu0.mjs} +2 -2
- package/dist/stream-broker/index.js +713 -80
- package/dist/stream-broker/index.mjs +713 -80
- 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-DfeHvW4k.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-BpXPNINd.mjs +0 -26
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $ as cameraStreamsCapability,
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
2
|
+
import { $ as cameraStreamsCapability, $n as union, $t as resolveEgressDecodeHwAccel, At as logChannelsCapability, C as EncodeProfileSchema, Cn as errMsg, Ct as gifFromMp4Args, Dn as CamProfileSchema, E as G711_SCALE_CORRECTION_DB, En as CAM_PROFILE_ORDER, F as RATE_CONTROL_TIGHT, Fn as makeProfileBrokerId, Ft as memoizePinnedFfmpeg, Gn as discriminatedUnion, H as RingBuffer, Hn as _enum, In as makeSourceBrokerId, Jn as number, K as addonWidgetsSourceCapability, Kt as rankStreams, Ln as nodePin, Mn as expandAudioChunkToF32le, Mt as maskUrlCredentials, P as RATE_CONTROL_RELAXED, Pn as isEvent, Qn as string, Sn as DeviceType, St as gifDeriveSpeed, Tn as BaseAddon, Un as array, Wn as boolean, Yn as object, Z as batteryCapability, Zn as record, _n as buildFfmpegArgs$1, at as declareLogChannel, bn as isSoftwareDecode, bt as ffmpegRate, c as CLIP_STILL_REASON_CODE_HEADER, er as EventCategory, ft as egressTranscodeSharingKey, it as createLogChannelsProvider, kn as createEvent, kt as loadContributionCapability, ln as streamBrokerCapability, mn as AUDIO_PRESETS, o as BatteryStatusSchema, pn as webrtcSessionCapability, pt as egressTransportFromRequest, qn as literal, rt as createHwAccelCache, s as CLIP_BROKER_PACED_RATES, st as deriveBatteryPresence, tt as clipExportFailureOf, wn as BOOT_RECOVERY_BACKOFF_MS, xn as DeviceFeature, yn as invocationFromEncodeProfile, zn as parseProfileBrokerId } from "../dist-CNXhBzn6.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-BmxmlYLx.mjs";
|
|
4
4
|
import { a as parseRestreamPath, d as PREBUFFER_MAX_MS, f as capPreRollRequestMs, i as isLiveEdgeOnlyDial, m as resolvePrebuffer, n as NO_DECLARATION, p as prebufferRetentionMs, s as sdpVariantFor, u as withSentryIntent } from "../restream-intent-DyGmLAU9.mjs";
|
|
5
5
|
import { n as profileForStreamId } from "../remote-restream-BeHi78PZ.mjs";
|
|
6
6
|
import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
|
|
7
|
-
import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
7
|
+
import { a as require_mp4box_all, c as parseNalLengthSize, i as runFfmpegOnce, n as demuxSegmentPreferJs, o as avccSampleToAnnexB, r as FfmpegFallbackRefusedError, s as buildKeyframeAnnexB, t as demuxSegmentFullJs } from "../segment-demux-js-VqsLAgQ0.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -24418,7 +24418,9 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
24418
24418
|
* tier when loss persists — smaller packets survive lossy links better.
|
|
24419
24419
|
* Must stay below H264_REPACKETIZER_MTU (1100). */
|
|
24420
24420
|
static ADAPTIVE_LOW_MTU = 900;
|
|
24421
|
-
/** Broker source-RTP pre-buffer accessor + one-shot replay guard.
|
|
24421
|
+
/** Broker source-RTP pre-buffer accessor + one-shot replay guard. Replaced
|
|
24422
|
+
* by `switchLiveSource` when an adaptive tier change moves the track onto
|
|
24423
|
+
* another broker in place. */
|
|
24422
24424
|
rtpBootstrap;
|
|
24423
24425
|
rtpBootstrapDone = false;
|
|
24424
24426
|
/**
|
|
@@ -24513,6 +24515,46 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
24513
24515
|
reason
|
|
24514
24516
|
} });
|
|
24515
24517
|
}
|
|
24518
|
+
/**
|
|
24519
|
+
* Move the live track onto ANOTHER source of the same codec, in place — an
|
|
24520
|
+
* adaptive tier change on the session the viewer already has (D733).
|
|
24521
|
+
*
|
|
24522
|
+
* Everything `onSourceRestarted` does for a re-dial, because a different
|
|
24523
|
+
* rung IS a different RTP stream (new SSRC, new sequence/timestamp base, new
|
|
24524
|
+
* parameter sets), plus the two things only a different broker changes:
|
|
24525
|
+
* - the bootstrap accessor now reads the NEW broker's pre-buffer, so the
|
|
24526
|
+
* next forwarded packet replays its current GOP from the keyframe — the
|
|
24527
|
+
* decoder switches on an IDR instead of waiting for the camera's next one;
|
|
24528
|
+
* - the repacketizer is re-seeded with the new rung's SDP parameter sets,
|
|
24529
|
+
* for a camera that publishes them only in SDP.
|
|
24530
|
+
* The MTU returns to the default a freshly built session would have: the
|
|
24531
|
+
* reduced MTU belongs to the lowest rung's loss recovery, not to the session.
|
|
24532
|
+
*/
|
|
24533
|
+
switchLiveSource(next) {
|
|
24534
|
+
if (this.closed) return;
|
|
24535
|
+
this.rtpBootstrap = next.rtpBootstrap;
|
|
24536
|
+
this.activeRtpReplay?.abort();
|
|
24537
|
+
this.activeRtpReplay = null;
|
|
24538
|
+
this.h264Repacketizer = null;
|
|
24539
|
+
this.h265Repacketizer = null;
|
|
24540
|
+
this.sourceVideoSsrc = null;
|
|
24541
|
+
this.rtpRebasePending = true;
|
|
24542
|
+
this.rtpBootstrapDone = false;
|
|
24543
|
+
this.repacketizerMtu = AdaptiveSession.H264_REPACKETIZER_MTU;
|
|
24544
|
+
const ps = next.parameterSets ?? [];
|
|
24545
|
+
if (ps.length > 0) if (this.sourceCodec === "H265") this.seedH265CodecInfoFromSdp(ps);
|
|
24546
|
+
else this.seedH264CodecInfoFromSdp(ps);
|
|
24547
|
+
this.logger.info("live source switched in place — replaying the new source from its keyframe", {
|
|
24548
|
+
tags: { deviceId: this.deviceId },
|
|
24549
|
+
meta: {
|
|
24550
|
+
phase: "session",
|
|
24551
|
+
sessionId: this.sessionId,
|
|
24552
|
+
deviceId: this.deviceId,
|
|
24553
|
+
reason: next.reason,
|
|
24554
|
+
seededParamSets: ps.length
|
|
24555
|
+
}
|
|
24556
|
+
});
|
|
24557
|
+
}
|
|
24516
24558
|
/** Drop the repacketizer + bootstrap/sync state so a newly bound source
|
|
24517
24559
|
* stream re-seeds its SPS/PPS and replays instant-start cleanly. */
|
|
24518
24560
|
resetRepacketizer() {
|
|
@@ -27205,6 +27247,32 @@ var AdaptiveController = class {
|
|
|
27205
27247
|
onRtt(rttMs, now) {
|
|
27206
27248
|
this.rtt.observe(rttMs, now);
|
|
27207
27249
|
}
|
|
27250
|
+
/**
|
|
27251
|
+
* The session now serves `tier`, decided OUTSIDE this ladder's own tick
|
|
27252
|
+
* (D733): the viewer's downlink probe placed it there, or the rung the ladder
|
|
27253
|
+
* asked for is not served and the switch landed on the one that is.
|
|
27254
|
+
*
|
|
27255
|
+
* Before D733 both were a close-and-recreate re-offer, which built a NEW
|
|
27256
|
+
* controller on the rung the session landed on. The ladder ends up where that
|
|
27257
|
+
* new controller started: on `tier`, not on trial (a probe IS capacity
|
|
27258
|
+
* evidence; a fallback rung was not tried), with its healthy clock and change
|
|
27259
|
+
* blackout starting now, and with no promotion counted. The measured-failure
|
|
27260
|
+
* memory is kept: a rung this session already proved unusable stays backed off.
|
|
27261
|
+
*/
|
|
27262
|
+
settleOn(tier, now) {
|
|
27263
|
+
this.armed = true;
|
|
27264
|
+
this.hold = null;
|
|
27265
|
+
this.tier = tier;
|
|
27266
|
+
this.mtuReduced = false;
|
|
27267
|
+
this.transcodeEngaged = false;
|
|
27268
|
+
this.trial = null;
|
|
27269
|
+
this.tryCarry = {
|
|
27270
|
+
...this.tryCarry,
|
|
27271
|
+
trialFrom: null
|
|
27272
|
+
};
|
|
27273
|
+
this.commitChange(now);
|
|
27274
|
+
this.healthySince = now;
|
|
27275
|
+
}
|
|
27208
27276
|
/** Why the ladder did not climb on the last evaluation; null = at the top, or it just moved. */
|
|
27209
27277
|
get climbHold() {
|
|
27210
27278
|
return this.hold;
|
|
@@ -31227,6 +31295,8 @@ var TimelineSession = class {
|
|
|
31227
31295
|
* paused across that required quality transition. */
|
|
31228
31296
|
rate = 1;
|
|
31229
31297
|
liveProfileReported = false;
|
|
31298
|
+
/** The live rung after an in-place tier switch; undefined = `initialLiveProfile`. */
|
|
31299
|
+
liveProfile = void 0;
|
|
31230
31300
|
/**
|
|
31231
31301
|
* The last position the viewer was SHOWN (every `_emitPosition`). Handed to
|
|
31232
31302
|
* `seek` as `travelFromMs` so the seek travel animation works across feeder
|
|
@@ -31701,11 +31771,29 @@ var TimelineSession = class {
|
|
|
31701
31771
|
}));
|
|
31702
31772
|
}
|
|
31703
31773
|
emitLiveProfile() {
|
|
31704
|
-
|
|
31774
|
+
const profile = this.liveProfile ?? this.deps.initialLiveProfile;
|
|
31775
|
+
if (this.liveProfileReported || profile === void 0) return;
|
|
31705
31776
|
this.liveProfileReported = true;
|
|
31706
31777
|
this.deps.session.sendControl(serializeServerMessage({
|
|
31707
31778
|
t: "liveProfile",
|
|
31708
|
-
profile
|
|
31779
|
+
profile
|
|
31780
|
+
}));
|
|
31781
|
+
}
|
|
31782
|
+
/**
|
|
31783
|
+
* The live rung moved under this coordinator — an adaptive tier switch that
|
|
31784
|
+
* stayed on the session (D733). Before D733 a tier change built a new
|
|
31785
|
+
* coordinator, which reported its own `initialLiveProfile`; now the same one
|
|
31786
|
+
* outlives the switch and must say what the viewer is watching. Reported at
|
|
31787
|
+
* once when the viewer has already been told a profile, otherwise on its
|
|
31788
|
+
* first control message as before.
|
|
31789
|
+
*/
|
|
31790
|
+
setLiveProfile(profile) {
|
|
31791
|
+
if (this.liveProfile === profile) return;
|
|
31792
|
+
this.liveProfile = profile;
|
|
31793
|
+
if (!this.liveProfileReported) return;
|
|
31794
|
+
this.deps.session.sendControl(serializeServerMessage({
|
|
31795
|
+
t: "liveProfile",
|
|
31796
|
+
profile
|
|
31709
31797
|
}));
|
|
31710
31798
|
}
|
|
31711
31799
|
dispose() {
|
|
@@ -34586,7 +34674,7 @@ async function demuxSegmentAudioOpusPreferJs(bytes, opts) {
|
|
|
34586
34674
|
if (direct) return direct;
|
|
34587
34675
|
} catch {}
|
|
34588
34676
|
if (opts.ffmpegPath === null) {
|
|
34589
|
-
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment) and the ffmpeg fallback is refused — no pinned ffmpeg on this node
|
|
34677
|
+
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment) and the ffmpeg fallback is refused — no pinned ffmpeg on this node");
|
|
34590
34678
|
return [];
|
|
34591
34679
|
}
|
|
34592
34680
|
opts.onFfmpegFallback?.("audio track is not Opus (legacy fragment)");
|
|
@@ -34628,6 +34716,98 @@ function preferLiveAdaptiveCandidates(candidates) {
|
|
|
34628
34716
|
return natives.length > 0 ? natives : [...candidates];
|
|
34629
34717
|
}
|
|
34630
34718
|
//#endregion
|
|
34719
|
+
//#region src/stream-broker/webrtc/in-place-tier-switch.ts
|
|
34720
|
+
/**
|
|
34721
|
+
* How long a switch waits for the new rendition's first switchable keyframe.
|
|
34722
|
+
* A warm broker commits on its next packet; a cold one needs its dial (2–3 s on
|
|
34723
|
+
* this hub) plus the camera's next IDR (≤ 4 s at the GOPs measured). 8 s covers
|
|
34724
|
+
* both; past it the old source has been shown the whole time, so nothing froze.
|
|
34725
|
+
*/
|
|
34726
|
+
var IN_PLACE_SWITCH_KEYFRAME_WAIT_MS = 8e3;
|
|
34727
|
+
/** Map a broker's codec string to the session codec it would negotiate; null = unknown. */
|
|
34728
|
+
function sessionCodecOf(brokerCodec) {
|
|
34729
|
+
if (brokerCodec === void 0 || brokerCodec === "") return null;
|
|
34730
|
+
const c = brokerCodec.toLowerCase();
|
|
34731
|
+
return c === "h265" || c === "hevc" ? "H265" : "H264";
|
|
34732
|
+
}
|
|
34733
|
+
/**
|
|
34734
|
+
* Whether the switch may stay on the session; the refusal names why not.
|
|
34735
|
+
* Facts not known yet (a cold target's codec or audio) do not refuse — the
|
|
34736
|
+
* judgement is repeated at commit time, when they are.
|
|
34737
|
+
*/
|
|
34738
|
+
function judgeInPlaceSwitch(session, target) {
|
|
34739
|
+
if (session.recorded) return "recorded";
|
|
34740
|
+
if (session.forceTranscodeDown) return "transcode-down";
|
|
34741
|
+
if (!session.useRtpRepacketizer) return "not-rtp-passthrough";
|
|
34742
|
+
if (session.needsTranscode) return "transcode";
|
|
34743
|
+
if (!target.tierMatches) return "tier-unavailable";
|
|
34744
|
+
if (!target.isRtp) return "target-not-rtp";
|
|
34745
|
+
if (target.codec !== null && target.codec !== session.sessionCodec) return "codec-differs";
|
|
34746
|
+
if (target.needsBaselineTranscode) return "target-needs-transcode";
|
|
34747
|
+
if (target.audioCodec !== void 0 && !sameAudioCodec(session.audioCodec, target.audioCodec)) return "audio-codec-differs";
|
|
34748
|
+
if (session.usesOpusEgress && !target.supportsOpusEgress) return "opus-egress-unsupported";
|
|
34749
|
+
return null;
|
|
34750
|
+
}
|
|
34751
|
+
function sameAudioCodec(a, b) {
|
|
34752
|
+
return (a ?? "").toLowerCase() === (b ?? "").toLowerCase();
|
|
34753
|
+
}
|
|
34754
|
+
/**
|
|
34755
|
+
* Subscribe `broker`'s RTP while the session's current source keeps feeding,
|
|
34756
|
+
* and commit when the new rendition can start on a keyframe.
|
|
34757
|
+
*
|
|
34758
|
+
* ONE subscription goes from pending to forwarding — never a second one added
|
|
34759
|
+
* during the delivery of the committing packet, which a broker iterating its
|
|
34760
|
+
* subscriber set would hand the same packet again (a duplicate behind the
|
|
34761
|
+
* replay). A warm-up consumer keeps a cold broker's dial up while pending and
|
|
34762
|
+
* is released only after the commit has attached the session's own consumers.
|
|
34763
|
+
*/
|
|
34764
|
+
function bindPendingSource(opts) {
|
|
34765
|
+
let state = "pending";
|
|
34766
|
+
let warm = opts.broker.onEncodedData(() => {}, {
|
|
34767
|
+
kind: "warmup",
|
|
34768
|
+
media: "both"
|
|
34769
|
+
});
|
|
34770
|
+
const releaseWarm = () => {
|
|
34771
|
+
warm?.();
|
|
34772
|
+
warm = null;
|
|
34773
|
+
};
|
|
34774
|
+
let unsubRtp = null;
|
|
34775
|
+
const timer = setTimeout(() => {
|
|
34776
|
+
if (state !== "pending") return;
|
|
34777
|
+
state = "gone";
|
|
34778
|
+
unsubRtp?.();
|
|
34779
|
+
releaseWarm();
|
|
34780
|
+
opts.onTimeout();
|
|
34781
|
+
}, opts.timeoutMs);
|
|
34782
|
+
unsubRtp = opts.broker.onVideoRtp((rtp) => {
|
|
34783
|
+
if (state === "gone") return;
|
|
34784
|
+
if (state === "committed") {
|
|
34785
|
+
opts.forward(rtp);
|
|
34786
|
+
return;
|
|
34787
|
+
}
|
|
34788
|
+
if (opts.broker.getRtpPreBuffer().length === 0) return;
|
|
34789
|
+
clearTimeout(timer);
|
|
34790
|
+
if (!opts.commit()) {
|
|
34791
|
+
state = "gone";
|
|
34792
|
+
unsubRtp?.();
|
|
34793
|
+
releaseWarm();
|
|
34794
|
+
return;
|
|
34795
|
+
}
|
|
34796
|
+
state = "committed";
|
|
34797
|
+
releaseWarm();
|
|
34798
|
+
opts.forward(rtp);
|
|
34799
|
+
}, opts.attribution);
|
|
34800
|
+
return {
|
|
34801
|
+
unsubscribe: () => {
|
|
34802
|
+
if (state === "pending") clearTimeout(timer);
|
|
34803
|
+
state = "gone";
|
|
34804
|
+
unsubRtp?.();
|
|
34805
|
+
releaseWarm();
|
|
34806
|
+
},
|
|
34807
|
+
committed: () => state === "committed"
|
|
34808
|
+
};
|
|
34809
|
+
}
|
|
34810
|
+
//#endregion
|
|
34631
34811
|
//#region src/stream-broker/webrtc/adaptive-tier.ts
|
|
34632
34812
|
var RANK = {
|
|
34633
34813
|
low: 1,
|
|
@@ -34807,6 +34987,107 @@ function adaptivePickLogLine(reason) {
|
|
|
34807
34987
|
if (reason === "default-low") return "Adaptive source selected (no client hints — smallest available)";
|
|
34808
34988
|
return "Adaptive source selected";
|
|
34809
34989
|
}
|
|
34990
|
+
/**
|
|
34991
|
+
* One broker audio packet onto a session's push source — the per-session codec
|
|
34992
|
+
* branch (mirrors the live/recorded split). The broker fans out BOTH a
|
|
34993
|
+
* PCMU/PCMA stream (always) and, when an Opus session is attached, a parallel
|
|
34994
|
+
* `codec:'opus'` stream. Forward only the stream matching THIS session's
|
|
34995
|
+
* negotiated codec; drop the other so a PCMU-fallback session never sees Opus
|
|
34996
|
+
* and vice-versa. Shared by the session's first broker and by every broker an
|
|
34997
|
+
* in-place tier switch moves it to (D733).
|
|
34998
|
+
*/
|
|
34999
|
+
function forwardBrokerAudio(packet, session, pushFrame) {
|
|
35000
|
+
const pktCodec = packet.codec?.toLowerCase();
|
|
35001
|
+
if (session?.isOpusAudio === true) {
|
|
35002
|
+
if (pktCodec !== "opus") return;
|
|
35003
|
+
pushFrame({
|
|
35004
|
+
type: "audio",
|
|
35005
|
+
frame: {
|
|
35006
|
+
data: packet.data,
|
|
35007
|
+
codec: "Opus",
|
|
35008
|
+
sampleRate: 48e3,
|
|
35009
|
+
channels: 2,
|
|
35010
|
+
timestampMicros: packet.pts * 1e3
|
|
35011
|
+
}
|
|
35012
|
+
});
|
|
35013
|
+
return;
|
|
35014
|
+
}
|
|
35015
|
+
if (pktCodec === "opus") return;
|
|
35016
|
+
pushFrame({
|
|
35017
|
+
type: "audio",
|
|
35018
|
+
frame: {
|
|
35019
|
+
data: packet.data,
|
|
35020
|
+
codec: pktCodec === "pcma" ? "Pcma" : "Pcmu",
|
|
35021
|
+
sampleRate: 8e3,
|
|
35022
|
+
channels: 1,
|
|
35023
|
+
timestampMicros: packet.pts * 1e3
|
|
35024
|
+
}
|
|
35025
|
+
});
|
|
35026
|
+
}
|
|
35027
|
+
/**
|
|
35028
|
+
* The `onVideoRtp` callback of an RTP-passthrough session: source RTP through
|
|
35029
|
+
* the codec repacketizer, unless a transcode owns the wire or recorded playback
|
|
35030
|
+
* owns the track. One implementation for the session's first broker and for
|
|
35031
|
+
* every broker an in-place tier switch moves it to (D733).
|
|
35032
|
+
*/
|
|
35033
|
+
function makeSourceRtpForwarder(input) {
|
|
35034
|
+
const { session, sessionLogger, sessionId, deviceId, brokerId, sessionCodec } = input;
|
|
35035
|
+
let firstRtpForwarded = false;
|
|
35036
|
+
let gateDropLogged = false;
|
|
35037
|
+
let forwardAfterGateLogged = false;
|
|
35038
|
+
return (rtpData) => {
|
|
35039
|
+
if (session.needsTranscode) return;
|
|
35040
|
+
if (session.isLiveFeedGated) {
|
|
35041
|
+
if (!gateDropLogged) {
|
|
35042
|
+
gateDropLogged = true;
|
|
35043
|
+
forwardAfterGateLogged = false;
|
|
35044
|
+
sessionLogger.info("recorded: live RTP GATED (dropping live video)", {
|
|
35045
|
+
tags: { deviceId },
|
|
35046
|
+
meta: {
|
|
35047
|
+
sessionId,
|
|
35048
|
+
deviceId
|
|
35049
|
+
}
|
|
35050
|
+
});
|
|
35051
|
+
}
|
|
35052
|
+
return;
|
|
35053
|
+
}
|
|
35054
|
+
if (gateDropLogged && !forwardAfterGateLogged) {
|
|
35055
|
+
forwardAfterGateLogged = true;
|
|
35056
|
+
gateDropLogged = false;
|
|
35057
|
+
sessionLogger.info("recorded: live RTP UNGATED (forwarding live video again)", {
|
|
35058
|
+
tags: { deviceId },
|
|
35059
|
+
meta: {
|
|
35060
|
+
sessionId,
|
|
35061
|
+
deviceId
|
|
35062
|
+
}
|
|
35063
|
+
});
|
|
35064
|
+
}
|
|
35065
|
+
if (!firstRtpForwarded) {
|
|
35066
|
+
firstRtpForwarded = true;
|
|
35067
|
+
sessionLogger.info("RTP session: first source RTP forwarded to repacketizer", {
|
|
35068
|
+
tags: { deviceId },
|
|
35069
|
+
meta: {
|
|
35070
|
+
sessionId,
|
|
35071
|
+
brokerId,
|
|
35072
|
+
codec: sessionCodec,
|
|
35073
|
+
bytes: rtpData.length
|
|
35074
|
+
}
|
|
35075
|
+
});
|
|
35076
|
+
}
|
|
35077
|
+
try {
|
|
35078
|
+
session.forwardSourceRtpVideo(rtpData);
|
|
35079
|
+
} catch (err) {
|
|
35080
|
+
sessionLogger.warn("forwardSourceRtpVideo threw", {
|
|
35081
|
+
tags: { deviceId },
|
|
35082
|
+
meta: {
|
|
35083
|
+
sessionId,
|
|
35084
|
+
codec: sessionCodec,
|
|
35085
|
+
error: errMsg(err)
|
|
35086
|
+
}
|
|
35087
|
+
});
|
|
35088
|
+
}
|
|
35089
|
+
};
|
|
35090
|
+
}
|
|
34810
35091
|
/** Push-queue overflow purges are aggregated and reported at most this often. */
|
|
34811
35092
|
var FRAME_DROP_LOG_INTERVAL_MS = 3e4;
|
|
34812
35093
|
/**
|
|
@@ -35432,34 +35713,7 @@ var BrokerWebrtcServer = class {
|
|
|
35432
35713
|
timestampMicros: packet.pts * 1e3
|
|
35433
35714
|
}
|
|
35434
35715
|
});
|
|
35435
|
-
} else if (packet.type === "audio")
|
|
35436
|
-
const pktCodec = packet.codec?.toLowerCase();
|
|
35437
|
-
if (session?.isOpusAudio === true) {
|
|
35438
|
-
if (pktCodec !== "opus") return;
|
|
35439
|
-
pushFrame({
|
|
35440
|
-
type: "audio",
|
|
35441
|
-
frame: {
|
|
35442
|
-
data: packet.data,
|
|
35443
|
-
codec: "Opus",
|
|
35444
|
-
sampleRate: 48e3,
|
|
35445
|
-
channels: 2,
|
|
35446
|
-
timestampMicros: packet.pts * 1e3
|
|
35447
|
-
}
|
|
35448
|
-
});
|
|
35449
|
-
} else {
|
|
35450
|
-
if (pktCodec === "opus") return;
|
|
35451
|
-
pushFrame({
|
|
35452
|
-
type: "audio",
|
|
35453
|
-
frame: {
|
|
35454
|
-
data: packet.data,
|
|
35455
|
-
codec: pktCodec === "pcma" ? "Pcma" : "Pcmu",
|
|
35456
|
-
sampleRate: 8e3,
|
|
35457
|
-
channels: 1,
|
|
35458
|
-
timestampMicros: packet.pts * 1e3
|
|
35459
|
-
}
|
|
35460
|
-
});
|
|
35461
|
-
}
|
|
35462
|
-
}
|
|
35716
|
+
} else if (packet.type === "audio") forwardBrokerAudio(packet, session, pushFrame);
|
|
35463
35717
|
}, annexbAttribution);
|
|
35464
35718
|
warmUpUnsubscribe?.();
|
|
35465
35719
|
this.brokerLinger.cancel(brokerId);
|
|
@@ -35580,49 +35834,14 @@ var BrokerWebrtcServer = class {
|
|
|
35580
35834
|
}
|
|
35581
35835
|
});
|
|
35582
35836
|
}
|
|
35583
|
-
|
|
35584
|
-
|
|
35585
|
-
|
|
35586
|
-
|
|
35587
|
-
|
|
35588
|
-
|
|
35589
|
-
|
|
35590
|
-
|
|
35591
|
-
forwardAfterGateLogged = false;
|
|
35592
|
-
sessionLogger.info("recorded: live RTP GATED (dropping live video)", { meta: {
|
|
35593
|
-
sessionId,
|
|
35594
|
-
deviceId
|
|
35595
|
-
} });
|
|
35596
|
-
}
|
|
35597
|
-
return;
|
|
35598
|
-
}
|
|
35599
|
-
if (gateDropLogged && !forwardAfterGateLogged) {
|
|
35600
|
-
forwardAfterGateLogged = true;
|
|
35601
|
-
gateDropLogged = false;
|
|
35602
|
-
sessionLogger.info("recorded: live RTP UNGATED (forwarding live video again)", { meta: {
|
|
35603
|
-
sessionId,
|
|
35604
|
-
deviceId
|
|
35605
|
-
} });
|
|
35606
|
-
}
|
|
35607
|
-
if (!firstRtpForwarded) {
|
|
35608
|
-
firstRtpForwarded = true;
|
|
35609
|
-
sessionLogger.info("RTP session: first source RTP forwarded to repacketizer", { meta: {
|
|
35610
|
-
sessionId,
|
|
35611
|
-
brokerId,
|
|
35612
|
-
codec: sessionCodec,
|
|
35613
|
-
bytes: rtpData.length
|
|
35614
|
-
} });
|
|
35615
|
-
}
|
|
35616
|
-
try {
|
|
35617
|
-
session.forwardSourceRtpVideo(rtpData);
|
|
35618
|
-
} catch (err) {
|
|
35619
|
-
sessionLogger.warn("forwardSourceRtpVideo threw", { meta: {
|
|
35620
|
-
sessionId,
|
|
35621
|
-
codec: sessionCodec,
|
|
35622
|
-
error: errMsg(err)
|
|
35623
|
-
} });
|
|
35624
|
-
}
|
|
35625
|
-
}, rtpAttribution);
|
|
35837
|
+
unsubRtp = broker.onVideoRtp(makeSourceRtpForwarder({
|
|
35838
|
+
session,
|
|
35839
|
+
sessionLogger,
|
|
35840
|
+
sessionId,
|
|
35841
|
+
deviceId,
|
|
35842
|
+
brokerId,
|
|
35843
|
+
sessionCodec
|
|
35844
|
+
}), rtpAttribution);
|
|
35626
35845
|
}
|
|
35627
35846
|
const unsubSourceRestart = broker.onSourceRestart((reason) => {
|
|
35628
35847
|
sessionLogger.info("broker re-dialed the source — re-syncing session", { meta: {
|
|
@@ -35648,7 +35867,17 @@ var BrokerWebrtcServer = class {
|
|
|
35648
35867
|
unsubRtp,
|
|
35649
35868
|
unsubSdpParams,
|
|
35650
35869
|
unsubSourceRestart,
|
|
35651
|
-
closeSource
|
|
35870
|
+
closeSource,
|
|
35871
|
+
rebind: {
|
|
35872
|
+
deviceId,
|
|
35873
|
+
sessionCodec,
|
|
35874
|
+
useRtpRepacketizer,
|
|
35875
|
+
offeredProfileLevelIds,
|
|
35876
|
+
hints,
|
|
35877
|
+
sessionLogger,
|
|
35878
|
+
pushFrame,
|
|
35879
|
+
baseAttribution
|
|
35880
|
+
}
|
|
35652
35881
|
};
|
|
35653
35882
|
this.sessions.set(sessionId, entry);
|
|
35654
35883
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
@@ -36375,6 +36604,88 @@ var BrokerWebrtcServer = class {
|
|
|
36375
36604
|
});
|
|
36376
36605
|
controller.onRtt(rttMs, now);
|
|
36377
36606
|
}
|
|
36607
|
+
/**
|
|
36608
|
+
* The viewer's downlink probe for ONE session (D733) — the server half of the
|
|
36609
|
+
* probe climb. The player used to answer its own probe with a second session
|
|
36610
|
+
* (a make-before-break reconnect carrying `downlinkMbps`); it now hands the
|
|
36611
|
+
* measurement to the session it already has, and the broker places that
|
|
36612
|
+
* session on the rung the probe picks, in place when the codec allows.
|
|
36613
|
+
*
|
|
36614
|
+
* The client sends a MEASUREMENT, never a tier: the pick is the same
|
|
36615
|
+
* `pickAdaptiveTier` an offer's hints get, against the same viewport the
|
|
36616
|
+
* offer carried, so a tile still ceilings to `low`. A probe only ever
|
|
36617
|
+
* raises the rung — lowering it is the ladder's job, on delivery evidence.
|
|
36618
|
+
*/
|
|
36619
|
+
applyDownlinkProbe(sessionId, downlinkMbps) {
|
|
36620
|
+
const entry = this.sessions.get(sessionId);
|
|
36621
|
+
if (entry === void 0) return {
|
|
36622
|
+
outcome: "held",
|
|
36623
|
+
tier: null,
|
|
36624
|
+
reason: "no-session"
|
|
36625
|
+
};
|
|
36626
|
+
const deviceId = deviceIdFromBrokerId(entry.brokerId);
|
|
36627
|
+
const served = this.profileTierResolver?.(entry.brokerId) ?? null;
|
|
36628
|
+
const controller = entry.adaptiveController;
|
|
36629
|
+
const rebind = entry.rebind;
|
|
36630
|
+
const now = Date.now();
|
|
36631
|
+
const held = (reason) => {
|
|
36632
|
+
this.logger.info("adaptive: downlink probe holds the session where it is", {
|
|
36633
|
+
tags: { deviceId },
|
|
36634
|
+
meta: {
|
|
36635
|
+
sessionId,
|
|
36636
|
+
deviceId,
|
|
36637
|
+
downlinkMbps,
|
|
36638
|
+
tier: served,
|
|
36639
|
+
reason
|
|
36640
|
+
}
|
|
36641
|
+
});
|
|
36642
|
+
return {
|
|
36643
|
+
outcome: "held",
|
|
36644
|
+
tier: served,
|
|
36645
|
+
reason
|
|
36646
|
+
};
|
|
36647
|
+
};
|
|
36648
|
+
if (controller === void 0 || rebind === void 0) return held("not-adaptive");
|
|
36649
|
+
this.recordDeviceProbe(deviceId, probeFromHints(downlinkMbps, now));
|
|
36650
|
+
if (rebind.hints.prefersTier !== void 0) return held("pinned");
|
|
36651
|
+
if (entry.timeline?.currentMode === "recorded") return held("recorded");
|
|
36652
|
+
const pick = pickAdaptiveTier({
|
|
36653
|
+
available: preferLiveAdaptiveCandidates(this.deviceBrokerCandidates(deviceId)).map((c) => c.tier),
|
|
36654
|
+
downlinkMbps,
|
|
36655
|
+
viewportWidth: rebind.hints.viewportWidth,
|
|
36656
|
+
viewportHeight: rebind.hints.viewportHeight
|
|
36657
|
+
});
|
|
36658
|
+
const current = entry.pendingSwitch?.tier ?? controller.currentTier;
|
|
36659
|
+
if (CAM_PROFILE_ORDER.indexOf(pick.tier) >= CAM_PROFILE_ORDER.indexOf(current)) return held(pick.tier === current ? "already-there" : "probe-below-current");
|
|
36660
|
+
controller.settleOn(pick.tier, now);
|
|
36661
|
+
this.logger.info("adaptive: downlink probe places the session on a higher rung", {
|
|
36662
|
+
tags: { deviceId },
|
|
36663
|
+
meta: {
|
|
36664
|
+
sessionId,
|
|
36665
|
+
deviceId,
|
|
36666
|
+
downlinkMbps,
|
|
36667
|
+
from: current,
|
|
36668
|
+
tier: pick.tier,
|
|
36669
|
+
pick: pick.reason
|
|
36670
|
+
}
|
|
36671
|
+
});
|
|
36672
|
+
this.switchSessionTier(entry, deviceId, pick.tier, sessionId);
|
|
36673
|
+
return {
|
|
36674
|
+
outcome: "switching",
|
|
36675
|
+
tier: pick.tier,
|
|
36676
|
+
reason: pick.reason
|
|
36677
|
+
};
|
|
36678
|
+
}
|
|
36679
|
+
/** Every registered broker of `deviceId`, with the tier it currently serves. */
|
|
36680
|
+
deviceBrokerCandidates(deviceId) {
|
|
36681
|
+
const prefix = `${String(deviceId)}/`;
|
|
36682
|
+
const out = [];
|
|
36683
|
+
for (const bid of this.brokers.keys()) if (bid.startsWith(prefix)) out.push({
|
|
36684
|
+
brokerId: bid,
|
|
36685
|
+
tier: this.profileTierResolver?.(bid) ?? null
|
|
36686
|
+
});
|
|
36687
|
+
return out;
|
|
36688
|
+
}
|
|
36378
36689
|
/** Adaptive sessions of `deviceId` currently on this broker server. */
|
|
36379
36690
|
adaptiveSessionCount(deviceId) {
|
|
36380
36691
|
const prefix = `${String(deviceId)}/`;
|
|
@@ -36387,6 +36698,322 @@ var BrokerWebrtcServer = class {
|
|
|
36387
36698
|
this.deviceProbe.record(deviceId, probe);
|
|
36388
36699
|
}
|
|
36389
36700
|
/**
|
|
36701
|
+
* Move an adaptive session to `tier` — ON THE SESSION when it can (D733),
|
|
36702
|
+
* by the close-and-recreate re-offer when it cannot, and the refusal that
|
|
36703
|
+
* forced the re-offer is logged by name.
|
|
36704
|
+
*
|
|
36705
|
+
* A newer switch replaces one still waiting for its keyframe: the ladder's
|
|
36706
|
+
* latest verdict is the one that must land, and an abandoned wait leaves the
|
|
36707
|
+
* old source on the track (nothing froze).
|
|
36708
|
+
*/
|
|
36709
|
+
switchSessionTier(entry, deviceId, tier, sessionId, forceTranscodeDown = false, decision) {
|
|
36710
|
+
entry.pendingSwitch?.cancel();
|
|
36711
|
+
entry.pendingSwitch = void 0;
|
|
36712
|
+
const why = decision?.cause ?? "probe";
|
|
36713
|
+
if (forceTranscodeDown) {
|
|
36714
|
+
this.logInPlaceFallback(entry, deviceId, tier, sessionId, "transcode-down", why);
|
|
36715
|
+
this.scheduleTierReoffer(entry, deviceId, tier, sessionId, true, decision);
|
|
36716
|
+
return;
|
|
36717
|
+
}
|
|
36718
|
+
this.switchTierInPlace(entry, deviceId, tier, sessionId, why).then((outcome) => {
|
|
36719
|
+
if (outcome.kind !== "refused") return;
|
|
36720
|
+
if (this.sessions.get(sessionId) !== entry) return;
|
|
36721
|
+
this.logInPlaceFallback(entry, deviceId, tier, sessionId, outcome.reason, why);
|
|
36722
|
+
this.scheduleTierReoffer(entry, deviceId, tier, sessionId, false, decision);
|
|
36723
|
+
}).catch((err) => {
|
|
36724
|
+
this.logger.warn("adaptive: in-place tier switch threw — falling back to re-offer", {
|
|
36725
|
+
tags: { deviceId },
|
|
36726
|
+
meta: {
|
|
36727
|
+
sessionId,
|
|
36728
|
+
deviceId,
|
|
36729
|
+
tier,
|
|
36730
|
+
error: errMsg(err)
|
|
36731
|
+
}
|
|
36732
|
+
});
|
|
36733
|
+
if (this.sessions.get(sessionId) !== entry) return;
|
|
36734
|
+
this.scheduleTierReoffer(entry, deviceId, tier, sessionId, false, decision);
|
|
36735
|
+
});
|
|
36736
|
+
}
|
|
36737
|
+
logInPlaceFallback(entry, deviceId, tier, sessionId, reason, why) {
|
|
36738
|
+
this.logger.info("adaptive: tier switch needs a re-offer — cannot stay on the session", {
|
|
36739
|
+
tags: { deviceId },
|
|
36740
|
+
meta: {
|
|
36741
|
+
sessionId,
|
|
36742
|
+
deviceId,
|
|
36743
|
+
tier,
|
|
36744
|
+
fromBrokerId: entry.brokerId,
|
|
36745
|
+
reason,
|
|
36746
|
+
cause: why
|
|
36747
|
+
}
|
|
36748
|
+
});
|
|
36749
|
+
}
|
|
36750
|
+
/** What the session is now, for `judgeInPlaceSwitch`. */
|
|
36751
|
+
inPlaceSessionShape(entry, rebind) {
|
|
36752
|
+
return {
|
|
36753
|
+
recorded: entry.timeline?.currentMode === "recorded",
|
|
36754
|
+
forceTranscodeDown: false,
|
|
36755
|
+
useRtpRepacketizer: rebind.useRtpRepacketizer,
|
|
36756
|
+
needsTranscode: entry.session.needsTranscode,
|
|
36757
|
+
sessionCodec: rebind.sessionCodec,
|
|
36758
|
+
audioCodec: this.brokers.get(entry.brokerId)?.getStats().audio?.codec ?? null,
|
|
36759
|
+
usesOpusEgress: entry.releaseOpusEgress !== void 0
|
|
36760
|
+
};
|
|
36761
|
+
}
|
|
36762
|
+
/** What the target rendition is, as far as it is known now. */
|
|
36763
|
+
inPlaceTargetShape(rebind, landed, target) {
|
|
36764
|
+
if (target === void 0) return {
|
|
36765
|
+
tierMatches: false,
|
|
36766
|
+
isRtp: false,
|
|
36767
|
+
codec: null,
|
|
36768
|
+
needsBaselineTranscode: false,
|
|
36769
|
+
audioCodec: void 0,
|
|
36770
|
+
supportsOpusEgress: false
|
|
36771
|
+
};
|
|
36772
|
+
const stats = target.getStats();
|
|
36773
|
+
const codec = sessionCodecOf(stats.codec);
|
|
36774
|
+
let needsBaselineTranscode = false;
|
|
36775
|
+
if (codec === "H264") {
|
|
36776
|
+
const profile = deriveH264ProfileLevelId(target.getSdpParameterSets(), target.getPreBuffer());
|
|
36777
|
+
needsBaselineTranscode = rebind.offeredProfileLevelIds !== void 0 ? resolveClientOfferTranscodeToBaseline("H264", profile, rebind.offeredProfileLevelIds) : resolveBrokerTranscodeToBaseline("H264", profile);
|
|
36778
|
+
}
|
|
36779
|
+
return {
|
|
36780
|
+
tierMatches: landed !== null,
|
|
36781
|
+
isRtp: target.isRtpSource(),
|
|
36782
|
+
codec,
|
|
36783
|
+
needsBaselineTranscode,
|
|
36784
|
+
audioCodec: codec === null ? void 0 : stats.audio?.codec ?? null,
|
|
36785
|
+
supportsOpusEgress: supportsOpusEgress(target)
|
|
36786
|
+
};
|
|
36787
|
+
}
|
|
36788
|
+
/**
|
|
36789
|
+
* The in-place half of a tier switch (D733): resolve the tier's broker,
|
|
36790
|
+
* judge whether the negotiated session carries it as is, subscribe it
|
|
36791
|
+
* make-before-break and commit on its first switchable keyframe.
|
|
36792
|
+
*/
|
|
36793
|
+
async switchTierInPlace(entry, deviceId, tier, sessionId, why) {
|
|
36794
|
+
const rebind = entry.rebind;
|
|
36795
|
+
if (rebind === void 0) return {
|
|
36796
|
+
kind: "refused",
|
|
36797
|
+
reason: "not-rtp-passthrough"
|
|
36798
|
+
};
|
|
36799
|
+
const early = judgeInPlaceSwitch(this.inPlaceSessionShape(entry, rebind), {
|
|
36800
|
+
tierMatches: true,
|
|
36801
|
+
isRtp: true,
|
|
36802
|
+
codec: null,
|
|
36803
|
+
needsBaselineTranscode: false,
|
|
36804
|
+
audioCodec: void 0,
|
|
36805
|
+
supportsOpusEgress: true
|
|
36806
|
+
});
|
|
36807
|
+
if (early !== null) return {
|
|
36808
|
+
kind: "refused",
|
|
36809
|
+
reason: early
|
|
36810
|
+
};
|
|
36811
|
+
try {
|
|
36812
|
+
await this.ensureAdaptiveTierBroker?.(deviceId, tier);
|
|
36813
|
+
} catch (err) {
|
|
36814
|
+
this.logger.warn("adaptive: could not prepare the tier broker for an in-place switch", {
|
|
36815
|
+
tags: { deviceId },
|
|
36816
|
+
meta: {
|
|
36817
|
+
sessionId,
|
|
36818
|
+
deviceId,
|
|
36819
|
+
tier,
|
|
36820
|
+
error: errMsg(err)
|
|
36821
|
+
}
|
|
36822
|
+
});
|
|
36823
|
+
return {
|
|
36824
|
+
kind: "refused",
|
|
36825
|
+
reason: "tier-unavailable"
|
|
36826
|
+
};
|
|
36827
|
+
}
|
|
36828
|
+
if (this.sessions.get(sessionId) !== entry) return { kind: "abandoned" };
|
|
36829
|
+
const targetId = this.selectBestBroker(String(deviceId), { prefersTier: tier });
|
|
36830
|
+
const target = this.brokers.get(targetId);
|
|
36831
|
+
const landed = this.profileTierResolver?.(targetId) ?? null;
|
|
36832
|
+
if (target !== void 0 && targetId === entry.brokerId) {
|
|
36833
|
+
if (landed !== null) entry.adaptiveController?.settleOn(landed, Date.now());
|
|
36834
|
+
rebind.sessionLogger.info("adaptive: wanted tier is not served — the session stays on its rung", {
|
|
36835
|
+
tags: { deviceId },
|
|
36836
|
+
meta: {
|
|
36837
|
+
sessionId,
|
|
36838
|
+
deviceId,
|
|
36839
|
+
tier,
|
|
36840
|
+
landed,
|
|
36841
|
+
brokerId: targetId,
|
|
36842
|
+
cause: why
|
|
36843
|
+
}
|
|
36844
|
+
});
|
|
36845
|
+
return { kind: "committed" };
|
|
36846
|
+
}
|
|
36847
|
+
const judge = () => judgeInPlaceSwitch(this.inPlaceSessionShape(entry, rebind), this.inPlaceTargetShape(rebind, landed, target));
|
|
36848
|
+
const refusal = judge();
|
|
36849
|
+
if (refusal !== null || target === void 0) return {
|
|
36850
|
+
kind: "refused",
|
|
36851
|
+
reason: refusal ?? "tier-unavailable"
|
|
36852
|
+
};
|
|
36853
|
+
const fromBrokerId = entry.brokerId;
|
|
36854
|
+
const startedAt = Date.now();
|
|
36855
|
+
rebind.sessionLogger.info("adaptive: tier switch staying on the session — waiting for a keyframe", {
|
|
36856
|
+
tags: { deviceId },
|
|
36857
|
+
meta: {
|
|
36858
|
+
sessionId,
|
|
36859
|
+
deviceId,
|
|
36860
|
+
tier,
|
|
36861
|
+
fromBrokerId,
|
|
36862
|
+
toBrokerId: targetId,
|
|
36863
|
+
cause: why
|
|
36864
|
+
}
|
|
36865
|
+
});
|
|
36866
|
+
return new Promise((resolve) => {
|
|
36867
|
+
const forward = makeSourceRtpForwarder({
|
|
36868
|
+
session: entry.session,
|
|
36869
|
+
sessionLogger: rebind.sessionLogger,
|
|
36870
|
+
sessionId,
|
|
36871
|
+
deviceId,
|
|
36872
|
+
brokerId: targetId,
|
|
36873
|
+
sessionCodec: rebind.sessionCodec
|
|
36874
|
+
});
|
|
36875
|
+
const pending = bindPendingSource({
|
|
36876
|
+
broker: target,
|
|
36877
|
+
attribution: {
|
|
36878
|
+
...rebind.baseAttribution,
|
|
36879
|
+
media: "video",
|
|
36880
|
+
targetCodec: rebind.sessionCodec,
|
|
36881
|
+
transport: "repacketize"
|
|
36882
|
+
},
|
|
36883
|
+
timeoutMs: IN_PLACE_SWITCH_KEYFRAME_WAIT_MS,
|
|
36884
|
+
commit: () => {
|
|
36885
|
+
if (this.sessions.get(sessionId) !== entry || entry.pendingSwitch !== handle) {
|
|
36886
|
+
resolve({ kind: "abandoned" });
|
|
36887
|
+
return false;
|
|
36888
|
+
}
|
|
36889
|
+
entry.pendingSwitch = void 0;
|
|
36890
|
+
const late = judge();
|
|
36891
|
+
if (late !== null) {
|
|
36892
|
+
resolve({
|
|
36893
|
+
kind: "refused",
|
|
36894
|
+
reason: late
|
|
36895
|
+
});
|
|
36896
|
+
return false;
|
|
36897
|
+
}
|
|
36898
|
+
if (landed !== null && landed !== tier) entry.adaptiveController?.settleOn(landed, Date.now());
|
|
36899
|
+
this.commitInPlaceSwitch(entry, rebind, {
|
|
36900
|
+
sessionId,
|
|
36901
|
+
tier: landed ?? tier,
|
|
36902
|
+
targetId,
|
|
36903
|
+
target,
|
|
36904
|
+
unsubRtp: pending.unsubscribe,
|
|
36905
|
+
waitedMs: Date.now() - startedAt,
|
|
36906
|
+
cause: why
|
|
36907
|
+
});
|
|
36908
|
+
resolve({ kind: "committed" });
|
|
36909
|
+
return true;
|
|
36910
|
+
},
|
|
36911
|
+
forward,
|
|
36912
|
+
onTimeout: () => {
|
|
36913
|
+
if (entry.pendingSwitch === handle) entry.pendingSwitch = void 0;
|
|
36914
|
+
resolve({
|
|
36915
|
+
kind: "refused",
|
|
36916
|
+
reason: "target-no-keyframe"
|
|
36917
|
+
});
|
|
36918
|
+
}
|
|
36919
|
+
});
|
|
36920
|
+
const handle = {
|
|
36921
|
+
tier,
|
|
36922
|
+
cancel: () => {
|
|
36923
|
+
pending.unsubscribe();
|
|
36924
|
+
resolve({ kind: "abandoned" });
|
|
36925
|
+
}
|
|
36926
|
+
};
|
|
36927
|
+
entry.pendingSwitch = handle;
|
|
36928
|
+
});
|
|
36929
|
+
}
|
|
36930
|
+
/**
|
|
36931
|
+
* Swap every subscription of `entry` onto `target` in one synchronous step,
|
|
36932
|
+
* inside the delivery of the target's committing packet: nothing from the old
|
|
36933
|
+
* broker can reach the track after this returns, and the target's next
|
|
36934
|
+
* forward replays its GOP from the keyframe onto a rebased RTP stream.
|
|
36935
|
+
*/
|
|
36936
|
+
commitInPlaceSwitch(entry, rebind, sw) {
|
|
36937
|
+
const { session } = entry;
|
|
36938
|
+
const { deviceId, sessionLogger } = rebind;
|
|
36939
|
+
const old = {
|
|
36940
|
+
brokerId: entry.brokerId,
|
|
36941
|
+
unsubBroker: entry.unsubBroker,
|
|
36942
|
+
unsubRtp: entry.unsubRtp,
|
|
36943
|
+
unsubSdpParams: entry.unsubSdpParams,
|
|
36944
|
+
unsubSourceRestart: entry.unsubSourceRestart,
|
|
36945
|
+
releaseOpusEgress: entry.releaseOpusEgress
|
|
36946
|
+
};
|
|
36947
|
+
const target = sw.target;
|
|
36948
|
+
entry.unsubBroker = target.onEncodedData((packet) => {
|
|
36949
|
+
if (packet.type === "audio") forwardBrokerAudio(packet, session, rebind.pushFrame);
|
|
36950
|
+
}, {
|
|
36951
|
+
...rebind.baseAttribution,
|
|
36952
|
+
media: "audio",
|
|
36953
|
+
transport: "passthrough"
|
|
36954
|
+
});
|
|
36955
|
+
entry.unsubRtp = sw.unsubRtp;
|
|
36956
|
+
if (old.releaseOpusEgress !== void 0 && supportsOpusEgress(target)) {
|
|
36957
|
+
target.acquireOpusEgress();
|
|
36958
|
+
entry.releaseOpusEgress = () => target.releaseOpusEgress();
|
|
36959
|
+
} else entry.releaseOpusEgress = void 0;
|
|
36960
|
+
const parameterSets = target.getSdpParameterSets();
|
|
36961
|
+
session.switchLiveSource({
|
|
36962
|
+
rtpBootstrap: () => target.getRtpPreBuffer(),
|
|
36963
|
+
parameterSets,
|
|
36964
|
+
reason: `tier ${sw.tier} (${sw.cause})`
|
|
36965
|
+
});
|
|
36966
|
+
entry.unsubSdpParams = parameterSets !== null && parameterSets.length > 0 ? null : target.onSdpParameterSets((ps) => {
|
|
36967
|
+
if (rebind.sessionCodec === "H265") session.seedH265CodecInfoFromSdp(ps);
|
|
36968
|
+
else session.seedH264CodecInfoFromSdp(ps);
|
|
36969
|
+
});
|
|
36970
|
+
entry.unsubSourceRestart = target.onSourceRestart((reason) => {
|
|
36971
|
+
sessionLogger.info("broker re-dialed the source — re-syncing session", {
|
|
36972
|
+
tags: { deviceId },
|
|
36973
|
+
meta: {
|
|
36974
|
+
sessionId: sw.sessionId,
|
|
36975
|
+
brokerId: sw.targetId,
|
|
36976
|
+
deviceId,
|
|
36977
|
+
reason
|
|
36978
|
+
}
|
|
36979
|
+
});
|
|
36980
|
+
session.onSourceRestarted(reason);
|
|
36981
|
+
});
|
|
36982
|
+
entry.brokerId = sw.targetId;
|
|
36983
|
+
old.unsubRtp?.();
|
|
36984
|
+
old.unsubBroker();
|
|
36985
|
+
old.unsubSdpParams?.();
|
|
36986
|
+
old.unsubSourceRestart();
|
|
36987
|
+
old.releaseOpusEgress?.();
|
|
36988
|
+
this.brokerLinger.cancel(sw.targetId);
|
|
36989
|
+
let oldStillServed = false;
|
|
36990
|
+
for (const other of this.sessions.values()) if (other.brokerId === old.brokerId) {
|
|
36991
|
+
oldStillServed = true;
|
|
36992
|
+
break;
|
|
36993
|
+
}
|
|
36994
|
+
if (!oldStillServed && !this.stopped) this.brokerLinger.hold(old.brokerId);
|
|
36995
|
+
const pair = session.readNominatedPair();
|
|
36996
|
+
if (pair !== null) {
|
|
36997
|
+
const remoteAddr = pair.remoteType === "host" ? pair.remoteAddr : `${pair.remoteAddr} (${pair.remoteType})`;
|
|
36998
|
+
target.updateConsumerAttributionBySession(sw.sessionId, { remoteAddr });
|
|
36999
|
+
}
|
|
37000
|
+
entry.timeline?.setLiveProfile(sw.tier);
|
|
37001
|
+
sessionLogger.info("adaptive: tier switched in place — same session, same connection", {
|
|
37002
|
+
tags: { deviceId },
|
|
37003
|
+
meta: {
|
|
37004
|
+
sessionId: sw.sessionId,
|
|
37005
|
+
deviceId,
|
|
37006
|
+
tier: sw.tier,
|
|
37007
|
+
fromBrokerId: old.brokerId,
|
|
37008
|
+
toBrokerId: sw.targetId,
|
|
37009
|
+
codec: rebind.sessionCodec,
|
|
37010
|
+
waitedMs: sw.waitedMs,
|
|
37011
|
+
cause: sw.cause,
|
|
37012
|
+
replayPackets: target.getRtpPreBuffer().length
|
|
37013
|
+
}
|
|
37014
|
+
});
|
|
37015
|
+
}
|
|
37016
|
+
/**
|
|
36390
37017
|
* Schedule an adaptive tier switch via a close-and-recreate re-offer.
|
|
36391
37018
|
*
|
|
36392
37019
|
* Records the controller's desired tier (+ forced transcode-down) in
|
|
@@ -36399,7 +37026,7 @@ var BrokerWebrtcServer = class {
|
|
|
36399
37026
|
* consumes the recorded intent to pin the recreated session to `tier` and
|
|
36400
37027
|
* apply the reduced transcode profile when `forceTranscodeDown` is set.
|
|
36401
37028
|
*/
|
|
36402
|
-
|
|
37029
|
+
scheduleTierReoffer(entry, deviceId, tier, sessionId, forceTranscodeDown = false, decision) {
|
|
36403
37030
|
const prev = this.adaptiveIntent.get(sessionId);
|
|
36404
37031
|
const failures = new Map(entry.adaptiveController?.failureCounts ?? prev?.failures);
|
|
36405
37032
|
const upgradeCount = entry.adaptiveController?.successfulUpgradeCount ?? prev?.upgradeCount ?? 0;
|
|
@@ -36447,6 +37074,8 @@ var BrokerWebrtcServer = class {
|
|
|
36447
37074
|
const entry = this.sessions.get(sessionId);
|
|
36448
37075
|
if (!entry) return;
|
|
36449
37076
|
this.sessions.delete(sessionId);
|
|
37077
|
+
entry.pendingSwitch?.cancel();
|
|
37078
|
+
entry.pendingSwitch = void 0;
|
|
36450
37079
|
let brokerStillServed = false;
|
|
36451
37080
|
for (const other of this.sessions.values()) if (other.brokerId === entry.brokerId) {
|
|
36452
37081
|
brokerStillServed = true;
|
|
@@ -37106,6 +37735,10 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
|
|
|
37106
37735
|
async getSessionState(input) {
|
|
37107
37736
|
return this.webrtcServer.getSessionRenegotiationState(input.sessionId);
|
|
37108
37737
|
}
|
|
37738
|
+
/** The viewer's downlink probe for one session (D733) — see the cap's docblock. */
|
|
37739
|
+
async applyDownlinkProbe(input) {
|
|
37740
|
+
return this.webrtcServer.applyDownlinkProbe(input.sessionId, input.downlinkMbps);
|
|
37741
|
+
}
|
|
37109
37742
|
/**
|
|
37110
37743
|
* The SERVER half of the stream-debug overlay. Thin facade — the gathering
|
|
37111
37744
|
* lives in the WebRTC server and the shaping in the pure
|