You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LocalSdpMunger.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { getLogger } from '@jitsi/logger';
  2. import { MediaDirection } from '../../service/RTC/MediaDirection';
  3. import { MediaType } from '../../service/RTC/MediaType';
  4. import { getSourceNameForJitsiTrack } from '../../service/RTC/SignalingLayer';
  5. import browser from '../browser';
  6. import { SdpTransformWrap } from './SdpTransformUtil';
  7. const logger = getLogger(__filename);
  8. /**
  9. * Fakes local SDP exposed to {@link JingleSessionPC} through the local
  10. * description getter. Modifies the SDP, so that it will contain muted local
  11. * video tracks description, even though their underlying {MediaStreamTrack}s
  12. * are no longer in the WebRTC peerconnection. That prevents from SSRC updates
  13. * being sent to Jicofo/remote peer and prevents sRD/sLD cycle on the remote
  14. * side.
  15. */
  16. export default class LocalSdpMunger {
  17. /**
  18. * Creates new <tt>LocalSdpMunger</tt> instance.
  19. *
  20. * @param {TraceablePeerConnection} tpc
  21. * @param {string} localEndpointId - The endpoint id of the local user.
  22. */
  23. constructor(tpc, localEndpointId) {
  24. this.tpc = tpc;
  25. this.localEndpointId = localEndpointId;
  26. this.audioSourcesToMsidMap = new Map();
  27. this.videoSourcesToMsidMap = new Map();
  28. }
  29. /**
  30. * Returns a string that can be set as the MSID attribute for a source.
  31. *
  32. * @param {string} mediaType - Media type of the source.
  33. * @param {string} trackId - Id of the MediaStreamTrack associated with the source.
  34. * @param {string} streamId - Id of the MediaStream associated with the source.
  35. * @returns {string|null}
  36. */
  37. _generateMsidAttribute(mediaType, trackId, streamId) {
  38. if (!(mediaType && trackId)) {
  39. logger.error(`Unable to munge local MSID - track id=${trackId} or media type=${mediaType} is missing`);
  40. return null;
  41. }
  42. const pcId = this.tpc.id;
  43. return `${streamId}-${pcId} ${trackId}-${pcId}`;
  44. }
  45. /**
  46. * Updates or adds a 'msid' attribute in the format '<endpoint_id>-<mediaType>-<trackIndex>-<tpcId>'
  47. * example - d8ff91-video-0-1
  48. * All other attributes like 'cname', 'label' and 'mslabel' are removed since these are not processed by Jicofo.
  49. *
  50. * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
  51. * modified in place.
  52. * @returns {void}
  53. * @private
  54. */
  55. _transformMediaIdentifiers(mediaSection) {
  56. const mediaType = mediaSection.mLine?.type;
  57. const mediaDirection = mediaSection.mLine?.direction;
  58. const msidLine = mediaSection.mLine?.msid;
  59. const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
  60. const streamId = `${this.localEndpointId}-${mediaType}`;
  61. const trackId = msidLine && msidLine.split(' ')[1];
  62. // Always overwrite msid since we want the msid to be in this format even if the browser generates one.
  63. for (const source of sources) {
  64. const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid');
  65. // Update the msid if the 'msid' attribute exists.
  66. if (msid) {
  67. const streamAndTrackIDs = msid.value.split(' ');
  68. const trackID = streamAndTrackIDs[1];
  69. this._updateSourcesToMsidMap(mediaType, streamId, trackID);
  70. // Update the msid.
  71. const storedStreamId = mediaType === MediaType.VIDEO
  72. ? this.videoSourcesToMsidMap.get(trackID)
  73. : this.audioSourcesToMsidMap.get(trackID);
  74. msid.value = this._generateMsidAttribute(mediaType, trackID, storedStreamId);
  75. // Generate the msid attribute using the 'trackId' from the msid line from the media description. Only
  76. // descriptions that have the direction set to 'sendonly' or 'sendrecv' will have the 'a=msid' line.
  77. } else if (trackId) {
  78. this._updateSourcesToMsidMap(mediaType, streamId, trackId);
  79. const storedStreamId = mediaType === MediaType.VIDEO
  80. ? this.videoSourcesToMsidMap.get(trackId)
  81. : this.audioSourcesToMsidMap.get(trackId);
  82. const generatedMsid = this._generateMsidAttribute(mediaType, trackId, storedStreamId);
  83. mediaSection.ssrcs.push({
  84. id: source,
  85. attribute: 'msid',
  86. value: generatedMsid
  87. });
  88. }
  89. }
  90. // Ignore the 'cname', 'label' and 'mslabel' attributes and only have the 'msid' attribute.
  91. mediaSection.ssrcs = mediaSection.ssrcs.filter(ssrc => ssrc.attribute === 'msid');
  92. // On FF when the user has started muted create answer will generate a recv only SSRC. We don't want to signal
  93. // this SSRC in order to reduce the load of the xmpp server for large calls. Therefore the SSRC needs to be
  94. // removed from the SDP.
  95. //
  96. // For all other use cases (when the user has had media but then the user has stopped it) we want to keep the
  97. // receive only SSRCs in the SDP. Otherwise source-remove will be triggered and the next time the user add a
  98. // track we will reuse the SSRCs and send source-add with the same SSRCs. This is problematic because of issues
  99. // on Chrome and FF (https://bugzilla.mozilla.org/show_bug.cgi?id=1768729) when removing and then adding the
  100. // same SSRC in the remote sdp the remote track is not rendered.
  101. if (browser.isFirefox()
  102. && (mediaDirection === MediaDirection.RECVONLY || mediaDirection === MediaDirection.INACTIVE)
  103. && (
  104. (mediaType === MediaType.VIDEO && !this.tpc._hasHadVideoTrack)
  105. || (mediaType === MediaType.AUDIO && !this.tpc._hasHadAudioTrack)
  106. )
  107. ) {
  108. mediaSection.ssrcs = undefined;
  109. mediaSection.ssrcGroups = undefined;
  110. }
  111. }
  112. /**
  113. * Updates the MSID map.
  114. *
  115. * @param {string} mediaType The media type.
  116. * @param {string} streamId The stream id.
  117. * @param {string} trackId The track id.
  118. * @returns {void}
  119. */
  120. _updateSourcesToMsidMap(mediaType, streamId, trackId) {
  121. if (mediaType === MediaType.VIDEO) {
  122. if (!this.videoSourcesToMsidMap.has(trackId)) {
  123. const generatedStreamId = `${streamId}-${this.videoSourcesToMsidMap.size}`;
  124. this.videoSourcesToMsidMap.set(trackId, generatedStreamId);
  125. }
  126. } else if (!this.audioSourcesToMsidMap.has(trackId)) {
  127. const generatedStreamId = `${streamId}-${this.audioSourcesToMsidMap.size}`;
  128. this.audioSourcesToMsidMap.set(trackId, generatedStreamId);
  129. }
  130. }
  131. /**
  132. * This transformation will make sure that stream identifiers are unique
  133. * across all of the local PeerConnections even if the same stream is used
  134. * by multiple instances at the same time.
  135. * Each PeerConnection assigns different SSRCs to the same local
  136. * MediaStream, but the MSID remains the same as it's used to identify
  137. * the stream by the WebRTC backend. The transformation will append
  138. * {@link TraceablePeerConnection#id} at the end of each stream's identifier
  139. * ("cname", "msid", "label" and "mslabel").
  140. *
  141. * @param {RTCSessionDescription} sessionDesc - The local session
  142. * description (this instance remains unchanged).
  143. * @return {RTCSessionDescription} - Transformed local session description
  144. * (a modified copy of the one given as the input).
  145. */
  146. transformStreamIdentifiers(sessionDesc) {
  147. // FIXME similar check is probably duplicated in all other transformers
  148. if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) {
  149. return sessionDesc;
  150. }
  151. const transformer = new SdpTransformWrap(sessionDesc.sdp);
  152. const audioMLine = transformer.selectMedia(MediaType.AUDIO)?.[0];
  153. if (audioMLine) {
  154. this._transformMediaIdentifiers(audioMLine);
  155. this._injectSourceNames(audioMLine);
  156. }
  157. const videoMlines = transformer.selectMedia(MediaType.VIDEO);
  158. for (const videoMLine of videoMlines) {
  159. this._transformMediaIdentifiers(videoMLine);
  160. this._injectSourceNames(videoMLine);
  161. }
  162. return new RTCSessionDescription({
  163. type: sessionDesc.type,
  164. sdp: transformer.toRawSDP()
  165. });
  166. }
  167. /**
  168. * Injects source names. Source names are need to for multiple streams per endpoint support. The final plan is to
  169. * use the "mid" attribute for source names, but because the SDP to Jingle conversion still operates in the Plan-B
  170. * semantics (one source name per media), a custom "name" attribute is injected into SSRC lines..
  171. *
  172. * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
  173. * modified in place.
  174. * @returns {void}
  175. * @private
  176. */
  177. _injectSourceNames(mediaSection) {
  178. const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
  179. const mediaType = mediaSection.mLine?.type;
  180. if (!mediaType) {
  181. throw new Error('_transformMediaIdentifiers - no media type in mediaSection');
  182. }
  183. for (const source of sources) {
  184. const nameExists = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'name');
  185. const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid').value;
  186. const streamId = msid.split(' ')[0];
  187. // Example stream id: d8ff91-video-8-1
  188. // In the example above 8 is the track index
  189. const trackIndexParts = streamId.split('-');
  190. const trackIndex = trackIndexParts[trackIndexParts.length - 2];
  191. const sourceName = getSourceNameForJitsiTrack(this.localEndpointId, mediaType, trackIndex);
  192. if (!nameExists) {
  193. // Inject source names as a=ssrc:3124985624 name:endpointA-v0
  194. mediaSection.ssrcs.push({
  195. id: source,
  196. attribute: 'name',
  197. value: sourceName
  198. });
  199. }
  200. if (mediaType === MediaType.VIDEO) {
  201. const videoType = this.tpc.getLocalVideoTracks().find(track => track.getSourceName() === sourceName)
  202. ?.getVideoType();
  203. if (videoType) {
  204. // Inject videoType as a=ssrc:1234 videoType:desktop.
  205. mediaSection.ssrcs.push({
  206. id: source,
  207. attribute: 'videoType',
  208. value: videoType
  209. });
  210. }
  211. }
  212. }
  213. }
  214. }