fix(voice): stop screen shares failing to reach their viewers (#2835)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-19 02:17:25 +02:00
1 parent f32356801d
commit bc47a724af
119 files changed
+27942 -12524

No files matched your search

@@ -2,9 +2,18 @@
//
// SPDX-License-Identifier: Apache-2.0
import type {MediaDescription} from 'sdp-transform';
import {parse} from 'sdp-transform';
import {describe, expect, it} from 'vitest';
import type {TrackBitrateInfo} from './PCTransport.ts';
import {applyVideoStartBitrate, collectStereoMids, ensureAudioNackAndStereo, ensureOpusFmtp} from './PCTransport.ts';
import {
applyVideoStartBitrate,
collectStereoMids,
ensureAudioNackAndStereo,
ensureOpusFmtp,
ensureVideoDDExtension,
videoSectionCanReceiveAV1,
} from './PCTransport.ts';
import {ddExtensionURI} from './utils.ts';
function videoMedia(
trackId: string,
@@ -166,3 +175,67 @@ describe('collectStereoMids', () => {
expect(collectStereoMids([audioBitrateInfo(null, 'mic-track', false)], media)).toEqual([]);
});
});
const singlePcOffer = `v=0
o=- 0 0 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0 1 2
m=video 9 UDP/TLS/RTP/SAVPF 96 45
c=IN IP4 0.0.0.0
a=mid:0
a=sendonly
a=msid:s cam
a=rtpmap:96 VP8/90000
a=rtpmap:45 AV1/90000
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
m=video 9 UDP/TLS/RTP/SAVPF 96 45
c=IN IP4 0.0.0.0
a=mid:1
a=recvonly
a=rtpmap:96 VP8/90000
a=rtpmap:45 AV1/90000
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
m=video 9 UDP/TLS/RTP/SAVPF 96
c=IN IP4 0.0.0.0
a=mid:2
a=recvonly
a=rtpmap:96 VP8/90000
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time`;
const sectionOf = (sdp: ReturnType<typeof parse>, mid: string) => sdp.media.find((media) => `${media.mid}` === mid)!;
const ddOf = (sdp: ReturnType<typeof parse>, mid: string) =>
sectionOf(sdp, mid).ext?.find((ext) => ext.uri === ddExtensionURI)?.value;
describe('videoSectionCanReceiveAV1', () => {
it('is true for a section we receive on that kept AV1, false otherwise', () => {
const sdp = parse(singlePcOffer);
expect(videoSectionCanReceiveAV1(sectionOf(sdp, '1'))).toBe(true);
expect(videoSectionCanReceiveAV1(sectionOf(sdp, '0'))).toBe(false);
expect(videoSectionCanReceiveAV1(sectionOf(sdp, '2'))).toBe(false);
});
});
describe('ensureVideoDDExtension', () => {
it('assigns an id above every extension in the bundle and reuses it', () => {
const sdp = parse(singlePcOffer);
expect(ensureVideoDDExtension(sectionOf(sdp, '1'), sdp, 0)).toBe(12);
expect(ddOf(sdp, '1')).toBe(12);
expect(ensureVideoDDExtension(sectionOf(sdp, '2'), sdp, 12)).toBe(12);
expect(ddOf(sdp, '2')).toBe(12);
});
it('adopts an id the bundle already maps the extension to', () => {
const sdp = parse(singlePcOffer);
sectionOf(sdp, '0').ext!.push({value: 13, uri: ddExtensionURI});
expect(ensureVideoDDExtension(sectionOf(sdp, '1'), sdp, 7)).toBe(13);
expect(ddOf(sdp, '1')).toBe(13);
});
it('leaves a section that already carries the extension alone', () => {
const sdp = parse(`${singlePcOffer}\na=extmap:3 ${ddExtensionURI}`);
expect(ensureVideoDDExtension(sectionOf(sdp, '2'), sdp, 0)).toBe(3);
expect(sectionOf(sdp, '2').ext).toHaveLength(2);
});
});
@@ -10,7 +10,7 @@ import log, {getLogger, LoggerNames} from '../logger.ts';
import {debounce} from './debounce.ts';
import {NegotiationError, UnexpectedConnectionState} from './errors.ts';
import type {LoggerOptions} from './types.ts';
import {ddExtensionURI, isFireFox, isSafari, isSVCCodec} from './utils.ts';
import {ddExtensionURI, isChromiumBased, isFireFox, isSafari, isSVCCodec} from './utils.ts';
export interface TrackBitrateInfo {
cid?: string;
@@ -372,6 +372,9 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
if (media.type === 'audio') {
ensureAudioNackAndStereo(media, stereoMids, []);
} else if (media.type === 'video') {
if (isChromiumBased() && videoSectionCanReceiveAV1(media)) {
this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
}
this.trackBitrates.some((trackbr): boolean => {
if (!trackbr.cid) {
return false;
@@ -677,6 +680,11 @@ export function ensureVideoDDExtension(
return id;
}
export function videoSectionCanReceiveAV1(media: MediaDescription): boolean {
if (media.direction !== 'recvonly' && media.direction !== 'sendrecv') return false;
return media.rtp.some((rtp) => rtp.codec.toLowerCase() === 'av1');
}
function ddExtensionIDFor(sdp: SessionDescription, cachedID: number): number | undefined {
const mapped = mappedExtensionID(sdp, ddExtensionURI);
if (mapped !== undefined) {
@@ -82,10 +82,33 @@ describe('selectPublisherCodecPreferences', () => {
expect(preferences).toEqual([constrainedBaselineMode1, constrainedBaselineMode0, highProfile]);
});
it('keeps non-H.264 codecs in browser capability order and appends RTX', () => {
it('puts the chosen codec first and keeps every other codec in browser capability order', () => {
const vp9 = codec('video/VP9');
const vp8 = codec('video/VP8');
const rtx = codec('video/rtx');
expect(selectPublisherCodecPreferences('vp9', [vp8, rtx, vp9])).toEqual([vp9, rtx]);
expect(selectPublisherCodecPreferences('vp9', [vp8, rtx, vp9])).toEqual([vp9, vp8, rtx]);
});
it('keeps the other codecs so a later publication on the same connection can negotiate them', () => {
const vp9 = codec('video/VP9');
const av1 = codec('video/AV1');
const h264 = codec('video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f');
const preferences = selectPublisherCodecPreferences('vp9', [av1, h264, vp9]);
expect(preferences.map((entry) => entry.mimeType)).toEqual(['video/VP9', 'video/AV1', 'video/H264']);
});
it('ranks the H.264 profiles it leaves behind the chosen codec', () => {
const vp8 = codec('video/VP8');
const highProfile = codec('video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640033');
const constrainedBaseline = codec(
'video/H264',
'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
);
const preferences = selectPublisherCodecPreferences('vp8', [vp8, highProfile, constrainedBaseline]);
expect(preferences).toEqual([vp8, constrainedBaseline, highProfile]);
});
it('returns nothing when the sender cannot encode the chosen codec', () => {
expect(selectPublisherCodecPreferences('av1', [codec('video/VP8'), codec('video/rtx')])).toEqual([]);
});
});
@@ -1034,7 +1034,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
const capabilities = RTCRtpSender.getCapabilities('video');
if (!capabilities) return;
const preferences = selectPublisherCodecPreferences(codec, capabilities.codecs);
if (preferences.length === 0) return;
if (preferences.length === 0) {
this.log.warn('sender cannot encode the requested codec, leaving the browser order in place', {
...this.logContext,
codec,
});
return;
}
try {
transceiver.setCodecPreferences(preferences);
} catch (error) {
@@ -1828,8 +1834,12 @@ export function selectPublisherCodecPreferences(
const selected = codecs.filter((entry) => mimeTypes.has(entry.mimeType.toLowerCase()));
if (selected.length === 0) return [];
const preferred = codec === 'h264' ? preferHardwareH264Codecs(selected) : selected;
const rtx = codecs.filter((entry) => entry.mimeType.toLowerCase() === 'video/rtx');
return [...preferred, ...rtx];
const isH264 = (entry: RtpCodecCapability): boolean => entry.mimeType.toLowerCase() === 'video/h264';
const remaining = codecs.filter((entry) => !mimeTypes.has(entry.mimeType.toLowerCase()));
const rankedH264 = preferHardwareH264Codecs(remaining.filter(isH264));
let nextH264 = 0;
const rest = remaining.map((entry) => (isH264(entry) ? rankedH264[nextH264++] : entry));
return [...preferred, ...rest];
}
export type EngineEventCallbacks = {