Skip to content

Commit ae4af63

Browse files
authored
Apply user provided bitrate to maxaveragebitrates for firefox (livekit#752)
* Apply user provided bitrate to maxaveragebitrates for firefox * Solve comments * Solve comments
1 parent d0972b0 commit ae4af63

3 files changed

Lines changed: 151 additions & 54 deletions

File tree

.changeset/lucky-cooks-fly.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'livekit-client': patch
3+
---
4+
5+
Apply user setting bitrate to maxaveragebitrates for firefox

src/room/PCTransport.ts

Lines changed: 116 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,20 @@ import { ddExtensionURI, isChromiumBased, isSVCCodec } from './utils';
88

99
/** @internal */
1010
interface TrackBitrateInfo {
11-
sid: string;
11+
cid?: string;
12+
transceiver?: RTCRtpTransceiver;
1213
codec: string;
1314
maxbr: number;
1415
}
1516

17+
/* The svc codec (av1/vp9) would use a very low bitrate at the begining and
18+
increase slowly by the bandwidth estimator until it reach the target bitrate. The
19+
process commonly cost more than 10 seconds cause subscriber will get blur video at
20+
the first few seconds. So we use a 70% of target bitrate here as the start bitrate to
21+
eliminate this issue.
22+
*/
23+
const startBitrateForSVC = 0.7;
24+
1625
export const PCEvents = {
1726
NegotiationStarted: 'negotiationStarted',
1827
NegotiationComplete: 'negotiationComplete',
@@ -56,12 +65,65 @@ export default class PCTransport extends EventEmitter {
5665
}
5766

5867
async setRemoteDescription(sd: RTCSessionDescriptionInit): Promise<void> {
68+
let mungedSDP: string | undefined = undefined;
5969
if (sd.type === 'offer') {
6070
let { stereoMids, nackMids } = extractStereoAndNackAudioFromOffer(sd);
6171
this.remoteStereoMids = stereoMids;
6272
this.remoteNackMids = nackMids;
73+
} else if (sd.type === 'answer') {
74+
const sdpParsed = parse(sd.sdp ?? '');
75+
sdpParsed.media.forEach((media) => {
76+
if (media.type === 'audio') {
77+
// mung sdp for opus bitrate settings
78+
this.trackBitrates.some((trackbr): boolean => {
79+
if (!trackbr.transceiver || media.mid != trackbr.transceiver.mid) {
80+
return false;
81+
}
82+
83+
let codecPayload = 0;
84+
media.rtp.some((rtp): boolean => {
85+
if (rtp.codec.toUpperCase() === trackbr.codec.toUpperCase()) {
86+
codecPayload = rtp.payload;
87+
return true;
88+
}
89+
return false;
90+
});
91+
92+
if (codecPayload === 0) {
93+
return true;
94+
}
95+
96+
let fmtpFound = false;
97+
for (const fmtp of media.fmtp) {
98+
if (fmtp.payload === codecPayload) {
99+
fmtp.config = fmtp.config
100+
.split(';')
101+
.filter((attr) => !attr.includes('maxaveragebitrate'))
102+
.join(';');
103+
if (trackbr.maxbr > 0) {
104+
fmtp.config += `;maxaveragebitrate=${trackbr.maxbr * 1000}`;
105+
}
106+
fmtpFound = true;
107+
break;
108+
}
109+
}
110+
111+
if (!fmtpFound) {
112+
if (trackbr.maxbr > 0) {
113+
media.fmtp.push({
114+
payload: codecPayload,
115+
config: `maxaveragebitrate=${trackbr.maxbr * 1000}`,
116+
});
117+
}
118+
}
119+
120+
return true;
121+
});
122+
}
123+
});
124+
mungedSDP = write(sdpParsed);
63125
}
64-
await this.pc.setRemoteDescription(sd);
126+
await this.setMungedSDP(sd, mungedSDP, true);
65127

66128
this.pendingCandidates.forEach((candidate) => {
67129
this.pc.addIceCandidate(candidate);
@@ -130,7 +192,7 @@ export default class PCTransport extends EventEmitter {
130192
ensureVideoDDExtensionForSVC(media);
131193
// mung sdp for codec bitrate setting that can't apply by sendEncoding
132194
this.trackBitrates.some((trackbr): boolean => {
133-
if (!media.msid || !media.msid.includes(trackbr.sid)) {
195+
if (!media.msid || !trackbr.cid || !media.msid.includes(trackbr.cid)) {
134196
return false;
135197
}
136198

@@ -143,39 +205,39 @@ export default class PCTransport extends EventEmitter {
143205
return false;
144206
});
145207

146-
// add x-google-max-bitrate to fmtp line if not exist
147-
if (codecPayload > 0) {
148-
if (
149-
!media.fmtp.some((fmtp): boolean => {
150-
if (fmtp.payload === codecPayload) {
151-
if (!fmtp.config.includes('x-google-start-bitrate')) {
152-
fmtp.config += `;x-google-start-bitrate=${trackbr.maxbr * 0.7}`;
153-
}
154-
if (!fmtp.config.includes('x-google-max-bitrate')) {
155-
fmtp.config += `;x-google-max-bitrate=${trackbr.maxbr}`;
156-
}
157-
return true;
158-
}
159-
return false;
160-
})
161-
) {
162-
media.fmtp.push({
163-
payload: codecPayload,
164-
config: `x-google-start-bitrate=${trackbr.maxbr * 0.7};x-google-max-bitrate=${
165-
trackbr.maxbr
166-
}`,
167-
});
208+
if (codecPayload === 0) {
209+
return true;
210+
}
211+
212+
let fmtpFound = false;
213+
for (const fmtp of media.fmtp) {
214+
if (fmtp.payload === codecPayload) {
215+
if (!fmtp.config.includes('x-google-start-bitrate')) {
216+
fmtp.config += `;x-google-start-bitrate=${trackbr.maxbr * startBitrateForSVC}`;
217+
}
218+
if (!fmtp.config.includes('x-google-max-bitrate')) {
219+
fmtp.config += `;x-google-max-bitrate=${trackbr.maxbr}`;
220+
}
221+
fmtpFound = true;
222+
break;
168223
}
169224
}
170225

226+
if (!fmtpFound) {
227+
media.fmtp.push({
228+
payload: codecPayload,
229+
config: `x-google-start-bitrate=${
230+
trackbr.maxbr * startBitrateForSVC
231+
};x-google-max-bitrate=${trackbr.maxbr}`,
232+
});
233+
}
234+
171235
return true;
172236
});
173237
}
174238
});
175239

176-
this.trackBitrates = [];
177-
178-
await this.setMungedLocalDescription(offer, write(sdpParsed));
240+
await this.setMungedSDP(offer, write(sdpParsed));
179241
this.onOffer(offer);
180242
}
181243

@@ -187,16 +249,12 @@ export default class PCTransport extends EventEmitter {
187249
ensureAudioNackAndStereo(media, this.remoteStereoMids, this.remoteNackMids);
188250
}
189251
});
190-
await this.setMungedLocalDescription(answer, write(sdpParsed));
252+
await this.setMungedSDP(answer, write(sdpParsed));
191253
return answer;
192254
}
193255

194-
setTrackCodecBitrate(sid: string, codec: string, maxbr: number) {
195-
this.trackBitrates.push({
196-
sid,
197-
codec,
198-
maxbr,
199-
});
256+
setTrackCodecBitrate(info: TrackBitrateInfo) {
257+
this.trackBitrates.push(info);
200258
}
201259

202260
close() {
@@ -205,22 +263,32 @@ export default class PCTransport extends EventEmitter {
205263
this.pc.close();
206264
}
207265

208-
private async setMungedLocalDescription(sd: RTCSessionDescriptionInit, munged: string) {
209-
const originalSdp = sd.sdp;
210-
sd.sdp = munged;
211-
try {
212-
log.debug('setting munged local description');
213-
await this.pc.setLocalDescription(sd);
214-
return;
215-
} catch (e) {
216-
log.warn(`not able to set ${sd.type}, falling back to unmodified sdp`, {
217-
error: e,
218-
});
219-
sd.sdp = originalSdp;
266+
private async setMungedSDP(sd: RTCSessionDescriptionInit, munged?: string, remote?: boolean) {
267+
if (munged) {
268+
const originalSdp = sd.sdp;
269+
sd.sdp = munged;
270+
try {
271+
log.debug(`setting munged ${remote ? 'remote' : 'local'} description`);
272+
if (remote) {
273+
await this.pc.setRemoteDescription(sd);
274+
} else {
275+
await this.pc.setLocalDescription(sd);
276+
}
277+
return;
278+
} catch (e) {
279+
log.warn(`not able to set ${sd.type}, falling back to unmodified sdp`, {
280+
error: e,
281+
});
282+
sd.sdp = originalSdp;
283+
}
220284
}
221285

222286
try {
223-
await this.pc.setLocalDescription(sd);
287+
if (remote) {
288+
await this.pc.setRemoteDescription(sd);
289+
} else {
290+
await this.pc.setLocalDescription(sd);
291+
}
224292
} catch (e) {
225293
// this error cannot always be caught.
226294
// If the local description has a setCodecPreferences error, this error will be uncaught

src/room/participant/LocalParticipant.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -730,12 +730,36 @@ export default class LocalParticipant extends Participant {
730730
// store RTPSender
731731
track.sender = await this.engine.createSender(track, opts, encodings);
732732

733-
if (track.codec && isSVCCodec(track.codec) && encodings && encodings[0]?.maxBitrate) {
734-
this.engine.publisher.setTrackCodecBitrate(
735-
req.cid,
736-
track.codec,
737-
encodings[0].maxBitrate / 1000,
738-
);
733+
if (encodings) {
734+
if (isFireFox() && track.kind === Track.Kind.Audio) {
735+
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
736+
livekit-server uses maxaveragebitrate=510000in the answer sdp to permit client to
737+
publish high quality audio track. But firefox always uses this value as the actual
738+
bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
739+
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
740+
fix the issue.
741+
*/
742+
let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
743+
for (const transceiver of this.engine.publisher.pc.getTransceivers()) {
744+
if (transceiver.sender === track.sender) {
745+
trackTransceiver = transceiver;
746+
break;
747+
}
748+
}
749+
if (trackTransceiver) {
750+
this.engine.publisher.setTrackCodecBitrate({
751+
transceiver: trackTransceiver,
752+
codec: 'opus',
753+
maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
754+
});
755+
}
756+
} else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
757+
this.engine.publisher.setTrackCodecBitrate({
758+
cid: req.cid,
759+
codec: track.codec,
760+
maxbr: encodings[0].maxBitrate / 1000,
761+
});
762+
}
739763
}
740764

741765
this.engine.negotiate();

0 commit comments

Comments
 (0)