@camstack/addon-pipeline 1.2.290 → 1.2.291
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-B64MIW2h.mjs → default-detection-model-DcTy-nJl.mjs} +1 -1
- package/dist/{default-detection-model-CN_Iu8NF.js → default-detection-model-jjdmKKr3.js} +1 -1
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-C4JqXCw-.mjs → dist-Bn6BQW8q.mjs} +487 -60
- package/dist/{dist-BIlnUdBn.js → dist-D7FonEUB.js} +487 -60
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-C-WfKJ6b.mjs → node-BN0Na3HN.mjs} +1 -1
- package/dist/{node-CVlJhqng.js → node-D3C1krEi.js} +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/{process-memory-CJKKFNar.mjs → process-memory-CeZ8ms80.mjs} +1 -1
- package/dist/{process-memory-ZUucMC8Z.js → process-memory-DI4jrCtX.js} +1 -1
- package/dist/recorder/index.js +8 -2
- package/dist/recorder/index.mjs +8 -2
- package/dist/{segment-demux-js-4OAHyQ3t.mjs → segment-demux-js-CNPzlcyO.mjs} +1 -1
- package/dist/{segment-demux-js-D0mqN4RC.js → segment-demux-js-COooJ-t0.js} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-r5TULO-I.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BrD7TK3W.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-pSFdAaR0.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-DhImOdbA.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-DPdwl8-T.mjs → hostInit-CbSGtrCj.mjs} +2 -2
- package/dist/stream-broker/index.js +1056 -92
- package/dist/stream-broker/index.mjs +1056 -92
- 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-D7VLO8g1.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-UqRrHdtw.mjs +0 -26
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { r as __toESM } from "../chunk-DnnnRqeS.mjs";
|
|
2
|
-
import { $t as errMsg, An as literal, Dn as boolean, En as array, Fn as string, H as addonWidgetsSourceCapability, In as union, Jt as streamBrokerCapability, K as batteryCapability, Ln as EventCategory, M as RATE_CONTROL_TIGHT, Mn as object, On as discriminatedUnion, Pn as record, Pt as rankStreams, Q as declareLogChannel, Qt as webrtcSessionCapability, R as RingBuffer, Sn as parseProfileBrokerId, Tn as _enum, Vt as resolveEgressDecodeHwAccel, X as createHwAccelCache, Y as clipExportFailureOf, Z as createLogChannelsProvider, _n as isEvent, _t as loadContributionCapability, a as CLIP_BROKER_PACED_RATES, an as BOOT_RECOVERY_BACKOFF_MS, at as egressTransportFromRequest, bn as nodePin, bt as maskUrlCredentials, cn as CamProfileSchema, en as AUDIO_PRESETS, et as deriveBatteryPresence, fn as createEvent, hn as expandAudioChunkToF32le, i as BatteryStatusSchema, in as isSoftwareDecode, it as egressTranscodeSharingKey, j as RATE_CONTROL_RELAXED, jn as number, ln as DeviceFeature, nn as buildFfmpegArgs$1, o as CLIP_STILL_REASON_CODE_HEADER, on as BaseAddon, q as cameraStreamsCapability, rn as invocationFromEncodeProfile, sn as CAM_PROFILE_ORDER, un as DeviceType, vn as makeProfileBrokerId, vt as logChannelsCapability, w as G711_SCALE_CORRECTION_DB, x as EncodeProfileSchema, yn as makeSourceBrokerId } from "../dist-
|
|
3
|
-
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-
|
|
2
|
+
import { $t as errMsg, An as literal, Dn as boolean, En as array, Fn as string, H as addonWidgetsSourceCapability, In as union, Jt as streamBrokerCapability, K as batteryCapability, Ln as EventCategory, M as RATE_CONTROL_TIGHT, Mn as object, On as discriminatedUnion, Pn as record, Pt as rankStreams, Q as declareLogChannel, Qt as webrtcSessionCapability, R as RingBuffer, Sn as parseProfileBrokerId, Tn as _enum, Vt as resolveEgressDecodeHwAccel, X as createHwAccelCache, Y as clipExportFailureOf, Z as createLogChannelsProvider, _n as isEvent, _t as loadContributionCapability, a as CLIP_BROKER_PACED_RATES, an as BOOT_RECOVERY_BACKOFF_MS, at as egressTransportFromRequest, bn as nodePin, bt as maskUrlCredentials, cn as CamProfileSchema, en as AUDIO_PRESETS, et as deriveBatteryPresence, fn as createEvent, hn as expandAudioChunkToF32le, i as BatteryStatusSchema, in as isSoftwareDecode, it as egressTranscodeSharingKey, j as RATE_CONTROL_RELAXED, jn as number, ln as DeviceFeature, nn as buildFfmpegArgs$1, o as CLIP_STILL_REASON_CODE_HEADER, on as BaseAddon, q as cameraStreamsCapability, rn as invocationFromEncodeProfile, sn as CAM_PROFILE_ORDER, un as DeviceType, vn as makeProfileBrokerId, vt as logChannelsCapability, w as G711_SCALE_CORRECTION_DB, x as EncodeProfileSchema, yn as makeSourceBrokerId } from "../dist-Bn6BQW8q.mjs";
|
|
3
|
+
import { i as NO_COST_CLAIM, n as Fmp4FragmentChild, o as probeFfmpegBinary, r as Fmp4FragmentPlane, t as ChildCostRegistry } from "../node-BN0Na3HN.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 avccSampleToAnnexB, i as require_mp4box_all, n as demuxSegmentPreferJs, o as buildKeyframeAnnexB, r as runFfmpegOnce, s as parseNalLengthSize, t as demuxSegmentFullJs } from "../segment-demux-js-
|
|
7
|
+
import { a as avccSampleToAnnexB, i as require_mp4box_all, n as demuxSegmentPreferJs, o as buildKeyframeAnnexB, r as runFfmpegOnce, s as parseNalLengthSize, t as demuxSegmentFullJs } from "../segment-demux-js-CNPzlcyO.mjs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
@@ -23177,6 +23177,246 @@ var H264Repacketizer = class {
|
|
|
23177
23177
|
}
|
|
23178
23178
|
};
|
|
23179
23179
|
//#endregion
|
|
23180
|
+
//#region src/stream-broker/webrtc/delivery-meter.ts
|
|
23181
|
+
/** The window the ladder judges delivery over: at least one GOP. */
|
|
23182
|
+
function deliveryWindowMs(idrIntervalMs, config) {
|
|
23183
|
+
const gop = idrIntervalMs !== null && Number.isFinite(idrIntervalMs) && idrIntervalMs > 0 ? idrIntervalMs : 0;
|
|
23184
|
+
return Math.min(config.maxWindowMs, Math.max(config.minWindowMs, gop));
|
|
23185
|
+
}
|
|
23186
|
+
/**
|
|
23187
|
+
* When a session's meter may start counting client reports. A session created
|
|
23188
|
+
* by a tier change starts after the change blackout (review I1): until the
|
|
23189
|
+
* client swaps its player onto the new PC (after the new stream's first
|
|
23190
|
+
* decode, 2–5 s), its reporter still reads the OLD PC — 0 kbps — and the
|
|
23191
|
+
* first report after the swap is 0 too; both fall inside the new session's
|
|
23192
|
+
* sends and read as a failed rung.
|
|
23193
|
+
*/
|
|
23194
|
+
function deliveryMeterStart(now, afterTierChange, waitAfterChangeMs) {
|
|
23195
|
+
return afterTierChange ? now + waitAfterChangeMs : now;
|
|
23196
|
+
}
|
|
23197
|
+
/**
|
|
23198
|
+
* Bucket width. 100 ms, and edges round to the NEAREST boundary, so a frame is
|
|
23199
|
+
* put on the wrong side of a window edge only when it was sent within 50 ms of
|
|
23200
|
+
* it — about one frame at 25 fps. Rounding edges inward instead dropped a whole
|
|
23201
|
+
* IDR that sat 160 ms inside a window and read a whole rung as 1.5× delivered.
|
|
23202
|
+
* 32 s of history is 320 slots.
|
|
23203
|
+
*/
|
|
23204
|
+
var SEND_BUCKET_MS = 100;
|
|
23205
|
+
/**
|
|
23206
|
+
* Bytes this session sent, bucketed by time. A fixed ring: `record` is on the
|
|
23207
|
+
* RTP send path, so it allocates nothing.
|
|
23208
|
+
*/
|
|
23209
|
+
var SendHistory = class {
|
|
23210
|
+
bytes;
|
|
23211
|
+
/** The bucket index each slot currently holds; -1 = empty. */
|
|
23212
|
+
ids;
|
|
23213
|
+
constructor(spanMs) {
|
|
23214
|
+
const slots = Math.max(1, Math.ceil(spanMs / SEND_BUCKET_MS));
|
|
23215
|
+
this.bytes = new Float64Array(slots);
|
|
23216
|
+
this.ids = new Float64Array(slots).fill(-1);
|
|
23217
|
+
}
|
|
23218
|
+
record(byteCount, now) {
|
|
23219
|
+
const id = Math.floor(now / SEND_BUCKET_MS);
|
|
23220
|
+
const slot = id % this.ids.length;
|
|
23221
|
+
if (this.ids[slot] !== id) {
|
|
23222
|
+
this.ids[slot] = id;
|
|
23223
|
+
this.bytes[slot] = 0;
|
|
23224
|
+
}
|
|
23225
|
+
this.bytes[slot] = (this.bytes[slot] ?? 0) + byteCount;
|
|
23226
|
+
}
|
|
23227
|
+
bitsBetween(fromMs, toMs) {
|
|
23228
|
+
const first = Math.round(fromMs / SEND_BUCKET_MS);
|
|
23229
|
+
const last = Math.round(toMs / SEND_BUCKET_MS) - 1;
|
|
23230
|
+
let bytes = 0;
|
|
23231
|
+
for (let id = first; id <= last; id += 1) {
|
|
23232
|
+
const slot = id % this.ids.length;
|
|
23233
|
+
if (this.ids[slot] === id) bytes += this.bytes[slot] ?? 0;
|
|
23234
|
+
}
|
|
23235
|
+
return bytes * 8;
|
|
23236
|
+
}
|
|
23237
|
+
};
|
|
23238
|
+
/**
|
|
23239
|
+
* The client's recent reports, and the ratio they imply against a
|
|
23240
|
+
* {@link SendHistoryReader}. One meter per adaptive session: `notBefore` is
|
|
23241
|
+
* the session's start, because the reports are DEVICE-scoped and a report
|
|
23242
|
+
* about time before this session describes the previous one (a re-offer).
|
|
23243
|
+
*/
|
|
23244
|
+
var DeliveryMeter = class {
|
|
23245
|
+
config;
|
|
23246
|
+
notBefore;
|
|
23247
|
+
reports = [];
|
|
23248
|
+
constructor(config, notBefore) {
|
|
23249
|
+
this.config = config;
|
|
23250
|
+
this.notBefore = notBefore;
|
|
23251
|
+
}
|
|
23252
|
+
/** Fold one client report. The same report read on several ticks counts once. */
|
|
23253
|
+
observe(kbps, endAt) {
|
|
23254
|
+
if (!Number.isFinite(kbps) || kbps < 0 || !Number.isFinite(endAt)) return;
|
|
23255
|
+
if (endAt - this.config.clientSampleSpanMs < this.notBefore) return;
|
|
23256
|
+
if (this.reports.some((r) => r.endAt === endAt)) return;
|
|
23257
|
+
const horizon = endAt - this.config.historySpanMs;
|
|
23258
|
+
this.reports = [...this.reports.filter((r) => r.endAt > horizon), {
|
|
23259
|
+
kbps,
|
|
23260
|
+
endAt
|
|
23261
|
+
}].sort((a, b) => a.endAt - b.endAt);
|
|
23262
|
+
}
|
|
23263
|
+
/**
|
|
23264
|
+
* Is the client reporting at all? A fresh report that does not yet cover a
|
|
23265
|
+
* whole window is "not measured YET" — different from a client that sends
|
|
23266
|
+
* no telemetry, and it must not be climbed on (a 10 %-delivery link would
|
|
23267
|
+
* otherwise promote in the seconds before its first window closes).
|
|
23268
|
+
*/
|
|
23269
|
+
isReporting(now) {
|
|
23270
|
+
const newest = this.reports.at(-1);
|
|
23271
|
+
return newest !== void 0 && now - newest.endAt <= this.config.maxClientSampleAgeMs;
|
|
23272
|
+
}
|
|
23273
|
+
/**
|
|
23274
|
+
* The newest report alone against the same 2 s of sends — the short window
|
|
23275
|
+
* a total freeze is judged on. Null when there is none, or it is stale.
|
|
23276
|
+
*/
|
|
23277
|
+
evaluateNewest(history, now) {
|
|
23278
|
+
const newest = this.reports.at(-1);
|
|
23279
|
+
if (newest === void 0 || now - newest.endAt > this.config.maxClientSampleAgeMs) return null;
|
|
23280
|
+
const span = this.config.clientSampleSpanMs;
|
|
23281
|
+
return {
|
|
23282
|
+
receivedKbps: newest.kbps,
|
|
23283
|
+
sentKbps: history.bitsBetween(newest.endAt - span, newest.endAt) / span,
|
|
23284
|
+
windowMs: span
|
|
23285
|
+
};
|
|
23286
|
+
}
|
|
23287
|
+
/**
|
|
23288
|
+
* Received vs sent over the newest reports that together cover `windowMs`,
|
|
23289
|
+
* or `null` when they do not (yet) — or the newest one is stale.
|
|
23290
|
+
*/
|
|
23291
|
+
evaluate(history, windowMs, now) {
|
|
23292
|
+
const newest = this.reports.at(-1);
|
|
23293
|
+
if (newest === void 0 || now - newest.endAt > this.config.maxClientSampleAgeMs) return null;
|
|
23294
|
+
const span = this.config.clientSampleSpanMs;
|
|
23295
|
+
let covered = 0;
|
|
23296
|
+
let receivedBits = 0;
|
|
23297
|
+
let sentBits = 0;
|
|
23298
|
+
for (let i = this.reports.length - 1; i >= 0 && covered < windowMs; i -= 1) {
|
|
23299
|
+
const r = this.reports[i];
|
|
23300
|
+
if (r === void 0 || now - (r.endAt - span) > this.config.historySpanMs) break;
|
|
23301
|
+
covered += span;
|
|
23302
|
+
receivedBits += r.kbps * span;
|
|
23303
|
+
sentBits += history.bitsBetween(r.endAt - span, r.endAt);
|
|
23304
|
+
}
|
|
23305
|
+
if (covered < windowMs) return null;
|
|
23306
|
+
return {
|
|
23307
|
+
receivedKbps: receivedBits / covered,
|
|
23308
|
+
sentKbps: sentBits / covered,
|
|
23309
|
+
windowMs: covered
|
|
23310
|
+
};
|
|
23311
|
+
}
|
|
23312
|
+
};
|
|
23313
|
+
//#endregion
|
|
23314
|
+
//#region src/stream-broker/webrtc/adaptive-config.ts
|
|
23315
|
+
/** How often the adaptive controller re-evaluates the ladder (ms). */
|
|
23316
|
+
var ADAPTIVE_TICK_MS = 2e3;
|
|
23317
|
+
/** Ladder tuning shared by every adaptive session. */
|
|
23318
|
+
var ADAPTIVE_CONFIG = {
|
|
23319
|
+
analyzeWindowMs: 15e3,
|
|
23320
|
+
minFailPercent: .05,
|
|
23321
|
+
waitAfterResetMs: 8e3,
|
|
23322
|
+
upgradeHealthyMs: 4e3,
|
|
23323
|
+
upgradeHealthyGrowthFactor: 3,
|
|
23324
|
+
maxProgressiveUpgradeHealthyMs: 3e4,
|
|
23325
|
+
minTimeBetweenChangesMs: 4e3,
|
|
23326
|
+
maxUpgradeBackoffMs: 9e5,
|
|
23327
|
+
stallDeliveryRatio: .25,
|
|
23328
|
+
recoverDeliveryRatio: .5,
|
|
23329
|
+
freezeDeliveryRatio: .1,
|
|
23330
|
+
freezeMinSentKbps: 16,
|
|
23331
|
+
climbDeliveryRatio: .9,
|
|
23332
|
+
stallConfirmMs: 6e3,
|
|
23333
|
+
waitAfterChangeMs: 8e3,
|
|
23334
|
+
climbCapacityMargin: 1.2,
|
|
23335
|
+
tryClimbDeliveryRatio: .95,
|
|
23336
|
+
tryClimbMaxLossRatio: .01,
|
|
23337
|
+
tryBackoffBaseMs: 3e4,
|
|
23338
|
+
tryBackoffMaxMs: 6e5,
|
|
23339
|
+
trialMs: 2e4,
|
|
23340
|
+
trialMaxMs: 4e4,
|
|
23341
|
+
trialRevertRatio: .9,
|
|
23342
|
+
trialRevertConsecutive: 2
|
|
23343
|
+
};
|
|
23344
|
+
/** How the delivery window is built from the client's reports. */
|
|
23345
|
+
var ADAPTIVE_DELIVERY_CONFIG = {
|
|
23346
|
+
clientSampleSpanMs: 2e3,
|
|
23347
|
+
minWindowMs: 2e3,
|
|
23348
|
+
maxWindowMs: 16e3,
|
|
23349
|
+
maxClientSampleAgeMs: 1e4,
|
|
23350
|
+
historySpanMs: 32e3
|
|
23351
|
+
};
|
|
23352
|
+
//#endregion
|
|
23353
|
+
//#region src/stream-broker/webrtc/climb-capacity.ts
|
|
23354
|
+
/** The client ladder's thresholds (`DOWNLINK_MID_MBPS`, `DOWNLINK_HIGH_MBPS`), kbps. */
|
|
23355
|
+
var UNMEASURED_RUNG_BAR_KBPS = {
|
|
23356
|
+
low: 0,
|
|
23357
|
+
mid: 2500,
|
|
23358
|
+
high: 8e3
|
|
23359
|
+
};
|
|
23360
|
+
/** The kbps the next rung needs; `(1800 × 1.2)` or the client threshold. */
|
|
23361
|
+
function capacityBarKbps(tier, rungKbps, margin) {
|
|
23362
|
+
return rungKbps !== null && rungKbps > 0 ? rungKbps * margin : UNMEASURED_RUNG_BAR_KBPS[tier];
|
|
23363
|
+
}
|
|
23364
|
+
function known(v) {
|
|
23365
|
+
return v !== null && Number.isFinite(v) && v > 0;
|
|
23366
|
+
}
|
|
23367
|
+
function judgeCapacity(evidence, nextTier, nextRungKbps, margin) {
|
|
23368
|
+
const neededKbps = capacityBarKbps(nextTier, nextRungKbps, margin);
|
|
23369
|
+
const { rembKbps, probeKbps, probeFresh } = evidence;
|
|
23370
|
+
if (known(probeKbps) && probeFresh && probeKbps >= neededKbps) return {
|
|
23371
|
+
kind: "sufficient",
|
|
23372
|
+
source: "probe",
|
|
23373
|
+
kbps: probeKbps,
|
|
23374
|
+
neededKbps
|
|
23375
|
+
};
|
|
23376
|
+
if (known(rembKbps) && rembKbps >= neededKbps) return {
|
|
23377
|
+
kind: "sufficient",
|
|
23378
|
+
source: "remb",
|
|
23379
|
+
kbps: rembKbps,
|
|
23380
|
+
neededKbps
|
|
23381
|
+
};
|
|
23382
|
+
if (known(probeKbps) && probeKbps < neededKbps) return {
|
|
23383
|
+
kind: "probe-short",
|
|
23384
|
+
kbps: probeKbps,
|
|
23385
|
+
neededKbps
|
|
23386
|
+
};
|
|
23387
|
+
return {
|
|
23388
|
+
kind: "none",
|
|
23389
|
+
bestKbps: known(rembKbps) ? rembKbps : null,
|
|
23390
|
+
neededKbps
|
|
23391
|
+
};
|
|
23392
|
+
}
|
|
23393
|
+
/**
|
|
23394
|
+
* REMB as it can be trusted. werift stores the LAST received estimate on the
|
|
23395
|
+
* sender and never ages it, so a peer that stopped sending REMB (backgrounded,
|
|
23396
|
+
* gone) would vouch for room indefinitely (review m5). Chrome re-estimates
|
|
23397
|
+
* about once a second and the value moves; an estimate unchanged for
|
|
23398
|
+
* `staleMs` (30 000 ms — fifteen ticks) is treated as no estimate at all.
|
|
23399
|
+
*/
|
|
23400
|
+
var RembWatch = class {
|
|
23401
|
+
staleMs;
|
|
23402
|
+
last = null;
|
|
23403
|
+
changedAt = 0;
|
|
23404
|
+
constructor(staleMs) {
|
|
23405
|
+
this.staleMs = staleMs;
|
|
23406
|
+
}
|
|
23407
|
+
read(kbps, now) {
|
|
23408
|
+
if (kbps === null) {
|
|
23409
|
+
this.last = null;
|
|
23410
|
+
return null;
|
|
23411
|
+
}
|
|
23412
|
+
if (kbps !== this.last) {
|
|
23413
|
+
this.last = kbps;
|
|
23414
|
+
this.changedAt = now;
|
|
23415
|
+
}
|
|
23416
|
+
return now - this.changedAt > this.staleMs ? null : kbps;
|
|
23417
|
+
}
|
|
23418
|
+
};
|
|
23419
|
+
//#endregion
|
|
23180
23420
|
//#region src/stream-broker/webrtc/transcode-egress-leg.ts
|
|
23181
23421
|
/**
|
|
23182
23422
|
* The request a browser session makes. Structured only: the primitive's schema
|
|
@@ -23291,6 +23531,8 @@ function selectAudioClock(mimeType) {
|
|
|
23291
23531
|
*
|
|
23292
23532
|
* Uses werift (optional peer dependency) for server-side WebRTC.
|
|
23293
23533
|
*/
|
|
23534
|
+
/** A REMB unchanged this long is no estimate (`RembWatch`). */
|
|
23535
|
+
var REMB_STALE_MS = 3e4;
|
|
23294
23536
|
var _werift;
|
|
23295
23537
|
async function loadWerift() {
|
|
23296
23538
|
if (_werift) return _werift;
|
|
@@ -24812,6 +25054,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
24812
25054
|
this.maybeRebaseRtp(pkt.header.sequenceNumber, pkt.header.timestamp);
|
|
24813
25055
|
try {
|
|
24814
25056
|
this.videoSender.sendRtp(pkt);
|
|
25057
|
+
this.videoSendHistory.record(pkt.payload.length, Date.now());
|
|
24815
25058
|
this.lastVideoRtpTs = pkt.header.timestamp >>> 0;
|
|
24816
25059
|
if (this.firstVideoRtpTs === null) {
|
|
24817
25060
|
this.firstVideoRtpTs = this.lastVideoRtpTs;
|
|
@@ -25383,6 +25626,26 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25383
25626
|
get rtpPacketsSentTotal() {
|
|
25384
25627
|
return this.rtpPacketsSent;
|
|
25385
25628
|
}
|
|
25629
|
+
/**
|
|
25630
|
+
* Video RTP PAYLOAD bytes this session sent, bucketed by time — the "sent"
|
|
25631
|
+
* half of the adaptive ladder's delivery ratio (D635). Payload, not wire
|
|
25632
|
+
* size, because the client's `bytesReceived` excludes RTP headers.
|
|
25633
|
+
*/
|
|
25634
|
+
videoSendHistory = new SendHistory(ADAPTIVE_DELIVERY_CONFIG.historySpanMs);
|
|
25635
|
+
get sentVideoHistory() {
|
|
25636
|
+
return this.videoSendHistory;
|
|
25637
|
+
}
|
|
25638
|
+
/**
|
|
25639
|
+
* The receiver's bandwidth estimate (REMB) in kbps, or `null` when the peer
|
|
25640
|
+
* has sent none. werift stores the last REMB on the sender; `-1`/absent in
|
|
25641
|
+
* the loss line means the same "no estimate", which is not "zero".
|
|
25642
|
+
*/
|
|
25643
|
+
receiverEstimateKbps(now) {
|
|
25644
|
+
const raw = this.videoSender?.receiverEstimatedMaxBitrate;
|
|
25645
|
+
return this.rembWatch.read(typeof raw === "bigint" && raw > 0n ? Number(raw) / 1e3 : null, now);
|
|
25646
|
+
}
|
|
25647
|
+
/** Ages the REMB werift keeps forever — see `RembWatch`. */
|
|
25648
|
+
rembWatch = new RembWatch(REMB_STALE_MS);
|
|
25386
25649
|
rtpLogCounter = 0;
|
|
25387
25650
|
/** One-shot guards for the "first video/audio RTP" debug markers. */
|
|
25388
25651
|
firstVideoRtpLogged = false;
|
|
@@ -25705,6 +25968,7 @@ var AdaptiveSession = class AdaptiveSession {
|
|
|
25705
25968
|
} });
|
|
25706
25969
|
}
|
|
25707
25970
|
this.videoSender.sendRtp(pkt);
|
|
25971
|
+
this.videoSendHistory.record(payload.length, Date.now());
|
|
25708
25972
|
this.rtpPacketsSent++;
|
|
25709
25973
|
this.logFirstVideoRtp("writeVideoNals", codec);
|
|
25710
25974
|
if (this.debug && (this.rtpPacketsSent === 1 || this.rtpPacketsSent % 500 === 0)) this.logger.info("RTP packets sent", { meta: {
|
|
@@ -26172,6 +26436,42 @@ var LossTracker = class {
|
|
|
26172
26436
|
//#endregion
|
|
26173
26437
|
//#region src/stream-broker/webrtc/stall-tracker.ts
|
|
26174
26438
|
/**
|
|
26439
|
+
* The one place the device-scoped telemetry is judged ownable. With more than
|
|
26440
|
+
* one adaptive session of the device, the record may be another viewer's:
|
|
26441
|
+
* delivery is unknown ("no telemetry" semantics — the loss path judges) and
|
|
26442
|
+
* the sample says so, so the ladder refuses a try-climb on it.
|
|
26443
|
+
*/
|
|
26444
|
+
function buildStallSample(input) {
|
|
26445
|
+
if (input.adaptiveSessionsOnDevice > 1) return {
|
|
26446
|
+
delivery: null,
|
|
26447
|
+
recent: null,
|
|
26448
|
+
clientReporting: false,
|
|
26449
|
+
telemetryShared: true,
|
|
26450
|
+
serverSending: input.serverSending
|
|
26451
|
+
};
|
|
26452
|
+
return {
|
|
26453
|
+
delivery: input.delivery,
|
|
26454
|
+
recent: input.recent,
|
|
26455
|
+
clientReporting: input.clientReporting,
|
|
26456
|
+
telemetryShared: false,
|
|
26457
|
+
serverSending: input.serverSending
|
|
26458
|
+
};
|
|
26459
|
+
}
|
|
26460
|
+
var UNKNOWN_EVIDENCE = {
|
|
26461
|
+
clientKbps: null,
|
|
26462
|
+
sentKbps: null,
|
|
26463
|
+
deliveryRatio: null,
|
|
26464
|
+
deliveryWindowMs: null,
|
|
26465
|
+
stalledForMs: 0,
|
|
26466
|
+
serverSending: false
|
|
26467
|
+
};
|
|
26468
|
+
function ratioOf(delivery) {
|
|
26469
|
+
if (delivery === null) return null;
|
|
26470
|
+
const { receivedKbps, sentKbps } = delivery;
|
|
26471
|
+
if (!Number.isFinite(receivedKbps) || !Number.isFinite(sentKbps) || sentKbps <= 0) return null;
|
|
26472
|
+
return receivedKbps / sentKbps;
|
|
26473
|
+
}
|
|
26474
|
+
/**
|
|
26175
26475
|
* Windowed "the media stopped arriving" detector. Pure state machine: no
|
|
26176
26476
|
* clock of its own, no side effects — `observe` is the only mutator and every
|
|
26177
26477
|
* decision is a function of the samples it was given.
|
|
@@ -26181,11 +26481,9 @@ var StallTracker = class {
|
|
|
26181
26481
|
/** When the current uninterrupted stall began; null when not stalling. */
|
|
26182
26482
|
stallStartedAt = null;
|
|
26183
26483
|
blackoutUntil = 0;
|
|
26184
|
-
lastEvidence =
|
|
26185
|
-
|
|
26186
|
-
|
|
26187
|
-
serverSending: false
|
|
26188
|
-
};
|
|
26484
|
+
lastEvidence = UNKNOWN_EVIDENCE;
|
|
26485
|
+
clientReporting = false;
|
|
26486
|
+
telemetryShared = false;
|
|
26189
26487
|
constructor(config) {
|
|
26190
26488
|
this.config = config;
|
|
26191
26489
|
}
|
|
@@ -26198,38 +26496,36 @@ var StallTracker = class {
|
|
|
26198
26496
|
* Fold one evaluation into the window. Returns nothing; ask `isStalled`.
|
|
26199
26497
|
*
|
|
26200
26498
|
* Three outcomes, and the middle one is the reason this is not a simple
|
|
26201
|
-
* boolean: a
|
|
26202
|
-
* below `
|
|
26203
|
-
* (or unknown, or
|
|
26204
|
-
*
|
|
26499
|
+
* boolean: a ratio at or above `recoverDeliveryRatio` CLEARS the stall, a
|
|
26500
|
+
* ratio below `stallDeliveryRatio` with the server sending starts or extends
|
|
26501
|
+
* it, and anything between the two (or unknown, or nothing sent) HOLDS
|
|
26502
|
+
* whatever the window already believed.
|
|
26205
26503
|
*/
|
|
26206
26504
|
observe(sample, now) {
|
|
26505
|
+
const ratio = ratioOf(sample.delivery);
|
|
26506
|
+
const measured = (stalledForMs) => ({
|
|
26507
|
+
clientKbps: sample.delivery?.receivedKbps ?? null,
|
|
26508
|
+
sentKbps: sample.delivery?.sentKbps ?? null,
|
|
26509
|
+
deliveryRatio: ratio,
|
|
26510
|
+
deliveryWindowMs: sample.delivery?.windowMs ?? null,
|
|
26511
|
+
stalledForMs,
|
|
26512
|
+
serverSending: sample.serverSending
|
|
26513
|
+
});
|
|
26514
|
+
this.clientReporting = sample.clientReporting;
|
|
26515
|
+
this.telemetryShared = sample.telemetryShared;
|
|
26207
26516
|
if (now < this.blackoutUntil) {
|
|
26208
26517
|
this.stallStartedAt = null;
|
|
26209
|
-
this.lastEvidence =
|
|
26210
|
-
clientKbps: sample.clientKbps,
|
|
26211
|
-
stalledForMs: 0,
|
|
26212
|
-
serverSending: sample.serverSending
|
|
26213
|
-
};
|
|
26518
|
+
this.lastEvidence = measured(0);
|
|
26214
26519
|
return;
|
|
26215
26520
|
}
|
|
26216
|
-
const
|
|
26217
|
-
const
|
|
26218
|
-
|
|
26219
|
-
|
|
26220
|
-
|
|
26221
|
-
|
|
26222
|
-
|
|
26223
|
-
|
|
26224
|
-
return;
|
|
26225
|
-
}
|
|
26226
|
-
if (kbps >= this.config.recoverKbpsFloor) this.stallStartedAt = null;
|
|
26227
|
-
else if (kbps < this.config.stallKbpsFloor && sample.serverSending) this.stallStartedAt ??= now;
|
|
26228
|
-
this.lastEvidence = {
|
|
26229
|
-
clientKbps: kbps,
|
|
26230
|
-
stalledForMs: this.stalledForMs(now),
|
|
26231
|
-
serverSending: sample.serverSending
|
|
26232
|
-
};
|
|
26521
|
+
const recent = sample.recent !== null && sample.recent.sentKbps >= this.config.freezeMinSentKbps ? ratioOf(sample.recent) : null;
|
|
26522
|
+
const frozen = recent !== null && recent < this.config.freezeDeliveryRatio;
|
|
26523
|
+
const recovered = recent !== null && recent >= this.config.recoverDeliveryRatio;
|
|
26524
|
+
const starved = ratio !== null && ratio < this.config.stallDeliveryRatio && !recovered;
|
|
26525
|
+
const current = recent ?? ratio;
|
|
26526
|
+
if ((frozen || starved) && sample.serverSending) this.stallStartedAt ??= now;
|
|
26527
|
+
else if (current !== null && current >= this.config.recoverDeliveryRatio) this.stallStartedAt = null;
|
|
26528
|
+
this.lastEvidence = measured(this.stalledForMs(now));
|
|
26233
26529
|
}
|
|
26234
26530
|
stalledForMs(now) {
|
|
26235
26531
|
return this.stallStartedAt === null ? 0 : now - this.stallStartedAt;
|
|
@@ -26240,25 +26536,42 @@ var StallTracker = class {
|
|
|
26240
26536
|
return this.stalledForMs(now) >= this.config.stallConfirmMs;
|
|
26241
26537
|
}
|
|
26242
26538
|
/**
|
|
26243
|
-
* May the ladder PROMOTE on this evaluation?
|
|
26539
|
+
* May the ladder PROMOTE on this evaluation, as far as the transport knows?
|
|
26244
26540
|
*
|
|
26245
|
-
*
|
|
26246
|
-
*
|
|
26247
|
-
*
|
|
26248
|
-
*
|
|
26249
|
-
* reports almost no loss. Same doctrine as `LossTracker.hasEvidence` — a
|
|
26250
|
-
* climb needs POSITIVE evidence, not the absence of a complaint.
|
|
26541
|
+
* No while a stall is building, and no while the rung we are on is not
|
|
26542
|
+
* landing (nearly) whole — promoting there spends bandwidth the session has
|
|
26543
|
+
* just been shown not to have, and the loss path cannot object: a stream
|
|
26544
|
+
* delivering almost nothing reports almost no loss.
|
|
26251
26545
|
*
|
|
26252
|
-
* Unknown
|
|
26546
|
+
* Unknown refuses only while the client IS reporting — the window is simply
|
|
26547
|
+
* not covered yet, and climbing then would promote a link measured at 10 %
|
|
26548
|
+
* a few seconds later. A deployment with no client telemetry at all must
|
|
26253
26549
|
* behave exactly as it did before this tracker existed; the loss path's own
|
|
26254
26550
|
* `hasEvidence` is the gate there.
|
|
26255
26551
|
*/
|
|
26256
|
-
|
|
26257
|
-
if (now < this.blackoutUntil) return true;
|
|
26258
|
-
if (this.stallStartedAt !== null) return
|
|
26259
|
-
|
|
26260
|
-
|
|
26261
|
-
|
|
26552
|
+
climbSafety(now) {
|
|
26553
|
+
if (now < this.blackoutUntil) return { safe: true };
|
|
26554
|
+
if (this.stallStartedAt !== null) return {
|
|
26555
|
+
safe: false,
|
|
26556
|
+
reason: "stall-building"
|
|
26557
|
+
};
|
|
26558
|
+
const ratio = this.lastEvidence.deliveryRatio;
|
|
26559
|
+
if (ratio === null) return this.clientReporting ? {
|
|
26560
|
+
safe: false,
|
|
26561
|
+
reason: "delivery"
|
|
26562
|
+
} : { safe: true };
|
|
26563
|
+
return ratio >= this.config.climbDeliveryRatio ? { safe: true } : {
|
|
26564
|
+
safe: false,
|
|
26565
|
+
reason: "delivery"
|
|
26566
|
+
};
|
|
26567
|
+
}
|
|
26568
|
+
/** Was the device's client record possibly another viewer's at the last evaluation? */
|
|
26569
|
+
get isTelemetryShared() {
|
|
26570
|
+
return this.telemetryShared;
|
|
26571
|
+
}
|
|
26572
|
+
/** Was the client reporting at the last evaluation (even if not yet covering a window)? */
|
|
26573
|
+
get isClientReporting() {
|
|
26574
|
+
return this.clientReporting;
|
|
26262
26575
|
}
|
|
26263
26576
|
/** What the last `observe` measured — for the switch's log line. */
|
|
26264
26577
|
evidence(now) {
|
|
@@ -26269,9 +26582,142 @@ var StallTracker = class {
|
|
|
26269
26582
|
}
|
|
26270
26583
|
};
|
|
26271
26584
|
//#endregion
|
|
26585
|
+
//#region src/stream-broker/webrtc/rtt-stability.ts
|
|
26586
|
+
/** 16 s — the longest delivery window, so both halves of a try look at the same span. */
|
|
26587
|
+
var RTT_WINDOW_MS = 16e3;
|
|
26588
|
+
var MIN_SAMPLES = 3;
|
|
26589
|
+
var MIN_SPREAD_MS = 20;
|
|
26590
|
+
var SPREAD_FRACTION = .5;
|
|
26591
|
+
var RttWindow = class {
|
|
26592
|
+
samples = [];
|
|
26593
|
+
observe(rttMs, now) {
|
|
26594
|
+
if (rttMs === null || !Number.isFinite(rttMs) || rttMs <= 0) return;
|
|
26595
|
+
this.samples = [...this.samples.filter((s) => now - s.at <= RTT_WINDOW_MS), {
|
|
26596
|
+
rttMs,
|
|
26597
|
+
at: now
|
|
26598
|
+
}];
|
|
26599
|
+
}
|
|
26600
|
+
stability(now) {
|
|
26601
|
+
const recent = this.samples.filter((s) => now - s.at <= RTT_WINDOW_MS);
|
|
26602
|
+
if (recent.length < MIN_SAMPLES) return "unknown";
|
|
26603
|
+
const values = recent.map((s) => s.rttMs);
|
|
26604
|
+
const min = Math.min(...values);
|
|
26605
|
+
return Math.max(...values) - min <= Math.max(MIN_SPREAD_MS, min * SPREAD_FRACTION) ? "stable" : "unstable";
|
|
26606
|
+
}
|
|
26607
|
+
};
|
|
26608
|
+
//#endregion
|
|
26609
|
+
//#region src/stream-broker/webrtc/try-climb.ts
|
|
26610
|
+
var NO_TRY_HISTORY = {
|
|
26611
|
+
failures: 0,
|
|
26612
|
+
notBefore: 0,
|
|
26613
|
+
lastRevert: null,
|
|
26614
|
+
trialFrom: null
|
|
26615
|
+
};
|
|
26616
|
+
/** Backoff after the Nth consecutive failed try: base × 2^(N−1), capped. */
|
|
26617
|
+
function tryBackoffMs(failures, baseMs, maxMs) {
|
|
26618
|
+
if (failures <= 0) return 0;
|
|
26619
|
+
return Math.min(baseMs * 2 ** (failures - 1), maxMs);
|
|
26620
|
+
}
|
|
26621
|
+
/** Watches one tried rung through its first window. Pure: fed, never reads a clock. */
|
|
26622
|
+
var TrialWatch = class {
|
|
26623
|
+
from;
|
|
26624
|
+
config;
|
|
26625
|
+
startedAt = null;
|
|
26626
|
+
lowStreak = 0;
|
|
26627
|
+
measured = 0;
|
|
26628
|
+
constructor(from, config) {
|
|
26629
|
+
this.from = from;
|
|
26630
|
+
this.config = config;
|
|
26631
|
+
}
|
|
26632
|
+
/**
|
|
26633
|
+
* Start the trial over: it must be judged afresh on live media. Called when
|
|
26634
|
+
* recorded playback takes the track (review m14) — time off live must never
|
|
26635
|
+
* count toward keeping a rung that was never measured.
|
|
26636
|
+
*/
|
|
26637
|
+
restart() {
|
|
26638
|
+
this.startedAt = null;
|
|
26639
|
+
this.lowStreak = 0;
|
|
26640
|
+
this.measured = 0;
|
|
26641
|
+
}
|
|
26642
|
+
judge(now, input) {
|
|
26643
|
+
this.startedAt ??= now;
|
|
26644
|
+
if (input.stalled || input.lossFailing) return {
|
|
26645
|
+
kind: "revert",
|
|
26646
|
+
deliveryRatio: input.deliveryRatio,
|
|
26647
|
+
stalled: input.stalled
|
|
26648
|
+
};
|
|
26649
|
+
if (input.deliveryRatio !== null) {
|
|
26650
|
+
this.measured += 1;
|
|
26651
|
+
this.lowStreak = input.deliveryRatio < this.config.trialRevertRatio ? this.lowStreak + 1 : 0;
|
|
26652
|
+
if (this.lowStreak >= this.config.trialRevertConsecutive) return {
|
|
26653
|
+
kind: "revert",
|
|
26654
|
+
deliveryRatio: input.deliveryRatio,
|
|
26655
|
+
stalled: false
|
|
26656
|
+
};
|
|
26657
|
+
}
|
|
26658
|
+
const forMs = now - this.startedAt;
|
|
26659
|
+
const judged = this.measured >= this.config.trialRevertConsecutive || !input.clientReporting;
|
|
26660
|
+
if (forMs >= this.config.trialMs && judged || forMs >= this.config.trialMaxMs) return { kind: "kept" };
|
|
26661
|
+
return {
|
|
26662
|
+
kind: "running",
|
|
26663
|
+
forMs
|
|
26664
|
+
};
|
|
26665
|
+
}
|
|
26666
|
+
};
|
|
26667
|
+
/**
|
|
26668
|
+
* The try-climb memory per DEVICE on this broker, with a TTL (review I4).
|
|
26669
|
+
* The per-session intent survives only a re-offer; without this, every new
|
|
26670
|
+
* viewer session of a camera whose next rung keeps failing started again at
|
|
26671
|
+
* try #1. The trial itself (`trialFrom`) never carries to a new session.
|
|
26672
|
+
*/
|
|
26673
|
+
var DeviceTryMemory = class {
|
|
26674
|
+
ttlMs;
|
|
26675
|
+
entries = /* @__PURE__ */ new Map();
|
|
26676
|
+
/** @param ttlMs forget a device not refreshed for this long — 600 000 ms, the backoff cap. */
|
|
26677
|
+
constructor(ttlMs) {
|
|
26678
|
+
this.ttlMs = ttlMs;
|
|
26679
|
+
}
|
|
26680
|
+
set(deviceId, carry, now) {
|
|
26681
|
+
this.entries.set(deviceId, {
|
|
26682
|
+
carry,
|
|
26683
|
+
at: now
|
|
26684
|
+
});
|
|
26685
|
+
}
|
|
26686
|
+
get(deviceId, now) {
|
|
26687
|
+
const entry = this.entries.get(deviceId);
|
|
26688
|
+
if (entry === void 0) return null;
|
|
26689
|
+
if (now - entry.at > this.ttlMs) {
|
|
26690
|
+
this.entries.delete(deviceId);
|
|
26691
|
+
return null;
|
|
26692
|
+
}
|
|
26693
|
+
return {
|
|
26694
|
+
...entry.carry,
|
|
26695
|
+
trialFrom: null
|
|
26696
|
+
};
|
|
26697
|
+
}
|
|
26698
|
+
};
|
|
26699
|
+
//#endregion
|
|
26272
26700
|
//#region src/stream-broker/webrtc/adaptive-controller.ts
|
|
26273
26701
|
var TIER_ORDER = CAM_PROFILE_ORDER;
|
|
26274
26702
|
/**
|
|
26703
|
+
* The cause as the wire (`WebrtcLadderDecision.cause`) can carry it. The wire
|
|
26704
|
+
* enum predates the try-climb, and widening it would make a server built
|
|
26705
|
+
* before it reject this broker's debug answer (the router validates output).
|
|
26706
|
+
* A trial revert is reported as what undelivered bytes are — `stall` when the
|
|
26707
|
+
* stall confirmed, else `loss` — and the climb hold carries the exact account
|
|
26708
|
+
* ("tried mid, reverted: delivery 0.60").
|
|
26709
|
+
*/
|
|
26710
|
+
function wireLadderCause(decision, stallConfirmMs) {
|
|
26711
|
+
if (decision.cause !== "trial-revert") return { cause: decision.cause };
|
|
26712
|
+
return {
|
|
26713
|
+
cause: decision.stall.stalledForMs >= stallConfirmMs ? "stall" : "loss",
|
|
26714
|
+
causeDetail: "trial-revert"
|
|
26715
|
+
};
|
|
26716
|
+
}
|
|
26717
|
+
function positiveOrNull(v) {
|
|
26718
|
+
return v !== null && Number.isFinite(v) && v > 0 ? v : null;
|
|
26719
|
+
}
|
|
26720
|
+
/**
|
|
26275
26721
|
* Ladder state machine for an adaptive WebRTC session. Fed loss samples
|
|
26276
26722
|
* (RTCP RR or client-reported) via `onLoss`; `tick` evaluates the ladder.
|
|
26277
26723
|
* Downgrade order: tier high→mid→low, then MTU reduction at low, then
|
|
@@ -26285,7 +26731,16 @@ var TIER_ORDER = CAM_PROFILE_ORDER;
|
|
|
26285
26731
|
* a session that has produced no RTCP at all is not "healthy", it is
|
|
26286
26732
|
* silent, and climbing on silence spends bandwidth never shown to exist;
|
|
26287
26733
|
* 3. the target tier's re-attempt backoff, which doubles per measured
|
|
26288
|
-
* failure of that tier in this session
|
|
26734
|
+
* failure of that tier in this session;
|
|
26735
|
+
* 4. the transport showing the CURRENT rung landing whole
|
|
26736
|
+
* (`StallTracker.climbSafety`, a delivery ratio over one GOP — D635);
|
|
26737
|
+
* 5. capacity: evidence (REMB or the viewer's probe) clearing the next
|
|
26738
|
+
* rung's bar confirms the climb; a MEASURED probe below it holds; with no
|
|
26739
|
+
* evidence either way the ladder TRIES the rung on a clean path (delivery
|
|
26740
|
+
* ≥ 0.95, loss < 1 %, RTT stable) and reverts within the rung's first
|
|
26741
|
+
* window if it does not land, backing off 30 s → 10 min (D635, round 2).
|
|
26742
|
+
*
|
|
26743
|
+
* Every tick that does not promote leaves a {@link ClimbHold} naming why.
|
|
26289
26744
|
*/
|
|
26290
26745
|
var AdaptiveController = class {
|
|
26291
26746
|
opts;
|
|
@@ -26306,6 +26761,16 @@ var AdaptiveController = class {
|
|
|
26306
26761
|
armed = false;
|
|
26307
26762
|
failures;
|
|
26308
26763
|
upgradeCount;
|
|
26764
|
+
capacity = {
|
|
26765
|
+
rembKbps: null,
|
|
26766
|
+
probeKbps: null,
|
|
26767
|
+
probeFresh: false
|
|
26768
|
+
};
|
|
26769
|
+
rtt = new RttWindow();
|
|
26770
|
+
tryCarry;
|
|
26771
|
+
/** Non-null while the current rung is a trial. */
|
|
26772
|
+
trial;
|
|
26773
|
+
hold = null;
|
|
26309
26774
|
constructor(opts) {
|
|
26310
26775
|
this.opts = opts;
|
|
26311
26776
|
this.tracker = new LossTracker(opts.config);
|
|
@@ -26313,6 +26778,16 @@ var AdaptiveController = class {
|
|
|
26313
26778
|
this.tier = opts.initialTier;
|
|
26314
26779
|
this.failures = new Map(opts.priorFailures ?? []);
|
|
26315
26780
|
this.upgradeCount = Math.max(0, opts.priorUpgradeCount ?? 0);
|
|
26781
|
+
const carried = opts.tryClimb ?? NO_TRY_HISTORY;
|
|
26782
|
+
this.tryCarry = carried.trialFrom === opts.initialTier ? {
|
|
26783
|
+
...carried,
|
|
26784
|
+
trialFrom: null
|
|
26785
|
+
} : carried;
|
|
26786
|
+
this.trial = this.tryCarry.trialFrom !== null ? new TrialWatch(this.tryCarry.trialFrom, opts.config) : null;
|
|
26787
|
+
}
|
|
26788
|
+
/** What the NEXT controller of this viewer session must inherit about tries. */
|
|
26789
|
+
get tryClimbCarry() {
|
|
26790
|
+
return this.tryCarry;
|
|
26316
26791
|
}
|
|
26317
26792
|
get currentTier() {
|
|
26318
26793
|
return this.tier;
|
|
@@ -26345,6 +26820,27 @@ var AdaptiveController = class {
|
|
|
26345
26820
|
this.stalls.observe(sample, now);
|
|
26346
26821
|
}
|
|
26347
26822
|
/**
|
|
26823
|
+
* The capacity witnesses, kbps — `null` when unknown: the receiver's REMB
|
|
26824
|
+
* (capped by what is sent, so evidence only FOR room) and the viewer's
|
|
26825
|
+
* downlink probe for this session (a path measurement, evidence both ways).
|
|
26826
|
+
* See `climb-capacity.ts`.
|
|
26827
|
+
*/
|
|
26828
|
+
onCapacity(evidence) {
|
|
26829
|
+
this.capacity = {
|
|
26830
|
+
rembKbps: positiveOrNull(evidence.rembKbps),
|
|
26831
|
+
probeKbps: positiveOrNull(evidence.probeKbps),
|
|
26832
|
+
probeFresh: evidence.probeFresh
|
|
26833
|
+
};
|
|
26834
|
+
}
|
|
26835
|
+
/** The client's round-trip time, ms; null/0 = unknown. One of a try's conditions. */
|
|
26836
|
+
onRtt(rttMs, now) {
|
|
26837
|
+
this.rtt.observe(rttMs, now);
|
|
26838
|
+
}
|
|
26839
|
+
/** Why the ladder did not climb on the last evaluation; null = at the top, or it just moved. */
|
|
26840
|
+
get climbHold() {
|
|
26841
|
+
return this.hold;
|
|
26842
|
+
}
|
|
26843
|
+
/**
|
|
26348
26844
|
* Freeze the ladder for this evaluation instead of ticking it.
|
|
26349
26845
|
*
|
|
26350
26846
|
* Called while the session's track is NOT carrying the live source — D42
|
|
@@ -26365,6 +26861,8 @@ var AdaptiveController = class {
|
|
|
26365
26861
|
this.lastChangeAt = now;
|
|
26366
26862
|
}
|
|
26367
26863
|
this.healthySince = now;
|
|
26864
|
+
this.trial?.restart();
|
|
26865
|
+
this.setHold("suspended", now);
|
|
26368
26866
|
this.stalls.markChanged(now);
|
|
26369
26867
|
}
|
|
26370
26868
|
tick(now) {
|
|
@@ -26373,26 +26871,132 @@ var AdaptiveController = class {
|
|
|
26373
26871
|
this.healthySince = now;
|
|
26374
26872
|
this.lastChangeAt = now;
|
|
26375
26873
|
}
|
|
26874
|
+
if (this.trial !== null && this.judgeTrial(now)) return;
|
|
26376
26875
|
if (this.stalls.isStalled(now)) {
|
|
26377
26876
|
this.healthySince = now;
|
|
26877
|
+
this.hold = null;
|
|
26378
26878
|
this.handleFailing(now, "stall");
|
|
26379
26879
|
return;
|
|
26380
26880
|
}
|
|
26381
26881
|
if (this.tracker.isFailing(now)) {
|
|
26382
26882
|
this.healthySince = now;
|
|
26883
|
+
this.hold = null;
|
|
26383
26884
|
this.handleFailing(now, "loss");
|
|
26384
26885
|
return;
|
|
26385
26886
|
}
|
|
26386
|
-
|
|
26887
|
+
const safety = this.stalls.climbSafety(now);
|
|
26888
|
+
if (!safety.safe) {
|
|
26387
26889
|
this.healthySince = now;
|
|
26890
|
+
this.setHold(safety.reason, now);
|
|
26388
26891
|
return;
|
|
26389
26892
|
}
|
|
26390
26893
|
if (!this.tracker.hasEvidence(now)) {
|
|
26391
26894
|
this.healthySince = now;
|
|
26895
|
+
this.setHold("no-evidence", now);
|
|
26392
26896
|
return;
|
|
26393
26897
|
}
|
|
26394
26898
|
this.handleHealthy(now);
|
|
26395
26899
|
}
|
|
26900
|
+
nextTier() {
|
|
26901
|
+
const idx = TIER_ORDER.indexOf(this.tier);
|
|
26902
|
+
return idx <= 0 ? null : TIER_ORDER[idx - 1] ?? null;
|
|
26903
|
+
}
|
|
26904
|
+
/**
|
|
26905
|
+
* Record why this evaluation did not climb. At the top rung there is nothing
|
|
26906
|
+
* to hold — except a `trial`, which names the rung being tried.
|
|
26907
|
+
*/
|
|
26908
|
+
setHold(code, now, extra = {}) {
|
|
26909
|
+
const next = code === "trial" ? this.tier : this.nextTier();
|
|
26910
|
+
if (next === null) {
|
|
26911
|
+
this.hold = null;
|
|
26912
|
+
return;
|
|
26913
|
+
}
|
|
26914
|
+
const ev = this.stalls.evidence(now);
|
|
26915
|
+
const clockRunning = code === "warming" || code === "cooldown" || code === "capacity" || code === "try-unclean" || code === "try-backoff";
|
|
26916
|
+
const nextTierKbps = this.opts.expectedTierKbps?.(next) ?? null;
|
|
26917
|
+
this.hold = {
|
|
26918
|
+
code,
|
|
26919
|
+
nextTier: next,
|
|
26920
|
+
deliveryRatio: ev.deliveryRatio,
|
|
26921
|
+
deliveryWindowMs: ev.deliveryWindowMs,
|
|
26922
|
+
climbDeliveryRatio: this.opts.config.climbDeliveryRatio,
|
|
26923
|
+
capacityKbps: this.capacity.rembKbps,
|
|
26924
|
+
probeKbps: this.capacity.probeKbps,
|
|
26925
|
+
nextTierKbps,
|
|
26926
|
+
capacityMargin: this.opts.config.climbCapacityMargin,
|
|
26927
|
+
neededKbps: capacityBarKbps(next, nextTierKbps, this.opts.config.climbCapacityMargin),
|
|
26928
|
+
tryBlockedBy: extra.tryBlockedBy ?? null,
|
|
26929
|
+
tryDeliveryRatio: this.opts.config.tryClimbDeliveryRatio,
|
|
26930
|
+
tryMaxLossRatio: this.opts.config.tryClimbMaxLossRatio,
|
|
26931
|
+
retryInMs: code === "try-backoff" ? Math.max(0, this.tryCarry.notBefore - now) : null,
|
|
26932
|
+
lastRevert: this.tryCarry.lastRevert,
|
|
26933
|
+
trialForMs: code === "trial" ? extra.trialForMs ?? 0 : null,
|
|
26934
|
+
trialMs: code === "trial" ? this.opts.config.trialMs : null,
|
|
26935
|
+
healthyForMs: clockRunning ? now - this.healthySince : null,
|
|
26936
|
+
requiredHealthyMs: this.requiredHealthyMs(next),
|
|
26937
|
+
since: this.hold?.code === code ? this.hold.since : now
|
|
26938
|
+
};
|
|
26939
|
+
}
|
|
26940
|
+
/**
|
|
26941
|
+
* Judge the rung on trial. Returns true when this evaluation is consumed
|
|
26942
|
+
* (still running, or reverted); false when the trial was just kept and the
|
|
26943
|
+
* ordinary ladder takes over.
|
|
26944
|
+
*/
|
|
26945
|
+
judgeTrial(now) {
|
|
26946
|
+
const watch = this.trial;
|
|
26947
|
+
if (watch === null) return false;
|
|
26948
|
+
const verdict = watch.judge(now, {
|
|
26949
|
+
deliveryRatio: this.stalls.evidence(now).deliveryRatio,
|
|
26950
|
+
clientReporting: this.stalls.isClientReporting,
|
|
26951
|
+
stalled: this.stalls.isStalled(now),
|
|
26952
|
+
lossFailing: this.tracker.isFailing(now)
|
|
26953
|
+
});
|
|
26954
|
+
if (verdict.kind === "running") {
|
|
26955
|
+
this.healthySince = now;
|
|
26956
|
+
this.setHold("trial", now, { trialForMs: verdict.forMs });
|
|
26957
|
+
return true;
|
|
26958
|
+
}
|
|
26959
|
+
this.trial = null;
|
|
26960
|
+
if (verdict.kind === "kept") {
|
|
26961
|
+
this.tryCarry = NO_TRY_HISTORY;
|
|
26962
|
+
this.healthySince = now;
|
|
26963
|
+
return false;
|
|
26964
|
+
}
|
|
26965
|
+
const failures = this.tryCarry.failures + 1;
|
|
26966
|
+
const tried = this.tier;
|
|
26967
|
+
this.tryCarry = {
|
|
26968
|
+
failures,
|
|
26969
|
+
notBefore: now + tryBackoffMs(failures, this.opts.config.tryBackoffBaseMs, this.opts.config.tryBackoffMaxMs),
|
|
26970
|
+
lastRevert: {
|
|
26971
|
+
tier: tried,
|
|
26972
|
+
deliveryRatio: verdict.deliveryRatio,
|
|
26973
|
+
stalled: verdict.stalled,
|
|
26974
|
+
at: now
|
|
26975
|
+
},
|
|
26976
|
+
trialFrom: null
|
|
26977
|
+
};
|
|
26978
|
+
this.hold = null;
|
|
26979
|
+
this.tier = watch.from;
|
|
26980
|
+
this.mtuReduced = false;
|
|
26981
|
+
this.transcodeEngaged = false;
|
|
26982
|
+
this.opts.onSwitchTier(watch.from, this.decisionAt(now, "trial-revert"));
|
|
26983
|
+
this.commitChange(now);
|
|
26984
|
+
this.healthySince = now;
|
|
26985
|
+
return true;
|
|
26986
|
+
}
|
|
26987
|
+
/**
|
|
26988
|
+
* Which clean-path condition refuses a TRY, or null when the path is clean.
|
|
26989
|
+
* A client that sends no telemetry at all cannot measure delivery; there the
|
|
26990
|
+
* loss path is the judge, as it was before delivery existed.
|
|
26991
|
+
*/
|
|
26992
|
+
tryBlocker(now) {
|
|
26993
|
+
if (this.stalls.isTelemetryShared) return "shared-telemetry";
|
|
26994
|
+
const ratio = this.stalls.evidence(now).deliveryRatio;
|
|
26995
|
+
if (ratio === null ? this.stalls.isClientReporting : ratio < this.opts.config.tryClimbDeliveryRatio) return "delivery";
|
|
26996
|
+
if (this.tracker.lossRatio() >= this.opts.config.tryClimbMaxLossRatio) return "loss";
|
|
26997
|
+
if (this.rtt.stability(now) === "unstable") return "rtt";
|
|
26998
|
+
return null;
|
|
26999
|
+
}
|
|
26396
27000
|
canUpgrade(now) {
|
|
26397
27001
|
return now - this.lastChangeAt >= this.opts.config.minTimeBetweenChangesMs;
|
|
26398
27002
|
}
|
|
@@ -26413,11 +27017,12 @@ var AdaptiveController = class {
|
|
|
26413
27017
|
this.stalls.markChanged(now);
|
|
26414
27018
|
}
|
|
26415
27019
|
/** The measurement this decision rests on, snapshotted for the log line. */
|
|
26416
|
-
decisionAt(now, cause) {
|
|
27020
|
+
decisionAt(now, cause, trial = false) {
|
|
26417
27021
|
return {
|
|
26418
27022
|
cause,
|
|
26419
27023
|
lossRatio: this.tracker.lossRatio(),
|
|
26420
|
-
stall: this.stalls.evidence(now)
|
|
27024
|
+
stall: this.stalls.evidence(now),
|
|
27025
|
+
trial
|
|
26421
27026
|
};
|
|
26422
27027
|
}
|
|
26423
27028
|
handleFailing(now, cause) {
|
|
@@ -26444,21 +27049,223 @@ var AdaptiveController = class {
|
|
|
26444
27049
|
}
|
|
26445
27050
|
}
|
|
26446
27051
|
handleHealthy(now) {
|
|
26447
|
-
const
|
|
26448
|
-
if (
|
|
26449
|
-
|
|
26450
|
-
|
|
26451
|
-
|
|
27052
|
+
const next = this.nextTier();
|
|
27053
|
+
if (next === null) {
|
|
27054
|
+
this.hold = null;
|
|
27055
|
+
return;
|
|
27056
|
+
}
|
|
27057
|
+
const capacity = judgeCapacity(this.capacity, next, this.opts.expectedTierKbps?.(next) ?? null, this.opts.config.climbCapacityMargin);
|
|
27058
|
+
if (capacity.kind === "probe-short") {
|
|
27059
|
+
this.setHold("capacity", now);
|
|
27060
|
+
return;
|
|
27061
|
+
}
|
|
27062
|
+
if (capacity.kind === "none" && now < this.tryCarry.notBefore) {
|
|
27063
|
+
this.setHold("try-backoff", now);
|
|
27064
|
+
return;
|
|
27065
|
+
}
|
|
27066
|
+
if (now - this.healthySince < this.requiredHealthyMs(next)) {
|
|
27067
|
+
this.setHold("warming", now);
|
|
27068
|
+
return;
|
|
27069
|
+
}
|
|
27070
|
+
if (!this.canUpgrade(now)) {
|
|
27071
|
+
this.setHold("cooldown", now);
|
|
27072
|
+
return;
|
|
27073
|
+
}
|
|
27074
|
+
if (capacity.kind === "sufficient") {
|
|
27075
|
+
this.promote(now, next, false);
|
|
27076
|
+
return;
|
|
27077
|
+
}
|
|
27078
|
+
const blocker = this.tryBlocker(now);
|
|
27079
|
+
if (blocker !== null) {
|
|
27080
|
+
this.setHold("try-unclean", now, { tryBlockedBy: blocker });
|
|
27081
|
+
return;
|
|
27082
|
+
}
|
|
27083
|
+
this.promote(now, next, true);
|
|
27084
|
+
}
|
|
27085
|
+
promote(now, next, trial) {
|
|
27086
|
+
const from = this.tier;
|
|
27087
|
+
this.hold = null;
|
|
26452
27088
|
this.tier = next;
|
|
26453
27089
|
this.mtuReduced = false;
|
|
26454
27090
|
this.transcodeEngaged = false;
|
|
26455
27091
|
this.upgradeCount += 1;
|
|
26456
|
-
this.
|
|
27092
|
+
this.tryCarry = {
|
|
27093
|
+
...this.tryCarry,
|
|
27094
|
+
trialFrom: trial ? from : null
|
|
27095
|
+
};
|
|
27096
|
+
this.trial = trial ? new TrialWatch(from, this.opts.config) : null;
|
|
27097
|
+
this.opts.onSwitchTier(next, this.decisionAt(now, "healthy", trial));
|
|
26457
27098
|
this.commitChange(now);
|
|
26458
27099
|
this.healthySince = now;
|
|
26459
27100
|
}
|
|
26460
27101
|
};
|
|
26461
27102
|
//#endregion
|
|
27103
|
+
//#region src/stream-broker/webrtc/climb-hold.ts
|
|
27104
|
+
/** The wire shape: the hold plus the line the broker logs. */
|
|
27105
|
+
function toWireClimbHold(h) {
|
|
27106
|
+
return {
|
|
27107
|
+
...h,
|
|
27108
|
+
summary: describeClimbHold(h)
|
|
27109
|
+
};
|
|
27110
|
+
}
|
|
27111
|
+
function seconds(ms) {
|
|
27112
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
27113
|
+
}
|
|
27114
|
+
/** A rate, or "unknown" — never a fabricated 0 kbps (D393). */
|
|
27115
|
+
function kbps(v) {
|
|
27116
|
+
return v === null ? "unknown" : `${String(Math.round(v))} kbps`;
|
|
27117
|
+
}
|
|
27118
|
+
function deliveryPart(h) {
|
|
27119
|
+
if (h.deliveryRatio === null || h.deliveryWindowMs === null) return null;
|
|
27120
|
+
return `delivery ${h.deliveryRatio.toFixed(2)} over ${seconds(h.deliveryWindowMs)}`;
|
|
27121
|
+
}
|
|
27122
|
+
/** `mid needs 2160 kbps (1800 × 1.2)`, or `(client threshold)` for an unmeasured rung. */
|
|
27123
|
+
function barPart(h) {
|
|
27124
|
+
const basis = h.nextTierKbps !== null && h.nextTierKbps > 0 ? `(${String(Math.round(h.nextTierKbps))} × ${String(h.capacityMargin)})` : "(client threshold)";
|
|
27125
|
+
return `${h.nextTier} needs ${kbps(h.neededKbps)} ${basis}`;
|
|
27126
|
+
}
|
|
27127
|
+
function witnesses(h) {
|
|
27128
|
+
return `${h.capacityKbps === null ? "no REMB" : `REMB ${kbps(h.capacityKbps)}`}, ${h.probeKbps === null ? "no probe" : `probe ${kbps(h.probeKbps)}`}`;
|
|
27129
|
+
}
|
|
27130
|
+
function blockerPart(h) {
|
|
27131
|
+
switch (h.tryBlockedBy) {
|
|
27132
|
+
case "delivery": return h.deliveryRatio === null ? "delivery not measured" : `delivery ${h.deliveryRatio.toFixed(2)} < ${h.tryDeliveryRatio.toFixed(2)}`;
|
|
27133
|
+
case "loss": return `loss ≥ ${String(Math.round(h.tryMaxLossRatio * 1e3) / 10)}%`;
|
|
27134
|
+
case "rtt": return "RTT unstable";
|
|
27135
|
+
case "shared-telemetry": return "another viewer shares this camera's telemetry";
|
|
27136
|
+
case null: return "unknown";
|
|
27137
|
+
}
|
|
27138
|
+
}
|
|
27139
|
+
function revertPart(h) {
|
|
27140
|
+
const r = h.lastRevert;
|
|
27141
|
+
if (r === null) return "a try reverted";
|
|
27142
|
+
const why = r.stalled ? "stall" : r.deliveryRatio === null ? "loss" : `delivery ${r.deliveryRatio.toFixed(2)}`;
|
|
27143
|
+
return `tried ${r.tier}, reverted: ${why}`;
|
|
27144
|
+
}
|
|
27145
|
+
/**
|
|
27146
|
+
* One line, English, numbers included — the same text the broker logs and the
|
|
27147
|
+
* viewer shows under its localized hold label. Every number is one the
|
|
27148
|
+
* controller actually used; an unknown one is said to be unknown.
|
|
27149
|
+
*/
|
|
27150
|
+
function describeClimbHold(h) {
|
|
27151
|
+
const delivery = deliveryPart(h);
|
|
27152
|
+
switch (h.code) {
|
|
27153
|
+
case "stall-building": return `held: stall building (${delivery ?? "delivery unknown"})`;
|
|
27154
|
+
case "delivery": return delivery === null ? "held: delivery not yet measured over one GOP" : `held: ${delivery} < ${h.climbDeliveryRatio.toFixed(2)}`;
|
|
27155
|
+
case "no-evidence": return "held: no receiver feedback in the loss window";
|
|
27156
|
+
case "warming": return `climbing: healthy ${seconds(h.healthyForMs ?? 0)} of ${seconds(h.requiredHealthyMs ?? 0)} before ${h.nextTier}`;
|
|
27157
|
+
case "cooldown": return "held: too soon after the last change";
|
|
27158
|
+
case "capacity": return `held: probe ${kbps(h.probeKbps)}, ${h.capacityKbps === null ? "no REMB" : `REMB ${kbps(h.capacityKbps)}`} < ${barPart(h)}`;
|
|
27159
|
+
case "try-unclean": return `held: no capacity evidence for ${h.nextTier} (${witnesses(h)}); not clean enough to try: ${blockerPart(h)}`;
|
|
27160
|
+
case "try-backoff": return `held: ${revertPart(h)}; next try in ${seconds(h.retryInMs ?? 0)}`;
|
|
27161
|
+
case "trial": return `trying ${h.nextTier}: ${seconds(h.trialForMs ?? 0)} of ${seconds(h.trialMs ?? 0)} (reverts if it does not land)`;
|
|
27162
|
+
case "suspended": return "held: recorded playback on the track (ladder suspended)";
|
|
27163
|
+
}
|
|
27164
|
+
}
|
|
27165
|
+
/**
|
|
27166
|
+
* Which hold changes deserve a log line (review m3). A code logs when it
|
|
27167
|
+
* differs from the last one logged AND has not itself been logged within
|
|
27168
|
+
* `quietMs` — a link hovering at 0.90–0.95 delivery cycles delivery →
|
|
27169
|
+
* warming → try-unclean and used to write a line every 4–8 s for ever.
|
|
27170
|
+
* Progress (`warming`, `trial`) never logs; the switch line covers moves.
|
|
27171
|
+
*/
|
|
27172
|
+
var HoldLogGate = class {
|
|
27173
|
+
quietMs;
|
|
27174
|
+
last = null;
|
|
27175
|
+
loggedAt = /* @__PURE__ */ new Map();
|
|
27176
|
+
constructor(quietMs) {
|
|
27177
|
+
this.quietMs = quietMs;
|
|
27178
|
+
}
|
|
27179
|
+
shouldLog(code, now) {
|
|
27180
|
+
if (code === "warming" || code === "trial") return false;
|
|
27181
|
+
if (code === this.last) return false;
|
|
27182
|
+
const at = this.loggedAt.get(code);
|
|
27183
|
+
if (at !== void 0 && now - at < this.quietMs) return false;
|
|
27184
|
+
this.last = code;
|
|
27185
|
+
this.loggedAt.set(code, now);
|
|
27186
|
+
return true;
|
|
27187
|
+
}
|
|
27188
|
+
};
|
|
27189
|
+
//#endregion
|
|
27190
|
+
//#region src/stream-broker/webrtc/rung-bitrate.ts
|
|
27191
|
+
/**
|
|
27192
|
+
* Shortest lifetime whose average means anything: 4 000 ms ≈ one GOP on the
|
|
27193
|
+
* cameras this hub serves; less than that is one IDR divided by nothing.
|
|
27194
|
+
*/
|
|
27195
|
+
var MIN_AVERAGE_UPTIME_MS = 4e3;
|
|
27196
|
+
function expectedRungKbps(stats) {
|
|
27197
|
+
const lifetime = stats.totalBytes > 0 && stats.uptimeMs >= MIN_AVERAGE_UPTIME_MS ? Math.round(stats.totalBytes * 8 / stats.uptimeMs) : null;
|
|
27198
|
+
const windowKbps = stats.status === "streaming" && stats.bitrateKbps > 0 ? stats.bitrateKbps : null;
|
|
27199
|
+
if (windowKbps === null) return lifetime;
|
|
27200
|
+
return lifetime === null ? windowKbps : Math.max(windowKbps, lifetime);
|
|
27201
|
+
}
|
|
27202
|
+
/** The client's probe cache TTL (`DOWNLINK_TTL_MS`): the oldest a hinted probe can be. */
|
|
27203
|
+
var HINT_PROBE_MAX_AGE_MS = 3e4;
|
|
27204
|
+
function mbpsToKbps(mbps) {
|
|
27205
|
+
return mbps !== void 0 && Number.isFinite(mbps) && mbps > 0 ? mbps * 1e3 : null;
|
|
27206
|
+
}
|
|
27207
|
+
/** The probe an offer carried, dated conservatively; null when it carried none. */
|
|
27208
|
+
function probeFromHints(downlinkMbps, now) {
|
|
27209
|
+
const kbps = mbpsToKbps(downlinkMbps);
|
|
27210
|
+
return kbps === null ? null : {
|
|
27211
|
+
kbps,
|
|
27212
|
+
at: now - HINT_PROBE_MAX_AGE_MS
|
|
27213
|
+
};
|
|
27214
|
+
}
|
|
27215
|
+
/** The probe a telemetry report carried; null when it carried none. */
|
|
27216
|
+
function probeFromReport(report, now) {
|
|
27217
|
+
const kbps = mbpsToKbps(report.downlinkMbps);
|
|
27218
|
+
if (kbps === null) return null;
|
|
27219
|
+
return {
|
|
27220
|
+
kbps,
|
|
27221
|
+
at: now - Math.max(0, report.reportAgeMs) - Math.max(0, report.downlinkAgeMs)
|
|
27222
|
+
};
|
|
27223
|
+
}
|
|
27224
|
+
/** The more recent of two probes (either may be absent). */
|
|
27225
|
+
function newerProbe(a, b) {
|
|
27226
|
+
if (a === null) return b;
|
|
27227
|
+
if (b === null) return a;
|
|
27228
|
+
return b.at >= a.at ? b : a;
|
|
27229
|
+
}
|
|
27230
|
+
function isProbeFresh(probe, now) {
|
|
27231
|
+
return probe !== null && now - probe.at <= 12e4;
|
|
27232
|
+
}
|
|
27233
|
+
/**
|
|
27234
|
+
* How long the broker keeps a device's probe at all. 600 000 ms (10 min, the
|
|
27235
|
+
* try-backoff cap). A SHORT probe holds a climb until a newer probe replaces
|
|
27236
|
+
* it — and a viewer that cannot probe (no token yet, a share link, whose token
|
|
27237
|
+
* is not session-grade) never supplies one, so without an end one viewer's
|
|
27238
|
+
* 1.2 Mbps reading would hold every such viewer of the camera on `capacity`
|
|
27239
|
+
* until the broker restarted (review N1).
|
|
27240
|
+
*/
|
|
27241
|
+
var PROBE_MEMORY_TTL_MS = 6e5;
|
|
27242
|
+
/** The newest probe per device, bounded in time and forgotten with the device's last session. */
|
|
27243
|
+
var DeviceProbeMemory = class {
|
|
27244
|
+
ttlMs;
|
|
27245
|
+
probes = /* @__PURE__ */ new Map();
|
|
27246
|
+
constructor(ttlMs) {
|
|
27247
|
+
this.ttlMs = ttlMs;
|
|
27248
|
+
}
|
|
27249
|
+
/** Keep `probe` if it is newer than what is held; an absent one changes nothing. */
|
|
27250
|
+
record(deviceId, probe) {
|
|
27251
|
+
const kept = newerProbe(this.probes.get(deviceId) ?? null, probe);
|
|
27252
|
+
if (kept !== null) this.probes.set(deviceId, kept);
|
|
27253
|
+
}
|
|
27254
|
+
get(deviceId, now) {
|
|
27255
|
+
const probe = this.probes.get(deviceId);
|
|
27256
|
+
if (probe === void 0) return null;
|
|
27257
|
+
if (now - probe.at > this.ttlMs) {
|
|
27258
|
+
this.probes.delete(deviceId);
|
|
27259
|
+
return null;
|
|
27260
|
+
}
|
|
27261
|
+
return probe;
|
|
27262
|
+
}
|
|
27263
|
+
/** The device's last adaptive session ended: nobody's probe speaks for the next viewer. */
|
|
27264
|
+
forget(deviceId) {
|
|
27265
|
+
this.probes.delete(deviceId);
|
|
27266
|
+
}
|
|
27267
|
+
};
|
|
27268
|
+
//#endregion
|
|
26462
27269
|
//#region src/stream-broker/webrtc/session-debug.ts
|
|
26463
27270
|
/** `<deviceId>/<camStreamId>` → the camStreamId, or null when it is not one. */
|
|
26464
27271
|
function camStreamIdOf(brokerId) {
|
|
@@ -26497,7 +27304,8 @@ var DEAD = {
|
|
|
26497
27304
|
trackMode: null,
|
|
26498
27305
|
lastDecision: null,
|
|
26499
27306
|
tierFailures: [],
|
|
26500
|
-
upgradeCount: null
|
|
27307
|
+
upgradeCount: null,
|
|
27308
|
+
climbHold: null
|
|
26501
27309
|
};
|
|
26502
27310
|
function projectSessionDebug(input) {
|
|
26503
27311
|
const { session } = input;
|
|
@@ -26534,7 +27342,8 @@ function projectSessionDebug(input) {
|
|
|
26534
27342
|
tier,
|
|
26535
27343
|
failures
|
|
26536
27344
|
})),
|
|
26537
|
-
upgradeCount: input.ladder?.upgradeCount ?? null
|
|
27345
|
+
upgradeCount: input.ladder?.upgradeCount ?? null,
|
|
27346
|
+
climbHold: input.ladder?.climbHold ?? null
|
|
26538
27347
|
};
|
|
26539
27348
|
}
|
|
26540
27349
|
var PLAYBACK_PROFILE_FALLBACK_ORDER = [
|
|
@@ -33511,24 +34320,13 @@ function pickAdaptiveTier(input) {
|
|
|
33511
34320
|
*
|
|
33512
34321
|
* Flow: Camera → RTSP → Broker → EncodedPacket → RTP packetization → WebRTC
|
|
33513
34322
|
*/
|
|
33514
|
-
/**
|
|
33515
|
-
|
|
33516
|
-
|
|
33517
|
-
|
|
33518
|
-
|
|
33519
|
-
|
|
33520
|
-
|
|
33521
|
-
upgradeHealthyMs: 4e3,
|
|
33522
|
-
upgradeHealthyGrowthFactor: 3,
|
|
33523
|
-
maxProgressiveUpgradeHealthyMs: 3e4,
|
|
33524
|
-
minTimeBetweenChangesMs: 4e3,
|
|
33525
|
-
maxUpgradeBackoffMs: 9e5,
|
|
33526
|
-
stallKbpsFloor: 64,
|
|
33527
|
-
stallConfirmMs: 6e3,
|
|
33528
|
-
recoverKbpsFloor: 128,
|
|
33529
|
-
waitAfterChangeMs: 8e3,
|
|
33530
|
-
maxClientSampleAgeMs: 1e4
|
|
33531
|
-
};
|
|
34323
|
+
/**
|
|
34324
|
+
* A hold code that has been logged stays quiet this long before it may log
|
|
34325
|
+
* again (`HoldLogGate`). 60 000 ms: a hovering link changes code every 4–8 s.
|
|
34326
|
+
*/
|
|
34327
|
+
var HOLD_LOG_QUIET_MS = 6e4;
|
|
34328
|
+
/** A failing adaptive tick logs at most once per this long, per session. */
|
|
34329
|
+
var TICK_FAILURE_LOG_QUIET_MS = 6e4;
|
|
33532
34330
|
/**
|
|
33533
34331
|
* True when `broker` is an in-process StreamBroker exposing the Opus egress
|
|
33534
34332
|
* controls. Uses `in` narrowing + `typeof` checks (no cast) so the returned
|
|
@@ -33765,6 +34563,14 @@ var BrokerWebrtcServer = class {
|
|
|
33765
34563
|
*/
|
|
33766
34564
|
lastLadderDecision = /* @__PURE__ */ new Map();
|
|
33767
34565
|
/**
|
|
34566
|
+
* Try-climb memory per DEVICE (review I4): a new viewer session of a camera
|
|
34567
|
+
* whose next rung keeps failing inherits the backoff instead of starting at
|
|
34568
|
+
* try #1. Forgotten after the backoff cap without a refresh.
|
|
34569
|
+
*/
|
|
34570
|
+
deviceTryMemory = new DeviceTryMemory(ADAPTIVE_CONFIG.tryBackoffMaxMs);
|
|
34571
|
+
/** The newest viewer probe per device, from offers and telemetry (`probe-evidence.ts`). */
|
|
34572
|
+
deviceProbe = new DeviceProbeMemory(PROBE_MEMORY_TTL_MS);
|
|
34573
|
+
/**
|
|
33768
34574
|
* brokerId → where its media is ingested. Injected by the manager, which
|
|
33769
34575
|
* owns the ownership map; unset (or a null answer) means UNRESOLVED, and
|
|
33770
34576
|
* the overlay says so rather than naming the local node.
|
|
@@ -34762,7 +35568,8 @@ var BrokerWebrtcServer = class {
|
|
|
34762
35568
|
};
|
|
34763
35569
|
if (streamId.endsWith("/adaptive")) {
|
|
34764
35570
|
const initialTier = this.profileTierResolver?.(brokerId) ?? "high";
|
|
34765
|
-
this.
|
|
35571
|
+
this.recordDeviceProbe(deviceId, probeFromHints(hints.downlinkMbps, Date.now()));
|
|
35572
|
+
this.startAdaptive(entry, deviceId, initialTier, sessionId, intent);
|
|
34766
35573
|
}
|
|
34767
35574
|
return {
|
|
34768
35575
|
sessionId,
|
|
@@ -34880,11 +35687,13 @@ var BrokerWebrtcServer = class {
|
|
|
34880
35687
|
const ladder = controller !== void 0 ? {
|
|
34881
35688
|
lastDecision: decision,
|
|
34882
35689
|
failures: [...controller.failureCounts],
|
|
34883
|
-
upgradeCount: controller.successfulUpgradeCount
|
|
35690
|
+
upgradeCount: controller.successfulUpgradeCount,
|
|
35691
|
+
climbHold: controller.climbHold === null ? null : toWireClimbHold(controller.climbHold)
|
|
34884
35692
|
} : decision !== null ? {
|
|
34885
35693
|
lastDecision: decision,
|
|
34886
35694
|
failures: [],
|
|
34887
|
-
upgradeCount: 0
|
|
35695
|
+
upgradeCount: 0,
|
|
35696
|
+
climbHold: null
|
|
34888
35697
|
} : null;
|
|
34889
35698
|
let brokerStats = null;
|
|
34890
35699
|
try {
|
|
@@ -34927,7 +35736,7 @@ var BrokerWebrtcServer = class {
|
|
|
34927
35736
|
/** Snapshot the ladder's move so the overlay can name it later. */
|
|
34928
35737
|
recordLadderDecision(sessionId, decision, tier, forcedTranscodeDown) {
|
|
34929
35738
|
this.lastLadderDecision.set(sessionId, {
|
|
34930
|
-
|
|
35739
|
+
...wireLadderCause(decision, ADAPTIVE_CONFIG.stallConfirmMs),
|
|
34931
35740
|
tier,
|
|
34932
35741
|
forcedTranscodeDown,
|
|
34933
35742
|
lossRatio: decision.lossRatio,
|
|
@@ -34938,6 +35747,39 @@ var BrokerWebrtcServer = class {
|
|
|
34938
35747
|
});
|
|
34939
35748
|
}
|
|
34940
35749
|
/**
|
|
35750
|
+
* The IDR interval the broker feeding a session measured, or null when it
|
|
35751
|
+
* has measured none (a fresh broker reports 0). Sizes the delivery window.
|
|
35752
|
+
*/
|
|
35753
|
+
brokerIdrIntervalMs(brokerId) {
|
|
35754
|
+
try {
|
|
35755
|
+
const idr = this.brokers.get(brokerId)?.getStats().idrIntervalMs ?? null;
|
|
35756
|
+
return idr !== null && idr > 0 ? idr : null;
|
|
35757
|
+
} catch {
|
|
35758
|
+
return null;
|
|
35759
|
+
}
|
|
35760
|
+
}
|
|
35761
|
+
/**
|
|
35762
|
+
* The expected bitrate of `tier` for this device, from the broker serving
|
|
35763
|
+
* that rung — null when no such broker is registered here or it has
|
|
35764
|
+
* measured nothing. Unknown never vetoes a climb (D315/D393).
|
|
35765
|
+
*/
|
|
35766
|
+
expectedTierKbps(deviceId, tier) {
|
|
35767
|
+
const prefix = `${String(deviceId)}/`;
|
|
35768
|
+
const candidates = [];
|
|
35769
|
+
for (const [bid, broker] of this.brokers) if (bid.startsWith(prefix)) candidates.push({
|
|
35770
|
+
brokerId: bid,
|
|
35771
|
+
tier: this.profileTierResolver?.(bid) ?? null,
|
|
35772
|
+
broker
|
|
35773
|
+
});
|
|
35774
|
+
const match = preferLiveAdaptiveCandidates(candidates).find((c) => c.tier === tier);
|
|
35775
|
+
if (match === void 0) return null;
|
|
35776
|
+
try {
|
|
35777
|
+
return expectedRungKbps(match.broker.getStats());
|
|
35778
|
+
} catch {
|
|
35779
|
+
return null;
|
|
35780
|
+
}
|
|
35781
|
+
}
|
|
35782
|
+
/**
|
|
34941
35783
|
* Wire an {@link AdaptiveController} to a freshly-created adaptive session.
|
|
34942
35784
|
* The controller is fed loss from two sources — RTCP Receiver Reports
|
|
34943
35785
|
* (`onReceiverReport`) and, when injected, the device's client-reported
|
|
@@ -34954,12 +35796,20 @@ var BrokerWebrtcServer = class {
|
|
|
34954
35796
|
* whichever produces the higher loss ratio across its window. The async
|
|
34955
35797
|
* poll is guarded against overlapping ticks via `tickInFlight`.
|
|
34956
35798
|
*/
|
|
34957
|
-
startAdaptive(entry, deviceId, initialTier, sessionId,
|
|
35799
|
+
startAdaptive(entry, deviceId, initialTier, sessionId, intent) {
|
|
35800
|
+
const startedAt = Date.now();
|
|
35801
|
+
/** Rung bitrates, looked up at most once per tier per tick (review m12). */
|
|
35802
|
+
const rungKbps = /* @__PURE__ */ new Map();
|
|
34958
35803
|
const controller = new AdaptiveController({
|
|
34959
35804
|
initialTier,
|
|
34960
|
-
priorFailures,
|
|
34961
|
-
priorUpgradeCount,
|
|
35805
|
+
priorFailures: intent?.failures,
|
|
35806
|
+
priorUpgradeCount: intent?.upgradeCount,
|
|
35807
|
+
tryClimb: intent?.tryClimb ?? this.deviceTryMemory.get(deviceId, startedAt) ?? void 0,
|
|
34962
35808
|
config: ADAPTIVE_CONFIG,
|
|
35809
|
+
expectedTierKbps: (tier) => {
|
|
35810
|
+
if (!rungKbps.has(tier)) rungKbps.set(tier, this.expectedTierKbps(deviceId, tier));
|
|
35811
|
+
return rungKbps.get(tier) ?? null;
|
|
35812
|
+
},
|
|
34963
35813
|
onSwitchTier: (tier, decision) => {
|
|
34964
35814
|
this.recordLadderDecision(sessionId, decision, tier, false);
|
|
34965
35815
|
this.switchSessionTier(entry, deviceId, tier, sessionId, false, decision);
|
|
@@ -34976,6 +35826,9 @@ var BrokerWebrtcServer = class {
|
|
|
34976
35826
|
reason: decision.cause,
|
|
34977
35827
|
lossRatio: Number(decision.lossRatio.toFixed(4)),
|
|
34978
35828
|
clientKbps: decision.stall.clientKbps,
|
|
35829
|
+
sentKbps: decision.stall.sentKbps,
|
|
35830
|
+
deliveryRatio: decision.stall.deliveryRatio,
|
|
35831
|
+
deliveryWindowMs: decision.stall.deliveryWindowMs,
|
|
34979
35832
|
stalledForMs: decision.stall.stalledForMs,
|
|
34980
35833
|
serverSending: decision.stall.serverSending
|
|
34981
35834
|
}
|
|
@@ -34995,14 +35848,39 @@ var BrokerWebrtcServer = class {
|
|
|
34995
35848
|
* sending?" half of the stall verdict. */
|
|
34996
35849
|
let lastRtpSent = entry.session.rtpPacketsSentTotal;
|
|
34997
35850
|
const CLIENT_LOSS_SPAN = 1e3;
|
|
35851
|
+
const meter = new DeliveryMeter(ADAPTIVE_DELIVERY_CONFIG, deliveryMeterStart(startedAt, intent !== void 0, ADAPTIVE_CONFIG.waitAfterChangeMs));
|
|
35852
|
+
const holdLog = new HoldLogGate(HOLD_LOG_QUIET_MS);
|
|
35853
|
+
let lastTickFailureLogAt = Number.NEGATIVE_INFINITY;
|
|
35854
|
+
const logHoldChange = (now) => {
|
|
35855
|
+
const hold = controller.climbHold;
|
|
35856
|
+
if (hold === null || !holdLog.shouldLog(hold.code, now)) return;
|
|
35857
|
+
this.logger.info("adaptive: climb held", {
|
|
35858
|
+
tags: { deviceId },
|
|
35859
|
+
meta: {
|
|
35860
|
+
sessionId,
|
|
35861
|
+
deviceId,
|
|
35862
|
+
tier: controller.currentTier,
|
|
35863
|
+
nextTier: hold.nextTier,
|
|
35864
|
+
code: hold.code,
|
|
35865
|
+
summary: describeClimbHold(hold),
|
|
35866
|
+
deliveryRatio: hold.deliveryRatio,
|
|
35867
|
+
deliveryWindowMs: hold.deliveryWindowMs,
|
|
35868
|
+
capacityKbps: hold.capacityKbps,
|
|
35869
|
+
probeKbps: hold.probeKbps,
|
|
35870
|
+
nextTierKbps: hold.nextTierKbps
|
|
35871
|
+
}
|
|
35872
|
+
});
|
|
35873
|
+
};
|
|
34998
35874
|
entry.adaptiveTimer = setInterval(() => {
|
|
34999
35875
|
if (tickInFlight) return;
|
|
35000
35876
|
tickInFlight = true;
|
|
35001
35877
|
(async () => {
|
|
35002
35878
|
try {
|
|
35879
|
+
rungKbps.clear();
|
|
35003
35880
|
if (entry.timeline?.currentMode === "recorded") {
|
|
35004
35881
|
controller.suspend(Date.now());
|
|
35005
35882
|
lastRtpSent = entry.session.rtpPacketsSentTotal;
|
|
35883
|
+
logHoldChange(Date.now());
|
|
35006
35884
|
return;
|
|
35007
35885
|
}
|
|
35008
35886
|
const client = this.clientNetworkStats ? await this.clientNetworkStats(deviceId) : null;
|
|
@@ -35010,11 +35888,15 @@ var BrokerWebrtcServer = class {
|
|
|
35010
35888
|
const rtpSent = entry.session.rtpPacketsSentTotal;
|
|
35011
35889
|
const serverSending = rtpSent > lastRtpSent;
|
|
35012
35890
|
lastRtpSent = rtpSent;
|
|
35013
|
-
|
|
35014
|
-
|
|
35015
|
-
|
|
35016
|
-
|
|
35017
|
-
|
|
35891
|
+
this.feedAdaptive({
|
|
35892
|
+
entry,
|
|
35893
|
+
deviceId,
|
|
35894
|
+
controller,
|
|
35895
|
+
meter,
|
|
35896
|
+
client,
|
|
35897
|
+
serverSending,
|
|
35898
|
+
now
|
|
35899
|
+
});
|
|
35018
35900
|
const pct = client?.packetLossPercent ?? null;
|
|
35019
35901
|
if (pct !== null && pct !== void 0) {
|
|
35020
35902
|
clientHighestSequence += CLIENT_LOSS_SPAN;
|
|
@@ -35025,6 +35907,21 @@ var BrokerWebrtcServer = class {
|
|
|
35025
35907
|
}, now);
|
|
35026
35908
|
}
|
|
35027
35909
|
controller.tick(now);
|
|
35910
|
+
this.deviceTryMemory.set(deviceId, controller.tryClimbCarry, now);
|
|
35911
|
+
logHoldChange(now);
|
|
35912
|
+
} catch (err) {
|
|
35913
|
+
const now = Date.now();
|
|
35914
|
+
if (now - lastTickFailureLogAt >= TICK_FAILURE_LOG_QUIET_MS) {
|
|
35915
|
+
lastTickFailureLogAt = now;
|
|
35916
|
+
this.logger.warn("adaptive: tick failed", {
|
|
35917
|
+
tags: { deviceId },
|
|
35918
|
+
meta: {
|
|
35919
|
+
sessionId,
|
|
35920
|
+
deviceId,
|
|
35921
|
+
error: errMsg(err)
|
|
35922
|
+
}
|
|
35923
|
+
});
|
|
35924
|
+
}
|
|
35028
35925
|
} finally {
|
|
35029
35926
|
tickInFlight = false;
|
|
35030
35927
|
}
|
|
@@ -35032,6 +35929,55 @@ var BrokerWebrtcServer = class {
|
|
|
35032
35929
|
}, ADAPTIVE_TICK_MS);
|
|
35033
35930
|
}
|
|
35034
35931
|
/**
|
|
35932
|
+
* One evaluation's transport, capacity and RTT evidence into the controller.
|
|
35933
|
+
* The device-scoped client record is used as this session's own only when
|
|
35934
|
+
* this is the device's ONLY adaptive session here (review I2).
|
|
35935
|
+
*/
|
|
35936
|
+
feedAdaptive(feed) {
|
|
35937
|
+
const { entry, deviceId, controller, meter, client, now } = feed;
|
|
35938
|
+
const sessionsOnDevice = this.adaptiveSessionCount(deviceId);
|
|
35939
|
+
const shared = sessionsOnDevice > 1;
|
|
35940
|
+
let rttMs = null;
|
|
35941
|
+
if (client) {
|
|
35942
|
+
const reportAgeMs = Math.max(0, now - client.lastUpdated);
|
|
35943
|
+
meter.observe(client.estimatedBandwidthKbps, client.lastUpdated);
|
|
35944
|
+
if (!shared) {
|
|
35945
|
+
this.recordDeviceProbe(deviceId, probeFromReport({
|
|
35946
|
+
downlinkMbps: client.downlinkMbps,
|
|
35947
|
+
downlinkAgeMs: client.downlinkAgeMs ?? 0,
|
|
35948
|
+
reportAgeMs
|
|
35949
|
+
}, now));
|
|
35950
|
+
if (reportAgeMs <= ADAPTIVE_DELIVERY_CONFIG.maxClientSampleAgeMs) rttMs = client.rttMs ?? null;
|
|
35951
|
+
}
|
|
35952
|
+
}
|
|
35953
|
+
const history = entry.session.sentVideoHistory;
|
|
35954
|
+
controller.onThroughput(buildStallSample({
|
|
35955
|
+
delivery: meter.evaluate(history, deliveryWindowMs(this.brokerIdrIntervalMs(entry.brokerId), ADAPTIVE_DELIVERY_CONFIG), now),
|
|
35956
|
+
recent: meter.evaluateNewest(history, now),
|
|
35957
|
+
clientReporting: meter.isReporting(now),
|
|
35958
|
+
serverSending: feed.serverSending,
|
|
35959
|
+
adaptiveSessionsOnDevice: sessionsOnDevice
|
|
35960
|
+
}), now);
|
|
35961
|
+
const probe = this.deviceProbe.get(deviceId, now);
|
|
35962
|
+
controller.onCapacity({
|
|
35963
|
+
rembKbps: entry.session.receiverEstimateKbps(now),
|
|
35964
|
+
probeKbps: probe?.kbps ?? null,
|
|
35965
|
+
probeFresh: isProbeFresh(probe, now)
|
|
35966
|
+
});
|
|
35967
|
+
controller.onRtt(rttMs, now);
|
|
35968
|
+
}
|
|
35969
|
+
/** Adaptive sessions of `deviceId` currently on this broker server. */
|
|
35970
|
+
adaptiveSessionCount(deviceId) {
|
|
35971
|
+
const prefix = `${String(deviceId)}/`;
|
|
35972
|
+
let count = 0;
|
|
35973
|
+
for (const s of this.sessions.values()) if (s.adaptiveController !== void 0 && s.brokerId.startsWith(prefix)) count += 1;
|
|
35974
|
+
return count;
|
|
35975
|
+
}
|
|
35976
|
+
/** Keep the newest probe per device; an absent one changes nothing. */
|
|
35977
|
+
recordDeviceProbe(deviceId, probe) {
|
|
35978
|
+
this.deviceProbe.record(deviceId, probe);
|
|
35979
|
+
}
|
|
35980
|
+
/**
|
|
35035
35981
|
* Schedule an adaptive tier switch via a close-and-recreate re-offer.
|
|
35036
35982
|
*
|
|
35037
35983
|
* Records the controller's desired tier (+ forced transcode-down) in
|
|
@@ -35048,11 +35994,18 @@ var BrokerWebrtcServer = class {
|
|
|
35048
35994
|
const prev = this.adaptiveIntent.get(sessionId);
|
|
35049
35995
|
const failures = new Map(entry.adaptiveController?.failureCounts ?? prev?.failures);
|
|
35050
35996
|
const upgradeCount = entry.adaptiveController?.successfulUpgradeCount ?? prev?.upgradeCount ?? 0;
|
|
35997
|
+
const tryClimb = entry.adaptiveController?.tryClimbCarry ?? prev?.tryClimb;
|
|
35051
35998
|
this.adaptiveIntent.set(sessionId, {
|
|
35052
35999
|
prefersTier: tier,
|
|
35053
36000
|
forceTranscodeDown: forceTranscodeDown || (prev?.forceTranscodeDown ?? false),
|
|
35054
36001
|
failures,
|
|
35055
|
-
upgradeCount
|
|
36002
|
+
upgradeCount,
|
|
36003
|
+
tryClimb: tryClimb ?? {
|
|
36004
|
+
failures: 0,
|
|
36005
|
+
notBefore: 0,
|
|
36006
|
+
lastRevert: null,
|
|
36007
|
+
trialFrom: null
|
|
36008
|
+
}
|
|
35056
36009
|
});
|
|
35057
36010
|
const epoch = (entry.session.pendingRenegotiation?.epoch ?? 0) + 1;
|
|
35058
36011
|
entry.session.pendingRenegotiation = {
|
|
@@ -35070,8 +36023,12 @@ var BrokerWebrtcServer = class {
|
|
|
35070
36023
|
failures: Object.fromEntries(failures),
|
|
35071
36024
|
upgradeCount,
|
|
35072
36025
|
reason: decision?.cause ?? "unknown",
|
|
36026
|
+
trial: decision?.trial ?? false,
|
|
35073
36027
|
lossRatio: decision ? Number(decision.lossRatio.toFixed(4)) : null,
|
|
35074
36028
|
clientKbps: decision?.stall.clientKbps ?? null,
|
|
36029
|
+
sentKbps: decision?.stall.sentKbps ?? null,
|
|
36030
|
+
deliveryRatio: decision?.stall.deliveryRatio ?? null,
|
|
36031
|
+
deliveryWindowMs: decision?.stall.deliveryWindowMs ?? null,
|
|
35075
36032
|
stalledForMs: decision?.stall.stalledForMs ?? null,
|
|
35076
36033
|
serverSending: decision?.stall.serverSending ?? null
|
|
35077
36034
|
}
|
|
@@ -35088,6 +36045,10 @@ var BrokerWebrtcServer = class {
|
|
|
35088
36045
|
}
|
|
35089
36046
|
if (!brokerStillServed && !this.stopped) this.brokerLinger.hold(entry.brokerId);
|
|
35090
36047
|
this.adaptiveIntent.delete(sessionId);
|
|
36048
|
+
if (entry.adaptiveController !== void 0) {
|
|
36049
|
+
const deviceId = deviceIdFromBrokerId(entry.brokerId);
|
|
36050
|
+
if (this.adaptiveSessionCount(deviceId) === 0) this.deviceProbe.forget(deviceId);
|
|
36051
|
+
}
|
|
35091
36052
|
this.pruneLadderDecisions();
|
|
35092
36053
|
if (entry.adaptiveTimer) clearInterval(entry.adaptiveTimer);
|
|
35093
36054
|
if (entry.attributionTimer) clearTimeout(entry.attributionTimer);
|
|
@@ -36248,7 +37209,10 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
36248
37209
|
return {
|
|
36249
37210
|
packetLossPercent: client.packetLossPercent,
|
|
36250
37211
|
estimatedBandwidthKbps: client.estimatedBandwidthKbps,
|
|
36251
|
-
lastUpdated: client.lastUpdated
|
|
37212
|
+
lastUpdated: client.lastUpdated,
|
|
37213
|
+
rttMs: client.rttMs,
|
|
37214
|
+
...client.downlinkMbps !== void 0 ? { downlinkMbps: client.downlinkMbps } : {},
|
|
37215
|
+
...client.downlinkAgeMs !== void 0 ? { downlinkAgeMs: client.downlinkAgeMs } : {}
|
|
36252
37216
|
};
|
|
36253
37217
|
} catch {
|
|
36254
37218
|
return null;
|