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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /* global __filename */
  2. import { getLogger } from 'jitsi-meet-logger';
  3. import MediaDirection from '../../service/RTC/MediaDirection';
  4. import * as MediaType from '../../service/RTC/MediaType';
  5. import { SdpTransformWrap } from './SdpTransformUtil';
  6. const logger = getLogger(__filename);
  7. /**
  8. * Fakes local SDP exposed to {@link JingleSessionPC} through the local
  9. * description getter. Modifies the SDP, so that it will contain muted local
  10. * video tracks description, even though their underlying {MediaStreamTrack}s
  11. * are no longer in the WebRTC peerconnection. That prevents from SSRC updates
  12. * being sent to Jicofo/remote peer and prevents sRD/sLD cycle on the remote
  13. * side.
  14. */
  15. export default class LocalSdpMunger {
  16. /**
  17. * Creates new <tt>LocalSdpMunger</tt> instance.
  18. *
  19. * @param {TraceablePeerConnection} tpc
  20. * @param {string} localEndpointId - The endpoint id of the local user.
  21. */
  22. constructor(tpc, localEndpointId) {
  23. this.tpc = tpc;
  24. this.localEndpointId = localEndpointId;
  25. }
  26. /**
  27. * Makes sure that muted local video tracks associated with the parent
  28. * {@link TraceablePeerConnection} are described in the local SDP. It's done
  29. * in order to prevent from sending 'source-remove'/'source-add' Jingle
  30. * notifications when local video track is muted (<tt>MediaStream</tt> is
  31. * removed from the peerconnection).
  32. *
  33. * NOTE 1 video track is assumed
  34. *
  35. * @param {SdpTransformWrap} transformer the transformer instance which will
  36. * be used to process the SDP.
  37. * @return {boolean} <tt>true</tt> if there were any modifications to
  38. * the SDP wrapped by <tt>transformer</tt>.
  39. * @private
  40. */
  41. _addMutedLocalVideoTracksToSDP(transformer) {
  42. // Go over each video tracks and check if the SDP has to be changed
  43. const localVideos = this.tpc.getLocalTracks(MediaType.VIDEO);
  44. if (!localVideos.length) {
  45. return false;
  46. } else if (localVideos.length !== 1) {
  47. logger.error(
  48. `${this.tpc} there is more than 1 video track ! `
  49. + 'Strange things may happen !', localVideos);
  50. }
  51. const videoMLine = transformer.selectMedia('video');
  52. if (!videoMLine) {
  53. logger.debug(
  54. `${this.tpc} unable to hack local video track SDP`
  55. + '- no "video" media');
  56. return false;
  57. }
  58. let modified = false;
  59. for (const videoTrack of localVideos) {
  60. const muted = videoTrack.isMuted();
  61. const mediaStream = videoTrack.getOriginalStream();
  62. // During the mute/unmute operation there are periods of time when
  63. // the track's underlying MediaStream is not added yet to
  64. // the PeerConnection. The SDP needs to be munged in such case.
  65. const isInPeerConnection
  66. = mediaStream && this.tpc.isMediaStreamInPc(mediaStream);
  67. const shouldFakeSdp = muted || !isInPeerConnection;
  68. if (!shouldFakeSdp) {
  69. continue; // eslint-disable-line no-continue
  70. }
  71. // Inject removed SSRCs
  72. const requiredSSRCs
  73. = this.tpc.isSimulcastOn()
  74. ? this.tpc.simulcast.ssrcCache
  75. : [ this.tpc.sdpConsistency.cachedPrimarySsrc ];
  76. if (!requiredSSRCs.length) {
  77. logger.error(`No SSRCs stored for: ${videoTrack} in ${this.tpc}`);
  78. continue; // eslint-disable-line no-continue
  79. }
  80. modified = true;
  81. // We need to fake sendrecv.
  82. // NOTE the SDP produced here goes only to Jicofo and is never set
  83. // as localDescription. That's why
  84. // TraceablePeerConnection.mediaTransferActive is ignored here.
  85. videoMLine.direction = MediaDirection.SENDRECV;
  86. // Check if the recvonly has MSID
  87. const primarySSRC = requiredSSRCs[0];
  88. // FIXME The cname could come from the stream, but may turn out to
  89. // be too complex. It is fine to come up with any value, as long as
  90. // we only care about the actual SSRC values when deciding whether
  91. // or not an update should be sent.
  92. const primaryCname = `injected-${primarySSRC}`;
  93. for (const ssrcNum of requiredSSRCs) {
  94. // Remove old attributes
  95. videoMLine.removeSSRC(ssrcNum);
  96. // Inject
  97. videoMLine.addSSRCAttribute({
  98. id: ssrcNum,
  99. attribute: 'cname',
  100. value: primaryCname
  101. });
  102. videoMLine.addSSRCAttribute({
  103. id: ssrcNum,
  104. attribute: 'msid',
  105. value: videoTrack.storedMSID
  106. });
  107. }
  108. if (requiredSSRCs.length > 1) {
  109. const group = {
  110. ssrcs: requiredSSRCs.join(' '),
  111. semantics: 'SIM'
  112. };
  113. if (!videoMLine.findGroup(group.semantics, group.ssrcs)) {
  114. // Inject the group
  115. videoMLine.addSSRCGroup(group);
  116. }
  117. }
  118. // Insert RTX
  119. // FIXME in P2P RTX is used by Chrome regardless of config option
  120. // status. Because of that 'source-remove'/'source-add'
  121. // notifications are still sent to remove/add RTX SSRC and FID group
  122. if (!this.tpc.options.disableRtx) {
  123. this.tpc.rtxModifier.modifyRtxSsrcs2(videoMLine);
  124. }
  125. }
  126. return modified;
  127. }
  128. /**
  129. * Returns a string that can be set as the MSID attribute for a source.
  130. *
  131. * @param {string} mediaType - Media type of the source.
  132. * @param {string} trackId - Id of the MediaStreamTrack associated with the source.
  133. * @param {string} streamId - Id of the MediaStream associated with the source.
  134. * @returns {string|null}
  135. */
  136. _generateMsidAttribute(mediaType, trackId, streamId = null) {
  137. if (!(mediaType && trackId)) {
  138. logger.warn(`Unable to munge local MSID - track id=${trackId} or media type=${mediaType} is missing`);
  139. return null;
  140. }
  141. const pcId = this.tpc.id;
  142. // Handle a case on Firefox when the browser doesn't produce a 'a:ssrc' line with the 'msid' attribute or has
  143. // '-' for the stream id part of the msid line. Jicofo needs an unique identifier to be associated with a ssrc
  144. // and uses the msid for that.
  145. if (streamId === '-' || !streamId) {
  146. return `${this.localEndpointId}-${mediaType}-${pcId} ${trackId}-${pcId}`;
  147. }
  148. return `${streamId}-${pcId} ${trackId}-${pcId}`;
  149. }
  150. /**
  151. * Modifies 'cname', 'msid', 'label' and 'mslabel' by appending
  152. * the id of {@link LocalSdpMunger#tpc} at the end, preceding by a dash
  153. * sign.
  154. *
  155. * @param {MLineWrap} mediaSection - The media part (audio or video) of the
  156. * session description which will be modified in place.
  157. * @returns {void}
  158. * @private
  159. */
  160. _transformMediaIdentifiers(mediaSection) {
  161. const pcId = this.tpc.id;
  162. for (const ssrcLine of mediaSection.ssrcs) {
  163. switch (ssrcLine.attribute) {
  164. case 'cname':
  165. case 'label':
  166. case 'mslabel':
  167. ssrcLine.value = ssrcLine.value && `${ssrcLine.value}-${pcId}`;
  168. break;
  169. case 'msid': {
  170. if (ssrcLine.value) {
  171. const streamAndTrackIDs = ssrcLine.value.split(' ');
  172. if (streamAndTrackIDs.length === 2) {
  173. ssrcLine.value
  174. = this._generateMsidAttribute(
  175. mediaSection.mLine?.type,
  176. streamAndTrackIDs[1],
  177. streamAndTrackIDs[0]);
  178. } else {
  179. logger.warn(`Unable to munge local MSID - weird format detected: ${ssrcLine.value}`);
  180. }
  181. }
  182. break;
  183. }
  184. }
  185. }
  186. // If the msid attribute is missing, then remove the ssrc from the transformed description so that a
  187. // source-remove is signaled to Jicofo. This happens when the direction of the transceiver (or m-line)
  188. // is set to 'inactive' or 'recvonly' on Firefox, Chrome (unified) and Safari.
  189. const msid = mediaSection.ssrcs.find(s => s.attribute === 'msid');
  190. if (!this.tpc.isP2P
  191. && (!msid
  192. || mediaSection.mLine?.direction === MediaDirection.RECVONLY
  193. || mediaSection.mLine?.direction === MediaDirection.INACTIVE)) {
  194. mediaSection.ssrcs = undefined;
  195. mediaSection.ssrcGroups = undefined;
  196. // Add the msid attribute if it is missing for p2p sources. Firefox doesn't produce a a=ssrc line
  197. // with msid attribute.
  198. } else if (this.tpc.isP2P && mediaSection.mLine?.direction === MediaDirection.SENDRECV) {
  199. const msidLine = mediaSection.mLine?.msid;
  200. const trackId = msidLine && msidLine.split(' ')[1];
  201. const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
  202. for (const source of sources) {
  203. const msidExists = mediaSection.ssrcs
  204. .find(ssrc => ssrc.id === source && ssrc.attribute === 'msid');
  205. if (!msidExists) {
  206. const generatedMsid = this._generateMsidAttribute(mediaSection.mLine?.type, trackId);
  207. mediaSection.ssrcs.push({
  208. id: source,
  209. attribute: 'msid',
  210. value: generatedMsid
  211. });
  212. }
  213. }
  214. }
  215. }
  216. /**
  217. * Maybe modifies local description to fake local video tracks SDP when
  218. * those are muted.
  219. *
  220. * @param {object} desc the WebRTC SDP object instance for the local
  221. * description.
  222. * @returns {RTCSessionDescription}
  223. */
  224. maybeAddMutedLocalVideoTracksToSDP(desc) {
  225. if (!desc) {
  226. throw new Error('No local description passed in.');
  227. }
  228. const transformer = new SdpTransformWrap(desc.sdp);
  229. if (this._addMutedLocalVideoTracksToSDP(transformer)) {
  230. return new RTCSessionDescription({
  231. type: desc.type,
  232. sdp: transformer.toRawSDP()
  233. });
  234. }
  235. return desc;
  236. }
  237. /**
  238. * This transformation will make sure that stream identifiers are unique
  239. * across all of the local PeerConnections even if the same stream is used
  240. * by multiple instances at the same time.
  241. * Each PeerConnection assigns different SSRCs to the same local
  242. * MediaStream, but the MSID remains the same as it's used to identify
  243. * the stream by the WebRTC backend. The transformation will append
  244. * {@link TraceablePeerConnection#id} at the end of each stream's identifier
  245. * ("cname", "msid", "label" and "mslabel").
  246. *
  247. * @param {RTCSessionDescription} sessionDesc - The local session
  248. * description (this instance remains unchanged).
  249. * @return {RTCSessionDescription} - Transformed local session description
  250. * (a modified copy of the one given as the input).
  251. */
  252. transformStreamIdentifiers(sessionDesc) {
  253. // FIXME similar check is probably duplicated in all other transformers
  254. if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) {
  255. return sessionDesc;
  256. }
  257. const transformer = new SdpTransformWrap(sessionDesc.sdp);
  258. const audioMLine = transformer.selectMedia('audio');
  259. if (audioMLine) {
  260. this._transformMediaIdentifiers(audioMLine);
  261. }
  262. const videoMLine = transformer.selectMedia('video');
  263. if (videoMLine) {
  264. this._transformMediaIdentifiers(videoMLine);
  265. }
  266. return new RTCSessionDescription({
  267. type: sessionDesc.type,
  268. sdp: transformer.toRawSDP()
  269. });
  270. }
  271. }