Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions packages/client/src/rtc/Publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from './layers';
import { isSvcCodec } from './codecs';
import { isAudioTrackType } from './helpers/tracks';
import { extractMid, removeCodecsExcept } from './helpers/sdp';
import { extractMid, removeCodecsExcept, setStartBitrate } from './helpers/sdp';
import { withoutConcurrency } from '../helpers/concurrency';
import { isReactNative } from '../helpers/platforms';

Expand Down Expand Up @@ -382,11 +382,30 @@ export class Publisher extends BasePeerConnection {
await this.pc.setLocalDescription(offer);

const { sdp: baseSdp = '' } = offer;
const { dangerouslyForceCodec, fmtpLine } =
this.clientPublishOptions || {};
const sdp = dangerouslyForceCodec
const {
dangerouslyForceCodec,
dangerouslySetStartBitrateFactor,
fmtpLine,
} = this.clientPublishOptions || {};
let sdp = dangerouslyForceCodec
? removeCodecsExcept(baseSdp, dangerouslyForceCodec, fmtpLine)
: baseSdp;
if (dangerouslySetStartBitrateFactor) {
this.transceiverCache.items().forEach((t) => {
if (t.publishOption.trackType !== TrackType.VIDEO) return;
const maxBitrateBps = t.publishOption.bitrate;
const trackId = t.transceiver.sender.track?.id;
if (!trackId) return;
const mid = tracks.find((x) => x.trackId === trackId)?.mid;
if (!mid) return;
sdp = setStartBitrate(
sdp,
maxBitrateBps / 1000, // convert to kbps
dangerouslySetStartBitrateFactor,
mid,
);
});
}
const { response } = await this.sfuClient.setPublisher({ sdp, tracks });
if (response.error) throw new NegotiationError(response.error);

Expand Down
112 changes: 112 additions & 0 deletions packages/client/src/rtc/helpers/__tests__/sdp.startBitrate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, expect, it } from 'vitest';

import { setStartBitrate } from '../sdp';

describe('sdp - setStartBitrate', () => {
it('adds x-google-start-bitrate for AV1/VP9/H264 fmtp lines (but not VP8)', () => {
const offerSdp = `v=0
o=- 123 2 IN IP4 127.0.0.1
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 96 98 103 45
c=IN IP4 0.0.0.0
a=mid:0
a=rtpmap:96 VP8/90000
a=rtpmap:98 VP9/90000
a=fmtp:98 profile-id=0
a=rtpmap:103 H264/90000
a=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:45 AV1/90000
a=fmtp:45 level-idx=5;profile=0;tier=0
`;

const result = setStartBitrate(offerSdp, 1500, 0.5, '0'); // 750kbps

expect(result).toContain(
'a=fmtp:98 profile-id=0;x-google-start-bitrate=750',
);
expect(result).toContain(
'a=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f;x-google-start-bitrate=750',
);
expect(result).toContain(
'a=fmtp:45 level-idx=5;profile=0;tier=0;x-google-start-bitrate=750',
);

// VP8 is not a target codec
expect(result).not.toContain('a=fmtp:96');
expect(result).not.toContain(
'a=rtpmap:96 VP8/90000;x-google-start-bitrate',
);
});

it('does not add x-google-start-bitrate if already present', () => {
const offerSdp = `v=0
o=- 123 2 IN IP4 127.0.0.1
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 103
c=IN IP4 0.0.0.0
a=mid:0
a=rtpmap:103 H264/90000
a=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f;x-google-start-bitrate=500
`;

const result = setStartBitrate(offerSdp, 1500, 0.9, '0');
expect(result).toContain('x-google-start-bitrate=500');
expect(result).not.toContain('x-google-start-bitrate=1350');

// Ensure it's only present once
expect(result.match(/x-google-start-bitrate/g)).toHaveLength(1);
});

it('clamps computed start bitrate to a minimum of 300kbps', () => {
const offerSdp = `v=0
o=- 123 2 IN IP4 127.0.0.1
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 98
c=IN IP4 0.0.0.0
a=mid:0
a=rtpmap:98 VP9/90000
a=fmtp:98 profile-id=0
`;

const result = setStartBitrate(offerSdp, 1500, 0.1, '0'); // 150 -> clamped to 300
expect(result).toContain(
'a=fmtp:98 profile-id=0;x-google-start-bitrate=300',
);
});

it('can scope the change to a specific mid', () => {
const offerSdp = `v=0
o=- 123 2 IN IP4 127.0.0.1
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 98
c=IN IP4 0.0.0.0
a=mid:0
a=rtpmap:98 VP9/90000
a=fmtp:98 profile-id=0
m=video 9 UDP/TLS/RTP/SAVPF 103
c=IN IP4 0.0.0.0
a=mid:1
a=rtpmap:103 H264/90000
a=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
`;

const result = setStartBitrate(offerSdp, 1500, 0.7, '0'); // 1050kbps

// mid:0 updated
expect(result).toContain(
'a=fmtp:98 profile-id=0;x-google-start-bitrate=1050',
);

// mid:1 untouched
expect(result).toContain(
'a=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f',
);
expect(result).not.toContain(
'a=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f;x-google-start-bitrate=1050',
);
});
});
40 changes: 40 additions & 0 deletions packages/client/src/rtc/helpers/sdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,46 @@ export const extractMid = (
return String(transceiverInitIndex);
};

/*
* Sets the start bitrate for the VP9 and H264 codecs in the SDP.
*
* @param offerSdp the offer SDP to modify.
* @param startBitrate the start bitrate in kbps to set. Default is 1000 kbps.
*/
export const setStartBitrate = (
offerSdp: string,
maxBitrateKbps: number,
startBitrateFactor: number,
targetMid: string,
): string => {
// start bitrate should be between 300kbps and max-bitrate-kbps
const startBitrate = Math.max(
Math.min(maxBitrateKbps, startBitrateFactor * maxBitrateKbps),
300,
);
const parsedSdp = parse(offerSdp);
const targetCodecs = new Set(['av1', 'vp9', 'h264']);

for (const media of parsedSdp.media) {
if (media.type !== 'video') continue;
if (String(media.mid) !== targetMid) continue;

for (const rtp of media.rtp) {
if (!targetCodecs.has(rtp.codec.toLowerCase())) continue;

for (const fmtp of media.fmtp) {
if (fmtp.payload === rtp.payload) {
if (!fmtp.config.includes('x-google-start-bitrate')) {
fmtp.config += `;x-google-start-bitrate=${startBitrate}`;
}
break;
}
}
}
}
return write(parsedSdp);
};

/**
* Enables stereo in the answer SDP based on the offered stereo in the offer SDP.
*
Expand Down
8 changes: 8 additions & 0 deletions packages/client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ export type ClientPublishOptions = {
* @internal
*/
dangerouslyForceCodec?: PreferredCodec;
/**
* Sets the start bitrate factor to use when publishing a vp9 or av1 or h264 video stream.
* Must be between 0 and 1.
* By default the start bitrate is set to 300kbps.
* This value is used to calculate the start bitrate based on the max bitrate.
* For example, if the max bitrate is 1500kbps and the start bitrate factor is 0.5, the start bitrate will be 750kbps.
*/
dangerouslySetStartBitrateFactor?: number;
};

export type ScreenShareSettings = {
Expand Down
Loading