Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

LocalSdpMunger.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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 { VideoType } from '../../service/RTC/VideoType';
  6. import browser from '../browser';
  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. this.audioSourcesToMsidMap = new Map();
  29. this.videoSourcesToMsidMap = new Map();
  30. }
  31. /**
  32. * Makes sure that muted local video tracks associated with the parent
  33. * {@link TraceablePeerConnection} are described in the local SDP. It's done
  34. * in order to prevent from sending 'source-remove'/'source-add' Jingle
  35. * notifications when local video track is muted (<tt>MediaStream</tt> is
  36. * removed from the peerconnection).
  37. *
  38. * NOTE 1 video track is assumed
  39. *
  40. * @param {SdpTransformWrap} transformer the transformer instance which will
  41. * be used to process the SDP.
  42. * @return {boolean} <tt>true</tt> if there were any modifications to
  43. * the SDP wrapped by <tt>transformer</tt>.
  44. * @private
  45. */
  46. _addMutedLocalVideoTracksToSDP(transformer) {
  47. // Go over each video tracks and check if the SDP has to be changed
  48. const localVideos = this.tpc.getLocalTracks(MediaType.VIDEO);
  49. if (!localVideos.length) {
  50. return false;
  51. } else if (localVideos.length !== 1) {
  52. logger.error(
  53. `${this.tpc} there is more than 1 video track ! `
  54. + 'Strange things may happen !', localVideos);
  55. }
  56. const videoMLine = transformer.selectMedia(MediaType.VIDEO)?.[0];
  57. if (!videoMLine) {
  58. logger.debug(
  59. `${this.tpc} unable to hack local video track SDP`
  60. + '- no "video" media');
  61. return false;
  62. }
  63. let modified = false;
  64. for (const videoTrack of localVideos) {
  65. const muted = videoTrack.isMuted();
  66. const mediaStream = videoTrack.getOriginalStream();
  67. const isCamera = videoTrack.videoType === VideoType.CAMERA;
  68. // During the mute/unmute operation there are periods of time when
  69. // the track's underlying MediaStream is not added yet to
  70. // the PeerConnection. The SDP needs to be munged in such case.
  71. const isInPeerConnection
  72. = mediaStream && this.tpc.isMediaStreamInPc(mediaStream);
  73. const shouldFakeSdp = isCamera && (muted || !isInPeerConnection);
  74. if (!shouldFakeSdp) {
  75. continue; // eslint-disable-line no-continue
  76. }
  77. // Inject removed SSRCs
  78. const requiredSSRCs
  79. = this.tpc.isSimulcastOn()
  80. ? this.tpc.simulcast.ssrcCache
  81. : [ this.tpc.sdpConsistency.cachedPrimarySsrc ];
  82. if (!requiredSSRCs.length) {
  83. logger.error(`No SSRCs stored for: ${videoTrack} in ${this.tpc}`);
  84. continue; // eslint-disable-line no-continue
  85. }
  86. modified = true;
  87. // We need to fake sendrecv.
  88. // NOTE the SDP produced here goes only to Jicofo and is never set
  89. // as localDescription. That's why
  90. // TraceablePeerConnection.mediaTransferActive is ignored here.
  91. videoMLine.direction = MediaDirection.SENDRECV;
  92. // Check if the recvonly has MSID
  93. const primarySSRC = requiredSSRCs[0];
  94. // FIXME The cname could come from the stream, but may turn out to
  95. // be too complex. It is fine to come up with any value, as long as
  96. // we only care about the actual SSRC values when deciding whether
  97. // or not an update should be sent.
  98. const primaryCname = `injected-${primarySSRC}`;
  99. for (const ssrcNum of requiredSSRCs) {
  100. // Remove old attributes
  101. videoMLine.removeSSRC(ssrcNum);
  102. // Inject
  103. videoMLine.addSSRCAttribute({
  104. id: ssrcNum,
  105. attribute: 'cname',
  106. value: primaryCname
  107. });
  108. videoMLine.addSSRCAttribute({
  109. id: ssrcNum,
  110. attribute: 'msid',
  111. value: videoTrack.storedMSID
  112. });
  113. }
  114. if (requiredSSRCs.length > 1) {
  115. const group = {
  116. ssrcs: requiredSSRCs.join(' '),
  117. semantics: 'SIM'
  118. };
  119. if (!videoMLine.findGroup(group.semantics, group.ssrcs)) {
  120. // Inject the group
  121. videoMLine.addSSRCGroup(group);
  122. }
  123. }
  124. // Insert RTX
  125. // FIXME in P2P RTX is used by Chrome regardless of config option
  126. // status. Because of that 'source-remove'/'source-add'
  127. // notifications are still sent to remove/add RTX SSRC and FID group
  128. if (!this.tpc.options.disableRtx) {
  129. this.tpc.rtxModifier.modifyRtxSsrcs2(videoMLine);
  130. }
  131. }
  132. return modified;
  133. }
  134. /**
  135. * Returns a string that can be set as the MSID attribute for a source.
  136. *
  137. * @param {string} mediaType - Media type of the source.
  138. * @param {string} trackId - Id of the MediaStreamTrack associated with the source.
  139. * @param {string} streamId - Id of the MediaStream associated with the source.
  140. * @returns {string|null}
  141. */
  142. _generateMsidAttribute(mediaType, trackId, streamId = null) {
  143. if (!(mediaType && trackId)) {
  144. logger.error(`Unable to munge local MSID - track id=${trackId} or media type=${mediaType} is missing`);
  145. return null;
  146. }
  147. const pcId = this.tpc.id;
  148. // Handle a case on Firefox when the browser doesn't produce a 'a:ssrc' line with the 'msid' attribute or has
  149. // '-' for the stream id part of the msid line. Jicofo needs an unique identifier to be associated with a ssrc
  150. // and uses the msid for that.
  151. if (streamId === '-' || !streamId) {
  152. return `${this.localEndpointId}-${mediaType}-${pcId} ${trackId}-${pcId}`;
  153. }
  154. return `${streamId}-${pcId} ${trackId}-${pcId}`;
  155. }
  156. /**
  157. * Modifies 'cname', 'msid', 'label' and 'mslabel' by appending the id of {@link LocalSdpMunger#tpc} at the end,
  158. * preceding by a dash sign.
  159. *
  160. * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
  161. * modified in place.
  162. * @returns {void}
  163. * @private
  164. */
  165. _transformMediaIdentifiers(mediaSection) {
  166. const mediaType = mediaSection.mLine?.type;
  167. const pcId = this.tpc.id;
  168. for (const ssrcLine of mediaSection.ssrcs) {
  169. switch (ssrcLine.attribute) {
  170. case 'cname':
  171. case 'label':
  172. case 'mslabel':
  173. ssrcLine.value = ssrcLine.value && `${ssrcLine.value}-${pcId}`;
  174. break;
  175. case 'msid': {
  176. if (ssrcLine.value) {
  177. const streamAndTrackIDs = ssrcLine.value.split(' ');
  178. let streamId = streamAndTrackIDs[0];
  179. const trackId = streamAndTrackIDs[1];
  180. if (FeatureFlags.isSourceNameSignalingEnabled()) {
  181. // Always overwrite streamId since we want the msid to be in this format even if the browser
  182. // generates one (in p2p mode).
  183. streamId = `${this.localEndpointId}-${mediaType}`;
  184. // eslint-disable-next-line max-depth
  185. if (mediaType === MediaType.VIDEO) {
  186. // eslint-disable-next-line max-depth
  187. if (!this.videoSourcesToMsidMap.has(trackId)) {
  188. streamId = `${streamId}-${this.videoSourcesToMsidMap.size}`;
  189. this.videoSourcesToMsidMap.set(trackId, streamId);
  190. }
  191. } else if (!this.audioSourcesToMsidMap.has(trackId)) {
  192. streamId = `${streamId}-${this.audioSourcesToMsidMap.size}`;
  193. this.audioSourcesToMsidMap.set(trackId, streamId);
  194. }
  195. streamId = mediaType === MediaType.VIDEO
  196. ? this.videoSourcesToMsidMap.get(trackId)
  197. : this.audioSourcesToMsidMap.get(trackId);
  198. }
  199. ssrcLine.value = this._generateMsidAttribute(mediaType, trackId, streamId);
  200. } else {
  201. logger.warn(`Unable to munge local MSID - weird format detected: ${ssrcLine.value}`);
  202. }
  203. break;
  204. }
  205. }
  206. }
  207. // Additional transformations related to MSID are applicable to Unified-plan implementation only.
  208. if (!this.tpc.usesUnifiedPlan()) {
  209. return;
  210. }
  211. const mediaDirection = mediaSection.mLine?.direction;
  212. // On FF when the user has started muted create answer will generate a recv only SSRC. We don't want to signal
  213. // this SSRC in order to reduce the load of the xmpp server for large calls. Therefore the SSRC needs to be
  214. // removed from the SDP.
  215. //
  216. // For all other use cases (when the user has had media but then the user has stopped it) we want to keep the
  217. // receive only SSRCs in the SDP. Otherwise source-remove will be triggered and the next time the user add a
  218. // track we will reuse the SSRCs and send source-add with the same SSRCs. This is problematic because of issues
  219. // on Chrome and FF (https://bugzilla.mozilla.org/show_bug.cgi?id=1768729) when removing and then adding the
  220. // same SSRC in the remote sdp the remote track is not rendered.
  221. if (browser.isFirefox()
  222. && (mediaDirection === MediaDirection.RECVONLY || mediaDirection === MediaDirection.INACTIVE)
  223. && (
  224. (mediaType === MediaType.VIDEO && !this.tpc._hasHadVideoTrack)
  225. || (mediaType === MediaType.AUDIO && !this.tpc._hasHadAudioTrack)
  226. )
  227. ) {
  228. mediaSection.ssrcs = undefined;
  229. mediaSection.ssrcGroups = undefined;
  230. }
  231. const msidLine = mediaSection.mLine?.msid;
  232. const trackId = msidLine && msidLine.split(' ')[1];
  233. const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
  234. for (const source of sources) {
  235. const msidExists = mediaSection.ssrcs
  236. .find(ssrc => ssrc.id === source && ssrc.attribute === 'msid');
  237. if (!msidExists && trackId) {
  238. const generatedMsid = this._generateMsidAttribute(mediaType, trackId);
  239. mediaSection.ssrcs.push({
  240. id: source,
  241. attribute: 'msid',
  242. value: generatedMsid
  243. });
  244. }
  245. }
  246. }
  247. /**
  248. * Maybe modifies local description to fake local video tracks SDP when
  249. * those are muted.
  250. *
  251. * @param {object} desc the WebRTC SDP object instance for the local
  252. * description.
  253. * @returns {RTCSessionDescription}
  254. */
  255. maybeAddMutedLocalVideoTracksToSDP(desc) {
  256. if (!desc) {
  257. throw new Error('No local description passed in.');
  258. }
  259. const transformer = new SdpTransformWrap(desc.sdp);
  260. if (this._addMutedLocalVideoTracksToSDP(transformer)) {
  261. return new RTCSessionDescription({
  262. type: desc.type,
  263. sdp: transformer.toRawSDP()
  264. });
  265. }
  266. return desc;
  267. }
  268. /**
  269. * This transformation will make sure that stream identifiers are unique
  270. * across all of the local PeerConnections even if the same stream is used
  271. * by multiple instances at the same time.
  272. * Each PeerConnection assigns different SSRCs to the same local
  273. * MediaStream, but the MSID remains the same as it's used to identify
  274. * the stream by the WebRTC backend. The transformation will append
  275. * {@link TraceablePeerConnection#id} at the end of each stream's identifier
  276. * ("cname", "msid", "label" and "mslabel").
  277. *
  278. * @param {RTCSessionDescription} sessionDesc - The local session
  279. * description (this instance remains unchanged).
  280. * @return {RTCSessionDescription} - Transformed local session description
  281. * (a modified copy of the one given as the input).
  282. */
  283. transformStreamIdentifiers(sessionDesc) {
  284. // FIXME similar check is probably duplicated in all other transformers
  285. if (!sessionDesc || !sessionDesc.sdp || !sessionDesc.type) {
  286. return sessionDesc;
  287. }
  288. const transformer = new SdpTransformWrap(sessionDesc.sdp);
  289. const audioMLine = transformer.selectMedia(MediaType.AUDIO)?.[0];
  290. if (audioMLine) {
  291. this._transformMediaIdentifiers(audioMLine);
  292. this._injectSourceNames(audioMLine);
  293. }
  294. const videoMlines = transformer.selectMedia(MediaType.VIDEO);
  295. if (!FeatureFlags.isMultiStreamSupportEnabled()) {
  296. videoMlines.splice(1);
  297. }
  298. for (const videoMLine of videoMlines) {
  299. this._transformMediaIdentifiers(videoMLine);
  300. this._injectSourceNames(videoMLine);
  301. }
  302. // Plan-b clients generate new SSRCs and trackIds whenever tracks are removed and added back to the
  303. // peerconnection, therefore local track based map for msids needs to be reset after every transformation.
  304. if (FeatureFlags.isSourceNameSignalingEnabled() && !this.tpc._usesUnifiedPlan) {
  305. this.audioSourcesToMsidMap.clear();
  306. this.videoSourcesToMsidMap.clear();
  307. }
  308. return new RTCSessionDescription({
  309. type: sessionDesc.type,
  310. sdp: transformer.toRawSDP()
  311. });
  312. }
  313. /**
  314. * Injects source names. Source names are need to for multiple streams per endpoint support. The final plan is to
  315. * use the "mid" attribute for source names, but because the SDP to Jingle conversion still operates in the Plan-B
  316. * semantics (one source name per media), a custom "name" attribute is injected into SSRC lines..
  317. *
  318. * @param {MLineWrap} mediaSection - The media part (audio or video) of the session description which will be
  319. * modified in place.
  320. * @returns {void}
  321. * @private
  322. */
  323. _injectSourceNames(mediaSection) {
  324. if (!FeatureFlags.isSourceNameSignalingEnabled()) {
  325. return;
  326. }
  327. const sources = [ ...new Set(mediaSection.mLine?.ssrcs?.map(s => s.id)) ];
  328. const mediaType = mediaSection.mLine?.type;
  329. if (!mediaType) {
  330. throw new Error('_transformMediaIdentifiers - no media type in mediaSection');
  331. }
  332. for (const source of sources) {
  333. const nameExists = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'name');
  334. const msid = mediaSection.ssrcs.find(ssrc => ssrc.id === source && ssrc.attribute === 'msid')?.value;
  335. let trackIndex;
  336. if (msid) {
  337. const streamId = msid.split(' ')[0];
  338. trackIndex = streamId.split('-')[2];
  339. }
  340. const sourceName = getSourceNameForJitsiTrack(this.localEndpointId, mediaType, trackIndex);
  341. if (!nameExists) {
  342. // Inject source names as a=ssrc:3124985624 name:endpointA-v0
  343. mediaSection.ssrcs.push({
  344. id: source,
  345. attribute: 'name',
  346. value: sourceName
  347. });
  348. }
  349. if (mediaType === MediaType.VIDEO) {
  350. const videoType = this.tpc.getLocalVideoTracks().find(track => track.getSourceName() === sourceName)
  351. ?.getVideoType();
  352. if (videoType) {
  353. // Inject videoType as a=ssrc:1234 videoType:desktop.
  354. mediaSection.ssrcs.push({
  355. id: source,
  356. attribute: 'videoType',
  357. value: videoType
  358. });
  359. }
  360. }
  361. }
  362. }
  363. }