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.ts 13KB

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