@unboundcx/video-sdk-client 2.0.10 → 2.0.11
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.
|
@@ -995,28 +995,39 @@ export class LocalMediaManager extends EventEmitter {
|
|
|
995
995
|
// Keep a layer active only if its width is within the cap.
|
|
996
996
|
const capWidth =
|
|
997
997
|
{ '360p': 640, '540p': 960, '720p': 1280 }[sendResolution] || Infinity;
|
|
998
|
-
//
|
|
999
|
-
|
|
998
|
+
// Relative weights per rid — matches produce(); the budget is split so
|
|
999
|
+
// the ACTIVE layers SUM to maxBitrate (simulcast total = sum of layers).
|
|
1000
|
+
const WEIGHT = { l: 0.15, m: 0.35, h: 0.65 };
|
|
1000
1001
|
|
|
1001
1002
|
try {
|
|
1002
1003
|
const params = sender.getParameters();
|
|
1003
1004
|
if (!params.encodings || params.encodings.length < 2) return;
|
|
1004
1005
|
let changed = false;
|
|
1006
|
+
// First pass: decide which layers stay active under the res cap.
|
|
1007
|
+
const settings = this.streams.camera?.getVideoTracks()[0]?.getSettings();
|
|
1008
|
+
const captureWidth = settings?.width || 1920;
|
|
1005
1009
|
for (const enc of params.encodings) {
|
|
1006
1010
|
// scaleResolutionDownBy tells us this layer's output width
|
|
1007
1011
|
// relative to the captured frame; below the cap stays active.
|
|
1008
|
-
const settings = this.streams.camera?.getVideoTracks()[0]?.getSettings();
|
|
1009
|
-
const captureWidth = settings?.width || 1920;
|
|
1010
1012
|
const layerWidth = captureWidth / (enc.scaleResolutionDownBy || 1);
|
|
1011
1013
|
const wantActive = layerWidth <= capWidth + 1; // +1 for rounding
|
|
1012
1014
|
if (enc.active !== wantActive) {
|
|
1013
1015
|
enc.active = wantActive;
|
|
1014
1016
|
changed = true;
|
|
1015
1017
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1018
|
+
}
|
|
1019
|
+
// Second pass: split the budget across the layers that remain active,
|
|
1020
|
+
// normalizing weights so the active set sums to the budget.
|
|
1021
|
+
if (maxBitrate > 0) {
|
|
1022
|
+
const active = params.encodings.filter((e) => e.active !== false);
|
|
1023
|
+
const totalWeight = active.reduce(
|
|
1024
|
+
(s, e) => s + (WEIGHT[e.rid] ?? 1.0),
|
|
1025
|
+
0
|
|
1026
|
+
);
|
|
1027
|
+
for (const enc of active) {
|
|
1028
|
+
const w = WEIGHT[enc.rid] ?? 1.0;
|
|
1029
|
+
const share = Math.round((maxBitrate * w) / totalWeight);
|
|
1030
|
+
const next = Math.min(enc.maxBitrate || share, share);
|
|
1020
1031
|
if (enc.maxBitrate !== next) {
|
|
1021
1032
|
enc.maxBitrate = next;
|
|
1022
1033
|
changed = true;
|
|
@@ -674,27 +674,37 @@ export class MediasoupManager extends EventEmitter {
|
|
|
674
674
|
});
|
|
675
675
|
}
|
|
676
676
|
|
|
677
|
-
// HARD bitrate ceiling (data-saver / quality level).
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
677
|
+
// HARD bitrate ceiling (data-saver / quality level). On a metered link
|
|
678
|
+
// the profile's sendMaxBitrate is a budget the encoder must not exceed
|
|
679
|
+
// — even during high motion, when resolution/fps caps alone let the
|
|
680
|
+
// rate spike. With simulcast, total bandwidth is the SUM of all active
|
|
681
|
+
// layers, so the budget is split across the layers present such that
|
|
682
|
+
// the layers SUM to the budget (not each capped at the full budget —
|
|
683
|
+
// that earlier bug let the total reach ~1.6× the cap). The top layer
|
|
684
|
+
// gets the lion's share; lower layers get proportionally less.
|
|
685
|
+
// (Screen share never reaches here.)
|
|
684
686
|
const maxBitrate = Number(options.maxBitrate) || 0;
|
|
685
|
-
if (maxBitrate > 0 && produceOptions.encodings) {
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
687
|
+
if (maxBitrate > 0 && produceOptions.encodings?.length) {
|
|
688
|
+
// Relative weights per rid; we normalize over the layers actually
|
|
689
|
+
// present so the allocated total always equals the budget whether
|
|
690
|
+
// there are 1, 2, or 3 layers.
|
|
691
|
+
const WEIGHT = { l: 0.15, m: 0.35, h: 0.65 };
|
|
692
|
+
const encs = produceOptions.encodings;
|
|
693
|
+
const totalWeight = encs.reduce(
|
|
694
|
+
(sum, e) => sum + (WEIGHT[e.rid] ?? 1.0),
|
|
695
|
+
0,
|
|
696
|
+
);
|
|
697
|
+
for (const enc of encs) {
|
|
698
|
+
const w = WEIGHT[enc.rid] ?? 1.0;
|
|
699
|
+
const share = Math.round((maxBitrate * w) / totalWeight);
|
|
692
700
|
// Only ever lower — never raise a layer above its tuned value.
|
|
693
|
-
enc.maxBitrate = Math.min(enc.maxBitrate ||
|
|
701
|
+
enc.maxBitrate = Math.min(enc.maxBitrate || share, share);
|
|
694
702
|
}
|
|
703
|
+
const allocated = encs.reduce((s, e) => s + (e.maxBitrate || 0), 0);
|
|
695
704
|
this.logger.info("VIDEO_QUALITY :: applied hard bitrate ceiling", {
|
|
696
|
-
|
|
697
|
-
|
|
705
|
+
budget: `${(maxBitrate / 1000).toFixed(0)}kbps`,
|
|
706
|
+
allocatedTotal: `${(allocated / 1000).toFixed(0)}kbps`,
|
|
707
|
+
encodings: encs.map(
|
|
698
708
|
(e) => `${e.rid}: ${(e.maxBitrate / 1000).toFixed(0)}kbps`,
|
|
699
709
|
),
|
|
700
710
|
});
|
|
@@ -452,15 +452,31 @@ export class StatsCollector {
|
|
|
452
452
|
report.type === 'outbound-rtp' ||
|
|
453
453
|
report.type === 'remote-outbound-rtp'
|
|
454
454
|
) {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
videoStats.
|
|
463
|
-
|
|
455
|
+
// SIMULCAST: there's one outbound-rtp report PER active layer
|
|
456
|
+
// (l/m/h). Counters that represent the whole producer (bytes,
|
|
457
|
+
// packets, frames) accumulate across layers; resolution/fps must
|
|
458
|
+
// reflect the TOP active layer, so only overwrite those when this
|
|
459
|
+
// report is a bigger frame than what we've seen. Without this the
|
|
460
|
+
// last report (often the lowest layer) wins and the panel shows
|
|
461
|
+
// e.g. 270p while we're actually sending 540p.
|
|
462
|
+
videoStats.packetsSent =
|
|
463
|
+
(videoStats.packetsSent || 0) +
|
|
464
|
+
this.validateNumber(report.packetsSent);
|
|
465
|
+
videoStats.bytesSent =
|
|
466
|
+
(videoStats.bytesSent || 0) +
|
|
467
|
+
this.validateNumber(report.bytesSent);
|
|
468
|
+
videoStats.framesSent =
|
|
469
|
+
(videoStats.framesSent || 0) +
|
|
470
|
+
this.validateNumber(report.framesSent);
|
|
471
|
+
const w = this.validateNumber(report.frameWidth);
|
|
472
|
+
if (w >= (videoStats.frameWidth || 0)) {
|
|
473
|
+
videoStats.frameWidth = w;
|
|
474
|
+
videoStats.frameHeight = this.validateNumber(report.frameHeight);
|
|
475
|
+
videoStats.framesPerSecond = this.validateNumber(
|
|
476
|
+
report.framesPerSecond,
|
|
477
|
+
);
|
|
478
|
+
videoStats.scalabilityMode = report.scalabilityMode;
|
|
479
|
+
}
|
|
464
480
|
videoStats.qpSum = report.qpSum;
|
|
465
481
|
videoStats.totalPacketSendDelay = this.validateNumber(
|
|
466
482
|
report.totalPacketSendDelay,
|