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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. import { getLogger } from '@jitsi/logger';
  2. import transform from 'sdp-transform';
  3. import { MediaDirection } from '../../service/RTC/MediaDirection';
  4. import { MediaType } from '../../service/RTC/MediaType';
  5. import { VideoType } from '../../service/RTC/VideoType';
  6. import browser from '../browser';
  7. import FeatureFlags from '../flags/FeatureFlags';
  8. const logger = getLogger(__filename);
  9. const DESKTOP_SHARE_RATE = 500000;
  10. const LD_BITRATE = 200000;
  11. const SD_BITRATE = 700000;
  12. const SIM_LAYER_1_RID = '1';
  13. const SIM_LAYER_2_RID = '2';
  14. const SIM_LAYER_3_RID = '3';
  15. export const HD_BITRATE = 2500000;
  16. export const HD_SCALE_FACTOR = 1;
  17. export const LD_SCALE_FACTOR = 4;
  18. export const SD_SCALE_FACTOR = 2;
  19. export const SIM_LAYER_RIDS = [ SIM_LAYER_1_RID, SIM_LAYER_2_RID, SIM_LAYER_3_RID ];
  20. /**
  21. * Handles track related operations on TraceablePeerConnection when browser is
  22. * running in unified plan mode.
  23. */
  24. export class TPCUtils {
  25. /**
  26. * Creates a new instance for a given TraceablePeerConnection
  27. *
  28. * @param peerconnection - the tpc instance for which we have utility functions.
  29. */
  30. constructor(peerconnection) {
  31. this.pc = peerconnection;
  32. const bitrateSettings = this.pc.options?.videoQuality?.maxBitratesVideo;
  33. const standardBitrates = {
  34. low: LD_BITRATE,
  35. standard: SD_BITRATE,
  36. high: HD_BITRATE
  37. };
  38. // Check if the max. bitrates for video are specified through config.js videoQuality settings.
  39. // Right now only VP8 bitrates are configured on the simulcast encodings, VP9 bitrates have to be
  40. // configured on the SDP using b:AS line.
  41. this.videoBitrates = bitrateSettings ?? standardBitrates;
  42. const encodingBitrates = this.videoBitrates.VP8 ?? this.videoBitrates;
  43. /**
  44. * The startup configuration for the stream encodings that are applicable to
  45. * the video stream when a new sender is created on the peerconnection. The initial
  46. * config takes into account the differences in browser's simulcast implementation.
  47. *
  48. * Encoding parameters:
  49. * active - determine the on/off state of a particular encoding.
  50. * maxBitrate - max. bitrate value to be applied to that particular encoding
  51. * based on the encoding's resolution and config.js videoQuality settings if applicable.
  52. * rid - Rtp Stream ID that is configured for a particular simulcast stream.
  53. * scaleResolutionDownBy - the factor by which the encoding is scaled down from the
  54. * original resolution of the captured video.
  55. */
  56. this.localStreamEncodingsConfig = [
  57. {
  58. active: true,
  59. maxBitrate: browser.isFirefox() ? encodingBitrates.high : encodingBitrates.low,
  60. rid: SIM_LAYER_1_RID,
  61. scaleResolutionDownBy: browser.isFirefox() ? HD_SCALE_FACTOR : LD_SCALE_FACTOR
  62. },
  63. {
  64. active: true,
  65. maxBitrate: encodingBitrates.standard,
  66. rid: SIM_LAYER_2_RID,
  67. scaleResolutionDownBy: SD_SCALE_FACTOR
  68. },
  69. {
  70. active: true,
  71. maxBitrate: browser.isFirefox() ? encodingBitrates.low : encodingBitrates.high,
  72. rid: SIM_LAYER_3_RID,
  73. scaleResolutionDownBy: browser.isFirefox() ? LD_SCALE_FACTOR : HD_SCALE_FACTOR
  74. }
  75. ];
  76. }
  77. /**
  78. * Obtains stream encodings that need to be configured on the given track based
  79. * on the track media type and the simulcast setting.
  80. * @param {JitsiLocalTrack} localTrack
  81. */
  82. _getStreamEncodings(localTrack) {
  83. if (this.pc.isSimulcastOn() && localTrack.isVideoTrack()) {
  84. return this.localStreamEncodingsConfig;
  85. }
  86. return localTrack.isVideoTrack()
  87. ? [ {
  88. active: true,
  89. maxBitrate: this.videoBitrates.high
  90. } ]
  91. : [ { active: true } ];
  92. }
  93. /**
  94. * Ensures that the ssrcs associated with a FID ssrc-group appear in the correct order, i.e.,
  95. * the primary ssrc first and the secondary rtx ssrc later. This is important for unified
  96. * plan since we have only one FID group per media description.
  97. * @param {Object} description the webRTC session description instance for the remote
  98. * description.
  99. * @private
  100. */
  101. ensureCorrectOrderOfSsrcs(description) {
  102. const parsedSdp = transform.parse(description.sdp);
  103. parsedSdp.media.forEach(mLine => {
  104. if (mLine.type === MediaType.AUDIO) {
  105. return;
  106. }
  107. if (!mLine.ssrcGroups || !mLine.ssrcGroups.length) {
  108. return;
  109. }
  110. let reorderedSsrcs = [];
  111. const ssrcs = new Set();
  112. mLine.ssrcGroups.map(group =>
  113. group.ssrcs
  114. .split(' ')
  115. .filter(Boolean)
  116. .forEach(ssrc => ssrcs.add(ssrc))
  117. );
  118. ssrcs.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. * Returns the transceiver associated with a given RTCRtpSender/RTCRtpReceiver.
  131. *
  132. * @param {string} mediaType - type of track associated with the transceiver 'audio' or 'video'.
  133. * @param {JitsiLocalTrack} localTrack - local track to be used for lookup.
  134. * @returns {RTCRtpTransceiver}
  135. */
  136. findTransceiver(mediaType, localTrack = null) {
  137. const transceiver = localTrack?.track && localTrack.getOriginalStream()
  138. ? this.pc.peerconnection.getTransceivers().find(t => t.sender?.track?.id === localTrack.getTrackId())
  139. : this.pc.peerconnection.getTransceivers().find(t => t.receiver?.track?.kind === mediaType);
  140. return transceiver;
  141. }
  142. /**
  143. * Takes in a *unified plan* offer and inserts the appropriate
  144. * parameters for adding simulcast receive support.
  145. * @param {Object} desc - A session description object
  146. * @param {String} desc.type - the type (offer/answer)
  147. * @param {String} desc.sdp - the sdp content
  148. *
  149. * @return {Object} A session description (same format as above) object
  150. * with its sdp field modified to advertise simulcast receive support
  151. */
  152. insertUnifiedPlanSimulcastReceive(desc) {
  153. // a=simulcast line is not needed on browsers where we SDP munging is used for enabling on simulcast.
  154. // Remove this check when the client switches to RID/MID based simulcast on all browsers.
  155. if (browser.usesSdpMungingForSimulcast()) {
  156. return desc;
  157. }
  158. const sdp = transform.parse(desc.sdp);
  159. const idx = sdp.media.findIndex(mline => mline.type === MediaType.VIDEO);
  160. if (sdp.media[idx].rids && (sdp.media[idx].simulcast_03 || sdp.media[idx].simulcast)) {
  161. // Make sure we don't have the simulcast recv line on video descriptions other than
  162. // the first video description.
  163. sdp.media.forEach((mline, i) => {
  164. if (mline.type === MediaType.VIDEO && i !== idx) {
  165. sdp.media[i].rids = undefined;
  166. sdp.media[i].simulcast = undefined;
  167. // eslint-disable-next-line camelcase
  168. sdp.media[i].simulcast_03 = undefined;
  169. }
  170. });
  171. return new RTCSessionDescription({
  172. type: desc.type,
  173. sdp: transform.write(sdp)
  174. });
  175. }
  176. // In order of highest to lowest spatial quality
  177. sdp.media[idx].rids = [
  178. {
  179. id: SIM_LAYER_1_RID,
  180. direction: 'recv'
  181. },
  182. {
  183. id: SIM_LAYER_2_RID,
  184. direction: 'recv'
  185. },
  186. {
  187. id: SIM_LAYER_3_RID,
  188. direction: 'recv'
  189. }
  190. ];
  191. // Firefox 72 has stopped parsing the legacy rid= parameters in simulcast attributes.
  192. // eslint-disable-next-line max-len
  193. // https://www.fxsitecompat.dev/en-CA/docs/2019/pt-and-rid-in-webrtc-simulcast-attributes-are-no-longer-supported/
  194. const simulcastLine = browser.isFirefox() && browser.isVersionGreaterThan(71)
  195. ? `recv ${SIM_LAYER_RIDS.join(';')}`
  196. : `recv rid=${SIM_LAYER_RIDS.join(';')}`;
  197. // eslint-disable-next-line camelcase
  198. sdp.media[idx].simulcast_03 = {
  199. value: simulcastLine
  200. };
  201. return new RTCSessionDescription({
  202. type: desc.type,
  203. sdp: transform.write(sdp)
  204. });
  205. }
  206. /**
  207. * Adds {@link JitsiLocalTrack} to the WebRTC peerconnection for the first time.
  208. * @param {JitsiLocalTrack} track - track to be added to the peerconnection.
  209. * @param {boolean} isInitiator - boolean that indicates if the endpoint is offerer in a p2p connection.
  210. * @returns {void}
  211. */
  212. addTrack(localTrack, isInitiator) {
  213. const track = localTrack.getTrack();
  214. if (isInitiator) {
  215. const streams = [];
  216. if (localTrack.getOriginalStream()) {
  217. streams.push(localTrack.getOriginalStream());
  218. }
  219. // Use pc.addTransceiver() for the initiator case when local tracks are getting added
  220. // to the peerconnection before a session-initiate is sent over to the peer.
  221. const transceiverInit = {
  222. direction: MediaDirection.SENDRECV,
  223. streams,
  224. sendEncodings: []
  225. };
  226. if (!browser.isFirefox()) {
  227. transceiverInit.sendEncodings = this._getStreamEncodings(localTrack);
  228. }
  229. this.pc.peerconnection.addTransceiver(track, transceiverInit);
  230. } else {
  231. // Use pc.addTrack() for responder case so that we can re-use the m-lines that were created
  232. // when setRemoteDescription was called. pc.addTrack() automatically attaches to any existing
  233. // unused "recv-only" transceiver.
  234. this.pc.peerconnection.addTrack(track);
  235. }
  236. }
  237. /**
  238. * Returns the calculated active state of the simulcast encodings based on the frame height requested for the send
  239. * stream. All the encodings that have a resolution lower than the frame height requested will be enabled.
  240. *
  241. * @param {JitsiLocalTrack} localVideoTrack The local video track.
  242. * @param {number} newHeight The resolution requested for the video track.
  243. * @returns {Array<boolean>}
  244. */
  245. calculateEncodingsActiveState(localVideoTrack, newHeight) {
  246. const localTrack = localVideoTrack.getTrack();
  247. const { height } = localTrack.getSettings();
  248. const encodingsState = this.localStreamEncodingsConfig
  249. .map(encoding => height / encoding.scaleResolutionDownBy)
  250. .map((frameHeight, idx) => {
  251. let active = localVideoTrack.getVideoType() === VideoType.CAMERA
  252. // Keep the LD stream enabled even when the LD stream's resolution is higher than of the requested
  253. // resolution. This can happen when camera is captured at resolutions higher than 720p but the
  254. // requested resolution is 180. Since getParameters doesn't give us information about the resolutions
  255. // of the simulcast encodings, we have to rely on our initial config for the simulcast streams.
  256. ? newHeight > 0 && this.localStreamEncodingsConfig[idx]?.scaleResolutionDownBy === LD_SCALE_FACTOR
  257. ? true
  258. : frameHeight <= newHeight
  259. // Keep all the encodings for desktop track active.
  260. : true;
  261. // Disable the lower spatial layers for screensharing in Unified plan when low fps screensharing is in
  262. // progress. Sending all three streams often results in the browser suspending the high resolution in low
  263. // b/w and cpu cases, especially on the low end machines. Suspending the low resolution streams ensures
  264. // that the highest resolution stream is available always. Safari is an exception here since it does not
  265. // send the desktop stream at all if only the high resolution stream is enabled.
  266. if (localVideoTrack.getVideoType() === VideoType.DESKTOP
  267. && this.pc._capScreenshareBitrate
  268. && this.pc.usesUnifiedPlan()
  269. && !browser.isWebKitBased()
  270. && this.localStreamEncodingsConfig[idx].scaleResolutionDownBy !== HD_SCALE_FACTOR) {
  271. active = false;
  272. }
  273. return active;
  274. });
  275. return encodingsState;
  276. }
  277. /**
  278. * Returns the calculates max bitrates that need to be configured on the simulcast encodings based on the video
  279. * type and other considerations associated with screenshare.
  280. *
  281. * @param {JitsiLocalTrack} localVideoTrack The local video track.
  282. * @returns {Array<number>}
  283. */
  284. calculateEncodingsBitrates(localVideoTrack) {
  285. const videoType = localVideoTrack.getVideoType();
  286. const desktopShareBitrate = this.pc.options?.videoQuality?.desktopBitrate || DESKTOP_SHARE_RATE;
  287. const presenterEnabled = localVideoTrack._originalStream
  288. && localVideoTrack._originalStream.id !== localVideoTrack.getStreamId();
  289. const lowFpsScreenshare = localVideoTrack.getVideoType() === VideoType.DESKTOP
  290. && this.pc._capScreenshareBitrate
  291. && !browser.isWebKitBased();
  292. const encodingsBitrates = this.localStreamEncodingsConfig
  293. .map(encoding => {
  294. const bitrate = lowFpsScreenshare
  295. // For low fps screensharing, set a max bitrate of 500 Kbps when presenter is not turned on, 2500 Kbps
  296. // otherwise.
  297. ? presenterEnabled ? HD_BITRATE : desktopShareBitrate
  298. // For high fps screenshare, 'maxBitrate' setting must be cleared on Chrome in plan-b, because
  299. // if simulcast is enabled for screen and maxBitrates are set then Chrome will not send the
  300. // desktop stream.
  301. : videoType === VideoType.DESKTOP && browser.isChromiumBased() && !this.pc.usesUnifiedPlan()
  302. ? undefined
  303. : encoding.maxBitrate;
  304. return bitrate;
  305. });
  306. return encodingsBitrates;
  307. }
  308. /**
  309. * Replaces the existing track on a RTCRtpSender with the given track.
  310. *
  311. * @param {JitsiLocalTrack} oldTrack - existing track on the sender that needs to be removed.
  312. * @param {JitsiLocalTrack} newTrack - new track that needs to be added to the sender.
  313. * @returns {Promise<RTCRtpTransceiver>} - resolved with the associated transceiver when done, rejected otherwise.
  314. */
  315. replaceTrack(oldTrack, newTrack) {
  316. const mediaType = newTrack?.getType() ?? oldTrack?.getType();
  317. const localTracks = this.pc.getLocalTracks(mediaType);
  318. const track = newTrack?.getTrack() ?? null;
  319. const isNewLocalSource = FeatureFlags.isMultiStreamSupportEnabled()
  320. && localTracks?.length
  321. && !oldTrack
  322. && newTrack
  323. && !localTracks.find(t => t === newTrack);
  324. let transceiver;
  325. // If old track exists, replace the track on the corresponding sender.
  326. if (oldTrack && !oldTrack.isMuted()) {
  327. transceiver = this.pc.peerconnection.getTransceivers().find(t => t.sender.track === oldTrack.getTrack());
  328. // Find the first recvonly transceiver when more than one track of the same media type is being added to the pc.
  329. // As part of the track addition, a new m-line was added to the remote description with direction set to
  330. // recvonly.
  331. } else if (isNewLocalSource) {
  332. transceiver = this.pc.peerconnection.getTransceivers().find(
  333. t => t.receiver.track.kind === mediaType
  334. && t.direction === MediaDirection.RECVONLY
  335. // Re-use any existing recvonly transceiver (if available) for p2p case.
  336. && ((this.pc.isP2P && t.currentDirection === MediaDirection.RECVONLY)
  337. || t.currentDirection === MediaDirection.INACTIVE));
  338. // For mute/unmute operations, find the transceiver based on the track index in the source name if present,
  339. // otherwise it is assumed to be the first local track that was added to the peerconnection.
  340. } else {
  341. transceiver = this.pc.peerconnection.getTransceivers().find(t => t.receiver.track.kind === mediaType);
  342. const sourceName = newTrack?.getSourceName() ?? oldTrack?.getSourceName();
  343. if (sourceName) {
  344. const trackIndex = Number(sourceName.split('-')[1].substring(1));
  345. if (this.pc.isP2P) {
  346. transceiver = this.pc.peerconnection.getTransceivers()
  347. .filter(t => t.receiver.track.kind === mediaType)[trackIndex];
  348. } else if (oldTrack) {
  349. const transceiverMid = this.pc._localTrackTransceiverMids.get(oldTrack.rtcId);
  350. transceiver = this.pc.peerconnection.getTransceivers().find(t => t.mid === transceiverMid);
  351. } else if (trackIndex) {
  352. transceiver = this.pc.peerconnection.getTransceivers()
  353. .filter(t => t.receiver.track.kind === mediaType
  354. && t.direction !== MediaDirection.RECVONLY)[trackIndex];
  355. }
  356. }
  357. }
  358. if (!transceiver) {
  359. return Promise.reject(new Error('replace track failed'));
  360. }
  361. logger.debug(`${this.pc} Replacing ${oldTrack} with ${newTrack}`);
  362. return transceiver.sender.replaceTrack(track)
  363. .then(() => Promise.resolve(transceiver));
  364. }
  365. /**
  366. * Enables/disables audio transmission on the peer connection. When
  367. * disabled the audio transceiver direction will be set to 'inactive'
  368. * which means that no data will be sent nor accepted, but
  369. * the connection should be kept alive.
  370. * @param {boolean} active - true to enable audio media transmission or
  371. * false to disable.
  372. * @returns {void}
  373. */
  374. setAudioTransferActive(active) {
  375. this.setMediaTransferActive(MediaType.AUDIO, active);
  376. }
  377. /**
  378. * Set the simulcast stream encoding properties on the RTCRtpSender.
  379. * @param {JitsiLocalTrack} track - the current track in use for which
  380. * the encodings are to be set.
  381. * @returns {Promise<void>} - resolved when done.
  382. */
  383. setEncodings(track) {
  384. const mediaType = track.getType();
  385. const transceiver = this.findTransceiver(mediaType, track);
  386. const parameters = transceiver?.sender?.getParameters();
  387. // Resolve if the encodings are not available yet. This happens immediately after the track is added to the
  388. // peerconnection on chrome in unified-plan. It is ok to ignore and not report the error here since the
  389. // action that triggers 'addTrack' (like unmute) will also configure the encodings and set bitrates after that.
  390. if (!parameters?.encodings?.length) {
  391. return Promise.resolve();
  392. }
  393. parameters.encodings = this._getStreamEncodings(track);
  394. return transceiver.sender.setParameters(parameters);
  395. }
  396. /**
  397. * Enables/disables media transmission on the peerconnection by changing the direction
  398. * on the transceiver for the specified media type.
  399. * @param {String} mediaType - 'audio' or 'video'
  400. * @param {boolean} active - true to enable media transmission or false
  401. * to disable.
  402. * @returns {void}
  403. */
  404. setMediaTransferActive(mediaType, active) {
  405. const transceivers = this.pc.peerconnection.getTransceivers()
  406. .filter(t => t.receiver && t.receiver.track && t.receiver.track.kind === mediaType);
  407. logger.info(`${this.pc} ${active ? 'Enabling' : 'Suspending'} ${mediaType} media transfer.`);
  408. transceivers.forEach(transceiver => {
  409. if (active) {
  410. const localTrackMids = Array.from(this.pc._localTrackTransceiverMids);
  411. if (localTrackMids.find(mids => mids[1] === transceiver.mid)) {
  412. transceiver.direction = MediaDirection.SENDRECV;
  413. } else {
  414. transceiver.direction = MediaDirection.RECVONLY;
  415. }
  416. } else {
  417. transceiver.direction = MediaDirection.INACTIVE;
  418. }
  419. });
  420. }
  421. /**
  422. * Enables/disables video media transmission on the peer connection. When
  423. * disabled the SDP video media direction in the local SDP will be adjusted to
  424. * 'inactive' which means that no data will be sent nor accepted, but
  425. * the connection should be kept alive.
  426. * @param {boolean} active - true to enable video media transmission or
  427. * false to disable.
  428. * @returns {void}
  429. */
  430. setVideoTransferActive(active) {
  431. this.setMediaTransferActive(MediaType.VIDEO, active);
  432. }
  433. /**
  434. * Ensures that the resolution of the stream encodings are consistent with the values
  435. * that were configured on the RTCRtpSender when the source was added to the peerconnection.
  436. * This should prevent us from overriding the default values if the browser returns
  437. * erroneous values when RTCRtpSender.getParameters is used for getting the encodings info.
  438. * @param {Object} parameters - the RTCRtpEncodingParameters obtained from the browser.
  439. * @returns {void}
  440. */
  441. updateEncodingsResolution(parameters) {
  442. if (!(browser.isWebKitBased() && parameters.encodings && Array.isArray(parameters.encodings))) {
  443. return;
  444. }
  445. const allEqualEncodings
  446. = encodings => encodings.every(encoding => typeof encoding.scaleResolutionDownBy !== 'undefined'
  447. && encoding.scaleResolutionDownBy === encodings[0].scaleResolutionDownBy);
  448. // Implement the workaround only when all the encodings report the same resolution.
  449. if (allEqualEncodings(parameters.encodings)) {
  450. parameters.encodings.forEach((encoding, idx) => {
  451. encoding.scaleResolutionDownBy = this.localStreamEncodingsConfig[idx].scaleResolutionDownBy;
  452. });
  453. }
  454. }
  455. }