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 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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. let trackId = msidLine ? msidLine.split(' ')[1] : `${this.localEndpointId}-${mediaSection.mLine.mid}`;
  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. if (msid) {
  66. trackId = msid.value.split(' ')[1];
  67. }
  68. this._updateSourcesToMsidMap(mediaType, streamId, trackId);
  69. const storedStreamId = mediaType === MediaType.VIDEO
  70. ? this.videoSourcesToMsidMap.get(trackId)
  71. : this.audioSourcesToMsidMap.get(trackId);
  72. const generatedMsid = this._generateMsidAttribute(mediaType, trackId, storedStreamId);
  73. // Update the msid if the 'msid' attribute exists.
  74. if (msid) {
  75. msid.value = generatedMsid;
  76. // Generate the 'msid' attribute if there is a local source.
  77. } else if (mediaDirection === MediaDirection.SENDONLY || mediaDirection === MediaDirection.SENDRECV) {
  78. mediaSection.ssrcs.push({
  79. id: source,
  80. attribute: 'msid',
  81. value: generatedMsid
  82. });
  83. }
  84. }
  85. // Ignore the 'cname', 'label' and 'mslabel' attributes and only have the 'msid' attribute.
  86. mediaSection.ssrcs = mediaSection.ssrcs.filter(ssrc => ssrc.attribute === 'msid');
  87. // On FF when the user has started muted create answer will generate a recv only SSRC. We don't want to signal
  88. // this SSRC in order to reduce the load of the xmpp server for large calls. Therefore the SSRC needs to be
  89. // removed from the SDP.
  90. //
  91. // For all other use cases (when the user has had media but then the user has stopped it) we want to keep the
  92. // receive only SSRCs in the SDP. Otherwise source-remove will be triggered and the next time the user add a
  93. // track we will reuse the SSRCs and send source-add with the same SSRCs. This is problematic because of issues
  94. // on Chrome and FF (https://bugzilla.mozilla.org/show_bug.cgi?id=1768729) when removing and then adding the
  95. // same SSRC in the remote sdp the remote track is not rendered.
  96. if (browser.isFirefox()
  97. && (mediaDirection === MediaDirection.RECVONLY || mediaDirection === MediaDirection.INACTIVE)
  98. && (
  99. (mediaType === MediaType.VIDEO && !this.tpc._hasHadVideoTrack)
  100. || (mediaType === MediaType.AUDIO && !this.tpc._hasHadAudioTrack)
  101. )
  102. ) {
  103. mediaSection.ssrcs = undefined;
  104. mediaSection.ssrcGroups = undefined;
  105. }
  106. }
  107. /**
  108. * Updates the MSID map.
  109. *
  110. * @param {string} mediaType The media type.
  111. * @param {string} streamId The stream id.
  112. * @param {string} trackId The track id.
  113. * @returns {void}
  114. */
  115. _updateSourcesToMsidMap(mediaType, streamId, trackId) {
  116. if (mediaType === MediaType.VIDEO) {
  117. if (!this.videoSourcesToMsidMap.has(trackId)) {
  118. const generatedStreamId = `${streamId}-${this.videoSourcesToMsidMap.size}`;
  119. this.videoSourcesToMsidMap.set(trackId, generatedStreamId);
  120. }
  121. } else if (!this.audioSourcesToMsidMap.has(trackId)) {
  122. const generatedStreamId = `${streamId}-${this.audioSourcesToMsidMap.size}`;
  123. this.audioSourcesToMsidMap.set(trackId, generatedStreamId);
  124. }
  125. }
  126. /**
  127. * This transformation will make sure that stream identifiers are unique
  128. * across all of the local PeerConnections even if the same stream is used
  129. * by multiple instances at the same time.
  130. * Each PeerConnection assigns different SSRCs to the same local
  131. * MediaStream, but the MSID remains the same as it's used to identify
  132. * the stream by the WebRTC backend. The transformation will append
  133. * {@link TraceablePeerConnection#id} at the end of each stream's identifier
  134. * ("cname", "msid", "label" and "mslabel").
  135. *
  136. * @param {RTCSessionDescription} sessionDesc - The local session
  137. * description (this instance remains unchanged).
  138. * @return {RTCSessionDescription} - Transformed local session description
  139. * (a modified copy of the one given as the input).
  140. */
  141. transformStreamIdentifiers(sessionDesc) {
  142. if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) {
  143. return sessionDesc;
  144. }
  145. const transformer = new SdpTransformWrap(sessionDesc.sdp);
  146. const audioMLine = transformer.selectMedia(MediaType.AUDIO)?.[0];
  147. if (audioMLine) {
  148. this._transformMediaIdentifiers(audioMLine);
  149. this._injectSourceNames(audioMLine);
  150. }
  151. const videoMlines = transformer.selectMedia(MediaType.VIDEO);
  152. for (const videoMLine of videoMlines) {
  153. this._transformMediaIdentifiers(videoMLine);
  154. this._injectSourceNames(videoMLine);
  155. }
  156. // Reset the local tracks based maps for msid after every transformation since Chrome 122 is generating
  157. // a new set of SSRCs for the same source when the direction of transceiver changes because of a remote
  158. // source getting added on the p2p connection.
  159. this.audioSourcesToMsidMap.clear();
  160. this.videoSourcesToMsidMap.clear();
  161. return new RTCSessionDescription({
  162. type: sessionDesc.type,
  163. sdp: transformer.toRawSDP()
  164. });
  165. }
  166. /**
  167. * Injects source names. Source names are need to for multiple streams per endpoint support. The final plan is to
  168. * use the "mid" attribute for source names, but because the SDP to Jingle conversion still operates in the Plan-B
  169. * semantics (one source name per media), a custom "name" attribute is injected into SSRC lines..
  170. *
  171. * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
  172. * modified in place.
  173. * @returns {void}
  174. * @private
  175. */
  176. _injectSourceNames(mediaSection) {
  177. const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
  178. const mediaType = mediaSection.mLine?.type;
  179. if (!mediaType) {
  180. throw new Error('_transformMediaIdentifiers - no media type in mediaSection');
  181. }
  182. for (const source of sources) {
  183. const nameExists = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'name');
  184. const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid').value;
  185. const streamId = msid.split(' ')[0];
  186. // Example stream id: d8ff91-video-8-1
  187. // In the example above 8 is the track index
  188. const trackIndexParts = streamId.split('-');
  189. const trackIndex = trackIndexParts[trackIndexParts.length - 2];
  190. const sourceName = getSourceNameForJitsiTrack(this.localEndpointId, mediaType, trackIndex);
  191. if (!nameExists) {
  192. // Inject source names as a=ssrc:3124985624 name:endpointA-v0
  193. mediaSection.ssrcs.push({
  194. id: source,
  195. attribute: 'name',
  196. value: sourceName
  197. });
  198. }
  199. if (mediaType === MediaType.VIDEO) {
  200. const videoType = this.tpc.getLocalVideoTracks().find(track => track.getSourceName() === sourceName)
  201. ?.getVideoType();
  202. if (videoType) {
  203. // Inject videoType as a=ssrc:1234 videoType:desktop.
  204. mediaSection.ssrcs.push({
  205. id: source,
  206. attribute: 'videoType',
  207. value: videoType
  208. });
  209. }
  210. }
  211. }
  212. }
  213. }