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.

CodecSelection.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { getLogger } from '@jitsi/logger';
  2. import { CodecMimeType } from '../../service/RTC/CodecMimeType';
  3. import { MediaType } from '../../service/RTC/MediaType';
  4. import { VIDEO_CODECS_BY_COMPLEXITY } from '../../service/RTC/StandardVideoQualitySettings';
  5. import { VideoType } from '../../service/RTC/VideoType';
  6. import browser from '../browser';
  7. const logger = getLogger('modules/qualitycontrol/CodecSelection');
  8. // Default video codec preferences on mobile and desktop endpoints.
  9. const DESKTOP_VIDEO_CODEC_ORDER = [ CodecMimeType.AV1, CodecMimeType.VP9, CodecMimeType.VP8, CodecMimeType.H264 ];
  10. const MOBILE_P2P_VIDEO_CODEC_ORDER = [ CodecMimeType.H264, CodecMimeType.VP8, CodecMimeType.VP9, CodecMimeType.AV1 ];
  11. const MOBILE_VIDEO_CODEC_ORDER = [ CodecMimeType.VP8, CodecMimeType.VP9, CodecMimeType.H264, CodecMimeType.AV1 ];
  12. /**
  13. * This class handles the codec selection mechanism for the conference based on the config.js settings.
  14. * The preferred codec is selected based on the settings and the list of codecs supported by the browser.
  15. * The preferred codec is published in presence which is then used by the other endpoints in the
  16. * conference to pick a supported codec at join time and when the call transitions between p2p and jvb
  17. * connections.
  18. */
  19. export class CodecSelection {
  20. /**
  21. * Creates a new instance for a given conference.
  22. *
  23. * @param {JitsiConference} conference the conference instance
  24. * @param {*} options
  25. * @param {string} options.jvb settings (codec list, preferred and disabled) for the jvb connection.
  26. * @param {string} options.p2p settings (codec list, preferred and disabled) for the p2p connection.
  27. */
  28. constructor(conference, options) {
  29. this.codecPreferenceOrder = {};
  30. this.conference = conference;
  31. this.encodeTimeStats = new Map();
  32. this.options = options;
  33. this.screenshareCodec = {};
  34. this.visitorCodecs = [];
  35. for (const connectionType of Object.keys(options)) {
  36. let { disabledCodec, preferredCodec, preferenceOrder } = options[connectionType];
  37. const { enableAV1ForFF = false, screenshareCodec } = options[connectionType];
  38. const supportedCodecs = new Set(this._getSupportedVideoCodecs(connectionType));
  39. // Default preference codec order when no codec preferences are set in config.js
  40. let selectedOrder = Array.from(supportedCodecs);
  41. if (preferenceOrder) {
  42. preferenceOrder = preferenceOrder.map(codec => codec.toLowerCase());
  43. // Select all codecs that are supported by the browser.
  44. selectedOrder = preferenceOrder.filter(codec => supportedCodecs.has(codec));
  45. // Generate the codec list based on the supported codecs and the preferred/disabled (deprecated) settings
  46. } else if (preferredCodec || disabledCodec) {
  47. disabledCodec = disabledCodec?.toLowerCase();
  48. preferredCodec = preferredCodec?.toLowerCase();
  49. // VP8 cannot be disabled since it the default codec.
  50. if (disabledCodec && disabledCodec !== CodecMimeType.VP8) {
  51. selectedOrder = selectedOrder.filter(codec => codec !== disabledCodec);
  52. }
  53. const index = selectedOrder.findIndex(codec => codec === preferredCodec);
  54. // Move the preferred codec to the top of the list.
  55. if (preferredCodec && index !== -1) {
  56. selectedOrder.splice(index, 1);
  57. selectedOrder.unshift(preferredCodec);
  58. }
  59. }
  60. // Push VP9 to the end of the list so that the client continues to decode VP9 even if its not
  61. // preferable to encode VP9 (because of browser bugs on the encoding side or other reasons).
  62. const isVp9EncodeSupported = browser.supportsVP9() || (browser.isWebKitBased() && connectionType === 'p2p');
  63. if (!isVp9EncodeSupported) {
  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. // Adding this flag for testing purposes since AV1 is not properly supported by FF (it doesn't support SVC
  75. // and it doesn't send temporal layers with simulcast).
  76. if (browser.isFirefox() && !enableAV1ForFF) {
  77. // By default moving AV1 to the end of the list. This way AV1 won't be used for encoding but can be
  78. // used for decoding.
  79. const index = selectedOrder.findIndex(codec => codec === CodecMimeType.AV1);
  80. if (index !== -1) {
  81. selectedOrder.splice(index, 1);
  82. selectedOrder.push(CodecMimeType.AV1);
  83. }
  84. }
  85. // Safari retports AV1 as supported on M3+ macs. Because of some decoder/encoder issues reported AV1 should
  86. // be disabled until all issues are resolved.
  87. if (browser.isWebKitBased()) {
  88. selectedOrder = selectedOrder.filter(codec => codec !== CodecMimeType.AV1);
  89. }
  90. logger.info(`Codec preference order for ${connectionType} connection is ${selectedOrder}`);
  91. this.codecPreferenceOrder[connectionType] = selectedOrder;
  92. // Set the preferred screenshare codec.
  93. if (screenshareCodec && supportedCodecs.has(screenshareCodec.toLowerCase())) {
  94. this.screenshareCodec[connectionType] = screenshareCodec.toLowerCase();
  95. }
  96. }
  97. }
  98. /**
  99. * Returns a list of video codecs that are supported by the browser.
  100. *
  101. * @param {string} connectionType - media connection type, p2p or jvb.
  102. * @returns {Array}
  103. */
  104. _getSupportedVideoCodecs(connectionType) {
  105. const videoCodecMimeTypes = browser.isMobileDevice() && connectionType === 'p2p'
  106. ? MOBILE_P2P_VIDEO_CODEC_ORDER
  107. : browser.isMobileDevice() ? MOBILE_VIDEO_CODEC_ORDER : DESKTOP_VIDEO_CODEC_ORDER;
  108. const supportedCodecs = videoCodecMimeTypes.filter(codec =>
  109. (window.RTCRtpReceiver?.getCapabilities?.(MediaType.VIDEO)?.codecs ?? [])
  110. .some(supportedCodec => supportedCodec.mimeType.toLowerCase() === `${MediaType.VIDEO}/${codec}`));
  111. // Select VP8 as the default codec if RTCRtpReceiver.getCapabilities() is not supported by the browser or if it
  112. // returns an empty set.
  113. !supportedCodecs.length && supportedCodecs.push(CodecMimeType.VP8);
  114. return supportedCodecs;
  115. }
  116. /**
  117. * Returns the current codec preference order for the given connection type.
  118. *
  119. * @param {String} connectionType The media connection type, 'p2p' or 'jvb'.
  120. * @returns {Array<string>}
  121. */
  122. getCodecPreferenceList(connectionType) {
  123. return this.codecPreferenceOrder[connectionType];
  124. }
  125. /**
  126. * Returns the preferred screenshare codec for the given connection type.
  127. *
  128. * @param {String} connectionType The media connection type, 'p2p' or 'jvb'.
  129. * @returns CodecMimeType
  130. */
  131. getScreenshareCodec(connectionType) {
  132. return this.screenshareCodec[connectionType];
  133. }
  134. /**
  135. * Sets the codec on the media session based on the codec preference order configured in config.js and the supported
  136. * codecs published by the remote participants in their presence.
  137. *
  138. * @param {JingleSessionPC} mediaSession session for which the codec selection has to be made.
  139. */
  140. selectPreferredCodec(mediaSession) {
  141. const session = mediaSession ? mediaSession : this.conference.jvbJingleSession;
  142. if (!session) {
  143. return;
  144. }
  145. let localPreferredCodecOrder = this.codecPreferenceOrder.jvb;
  146. // E2EE is curently supported only for VP8 codec.
  147. if (this.conference.isE2EEEnabled()) {
  148. localPreferredCodecOrder = [ CodecMimeType.VP8 ];
  149. }
  150. const remoteParticipants = this.conference.getParticipants().map(participant => participant.getId());
  151. const remoteCodecsPerParticipant = remoteParticipants?.map(remote => {
  152. const peerMediaInfo = session._signalingLayer.getPeerMediaInfo(remote, MediaType.VIDEO);
  153. if (peerMediaInfo?.codecList) {
  154. return peerMediaInfo.codecList;
  155. } else if (peerMediaInfo?.codecType) {
  156. return [ peerMediaInfo.codecType ];
  157. }
  158. return [];
  159. });
  160. // Include the visitor codecs.
  161. this.visitorCodecs.length && remoteCodecsPerParticipant.push(this.visitorCodecs);
  162. const selectedCodecOrder = localPreferredCodecOrder.reduce((acc, localCodec) => {
  163. let codecNotSupportedByRemote = false;
  164. // Remove any codecs that are not supported by any of the remote endpoints. The order of the supported
  165. // codecs locally however will remain the same since we want to support asymmetric codecs.
  166. for (const remoteCodecs of remoteCodecsPerParticipant) {
  167. // Ignore remote participants that do not publish codec preference in presence (transcriber).
  168. if (remoteCodecs.length) {
  169. codecNotSupportedByRemote = codecNotSupportedByRemote
  170. || !remoteCodecs.find(participantCodec => participantCodec === localCodec);
  171. }
  172. }
  173. if (!codecNotSupportedByRemote) {
  174. acc.push(localCodec);
  175. }
  176. return acc;
  177. }, []);
  178. if (!selectedCodecOrder.length) {
  179. logger.warn('Invalid codec list generated because of a user joining/leaving the call');
  180. return;
  181. }
  182. session.setVideoCodecs(selectedCodecOrder, this.screenshareCodec?.jvb);
  183. }
  184. /**
  185. * Changes the codec preference order.
  186. *
  187. * @param {JitsiLocalTrack} localTrack - The local video track.
  188. * @param {CodecMimeType} codec - The codec used for encoding the given local video track.
  189. * @returns boolean - Returns true if the codec order has been updated, false otherwise.
  190. */
  191. changeCodecPreferenceOrder(localTrack, codec) {
  192. const session = this.conference.getActiveMediaSession();
  193. const connectionType = session.isP2P ? 'p2p' : 'jvb';
  194. const codecOrder = this.codecPreferenceOrder[connectionType];
  195. const videoType = localTrack.getVideoType();
  196. const codecsByVideoType = VIDEO_CODECS_BY_COMPLEXITY[videoType]
  197. .filter(val => Boolean(codecOrder.find(supportedCodec => supportedCodec === val)));
  198. const codecIndex = codecsByVideoType.findIndex(val => val === codec.toLowerCase());
  199. // Do nothing if we are using the lowest complexity codec already.
  200. if (codecIndex === codecsByVideoType.length - 1) {
  201. return false;
  202. }
  203. const newCodec = codecsByVideoType[codecIndex + 1];
  204. if (videoType === VideoType.CAMERA) {
  205. const idx = codecOrder.findIndex(val => val === newCodec);
  206. codecOrder.splice(idx, 1);
  207. codecOrder.unshift(newCodec);
  208. logger.info(`QualityController - switching camera codec to ${newCodec} because of cpu restriction`);
  209. } else {
  210. this.screenshareCodec[connectionType] = newCodec;
  211. logger.info(`QualityController - switching screenshare codec to ${newCodec} because of cpu restriction`);
  212. }
  213. this.selectPreferredCodec(session);
  214. return true;
  215. }
  216. /**
  217. * Updates the aggregate list of the codecs supported by all the visitors in the call and calculates the
  218. * selected codec if needed.
  219. * @param {Array} codecList - visitor codecs.
  220. * @returns {void}
  221. */
  222. updateVisitorCodecs(codecList) {
  223. if (this.visitorCodecs === codecList) {
  224. return;
  225. }
  226. this.visitorCodecs = codecList;
  227. this.selectPreferredCodec();
  228. }
  229. }