modified lib-jitsi-meet dev repo
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.

TPCUtils.js 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. import { getLogger } from 'jitsi-meet-logger';
  2. import transform from 'sdp-transform';
  3. import MediaDirection from '../../service/RTC/MediaDirection';
  4. import * as MediaType from '../../service/RTC/MediaType';
  5. import browser from '../browser';
  6. const logger = getLogger(__filename);
  7. const SIM_LAYER_1_RID = '1';
  8. const SIM_LAYER_2_RID = '2';
  9. const SIM_LAYER_3_RID = '3';
  10. export const SIM_LAYER_RIDS = [ SIM_LAYER_1_RID, SIM_LAYER_2_RID, SIM_LAYER_3_RID ];
  11. /**
  12. * Handles track related operations on TraceablePeerConnection when browser is
  13. * running in unified plan mode.
  14. */
  15. export class TPCUtils {
  16. /**
  17. * Creates a new instance for a given TraceablePeerConnection
  18. *
  19. * @param peerconnection - the tpc instance for which we have utility functions.
  20. * @param videoBitrates - the bitrates to be configured on the video senders for
  21. * different resolutions both in unicast and simulcast mode.
  22. */
  23. constructor(peerconnection, videoBitrates) {
  24. this.pc = peerconnection;
  25. this.videoBitrates = videoBitrates.VP8 || videoBitrates;
  26. /**
  27. * The startup configuration for the stream encodings that are applicable to
  28. * the video stream when a new sender is created on the peerconnection. The initial
  29. * config takes into account the differences in browser's simulcast implementation.
  30. *
  31. * Encoding parameters:
  32. * active - determine the on/off state of a particular encoding.
  33. * maxBitrate - max. bitrate value to be applied to that particular encoding
  34. * based on the encoding's resolution and config.js videoQuality settings if applicable.
  35. * rid - Rtp Stream ID that is configured for a particular simulcast stream.
  36. * scaleResolutionDownBy - the factor by which the encoding is scaled down from the
  37. * original resolution of the captured video.
  38. */
  39. this.localStreamEncodingsConfig = [
  40. {
  41. active: true,
  42. maxBitrate: browser.isFirefox() ? this.videoBitrates.high : this.videoBitrates.low,
  43. rid: SIM_LAYER_1_RID,
  44. scaleResolutionDownBy: browser.isFirefox() ? 1.0 : 4.0
  45. },
  46. {
  47. active: true,
  48. maxBitrate: this.videoBitrates.standard,
  49. rid: SIM_LAYER_2_RID,
  50. scaleResolutionDownBy: 2.0
  51. },
  52. {
  53. active: true,
  54. maxBitrate: browser.isFirefox() ? this.videoBitrates.low : this.videoBitrates.high,
  55. rid: SIM_LAYER_3_RID,
  56. scaleResolutionDownBy: browser.isFirefox() ? 4.0 : 1.0
  57. }
  58. ];
  59. }
  60. /**
  61. * Returns the transceiver associated with a given RTCRtpSender/RTCRtpReceiver.
  62. *
  63. * @param {string} mediaType - type of track associated with the transceiver 'audio' or 'video'.
  64. * @param {JitsiLocalTrack} localTrack - local track to be used for lookup.
  65. * @returns {RTCRtpTransceiver}
  66. */
  67. _findTransceiver(mediaType, localTrack = null) {
  68. let transceiver = null;
  69. // Check if the local track has been removed from the peerconnection already.
  70. const trackRemoved = !localTrack
  71. || (localTrack
  72. && browser.doesVideoMuteByStreamRemove()
  73. && localTrack.isVideoTrack()
  74. && localTrack.isMuted());
  75. if (trackRemoved) {
  76. transceiver = this.pc.peerconnection.getTransceivers()
  77. .find(t => t.receiver?.track?.kind === mediaType);
  78. } else if (localTrack) {
  79. transceiver = this.pc.peerconnection.getTransceivers()
  80. .find(t => t.sender?.track?.id === localTrack.getTrackId());
  81. }
  82. return transceiver;
  83. }
  84. /**
  85. * Obtains stream encodings that need to be configured on the given track based
  86. * on the track media type and the simulcast setting.
  87. * @param {JitsiLocalTrack} localTrack
  88. */
  89. _getStreamEncodings(localTrack) {
  90. if (this.pc.isSimulcastOn() && localTrack.isVideoTrack()) {
  91. return this.localStreamEncodingsConfig;
  92. }
  93. return localTrack.isVideoTrack()
  94. ? [ {
  95. active: true,
  96. maxBitrate: this.videoBitrates.high
  97. } ]
  98. : [ { active: true } ];
  99. }
  100. /**
  101. * Ensures that the ssrcs associated with a FID ssrc-group appear in the correct order, i.e.,
  102. * the primary ssrc first and the secondary rtx ssrc later. This is important for unified
  103. * plan since we have only one FID group per media description.
  104. * @param {Object} description the webRTC session description instance for the remote
  105. * description.
  106. * @private
  107. */
  108. ensureCorrectOrderOfSsrcs(description) {
  109. const parsedSdp = transform.parse(description.sdp);
  110. parsedSdp.media.forEach(mLine => {
  111. if (mLine.type === MediaType.AUDIO) {
  112. return;
  113. }
  114. if (!mLine.ssrcGroups || !mLine.ssrcGroups.length) {
  115. return;
  116. }
  117. let reorderedSsrcs = [];
  118. const ssrcs = new Set();
  119. mLine.ssrcGroups.map(group =>
  120. group.ssrcs
  121. .split(' ')
  122. .filter(Boolean)
  123. .forEach(ssrc => ssrcs.add(ssrc)),
  124. );
  125. ssrcs.forEach(ssrc => {
  126. const sources = mLine.ssrcs.filter(source => source.id.toString() === ssrc);
  127. reorderedSsrcs = reorderedSsrcs.concat(sources);
  128. });
  129. mLine.ssrcs = reorderedSsrcs;
  130. });
  131. return new RTCSessionDescription({
  132. type: description.type,
  133. sdp: transform.write(parsedSdp)
  134. });
  135. }
  136. /**
  137. * Takes in a *unified plan* offer and inserts the appropriate
  138. * parameters for adding simulcast receive support.
  139. * @param {Object} desc - A session description object
  140. * @param {String} desc.type - the type (offer/answer)
  141. * @param {String} desc.sdp - the sdp content
  142. *
  143. * @return {Object} A session description (same format as above) object
  144. * with its sdp field modified to advertise simulcast receive support
  145. */
  146. insertUnifiedPlanSimulcastReceive(desc) {
  147. // a=simulcast line is not needed on browsers where we SDP munging is used for enabling on simulcast.
  148. // Remove this check when the client switches to RID/MID based simulcast on all browsers.
  149. if (browser.usesSdpMungingForSimulcast()) {
  150. return desc;
  151. }
  152. const sdp = transform.parse(desc.sdp);
  153. const idx = sdp.media.findIndex(mline => mline.type === MediaType.VIDEO);
  154. if (sdp.media[idx].rids && (sdp.media[idx].simulcast_03 || sdp.media[idx].simulcast)) {
  155. // Make sure we don't have the simulcast recv line on video descriptions other than
  156. // the first video description.
  157. sdp.media.forEach((mline, i) => {
  158. if (mline.type === MediaType.VIDEO && i !== idx) {
  159. sdp.media[i].rids = undefined;
  160. sdp.media[i].simulcast = undefined;
  161. // eslint-disable-next-line camelcase
  162. sdp.media[i].simulcast_03 = undefined;
  163. }
  164. });
  165. return new RTCSessionDescription({
  166. type: desc.type,
  167. sdp: transform.write(sdp)
  168. });
  169. }
  170. // In order of highest to lowest spatial quality
  171. sdp.media[idx].rids = [
  172. {
  173. id: SIM_LAYER_1_RID,
  174. direction: 'recv'
  175. },
  176. {
  177. id: SIM_LAYER_2_RID,
  178. direction: 'recv'
  179. },
  180. {
  181. id: SIM_LAYER_3_RID,
  182. direction: 'recv'
  183. }
  184. ];
  185. // Firefox 72 has stopped parsing the legacy rid= parameters in simulcast attributes.
  186. // eslint-disable-next-line max-len
  187. // https://www.fxsitecompat.dev/en-CA/docs/2019/pt-and-rid-in-webrtc-simulcast-attributes-are-no-longer-supported/
  188. const simulcastLine = browser.isFirefox() && browser.isVersionGreaterThan(71)
  189. ? `recv ${SIM_LAYER_RIDS.join(';')}`
  190. : `recv rid=${SIM_LAYER_RIDS.join(';')}`;
  191. // eslint-disable-next-line camelcase
  192. sdp.media[idx].simulcast_03 = {
  193. value: simulcastLine
  194. };
  195. return new RTCSessionDescription({
  196. type: desc.type,
  197. sdp: transform.write(sdp)
  198. });
  199. }
  200. /**
  201. * Adds {@link JitsiLocalTrack} to the WebRTC peerconnection for the first time.
  202. * @param {JitsiLocalTrack} track - track to be added to the peerconnection.
  203. * @param {boolean} isInitiator - boolean that indicates if the endpoint is offerer in a p2p connection.
  204. * @returns {void}
  205. */
  206. addTrack(localTrack, isInitiator) {
  207. const track = localTrack.getTrack();
  208. if (isInitiator) {
  209. // Use pc.addTransceiver() for the initiator case when local tracks are getting added
  210. // to the peerconnection before a session-initiate is sent over to the peer.
  211. const transceiverInit = {
  212. direction: MediaDirection.SENDRECV,
  213. streams: [ localTrack.getOriginalStream() ],
  214. sendEncodings: []
  215. };
  216. if (!browser.isFirefox()) {
  217. transceiverInit.sendEncodings = this._getStreamEncodings(localTrack);
  218. }
  219. this.pc.peerconnection.addTransceiver(track, transceiverInit);
  220. } else {
  221. // Use pc.addTrack() for responder case so that we can re-use the m-lines that were created
  222. // when setRemoteDescription was called. pc.addTrack() automatically attaches to any existing
  223. // unused "recv-only" transceiver.
  224. this.pc.peerconnection.addTrack(track);
  225. }
  226. }
  227. /**
  228. * Adds a track on the RTCRtpSender as part of the unmute operation.
  229. * @param {JitsiLocalTrack} localTrack - track to be unmuted.
  230. * @returns {Promise<void>} - resolved when done.
  231. */
  232. addTrackUnmute(localTrack) {
  233. const mediaType = localTrack.getType();
  234. const track = localTrack.getTrack();
  235. const transceiver = this._findTransceiver(mediaType);
  236. if (!transceiver) {
  237. return Promise.reject(new Error(`RTCRtpTransceiver for ${mediaType} not found`));
  238. }
  239. logger.debug(`${this.pc} Adding ${localTrack}`);
  240. return transceiver.sender.replaceTrack(track);
  241. }
  242. /**
  243. * Obtains the current local video track's height constraints based on the
  244. * initial stream encodings configuration on the sender and the resolution
  245. * of the current local track added to the peerconnection.
  246. * @param {MediaStreamTrack} localTrack local video track
  247. * @returns {Array[number]} an array containing the resolution heights of
  248. * simulcast streams configured on the video sender.
  249. */
  250. getLocalStreamHeightConstraints(localTrack) {
  251. // React-native hasn't implemented MediaStreamTrack getSettings yet.
  252. if (browser.isReactNative()) {
  253. return null;
  254. }
  255. const localVideoHeightConstraints = [];
  256. // Firefox doesn't return the height of the desktop track, assume a min. height of 720.
  257. const { height = 720 } = localTrack.getSettings();
  258. for (const encoding of this.localStreamEncodingsConfig) {
  259. localVideoHeightConstraints.push(height / encoding.scaleResolutionDownBy);
  260. }
  261. return localVideoHeightConstraints;
  262. }
  263. /**
  264. * Removes the track from the RTCRtpSender as part of the mute operation.
  265. * @param {JitsiLocalTrack} localTrack - track to be removed.
  266. * @returns {Promise<void>} - resolved when done.
  267. */
  268. removeTrackMute(localTrack) {
  269. const mediaType = localTrack.getType();
  270. const transceiver = this._findTransceiver(mediaType, localTrack);
  271. if (!transceiver) {
  272. return Promise.reject(new Error(`RTCRtpTransceiver for ${mediaType} not found`));
  273. }
  274. logger.debug(`${this.pc} Removing ${localTrack}`);
  275. return transceiver.sender.replaceTrack(null);
  276. }
  277. /**
  278. * Replaces the existing track on a RTCRtpSender with the given track.
  279. * @param {JitsiLocalTrack} oldTrack - existing track on the sender that needs to be removed.
  280. * @param {JitsiLocalTrack} newTrack - new track that needs to be added to the sender.
  281. * @returns {Promise<void>} - resolved when done.
  282. */
  283. replaceTrack(oldTrack, newTrack) {
  284. if (oldTrack && newTrack) {
  285. const mediaType = newTrack.getType();
  286. const stream = newTrack.getOriginalStream();
  287. // Ignore cases when the track is replaced while the device is in a muted state,like
  288. // replacing camera when video muted or replacing mic when audio muted. These JitsiLocalTracks
  289. // do not have a mediastream attached. Replace track will be called again when the device is
  290. // unmuted and the track will be replaced on the peerconnection then.
  291. if (!stream) {
  292. this.pc.localTracks.delete(oldTrack.rtcId);
  293. this.pc.localTracks.set(newTrack.rtcId, newTrack);
  294. return Promise.resolve();
  295. }
  296. const transceiver = this._findTransceiver(mediaType, oldTrack);
  297. const track = newTrack.getTrack();
  298. if (!transceiver) {
  299. return Promise.reject(new Error('replace track failed'));
  300. }
  301. logger.debug(`${this.pc} Replacing ${oldTrack} with ${newTrack}`);
  302. return transceiver.sender.replaceTrack(track)
  303. .then(() => {
  304. const ssrc = this.pc.localSSRCs.get(oldTrack.rtcId);
  305. this.pc.localTracks.delete(oldTrack.rtcId);
  306. this.pc.localSSRCs.delete(oldTrack.rtcId);
  307. this.pc._addedStreams = this.pc._addedStreams.filter(s => s !== stream);
  308. this.pc.localTracks.set(newTrack.rtcId, newTrack);
  309. this.pc._addedStreams.push(stream);
  310. this.pc.localSSRCs.set(newTrack.rtcId, ssrc);
  311. });
  312. } else if (oldTrack && !newTrack) {
  313. return this.removeTrackMute(oldTrack)
  314. .then(() => {
  315. const mediaType = oldTrack.getType();
  316. const transceiver = this._findTransceiver(mediaType);
  317. // Change the direction on the transceiver to 'recvonly' so that a 'removetrack'
  318. // is fired on the associated media stream on the remote peer.
  319. if (transceiver) {
  320. transceiver.direction = MediaDirection.RECVONLY;
  321. }
  322. // Remove the old track from the list of local tracks.
  323. this.pc.localTracks.delete(oldTrack.rtcId);
  324. this.pc.localSSRCs.delete(oldTrack.rtcId);
  325. });
  326. } else if (newTrack && !oldTrack) {
  327. return this.addTrackUnmute(newTrack)
  328. .then(() => {
  329. const mediaType = newTrack.getType();
  330. const transceiver = this._findTransceiver(mediaType, newTrack);
  331. // Change the direction on the transceiver back to 'sendrecv' so that a 'track'
  332. // event is fired on the remote peer.
  333. if (transceiver) {
  334. transceiver.direction = MediaDirection.SENDRECV;
  335. }
  336. // Avoid configuring the encodings on Chromium/Safari until simulcast is configured
  337. // for the newly added track using SDP munging which happens during the renegotiation.
  338. const promise = browser.usesSdpMungingForSimulcast()
  339. ? Promise.resolve()
  340. : this.setEncodings(newTrack);
  341. return promise
  342. .then(() => {
  343. // Add the new track to the list of local tracks.
  344. this.pc.localTracks.set(newTrack.rtcId, newTrack);
  345. });
  346. });
  347. }
  348. logger.info(`${this.pc} TPCUtils.replaceTrack called with no new track and no old track`);
  349. return Promise.resolve();
  350. }
  351. /**
  352. * Enables/disables audio transmission on the peer connection. When
  353. * disabled the audio transceiver direction will be set to 'inactive'
  354. * which means that no data will be sent nor accepted, but
  355. * the connection should be kept alive.
  356. * @param {boolean} active - true to enable audio media transmission or
  357. * false to disable.
  358. * @returns {void}
  359. */
  360. setAudioTransferActive(active) {
  361. this.setMediaTransferActive(MediaType.AUDIO, active);
  362. }
  363. /**
  364. * Set the simulcast stream encoding properties on the RTCRtpSender.
  365. * @param {JitsiLocalTrack} track - the current track in use for which
  366. * the encodings are to be set.
  367. * @returns {Promise<void>} - resolved when done.
  368. */
  369. setEncodings(track) {
  370. const mediaType = track.getType();
  371. const transceiver = this._findTransceiver(mediaType, track);
  372. const parameters = transceiver?.sender?.getParameters();
  373. // Resolve if the encodings are not available yet. This happens immediately after the track is added to the
  374. // peerconnection on chrome in unified-plan. It is ok to ignore and not report the error here since the
  375. // action that triggers 'addTrack' (like unmute) will also configure the encodings and set bitrates after that.
  376. if (!parameters?.encodings?.length) {
  377. return Promise.resolve();
  378. }
  379. parameters.encodings = this._getStreamEncodings(track);
  380. return transceiver.sender.setParameters(parameters);
  381. }
  382. /**
  383. * Enables/disables media transmission on the peerconnection by changing the direction
  384. * on the transceiver for the specified media type.
  385. * @param {String} mediaType - 'audio' or 'video'
  386. * @param {boolean} active - true to enable media transmission or false
  387. * to disable.
  388. * @returns {void}
  389. */
  390. setMediaTransferActive(mediaType, active) {
  391. const transceivers = this.pc.peerconnection.getTransceivers()
  392. .filter(t => t.receiver && t.receiver.track && t.receiver.track.kind === mediaType);
  393. const localTracks = this.pc.getLocalTracks(mediaType);
  394. logger.info(`${this.pc} ${active ? 'Enabling' : 'Suspending'} ${mediaType} media transfer.`);
  395. transceivers.forEach((transceiver, idx) => {
  396. if (active) {
  397. // The first transceiver is for the local track and only this one can be set to 'sendrecv'
  398. if (idx === 0 && localTracks.length) {
  399. transceiver.direction = MediaDirection.SENDRECV;
  400. } else {
  401. transceiver.direction = MediaDirection.RECVONLY;
  402. }
  403. } else {
  404. transceiver.direction = MediaDirection.INACTIVE;
  405. }
  406. });
  407. }
  408. /**
  409. * Enables/disables video media transmission on the peer connection. When
  410. * disabled the SDP video media direction in the local SDP will be adjusted to
  411. * 'inactive' which means that no data will be sent nor accepted, but
  412. * the connection should be kept alive.
  413. * @param {boolean} active - true to enable video media transmission or
  414. * false to disable.
  415. * @returns {void}
  416. */
  417. setVideoTransferActive(active) {
  418. this.setMediaTransferActive(MediaType.VIDEO, active);
  419. }
  420. /**
  421. * Ensures that the resolution of the stream encodings are consistent with the values
  422. * that were configured on the RTCRtpSender when the source was added to the peerconnection.
  423. * This should prevent us from overriding the default values if the browser returns
  424. * erroneous values when RTCRtpSender.getParameters is used for getting the encodings info.
  425. * @param {Object} parameters - the RTCRtpEncodingParameters obtained from the browser.
  426. * @returns {void}
  427. */
  428. updateEncodingsResolution(parameters) {
  429. if (!(browser.isWebKitBased() && parameters.encodings && Array.isArray(parameters.encodings))) {
  430. return;
  431. }
  432. const allEqualEncodings
  433. = encodings => encodings.every(encoding => typeof encoding.scaleResolutionDownBy !== 'undefined'
  434. && encoding.scaleResolutionDownBy === encodings[0].scaleResolutionDownBy);
  435. // Implement the workaround only when all the encodings report the same resolution.
  436. if (allEqualEncodings(parameters.encodings)) {
  437. parameters.encodings.forEach((encoding, idx) => {
  438. encoding.scaleResolutionDownBy = this.localStreamEncodingsConfig[idx].scaleResolutionDownBy;
  439. });
  440. }
  441. }
  442. }