Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import { getLogger } from '@jitsi/logger';
  2. import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
  3. import { CodecMimeType } from '../../service/RTC/CodecMimeType';
  4. import { MediaType } from '../../service/RTC/MediaType';
  5. import browser from '../browser';
  6. const logger = getLogger(__filename);
  7. // Default video codec preferences on mobile and desktop endpoints.
  8. const DESKTOP_VIDEO_CODEC_ORDER = [ CodecMimeType.VP9, CodecMimeType.VP8, CodecMimeType.H264, CodecMimeType.AV1 ];
  9. const MOBILE_P2P_VIDEO_CODEC_ORDER = [ CodecMimeType.H264, CodecMimeType.VP8, CodecMimeType.VP9, CodecMimeType.AV1 ];
  10. const MOBILE_VIDEO_CODEC_ORDER = [ CodecMimeType.VP8, CodecMimeType.VP9, CodecMimeType.H264, CodecMimeType.AV1 ];
  11. /**
  12. * This class handles the codec selection mechanism for the conference based on the config.js settings.
  13. * The preferred codec is selected based on the settings and the list of codecs supported by the browser.
  14. * The preferred codec is published in presence which is then used by the other endpoints in the
  15. * conference to pick a supported codec at join time and when the call transitions between p2p and jvb
  16. * connections.
  17. */
  18. export class CodecSelection {
  19. /**
  20. * Creates a new instance for a given conference.
  21. *
  22. * @param {JitsiConference} conference the conference instance
  23. * @param {*} options
  24. * @param {string} options.jvb settings (codec list, preferred and disabled) for the jvb connection.
  25. * @param {string} options.p2p settings (codec list, preferred and disabled) for the p2p connection.
  26. */
  27. constructor(conference, options) {
  28. this.codecPreferenceOrder = {};
  29. this.conference = conference;
  30. this.encodeTimeStats = new Map();
  31. this.options = options;
  32. this.screenshareCodec = {};
  33. this.visitorCodecs = [];
  34. for (const connectionType of Object.keys(options)) {
  35. // eslint-disable-next-line prefer-const
  36. let { disabledCodec, preferredCodec, preferenceOrder, screenshareCodec } = options[connectionType];
  37. const supportedCodecs = new Set(this._getSupportedVideoCodecs(connectionType));
  38. // Default preference codec order when no codec preferences are set in config.js
  39. let selectedOrder = Array.from(supportedCodecs);
  40. if (preferenceOrder) {
  41. preferenceOrder = preferenceOrder.map(codec => codec.toLowerCase());
  42. // Select all codecs that are supported by the browser.
  43. selectedOrder = preferenceOrder.filter(codec => supportedCodecs.has(codec));
  44. // Generate the codec list based on the supported codecs and the preferred/disabled (deprecated) settings
  45. } else if (preferredCodec || disabledCodec) {
  46. disabledCodec = disabledCodec?.toLowerCase();
  47. preferredCodec = preferredCodec?.toLowerCase();
  48. // VP8 cannot be disabled since it the default codec.
  49. if (disabledCodec && disabledCodec !== CodecMimeType.VP8) {
  50. selectedOrder = selectedOrder.filter(codec => codec !== disabledCodec);
  51. }
  52. const index = selectedOrder.findIndex(codec => codec === preferredCodec);
  53. // Move the preferred codec to the top of the list.
  54. if (preferredCodec && index !== -1) {
  55. selectedOrder.splice(index, 1);
  56. selectedOrder.unshift(preferredCodec);
  57. }
  58. }
  59. // Push VP9 to the end of the list so that the client continues to decode VP9 even if its not
  60. // preferable to encode VP9 (because of browser bugs on the encoding side or added complexity on mobile
  61. // devices). Currently, VP9 encode is supported on Chrome and on Safari (only for p2p).
  62. const isVp9EncodeSupported = browser.supportsVP9() || (browser.isWebKitBased() && connectionType === 'p2p');
  63. if (!isVp9EncodeSupported || this.conference.isE2EEEnabled()) {
  64. const index = selectedOrder.findIndex(codec => codec === CodecMimeType.VP9);
  65. if (index !== -1) {
  66. selectedOrder.splice(index, 1);
  67. // Remove VP9 from the list when E2EE is enabled since it is not supported.
  68. // TODO - remove this check when support for VP9-E2EE is introduced.
  69. if (!this.conference.isE2EEEnabled()) {
  70. selectedOrder.push(CodecMimeType.VP9);
  71. }
  72. }
  73. }
  74. logger.info(`Codec preference order for ${connectionType} connection is ${selectedOrder}`);
  75. this.codecPreferenceOrder[connectionType] = selectedOrder;
  76. // Set the preferred screenshare codec.
  77. if (screenshareCodec && supportedCodecs.has(screenshareCodec)) {
  78. this.screenshareCodec[connectionType] = screenshareCodec;
  79. }
  80. }
  81. this.conference.on(
  82. JitsiConferenceEvents._MEDIA_SESSION_STARTED,
  83. session => this._selectPreferredCodec(session));
  84. this.conference.on(
  85. JitsiConferenceEvents.CONFERENCE_VISITOR_CODECS_CHANGED,
  86. codecList => this._updateVisitorCodecs(codecList));
  87. this.conference.on(
  88. JitsiConferenceEvents.USER_JOINED,
  89. () => this._selectPreferredCodec());
  90. this.conference.on(
  91. JitsiConferenceEvents.USER_LEFT,
  92. () => this._selectPreferredCodec());
  93. this.conference.on(
  94. JitsiConferenceEvents.ENCODE_TIME_STATS_RECEIVED,
  95. (tpc, stats) => this._processEncodeTimeStats(tpc, stats));
  96. }
  97. /**
  98. * Returns a list of video codecs that are supported by the browser.
  99. *
  100. * @param {string} connectionType - media connection type, p2p or jvb.
  101. * @returns {Array}
  102. */
  103. _getSupportedVideoCodecs(connectionType) {
  104. const videoCodecMimeTypes = browser.isMobileDevice() && connectionType === 'p2p'
  105. ? MOBILE_P2P_VIDEO_CODEC_ORDER
  106. : browser.isMobileDevice() ? MOBILE_VIDEO_CODEC_ORDER : DESKTOP_VIDEO_CODEC_ORDER;
  107. const supportedCodecs = videoCodecMimeTypes.filter(codec =>
  108. (window.RTCRtpReceiver?.getCapabilities?.(MediaType.VIDEO)?.codecs ?? [])
  109. .some(supportedCodec => supportedCodec.mimeType.toLowerCase() === `${MediaType.VIDEO}/${codec}`));
  110. // Select VP8 as the default codec if RTCRtpReceiver.getCapabilities() is not supported by the browser or if it
  111. // returns an empty set.
  112. !supportedCodecs.length && supportedCodecs.push(CodecMimeType.VP8);
  113. return supportedCodecs;
  114. }
  115. /**
  116. * Processes the encode time stats received for all the local video sources.
  117. *
  118. * @param {TraceablePeerConnection} tpc - the peerconnection for which stats were gathered.
  119. * @param {Object} stats - the encode time stats for local video sources.
  120. * @returns {void}
  121. */
  122. _processEncodeTimeStats(tpc, stats) {
  123. const activeSession = this.conference.getActiveMediaSession();
  124. // Process stats only for the active media session.
  125. if (activeSession.peerconnection !== tpc) {
  126. return;
  127. }
  128. const statsPerTrack = new Map();
  129. for (const ssrc of stats.keys()) {
  130. const { codec, encodeTime, qualityLimitationReason, resolution, timestamp } = stats.get(ssrc);
  131. const track = tpc.getTrackBySSRC(ssrc);
  132. let existingStats = statsPerTrack.get(track.rtcId);
  133. const encodeResolution = Math.min(resolution.height, resolution.width);
  134. const ssrcStats = {
  135. encodeResolution,
  136. encodeTime,
  137. qualityLimitationReason
  138. };
  139. if (existingStats) {
  140. existingStats.codec = codec;
  141. existingStats.timestamp = timestamp;
  142. existingStats.trackStats.push(ssrcStats);
  143. } else {
  144. existingStats = {
  145. codec,
  146. timestamp,
  147. trackStats: [ ssrcStats ]
  148. };
  149. statsPerTrack.set(track.rtcId, existingStats);
  150. }
  151. }
  152. // Aggregate the stats for multiple simulcast streams with different SSRCs but for the same video stream.
  153. for (const trackId of statsPerTrack.keys()) {
  154. const { codec, timestamp, trackStats } = statsPerTrack.get(trackId);
  155. const totalEncodeTime = trackStats
  156. .map(stat => stat.encodeTime)
  157. .reduce((totalValue, currentValue) => totalValue + currentValue, 0);
  158. const avgEncodeTime = totalEncodeTime / trackStats.length;
  159. const { qualityLimitationReason = 'none' }
  160. = trackStats.find(stat => stat.qualityLimitationReason !== 'none') ?? {};
  161. const encodeResolution = trackStats
  162. .map(stat => stat.encodeResolution)
  163. .reduce((resolution, currentValue) => Math.max(resolution, currentValue), 0);
  164. const localTrack = this.conference.getLocalVideoTracks().find(t => t.rtcId === trackId);
  165. const exisitingStats = this.encodeTimeStats.get(trackId);
  166. const sourceStats = {
  167. avgEncodeTime,
  168. codec,
  169. encodeResolution,
  170. qualityLimitationReason,
  171. localTrack,
  172. timestamp
  173. };
  174. if (exisitingStats) {
  175. exisitingStats.push(sourceStats);
  176. } else {
  177. this.encodeTimeStats.set(trackId, [ sourceStats ]);
  178. }
  179. logger.debug(`Encode stats for ${localTrack}: codec=${codec}, time=${avgEncodeTime},`
  180. + `resolution=${encodeResolution}, qualityLimitationReason=${qualityLimitationReason}`);
  181. }
  182. }
  183. /**
  184. * Sets the codec on the media session based on the codec preference order configured in config.js and the supported
  185. * codecs published by the remote participants in their presence.
  186. *
  187. * @param {JingleSessionPC} mediaSession session for which the codec selection has to be made.
  188. */
  189. _selectPreferredCodec(mediaSession) {
  190. const session = mediaSession ? mediaSession : this.conference.jvbJingleSession;
  191. if (!session) {
  192. return;
  193. }
  194. const isJvbSession = session === this.conference.jvbJingleSession;
  195. let localPreferredCodecOrder = isJvbSession ? this.codecPreferenceOrder.jvb : this.codecPreferenceOrder.p2p;
  196. // E2EE is curently supported only for VP8 codec.
  197. if (this.conference.isE2EEEnabled() && isJvbSession) {
  198. localPreferredCodecOrder = [ CodecMimeType.VP8 ];
  199. }
  200. const remoteParticipants = this.conference.getParticipants().map(participant => participant.getId());
  201. const remoteCodecsPerParticipant = remoteParticipants?.map(remote => {
  202. const peerMediaInfo = session._signalingLayer.getPeerMediaInfo(remote, MediaType.VIDEO);
  203. if (peerMediaInfo?.codecList) {
  204. return peerMediaInfo.codecList;
  205. } else if (peerMediaInfo?.codecType) {
  206. return [ peerMediaInfo.codecType ];
  207. }
  208. return [];
  209. });
  210. // Include the visitor codecs.
  211. this.visitorCodecs.length && remoteCodecsPerParticipant.push(this.visitorCodecs);
  212. const selectedCodecOrder = localPreferredCodecOrder.reduce((acc, localCodec) => {
  213. let codecNotSupportedByRemote = false;
  214. // Ignore remote codecs for p2p since only the JVB codec preferences are published in presence.
  215. // For p2p, we rely on the codec order present in the remote offer/answer.
  216. if (!session.isP2P) {
  217. // Remove any codecs that are not supported by any of the remote endpoints. The order of the supported
  218. // codecs locally however will remain the same since we want to support asymmetric codecs.
  219. for (const remoteCodecs of remoteCodecsPerParticipant) {
  220. // Ignore remote participants that do not publish codec preference in presence (transcriber).
  221. if (remoteCodecs.length) {
  222. codecNotSupportedByRemote = codecNotSupportedByRemote
  223. || !remoteCodecs.find(participantCodec => participantCodec === localCodec);
  224. }
  225. }
  226. }
  227. if (!codecNotSupportedByRemote) {
  228. acc.push(localCodec);
  229. }
  230. return acc;
  231. }, []);
  232. if (!selectedCodecOrder.length) {
  233. logger.warn('Invalid codec list generated because of a user joining/leaving the call');
  234. return;
  235. }
  236. session.setVideoCodecs(selectedCodecOrder);
  237. }
  238. /**
  239. * Updates the aggregate list of the codecs supported by all the visitors in the call and calculates the
  240. * selected codec if needed.
  241. * @param {Array} codecList - visitor codecs.
  242. * @returns {void}
  243. */
  244. _updateVisitorCodecs(codecList) {
  245. if (this.visitorCodecs === codecList) {
  246. return;
  247. }
  248. this.visitorCodecs = codecList;
  249. this._selectPreferredCodec();
  250. }
  251. /**
  252. * Returns the current codec preference order for the given connection type.
  253. *
  254. * @param {String} connectionType The media connection type, 'p2p' or 'jvb'.
  255. * @returns {Array<string>}
  256. */
  257. getCodecPreferenceList(connectionType) {
  258. return this.codecPreferenceOrder[connectionType];
  259. }
  260. /**
  261. * Returns the preferred screenshare codec for the given connection type.
  262. *
  263. * @param {String} connectionType The media connection type, 'p2p' or 'jvb'.
  264. * @returns CodecMimeType
  265. */
  266. getScreenshareCodec(connectionType) {
  267. return this.screenshareCodec[connectionType];
  268. }
  269. }