fix(voice): start bitrate for non-SVC screen share codecs (#2434)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-03 21:07:42 +02:00
1 parent 90c349392b
commit add0a3dfc6
6 files changed
+138 -19

No files matched your search

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import type {MediaDescription} from 'sdp-transform';
import {describe, expect, it} from 'vitest';
import {appendStartBitrateToFmtp} from './PCTransport.ts';
function mediaWithFmtp(entries: Array<{payload: number; config: string}>): MediaDescription {
return {fmtp: entries} as unknown as MediaDescription;
}
describe('appendStartBitrateToFmtp', () => {
it('appends the start bitrate to a non-SVC codec fmtp line', () => {
const media = mediaWithFmtp([
{payload: 96, config: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f'},
]);
appendStartBitrateToFmtp(media, 96, 2100);
expect(media.fmtp[0]?.config).toBe(
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f;x-google-start-bitrate=2100',
);
});
it('only touches the fmtp line for the matching payload', () => {
const media = mediaWithFmtp([
{payload: 96, config: 'profile-level-id=42e01f'},
{payload: 98, config: 'profile-id=0'},
]);
appendStartBitrateToFmtp(media, 98, 1400);
expect(media.fmtp[0]?.config).toBe('profile-level-id=42e01f');
expect(media.fmtp[1]?.config).toBe('profile-id=0;x-google-start-bitrate=1400');
});
it('never appends a second start bitrate', () => {
const media = mediaWithFmtp([{payload: 96, config: 'profile-level-id=42e01f;x-google-start-bitrate=900'}]);
appendStartBitrateToFmtp(media, 96, 2100);
expect(media.fmtp[0]?.config).toBe('profile-level-id=42e01f;x-google-start-bitrate=900');
});
it('does nothing without a usable bitrate or a matching payload', () => {
const media = mediaWithFmtp([{payload: 96, config: 'profile-level-id=42e01f'}]);
appendStartBitrateToFmtp(media, 96, 0);
appendStartBitrateToFmtp(media, 96, -1);
appendStartBitrateToFmtp(media, 111, 2100);
expect(media.fmtp[0]?.config).toBe('profile-level-id=42e01f');
});
});
@@ -18,7 +18,7 @@ interface TrackBitrateInfo {
maxbr: number;
}
const startBitrateForSVC = 0.7;
const startBitrateFraction = 0.7;
const opusMaxAverageBitrateBps = 510000;
const opusPacketTimeMs = 10;
const requiredOpusFmtpParameters = {
@@ -35,6 +35,21 @@ export const PCEvents = {
RTPVideoPayloadTypes: 'rtpVideoPayloadTypes',
} as const;
export function appendStartBitrateToFmtp(media: MediaDescription, codecPayload: number, startBitrate: number): void {
if (startBitrate <= 0) {
return;
}
for (const fmtp of media.fmtp) {
if (fmtp.payload !== codecPayload) {
continue;
}
if (!fmtp.config.includes('x-google-start-bitrate')) {
fmtp.config += `;x-google-start-bitrate=${startBitrate}`;
}
return;
}
}
export default class PCTransport extends EventEmitter {
private _pc: RTCPeerConnection | null;
@@ -248,18 +263,10 @@ export default class PCTransport extends EventEmitter {
if (isSVCCodec(trackbr.codec) && !isSafari()) {
this.ensureVideoDDExtensionForSVC(media, sdpParsed);
}
if (!isSVCCodec(trackbr.codec)) {
if (trackbr.maxbr <= 0) {
return true;
}
const startBitrate = Math.round(trackbr.maxbr * startBitrateForSVC);
for (const fmtp of media.fmtp) {
if (fmtp.payload === codecPayload) {
if (!fmtp.config.includes('x-google-start-bitrate')) {
fmtp.config += `;x-google-start-bitrate=${startBitrate}`;
}
break;
}
}
appendStartBitrateToFmtp(media, codecPayload, Math.round(trackbr.maxbr * startBitrateFraction));
return true;
});
}
@@ -336,7 +343,16 @@ export default class PCTransport extends EventEmitter {
}
setTrackCodecBitrate(info: TrackBitrateInfo) {
this.trackBitrates.push(info);
const existing = this.trackBitrates.findIndex(
(trackbr) =>
(info.cid !== undefined && trackbr.cid === info.cid) ||
(info.transceiver !== undefined && trackbr.transceiver === info.transceiver),
);
if (existing === -1) {
this.trackBitrates.push(info);
return;
}
this.trackBitrates[existing] = info;
}
setConfiguration(rtcConfig: RTCConfiguration) {
@@ -97,7 +97,12 @@ import {
import Participant from './Participant.ts';
import type {ParticipantTrackPermission} from './ParticipantTrackPermission.ts';
import {trackPermissionToProto} from './ParticipantTrackPermission.ts';
import {computeTrackBackupEncodings, computeVideoEncodings, getDefaultDegradationPreference} from './publishUtils.ts';
import {
computeTrackBackupEncodings,
computeVideoEncodings,
getDefaultDegradationPreference,
maxEncodingBitrate,
} from './publishUtils.ts';
import type RemoteParticipant from './RemoteParticipant.ts';
type PendingSignalRequestValues = {
@@ -1037,12 +1042,15 @@ export default class LocalParticipant extends Participant {
maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
});
}
} else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
this.engine.pcManager.publisher.setTrackCodecBitrate({
cid: req.cid,
codec: track.codec,
maxbr: encodings[0].maxBitrate / 1000,
});
} else if (track.codec) {
const maxBitrate = maxEncodingBitrate(encodings);
if (maxBitrate > 0) {
this.engine.pcManager.publisher.setTrackCodecBitrate({
cid: req.cid,
codec: track.codec,
maxbr: maxBitrate / 1000,
});
}
}
}
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2024 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import {describe, expect, it} from 'vitest';
import {maxEncodingBitrate} from './publishUtils.ts';
describe('maxEncodingBitrate', () => {
it('returns the highest layer bitrate for a simulcast ladder', () => {
expect(maxEncodingBitrate([{maxBitrate: 150_000}, {maxBitrate: 500_000}, {maxBitrate: 1_700_000}])).toBe(1_700_000);
});
it('returns the single encoding bitrate for a non-simulcast publish', () => {
expect(maxEncodingBitrate([{maxBitrate: 3_000_000}])).toBe(3_000_000);
});
it('returns zero when no encoding declares a bitrate', () => {
expect(maxEncodingBitrate([{}])).toBe(0);
expect(maxEncodingBitrate([])).toBe(0);
expect(maxEncodingBitrate(undefined)).toBe(0);
});
});
@@ -370,6 +370,19 @@ export class ScalabilityMode {
}
}
export function maxEncodingBitrate(encodings: Array<RTCRtpEncodingParameters> | undefined): number {
if (!encodings) {
return 0;
}
let maxBitrate = 0;
for (const encoding of encodings) {
if (typeof encoding.maxBitrate === 'number' && encoding.maxBitrate > maxBitrate) {
maxBitrate = encoding.maxBitrate;
}
}
return maxBitrate;
}
export function getDefaultDegradationPreference(track: LocalVideoTrack): RTCDegradationPreference {
if (track.source === Track.Source.ScreenShare) return 'maintain-resolution';
if (track.constraints.height && unwrapConstraint(track.constraints.height) >= 1080) return 'maintain-resolution';