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.

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