import { isEqual } from 'lodash-es'; import { MediaDirection } from '../../service/RTC/MediaDirection'; import { MediaType } from '../../service/RTC/MediaType'; import browser from '../browser'; import { SdpTransformWrap } from './SdpTransformUtil'; /** * Fakes local SDP exposed to {@link JingleSessionPC} through the local description getter. Modifies the SDP, so that * the stream identifiers are unique across all of the local PeerConnections and that the source names and video types * are injected so that Jicofo can use them to identify the sources. */ export default class LocalSdpMunger { /** * Creates new LocalSdpMunger instance. * * @param {TraceablePeerConnection} tpc * @param {string} localEndpointId - The endpoint id of the local user. */ constructor(tpc, localEndpointId) { this.tpc = tpc; this.localEndpointId = localEndpointId; } /** * Updates or adds a 'msid' attribute for the local sources in the SDP. Also adds 'sourceName' and 'videoType' * (if applicable) attributes. All other source attributes like 'cname', 'label' and 'mslabel' are removed since * these are not processed by Jicofo. * * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be * modified in place. * @returns {void} * @private */ _transformMediaIdentifiers(mediaSection, ssrcMap) { const mediaType = mediaSection.mLine.type; const mediaDirection = mediaSection.mLine.direction; const sources = [ ...new Set(mediaSection.mLine.ssrcs?.map(s => s.id)) ]; let sourceName; if (ssrcMap.size) { const sortedSources = sources.slice().sort(); for (const [ id, trackSsrcs ] of ssrcMap.entries()) { if (isEqual(sortedSources, [ ...trackSsrcs.ssrcs ].sort())) { sourceName = id; } } for (const source of sources) { if ((mediaDirection === MediaDirection.SENDONLY || mediaDirection === MediaDirection.SENDRECV) && sourceName) { const msid = ssrcMap.get(sourceName).msid; const generatedMsid = `${msid}-${this.tpc.id}`; const existingMsid = mediaSection.ssrcs .find(ssrc => ssrc.id === source && ssrc.attribute === 'msid'); // Always overwrite msid since we want the msid to be in this format even if the browser generates // one. '---' example - d8ff91-video-0-1 if (existingMsid) { existingMsid.value = generatedMsid; } else { mediaSection.ssrcs.push({ id: source, attribute: 'msid', value: generatedMsid }); } // Inject source names as a=ssrc:3124985624 name:endpointA-v0 mediaSection.ssrcs.push({ id: source, attribute: 'name', value: sourceName }); const videoType = this.tpc.getLocalVideoTracks() .find(track => track.getSourceName() === sourceName) ?.getVideoType(); if (mediaType === MediaType.VIDEO && videoType) { // Inject videoType as a=ssrc:1234 videoType:desktop. mediaSection.ssrcs.push({ id: source, attribute: 'videoType', value: videoType }); } } } } // Ignore the 'cname', 'label' and 'mslabel' attributes. mediaSection.ssrcs = mediaSection.ssrcs .filter(ssrc => ssrc.attribute === 'msid' || ssrc.attribute === 'name' || ssrc.attribute === 'videoType'); // On FF when the user has started muted create answer will generate a recv only SSRC. We don't want to signal // this SSRC in order to reduce the load of the xmpp server for large calls. Therefore the SSRC needs to be // removed from the SDP. // // For all other use cases (when the user has had media but then the user has stopped it) we want to keep the // receive only SSRCs in the SDP. Otherwise source-remove will be triggered and the next time the user add a // track we will reuse the SSRCs and send source-add with the same SSRCs. This is problematic because of issues // on Chrome and FF (https://bugzilla.mozilla.org/show_bug.cgi?id=1768729) when removing and then adding the // same SSRC in the remote sdp the remote track is not rendered. if (browser.isFirefox() && (mediaDirection === MediaDirection.RECVONLY || mediaDirection === MediaDirection.INACTIVE) && ( (mediaType === MediaType.VIDEO && !this.tpc._hasHadVideoTrack) || (mediaType === MediaType.AUDIO && !this.tpc._hasHadAudioTrack) ) ) { mediaSection.ssrcs = undefined; mediaSection.ssrcGroups = undefined; } } /** * This transformation will make sure that stream identifiers are unique across all of the local PeerConnections * even if the same stream is used by multiple instances at the same time. It also injects 'sourceName' and * 'videoType' attribute. * * @param {RTCSessionDescription} sessionDesc - The local session description (this instance remains unchanged). * @param {Map} ssrcMap - The SSRC and source map for the local tracks. * @return {RTCSessionDescription} - Transformed local session description * (a modified copy of the one given as the input). */ transformStreamIdentifiers(sessionDesc, ssrcMap) { if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) { return sessionDesc; } const transformer = new SdpTransformWrap(sessionDesc.sdp); const audioMLine = transformer.selectMedia(MediaType.AUDIO)?.[0]; if (audioMLine) { this._transformMediaIdentifiers(audioMLine, ssrcMap); } const videoMlines = transformer.selectMedia(MediaType.VIDEO); for (const videoMLine of videoMlines) { this._transformMediaIdentifiers(videoMLine, ssrcMap); } return new RTCSessionDescription({ type: sessionDesc.type, sdp: transformer.toRawSDP() }); } }