@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.
Files changed (33) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-ChH7Y219.js → default-detection-model-D3SnO2hN.js} +39 -67
  4. package/dist/{default-detection-model-j0nBpRoB.mjs → default-detection-model-DELTCQgv.mjs} +39 -67
  5. package/dist/detection-pipeline/index.js +197 -31
  6. package/dist/detection-pipeline/index.mjs +197 -31
  7. package/dist/{dist-0HoA3wAi.mjs → dist-CNXhBzn6.mjs} +847 -439
  8. package/dist/{dist-BlIWhjnu.js → dist-DrUx3EGR.js} +892 -466
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-BPpVMnZy.js → node-B4qy3lnV.js} +1 -1
  12. package/dist/{node-HOMWRPzW.mjs → node-BmxmlYLx.mjs} +1 -1
  13. package/dist/pipeline-runner/index.js +355 -104
  14. package/dist/pipeline-runner/index.mjs +355 -104
  15. package/dist/{process-memory-DuF8pa2w.mjs → process-memory-BGc5PzZI.mjs} +1 -1
  16. package/dist/{process-memory-BW06vOIQ.js → process-memory-CRtWJbjV.js} +1 -1
  17. package/dist/recorder/index.js +7 -7
  18. package/dist/recorder/index.mjs +7 -7
  19. package/dist/{segment-demux-js-Dl52Zz8w.js → segment-demux-js-CpSgZmSR.js} +2 -2
  20. package/dist/{segment-demux-js-BU2MXpln.mjs → segment-demux-js-VqsLAgQ0.mjs} +2 -2
  21. package/dist/stream-broker/_stub.js +2 -2
  22. 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
  23. 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
  24. 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
  25. package/dist/stream-broker/demux-worker-child.js +1 -1
  26. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  27. package/dist/stream-broker/{hostInit-Ciacp783.mjs → hostInit-CF_2Rlu0.mjs} +2 -2
  28. package/dist/stream-broker/index.js +713 -80
  29. package/dist/stream-broker/index.mjs +713 -80
  30. package/dist/stream-broker/remoteEntry.js +1 -1
  31. package/package.json +1 -1
  32. 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
  33. 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, At as maskUrlCredentials, Bn as array, C as EncodeProfileSchema, Cn as CAM_PROFILE_ORDER, Dt as loadContributionCapability, En as createEvent, Gn as number, Hn as discriminatedUnion, I as RATE_CONTROL_RELAXED, In as parseProfileBrokerId, Jn as record, Kn as object, L as RATE_CONTROL_TIGHT, Mn as makeProfileBrokerId, Nn as makeSourceBrokerId, Nt as memoizePinnedFfmpeg, O as G711_SCALE_CORRECTION_DB, Ot as logChannelsCapability, Pn as nodePin, Sn as BaseAddon, U as RingBuffer, Vn as boolean, Wn as literal, Wt as rankStreams, Xn as union, Yn as string, Z as batteryCapability, Zn as EventCategory, Zt as resolveEgressDecodeHwAccel, _n as isSoftwareDecode, bn as errMsg, bt as gifDeriveSpeed, c as CLIP_STILL_REASON_CODE_HEADER, dn as AUDIO_PRESETS, dt as egressTransportFromRequest, gn as invocationFromEncodeProfile, it as declareLogChannel, jn as isEvent, kn as expandAudioChunkToF32le, mn as buildFfmpegArgs$1, nt as createHwAccelCache, o as BatteryStatusSchema, on as streamBrokerCapability, ot as deriveBatteryPresence, q as addonWidgetsSourceCapability, rt as createLogChannelsProvider, s as CLIP_BROKER_PACED_RATES, tt as clipExportFailureOf, un as webrtcSessionCapability, ut as egressTranscodeSharingKey, vn as DeviceFeature, vt as ffmpegRate, wn as CamProfileSchema, xn as BOOT_RECOVERY_BACKOFF_MS, xt as gifFromMp4Args, yn as DeviceType, zn as _enum } from "../dist-0HoA3wAi.mjs";
3
- import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-HOMWRPzW.mjs";
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-BU2MXpln.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-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
- if (this.liveProfileReported || this.deps.initialLiveProfile === void 0) return;
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: this.deps.initialLiveProfile
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 (D540)");
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
- let firstRtpForwarded = false;
35584
- let gateDropLogged = false;
35585
- let forwardAfterGateLogged = false;
35586
- unsubRtp = broker.onVideoRtp((rtpData) => {
35587
- if (session.needsTranscode) return;
35588
- if (session.isLiveFeedGated) {
35589
- if (!gateDropLogged) {
35590
- gateDropLogged = true;
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
- switchSessionTier(entry, deviceId, tier, sessionId, forceTranscodeDown = false, decision) {
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