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

TPCUtils.js 20KB

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