diff --git a/src/handlers/sdp/commonUtils.ts b/src/handlers/sdp/commonUtils.ts index 70c38bc5..6d0cc201 100644 --- a/src/handlers/sdp/commonUtils.ts +++ b/src/handlers/sdp/commonUtils.ts @@ -27,8 +27,15 @@ export function extractRtpCapabilities({ }): RtpCapabilities { // Map of RtpCodecParameters indexed by payload type. const codecsMap: Map = new Map(); - // Map of RtpHeaderExtensions indexed by preferred id. - const headerExtensionMap: Map = new Map(); + // Map of RtpHeaderExtensions indexed by kind and preferred id. + // + // NOTE: It must be indexed by kind and id rather than just by id. Since all + // m= sections are bundled, the browser gives the same id to a given extension + // in every m= section that supports it, so indexing just by id would collapse + // the audio and the video entry of such an extension into a single one with + // the kind of the last processed m= section, making the extension vanish for + // the other kind. + const headerExtensionMap: Map = new Map(); for (const m of sdpObject.media) { const kind = m.type; @@ -122,7 +129,10 @@ export function extractRtpCapabilities({ preferredId: ext.value, }; - headerExtensionMap.set(headerExtension.preferredId, headerExtension); + headerExtensionMap.set( + `${kind}:${headerExtension.preferredId}`, + headerExtension + ); } } diff --git a/src/test/test.ts b/src/test/test.ts index 6b508dee..8b978722 100644 --- a/src/test/test.ts +++ b/src/test/test.ts @@ -4,7 +4,9 @@ */ import { FakeMediaStreamTrack } from 'fake-mediastreamtrack'; +import * as sdpTransform from 'sdp-transform'; import * as mediasoupClient from '../index'; +import * as sdpCommonUtils from '../handlers/sdp/commonUtils'; import { UnsupportedError, InvalidStateError } from '../errors'; import * as utils from '../utils'; import type { RtpCapabilities } from '../RtpParameters'; @@ -1762,3 +1764,62 @@ describe('detectDevice() and detectDeviceAsync() assign proper handler based on ); } }); + +test('sdpCommonUtils.extractRtpCapabilities() does not mix up audio and video extensions sharing the same id', () => { + // NOTE: Since all m= sections are bundled, the browser gives the same id to + // a given RTP header extension in every m= section that supports it (here + // sdes:mid has id 4 in both the audio and the video m= section). + const sdp = [ + 'v=0', + 'o=- 0 0 IN IP4 127.0.0.1', + 's=-', + 't=0 0', + 'a=group:BUNDLE 0 1', + 'm=audio 9 UDP/TLS/RTP/SAVPF 111', + 'c=IN IP4 0.0.0.0', + 'a=mid:0', + 'a=sendonly', + 'a=rtpmap:111 opus/48000/2', + 'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level', + 'a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid', + 'm=video 9 UDP/TLS/RTP/SAVPF 96', + 'c=IN IP4 0.0.0.0', + 'a=mid:1', + 'a=sendonly', + 'a=rtpmap:96 VP8/90000', + 'a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid', + 'a=extmap:13 urn:3gpp:video-orientation', + '', + ].join('\r\n'); + + const rtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ + sdpObject: sdpTransform.parse(sdp), + }); + + expect(rtpCapabilities.headerExtensions).toEqual( + expect.arrayContaining([ + { + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:ssrc-audio-level', + preferredId: 1, + }, + { + kind: 'audio', + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId: 4, + }, + { + kind: 'video', + uri: 'urn:ietf:params:rtp-hdrext:sdes:mid', + preferredId: 4, + }, + { + kind: 'video', + uri: 'urn:3gpp:video-orientation', + preferredId: 13, + }, + ]) + ); + + expect(rtpCapabilities.headerExtensions!.length).toBe(4); +});