Skip to content

Commit

Permalink
feat(SDP) Convert SDP->Jingle directly w/o sdp-interop layer.
Browse files Browse the repository at this point in the history
* ref(SDPDiffer) Convert to ES6 class.
Make it work directly with unified plan SDP that has multiple m-lines and add more unit tests.

* ref(xmpp) translate unified-plan SDP->Jingle directly.
Without having to run it through the SDPInterop.toPlanB cycle.

* ref(SDP) Always generate the MSID for signaling it to Jicofo.

* fix(SDPDiffer) Check explicitly for ssrc changes

* fix(SDP): Fix comments and cleanup.
Remove LOCAL_TRACK_SSRC_UPDATED event as the application ignores the event and no additional action needs to be taken when that event is fired.

* ref(SDP) Add a helper function for parsing the 'a=ssrc-group' line.

* squash: Address review comments
  • Loading branch information
jallamsetty1 authored Oct 22, 2024
1 parent a72936d commit a7476b1
Show file tree
Hide file tree
Showing 14 changed files with 1,706 additions and 629 deletions.
13 changes: 10 additions & 3 deletions modules/RTC/TPCUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,12 +360,14 @@ export class TPCUtils {

/**
* Adds {@link JitsiLocalTrack} to the WebRTC peerconnection for the first time.
*
* @param {JitsiLocalTrack} track - track to be added to the peerconnection.
* @param {boolean} isInitiator - boolean that indicates if the endpoint is offerer in a p2p connection.
* @returns {void}
* @returns {RTCRtpTransceiver} - the transceiver that the track was added to.
*/
addTrack(localTrack, isInitiator) {
const track = localTrack.getTrack();
let transceiver;

if (isInitiator) {
const streams = [];
Expand All @@ -385,13 +387,18 @@ export class TPCUtils {
if (!browser.isFirefox()) {
transceiverInit.sendEncodings = this._getStreamEncodings(localTrack);
}
this.pc.peerconnection.addTransceiver(track, transceiverInit);
transceiver = this.pc.peerconnection.addTransceiver(track, transceiverInit);
} else {
// Use pc.addTrack() for responder case so that we can re-use the m-lines that were created
// when setRemoteDescription was called. pc.addTrack() automatically attaches to any existing
// unused "recv-only" transceiver.
this.pc.peerconnection.addTrack(track);
const sender = this.pc.peerconnection.addTrack(track);

// Find the corresponding transceiver that the track was attached to.
transceiver = this.pc.peerconnection.getTransceivers().find(t => t.sender === sender);
}

return transceiver;
}

/**
Expand Down
263 changes: 95 additions & 168 deletions modules/RTC/TraceablePeerConnection.js

Large diffs are not rendered by default.

236 changes: 71 additions & 165 deletions modules/sdp/LocalSdpMunger.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import { getLogger } from '@jitsi/logger';
import { isEqual } from 'lodash-es';

import { MediaDirection } from '../../service/RTC/MediaDirection';
import { MediaType } from '../../service/RTC/MediaType';
import { getSourceNameForJitsiTrack } from '../../service/RTC/SignalingLayer';
import browser from '../browser';

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.
* 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 {

Expand All @@ -28,77 +22,78 @@ export default class LocalSdpMunger {
constructor(tpc, localEndpointId) {
this.tpc = tpc;
this.localEndpointId = localEndpointId;
this.audioSourcesToMsidMap = new Map();
this.videoSourcesToMsidMap = new Map();
}

/**
* Returns a string that can be set as the MSID attribute for a source.
*
* @param {string} mediaType - Media type of the source.
* @param {string} trackId - Id of the MediaStreamTrack associated with the source.
* @param {string} streamId - Id of the MediaStream associated with the source.
* @returns {string|null}
*/
_generateMsidAttribute(mediaType, trackId, streamId) {
if (!(mediaType && trackId)) {
logger.error(`Unable to munge local MSID - track id=${trackId} or media type=${mediaType} is missing`);

return null;
}
const pcId = this.tpc.id;

return `${streamId}-${pcId} ${trackId}-${pcId}`;
}

/**
* Updates or adds a 'msid' attribute in the format '<endpoint_id>-<mediaType>-<trackIndex>-<tpcId>'
* example - d8ff91-video-0-1
* All other attributes like 'cname', 'label' and 'mslabel' are removed since these are not processed by Jicofo.
* 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) {
const mediaType = mediaSection.mLine?.type;
const mediaDirection = mediaSection.mLine?.direction;
const msidLine = mediaSection.mLine?.msid;
const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
const streamId = `${this.localEndpointId}-${mediaType}`;
let trackId = msidLine ? msidLine.split(' ')[1] : `${this.localEndpointId}-${mediaSection.mLine.mid}`;

// Always overwrite msid since we want the msid to be in this format even if the browser generates one.
for (const source of sources) {
const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid');

if (msid) {
trackId = msid.value.split(' ')[1];
_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;
}
}
this._updateSourcesToMsidMap(mediaType, streamId, trackId);
const storedStreamId = mediaType === MediaType.VIDEO
? this.videoSourcesToMsidMap.get(trackId)
: this.audioSourcesToMsidMap.get(trackId);

const generatedMsid = this._generateMsidAttribute(mediaType, trackId, storedStreamId);

// Update the msid if the 'msid' attribute exists.
if (msid) {
msid.value = generatedMsid;

// Generate the 'msid' attribute if there is a local source.
} else if (mediaDirection === MediaDirection.SENDONLY || mediaDirection === MediaDirection.SENDRECV) {
mediaSection.ssrcs.push({
id: source,
attribute: 'msid',
value: generatedMsid
});
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. '<endpoint_id>-<mediaType>-<trackIndex>-<tpcId>' 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 and only have the 'msid' attribute.
mediaSection.ssrcs = mediaSection.ssrcs.filter(ssrc => ssrc.attribute === 'msid');
// 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
Expand All @@ -122,43 +117,16 @@ export default class LocalSdpMunger {
}

/**
* Updates the MSID map.
* 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 {string} mediaType The media type.
* @param {string} streamId The stream id.
* @param {string} trackId The track id.
* @returns {void}
*/
_updateSourcesToMsidMap(mediaType, streamId, trackId) {
if (mediaType === MediaType.VIDEO) {
if (!this.videoSourcesToMsidMap.has(trackId)) {
const generatedStreamId = `${streamId}-${this.videoSourcesToMsidMap.size}`;

this.videoSourcesToMsidMap.set(trackId, generatedStreamId);
}
} else if (!this.audioSourcesToMsidMap.has(trackId)) {
const generatedStreamId = `${streamId}-${this.audioSourcesToMsidMap.size}`;

this.audioSourcesToMsidMap.set(trackId, generatedStreamId);
}
}

/**
* 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).
* @param {RTCSessionDescription} sessionDesc - The local session description (this instance remains unchanged).
* @param {Map<string, TPCSSRCInfo>} 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) {
transformStreamIdentifiers(sessionDesc, ssrcMap) {
if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) {
return sessionDesc;
}
Expand All @@ -167,80 +135,18 @@ export default class LocalSdpMunger {
const audioMLine = transformer.selectMedia(MediaType.AUDIO)?.[0];

if (audioMLine) {
this._transformMediaIdentifiers(audioMLine);
this._injectSourceNames(audioMLine);
this._transformMediaIdentifiers(audioMLine, ssrcMap);
}

const videoMlines = transformer.selectMedia(MediaType.VIDEO);

for (const videoMLine of videoMlines) {
this._transformMediaIdentifiers(videoMLine);
this._injectSourceNames(videoMLine);
this._transformMediaIdentifiers(videoMLine, ssrcMap);
}

// Reset the local tracks based maps for msid after every transformation since Chrome 122 is generating
// a new set of SSRCs for the same source when the direction of transceiver changes because of a remote
// source getting added on the p2p connection.
this.audioSourcesToMsidMap.clear();
this.videoSourcesToMsidMap.clear();

return new RTCSessionDescription({
type: sessionDesc.type,
sdp: transformer.toRawSDP()
});
}

/**
* Injects source names. Source names are need to for multiple streams per endpoint support. The final plan is to
* use the "mid" attribute for source names, but because the SDP to Jingle conversion still operates in the Plan-B
* semantics (one source name per media), a custom "name" attribute is injected into SSRC lines..
*
* @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
* modified in place.
* @returns {void}
* @private
*/
_injectSourceNames(mediaSection) {
const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
const mediaType = mediaSection.mLine?.type;

if (!mediaType) {
throw new Error('_transformMediaIdentifiers - no media type in mediaSection');
}

for (const source of sources) {
const nameExists = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'name');
const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid').value;
const streamId = msid.split(' ')[0];

// Example stream id: d8ff91-video-8-1
// In the example above 8 is the track index
const trackIndexParts = streamId.split('-');
const trackIndex = trackIndexParts[trackIndexParts.length - 2];
const sourceName = getSourceNameForJitsiTrack(this.localEndpointId, mediaType, trackIndex);

if (!nameExists) {
// Inject source names as a=ssrc:3124985624 name:endpointA-v0
mediaSection.ssrcs.push({
id: source,
attribute: 'name',
value: sourceName
});
}

if (mediaType === MediaType.VIDEO) {
const videoType = this.tpc.getLocalVideoTracks().find(track => track.getSourceName() === sourceName)
?.getVideoType();

if (videoType) {
// Inject videoType as a=ssrc:1234 videoType:desktop.
mediaSection.ssrcs.push({
id: source,
attribute: 'videoType',
value: videoType
});
}
}
}
}
}
Loading

0 comments on commit a7476b1

Please sign in to comment.