/* global __filename */ import { getLogger } from 'jitsi-meet-logger'; import * as MediaType from '../../service/RTC/MediaType'; import { SdpTransformWrap } from './SdpTransformUtil'; const logger = getLogger(__filename); /** * Fakes local SDP exposed to {@link JingleSessionPC} through the local * description getter. Modifies the SDP, so that it will contain muted local * video tracks description, even though their underlying {MediaStreamTrack}s * are no longer in the WebRTC peerconnection. That prevents from SSRC updates * being sent to Jicofo/remote peer and prevents sRD/sLD cycle on the remote * side. */ export default class LocalSdpMunger { /** * Creates new LocalSdpMunger instance. * * @param {TraceablePeerConnection} tpc */ constructor(tpc) { this.tpc = tpc; } /** * Makes sure that muted local video tracks associated with the parent * {@link TraceablePeerConnection} are described in the local SDP. It's done * in order to prevent from sending 'source-remove'/'source-add' Jingle * notifications when local video track is muted (MediaStream is * removed from the peerconnection). * * NOTE 1 video track is assumed * * @param {SdpTransformWrap} transformer the transformer instance which will * be used to process the SDP. * @return {boolean} true if there were any modifications to * the SDP wrapped by transformer. * @private */ _addMutedLocalVideoTracksToSDP(transformer) { // Go over each video tracks and check if the SDP has to be changed const localVideos = this.tpc.getLocalTracks(MediaType.VIDEO); if (!localVideos.length) { return false; } else if (localVideos.length !== 1) { logger.error( `${this.tpc} there is more than 1 video track ! ` + 'Strange things may happen !', localVideos); } const videoMLine = transformer.selectMedia('video'); if (!videoMLine) { logger.debug( `${this.tpc} unable to hack local video track SDP` + '- no "video" media'); return false; } let modified = false; for (const videoTrack of localVideos) { const muted = videoTrack.isMuted(); const mediaStream = videoTrack.getOriginalStream(); // During the mute/unmute operation there are periods of time when // the track's underlying MediaStream is not added yet to // the PeerConnection. The SDP needs to be munged in such case. const isInPeerConnection = mediaStream && this.tpc.isMediaStreamInPc(mediaStream); const shouldFakeSdp = muted || !isInPeerConnection; logger.debug( `${this.tpc} ${videoTrack} muted: ${ muted}, is in PeerConnection: ${ isInPeerConnection} => should fake sdp ? : ${ shouldFakeSdp}`); if (!shouldFakeSdp) { continue; // eslint-disable-line no-continue } // Inject removed SSRCs const requiredSSRCs = this.tpc.isSimulcastOn() ? this.tpc.simulcast.ssrcCache : [ this.tpc.sdpConsistency.cachedPrimarySsrc ]; if (!requiredSSRCs.length) { logger.error( `No SSRCs stored for: ${videoTrack} in ${this.tpc}`); continue; // eslint-disable-line no-continue } modified = true; // We need to fake sendrecv. // NOTE the SDP produced here goes only to Jicofo and is never set // as localDescription. That's why // TraceablePeerConnection.mediaTransferActive is ignored here. videoMLine.direction = 'sendrecv'; // Check if the recvonly has MSID const primarySSRC = requiredSSRCs[0]; // FIXME The cname could come from the stream, but may turn out to // be too complex. It is fine to come up with any value, as long as // we only care about the actual SSRC values when deciding whether // or not an update should be sent. const primaryCname = `injected-${primarySSRC}`; for (const ssrcNum of requiredSSRCs) { // Remove old attributes videoMLine.removeSSRC(ssrcNum); // Inject logger.debug( `${this.tpc} injecting video SSRC: ${ssrcNum} for ${ videoTrack}`); videoMLine.addSSRCAttribute({ id: ssrcNum, attribute: 'cname', value: primaryCname }); videoMLine.addSSRCAttribute({ id: ssrcNum, attribute: 'msid', value: videoTrack.storedMSID }); } if (requiredSSRCs.length > 1) { const group = { ssrcs: requiredSSRCs.join(' '), semantics: 'SIM' }; if (!videoMLine.findGroup(group.semantics, group.ssrcs)) { // Inject the group logger.debug( `${this.tpc} injecting SIM group for ${videoTrack}`, group); videoMLine.addSSRCGroup(group); } } // Insert RTX // FIXME in P2P RTX is used by Chrome regardless of config option // status. Because of that 'source-remove'/'source-add' // notifications are still sent to remove/add RTX SSRC and FID group if (!this.tpc.options.disableRtx) { this.tpc.rtxModifier.modifyRtxSsrcs2(videoMLine); } } return modified; } /** * Modifies 'cname', 'msid', 'label' and 'mslabel' by appending * the id of {@link LocalSdpMunger#tpc} at the end, preceding by a dash * sign. * * @param {MLineWrap} mediaSection - The media part (audio or video) of the * session description which will be modified in place. * @returns {void} * @private */ _transformMediaIdentifiers(mediaSection) { const pcId = this.tpc.id; for (const ssrcLine of mediaSection.ssrcs) { switch (ssrcLine.attribute) { case 'cname': case 'label': case 'mslabel': ssrcLine.value = ssrcLine.value && `${ssrcLine.value}-${pcId}`; break; case 'msid': { if (ssrcLine.value) { const streamAndTrackIDs = ssrcLine.value.split(' '); if (streamAndTrackIDs.length === 2) { const streamId = streamAndTrackIDs[0]; const trackId = streamAndTrackIDs[1]; ssrcLine.value = `${streamId}-${pcId} ${trackId}-${pcId}`; } else { logger.warn( 'Unable to munge local MSID' + `- weird format detected: ${ssrcLine.value}`); } } break; } } } } /** * Maybe modifies local description to fake local video tracks SDP when * those are muted. * * @param {object} desc the WebRTC SDP object instance for the local * description. * @returns {RTCSessionDescription} */ maybeAddMutedLocalVideoTracksToSDP(desc) { if (!desc) { throw new Error('No local description passed in.'); } const transformer = new SdpTransformWrap(desc.sdp); if (this._addMutedLocalVideoTracksToSDP(transformer)) { return new RTCSessionDescription({ type: desc.type, sdp: transformer.toRawSDP() }); } return desc; } /** * 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. * Each PeerConnection assigns different SSRCs to the same local * MediaStream, but the MSID remains the same as it's used to identify * the stream by the WebRTC backend. The transformation will append * {@link TraceablePeerConnection#id} at the end of each stream's identifier * ("cname", "msid", "label" and "mslabel"). * * @param {RTCSessionDescription} sessionDesc - The local session * description (this instance remains unchanged). * @return {RTCSessionDescription} - Transformed local session description * (a modified copy of the one given as the input). */ transformStreamIdentifiers(sessionDesc) { // FIXME similar check is probably duplicated in all other transformers if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) { return sessionDesc; } const transformer = new SdpTransformWrap(sessionDesc.sdp); const audioMLine = transformer.selectMedia('audio'); if (audioMLine) { this._transformMediaIdentifiers(audioMLine); } const videoMLine = transformer.selectMedia('video'); if (videoMLine) { this._transformMediaIdentifiers(videoMLine); } return new RTCSessionDescription({ type: sessionDesc.type, sdp: transformer.toRawSDP() }); } }