您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

LocalSdpMunger.js 14KB

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