/* global __filename */
import { getLogger } from 'jitsi-meet-logger';
import * as MediaType from '../../service/RTC/MediaType';
import { SdpTransformWrap } from '../xmpp/SdpTransformUtil';
const logger = getLogger(__filename);
/**
* Fakes local SDP, so that it will reflect detached local tracks associated
* with the {@link TraceablePeerConnection} and make operations like
* attach/detach and video mute/unmute local operations. That means it prevents
* from SSRC updates being sent to Jicofo/remote peer, so that there is no
* 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 detached local audio tracks stored in 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 audio track is detached from
* the {@link TraceablePeerConnection}.
* @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
*/
_addDetachedLocalAudioTracksToSDP(transformer) {
const localAudio = this.tpc.getLocalTracks(MediaType.AUDIO);
if (!localAudio.length) {
return false;
}
const audioMLine = transformer.selectMedia('audio');
if (!audioMLine) {
logger.error(
'Unable to hack local audio track SDP - no "audio" media');
return false;
}
if (audioMLine.direction === 'inactive') {
logger.error(
'Not doing local audio transform for "inactive" direction');
return false;
}
let modified = false;
for (const audioTrack of localAudio) {
const isAttached = audioTrack._isAttachedToPC(this.tpc);
const shouldFake = !isAttached;
logger.debug(
`${audioTrack} isAttached: ${isAttached
} => should fake audio SDP ?: ${shouldFake}`);
if (!shouldFake) {
// not using continue increases indentation
// eslint-disable-next-line no-continue
continue;
}
// Inject removed SSRCs
const audioSSRC = this.tpc.getLocalSSRC(audioTrack);
const audioMSID = audioTrack.storedMSID;
if (!audioSSRC) {
logger.error(
`Can't fake SDP for ${audioTrack} - no SSRC stored`);
// Aborts the forEach on this particular track,
// but will continue with the other ones
// eslint-disable-next-line no-continue
continue;
} else if (!audioMSID) {
logger.error(
`No MSID stored for local audio SSRC: ${audioSSRC}`);
// eslint-disable-next-line no-continue
continue;
}
if (audioMLine.getSSRCCount() > 0) {
logger.debug(
'Doing nothing - audio SSRCs are still there');
// audio SSRCs are still there
// eslint-disable-next-line no-continue
continue;
}
modified = true;
// We need to fake sendrecv
audioMLine.direction = 'sendrecv';
logger.debug(`Injecting audio SSRC: ${audioSSRC}`);
audioMLine.addSSRCAttribute({
id: audioSSRC,
attribute: 'cname',
value: `injected-${audioSSRC}`
});
audioMLine.addSSRCAttribute({
id: audioSSRC,
attribute: 'msid',
value: audioMSID
});
}
return modified;
}
/**
* Makes sure that detached (or 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 detached from the {@link TraceablePeerConnection} (or muted).
*
* 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
*/
_addDetachedLocalVideoTracksToSDP(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(
'There is more than 1 video track ! '
+ 'Strange things may happen !', localVideos);
}
const videoMLine = transformer.selectMedia('video');
if (!videoMLine) {
logger.error(
'Unable to hack local video track SDP - no "video" media');
return false;
}
if (videoMLine.direction === 'inactive') {
logger.error(
'Not doing local video transform for "inactive" direction.');
return false;
}
let modified = false;
for (const videoTrack of localVideos) {
const isMuted = videoTrack.isMuted();
const muteInProgress = videoTrack.inMuteOrUnmuteProgress;
const isAttached = videoTrack._isAttachedToPC(this.tpc);
const shouldFakeSdp = isMuted || muteInProgress || !isAttached;
logger.debug(
`${videoTrack
} isMuted: ${isMuted
}, is mute in progress: ${muteInProgress
}, is attached ? : ${isAttached
} => should fake sdp ? : ${shouldFakeSdp}`);
if (!shouldFakeSdp) {
// eslint-disable-next-line no-continue
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}`);
// eslint-disable-next-line no-continue
continue;
}
if (!videoMLine.getSSRCCount()) {
logger.error(
'No video SSRCs found '
+ '(should be at least the recv-only one');
// eslint-disable-next-line no-continue
continue;
}
modified = true;
// We need to fake sendrecv
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(
`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(
`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;
}
/**
* Maybe modifies local description to fake local tracks SDP when those are
* either muted or detached from the PeerConnection.
*
* @param {object} desc the WebRTC SDP object instance for the local
* description.
*/
maybeMungeLocalSdp(desc) {
// Nothing to be done in early stage when localDescription
// is not available yet
if (!desc || !desc.sdp) {
return;
}
const transformer = new SdpTransformWrap(desc.sdp);
let modified = this._addDetachedLocalAudioTracksToSDP(transformer);
if (this._addDetachedLocalVideoTracksToSDP(transformer)) {
modified = true;
}
if (modified) {
// Write
desc.sdp = transformer.toRawSDP();
// logger.info("Post TRANSFORM: ", desc.sdp);
}
}
}