Source: lib/polyfill/media_capabilities.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.polyfill.MediaCapabilities');
  7. goog.require('shaka.log');
  8. goog.require('shaka.media.Capabilities');
  9. goog.require('shaka.polyfill');
  10. goog.require('shaka.util.DrmUtils');
  11. goog.require('shaka.util.MimeUtils');
  12. goog.require('shaka.util.Platform');
  13. /**
  14. * @summary A polyfill to provide navigator.mediaCapabilities on all browsers.
  15. * This is necessary for Tizen 3, Xbox One and possibly others we have yet to
  16. * discover.
  17. * @export
  18. */
  19. shaka.polyfill.MediaCapabilities = class {
  20. /**
  21. * Install the polyfill if needed.
  22. * @suppress {const}
  23. * @export
  24. */
  25. static install() {
  26. // We can enable MediaCapabilities in Android and Fuchsia devices, but not
  27. // in Linux devices because the implementation is buggy.
  28. // Since MediaCapabilities implementation is buggy in Apple browsers, we
  29. // should always install polyfill for Apple browsers.
  30. // See: https://github.com/shaka-project/shaka-player/issues/3530
  31. // TODO: re-evaluate MediaCapabilities in the future versions of Apple
  32. // Browsers.
  33. // Since MediaCapabilities implementation is buggy in PS5 browsers, we
  34. // should always install polyfill for PS5 browsers.
  35. // See: https://github.com/shaka-project/shaka-player/issues/3582
  36. // TODO: re-evaluate MediaCapabilities in the future versions of PS5
  37. // Browsers.
  38. // Since MediaCapabilities implementation does not exist in PS4 browsers, we
  39. // should always install polyfill.
  40. // Since MediaCapabilities implementation is buggy in Tizen browsers, we
  41. // should always install polyfill for Tizen browsers.
  42. // Since MediaCapabilities implementation is buggy in WebOS browsers, we
  43. // should always install polyfill for WebOS browsers.
  44. // Since MediaCapabilities implementation is buggy in EOS browsers, we
  45. // should always install polyfill for EOS browsers.
  46. // Since MediaCapabilities implementation is buggy in Hisense browsers, we
  47. // should always install polyfill for Hisense browsers.
  48. let canUseNativeMCap = true;
  49. if (shaka.util.Platform.isOlderChromecast() ||
  50. shaka.util.Platform.isApple() ||
  51. shaka.util.Platform.isPS5() ||
  52. shaka.util.Platform.isPS4() ||
  53. shaka.util.Platform.isWebOS() ||
  54. shaka.util.Platform.isTizen() ||
  55. shaka.util.Platform.isEOS() ||
  56. shaka.util.Platform.isHisense() ||
  57. shaka.util.Platform.isComcastX1()) {
  58. canUseNativeMCap = false;
  59. }
  60. if (canUseNativeMCap && navigator.mediaCapabilities) {
  61. shaka.log.info(
  62. 'MediaCapabilities: Native mediaCapabilities support found.');
  63. return;
  64. }
  65. shaka.log.info('MediaCapabilities: install');
  66. if (!navigator.mediaCapabilities) {
  67. navigator.mediaCapabilities = /** @type {!MediaCapabilities} */ ({});
  68. }
  69. // Keep the patched MediaCapabilities object from being garbage-collected in
  70. // Safari.
  71. // See https://github.com/shaka-project/shaka-player/issues/3696#issuecomment-1009472718
  72. shaka.polyfill.MediaCapabilities.originalMcap =
  73. navigator.mediaCapabilities;
  74. navigator.mediaCapabilities.decodingInfo =
  75. shaka.polyfill.MediaCapabilities.decodingInfo_;
  76. }
  77. /**
  78. * @param {!MediaDecodingConfiguration} mediaDecodingConfig
  79. * @return {!Promise.<!MediaCapabilitiesDecodingInfo>}
  80. * @private
  81. */
  82. static async decodingInfo_(mediaDecodingConfig) {
  83. /** @type {!MediaCapabilitiesDecodingInfo} */
  84. const res = {
  85. supported: false,
  86. powerEfficient: true,
  87. smooth: true,
  88. keySystemAccess: null,
  89. configuration: mediaDecodingConfig,
  90. };
  91. const videoConfig = mediaDecodingConfig['video'];
  92. const audioConfig = mediaDecodingConfig['audio'];
  93. if (mediaDecodingConfig.type == 'media-source') {
  94. if (!shaka.util.Platform.supportsMediaSource()) {
  95. return res;
  96. }
  97. if (videoConfig) {
  98. const isSupported =
  99. await shaka.polyfill.MediaCapabilities.checkVideoSupport_(
  100. videoConfig);
  101. if (!isSupported) {
  102. return res;
  103. }
  104. }
  105. if (audioConfig) {
  106. const isSupported =
  107. shaka.polyfill.MediaCapabilities.checkAudioSupport_(audioConfig);
  108. if (!isSupported) {
  109. return res;
  110. }
  111. }
  112. } else if (mediaDecodingConfig.type == 'file') {
  113. if (videoConfig) {
  114. const contentType = videoConfig.contentType;
  115. const isSupported = shaka.util.Platform.supportsMediaType(contentType);
  116. if (!isSupported) {
  117. return res;
  118. }
  119. }
  120. if (audioConfig) {
  121. const contentType = audioConfig.contentType;
  122. const isSupported = shaka.util.Platform.supportsMediaType(contentType);
  123. if (!isSupported) {
  124. return res;
  125. }
  126. }
  127. } else {
  128. // Otherwise not supported.
  129. return res;
  130. }
  131. if (!mediaDecodingConfig.keySystemConfiguration) {
  132. // The variant is supported if it's unencrypted.
  133. res.supported = true;
  134. return res;
  135. } else {
  136. const mcapKeySystemConfig = mediaDecodingConfig.keySystemConfiguration;
  137. const keySystemAccess =
  138. await shaka.polyfill.MediaCapabilities.checkDrmSupport_(
  139. videoConfig, audioConfig, mcapKeySystemConfig);
  140. if (keySystemAccess) {
  141. res.supported = true;
  142. res.keySystemAccess = keySystemAccess;
  143. }
  144. }
  145. return res;
  146. }
  147. /**
  148. * @param {!VideoConfiguration} videoConfig The 'video' field of the
  149. * MediaDecodingConfiguration.
  150. * @return {!Promise<boolean>}
  151. * @private
  152. */
  153. static async checkVideoSupport_(videoConfig) {
  154. // Use 'shaka.media.Capabilities.isTypeSupported' to check if
  155. // the stream is supported.
  156. // Cast platforms will additionally check canDisplayType(), which
  157. // accepts extended MIME type parameters.
  158. // See: https://github.com/shaka-project/shaka-player/issues/4726
  159. if (shaka.util.Platform.isChromecast()) {
  160. const isSupported =
  161. await shaka.polyfill.MediaCapabilities.canCastDisplayType_(
  162. videoConfig);
  163. return isSupported;
  164. } else if (shaka.util.Platform.isTizen()) {
  165. let extendedType = videoConfig.contentType;
  166. if (videoConfig.width && videoConfig.height) {
  167. extendedType += `; width=${videoConfig.width}`;
  168. extendedType += `; height=${videoConfig.height}`;
  169. }
  170. if (videoConfig.framerate) {
  171. extendedType += `; framerate=${videoConfig.framerate}`;
  172. }
  173. if (videoConfig.bitrate) {
  174. extendedType += `; bitrate=${videoConfig.bitrate}`;
  175. }
  176. return shaka.media.Capabilities.isTypeSupported(extendedType);
  177. }
  178. return shaka.media.Capabilities.isTypeSupported(videoConfig.contentType);
  179. }
  180. /**
  181. * @param {!AudioConfiguration} audioConfig The 'audio' field of the
  182. * MediaDecodingConfiguration.
  183. * @return {boolean}
  184. * @private
  185. */
  186. static checkAudioSupport_(audioConfig) {
  187. let extendedType = audioConfig.contentType;
  188. if (shaka.util.Platform.isChromecast() && audioConfig.spatialRendering) {
  189. extendedType += '; spatialRendering=true';
  190. }
  191. return shaka.media.Capabilities.isTypeSupported(extendedType);
  192. }
  193. /**
  194. * @param {VideoConfiguration} videoConfig The 'video' field of the
  195. * MediaDecodingConfiguration.
  196. * @param {AudioConfiguration} audioConfig The 'audio' field of the
  197. * MediaDecodingConfiguration.
  198. * @param {!MediaCapabilitiesKeySystemConfiguration} mcapKeySystemConfig The
  199. * 'keySystemConfiguration' field of the MediaDecodingConfiguration.
  200. * @return {Promise<MediaKeySystemAccess>}
  201. * @private
  202. */
  203. static async checkDrmSupport_(videoConfig, audioConfig, mcapKeySystemConfig) {
  204. const MimeUtils = shaka.util.MimeUtils;
  205. const audioCapabilities = [];
  206. const videoCapabilities = [];
  207. if (mcapKeySystemConfig.audio) {
  208. const capability = {
  209. robustness: mcapKeySystemConfig.audio.robustness || '',
  210. contentType: audioConfig.contentType,
  211. };
  212. // Some Tizen devices seem to misreport AC-3 support, but correctly
  213. // report EC-3 support. So query EC-3 as a fallback for AC-3.
  214. // See https://github.com/shaka-project/shaka-player/issues/2989 for
  215. // details.
  216. if (shaka.util.Platform.isTizen() &&
  217. audioConfig.contentType.includes('codecs="ac-3"')) {
  218. capability.contentType = 'audio/mp4; codecs="ec-3"';
  219. }
  220. if (mcapKeySystemConfig.audio.encryptionScheme) {
  221. capability.encryptionScheme =
  222. mcapKeySystemConfig.audio.encryptionScheme;
  223. }
  224. audioCapabilities.push(capability);
  225. }
  226. if (mcapKeySystemConfig.video) {
  227. const capability = {
  228. robustness: mcapKeySystemConfig.video.robustness || '',
  229. contentType: videoConfig.contentType,
  230. };
  231. if (mcapKeySystemConfig.video.encryptionScheme) {
  232. capability.encryptionScheme =
  233. mcapKeySystemConfig.video.encryptionScheme;
  234. }
  235. videoCapabilities.push(capability);
  236. }
  237. /** @type {MediaKeySystemConfiguration} */
  238. const mediaKeySystemConfig = {
  239. initDataTypes: [mcapKeySystemConfig.initDataType],
  240. distinctiveIdentifier: mcapKeySystemConfig.distinctiveIdentifier,
  241. persistentState: mcapKeySystemConfig.persistentState,
  242. sessionTypes: mcapKeySystemConfig.sessionTypes,
  243. };
  244. // Only add audio / video capabilities if they have valid data.
  245. // Otherwise the query will fail.
  246. if (audioCapabilities.length) {
  247. mediaKeySystemConfig.audioCapabilities = audioCapabilities;
  248. }
  249. if (videoCapabilities.length) {
  250. mediaKeySystemConfig.videoCapabilities = videoCapabilities;
  251. }
  252. const videoMimeType = videoConfig ? videoConfig.contentType : '';
  253. const audioMimeType = audioConfig ? audioConfig.contentType : '';
  254. const videoCodec = MimeUtils.getBasicType(videoMimeType) + ';' +
  255. MimeUtils.getCodecBase(videoMimeType);
  256. const audioCodec = MimeUtils.getBasicType(audioMimeType) + ';' +
  257. MimeUtils.getCodecBase(audioMimeType);
  258. const keySystem = mcapKeySystemConfig.keySystem;
  259. /** @type {MediaKeySystemAccess} */
  260. let keySystemAccess = null;
  261. try {
  262. if (shaka.util.DrmUtils.hasMediaKeySystemAccess(
  263. videoCodec, audioCodec, keySystem)) {
  264. keySystemAccess = shaka.util.DrmUtils.getMediaKeySystemAccess(
  265. videoCodec, audioCodec, keySystem);
  266. } else {
  267. keySystemAccess = await navigator.requestMediaKeySystemAccess(
  268. mcapKeySystemConfig.keySystem, [mediaKeySystemConfig]);
  269. shaka.util.DrmUtils.setMediaKeySystemAccess(
  270. videoCodec, audioCodec, keySystem, keySystemAccess);
  271. }
  272. } catch (e) {
  273. shaka.log.info('navigator.requestMediaKeySystemAccess failed.');
  274. }
  275. return keySystemAccess;
  276. }
  277. /**
  278. * Checks if the given media parameters of the video or audio streams are
  279. * supported by the Cast platform.
  280. * @param {!VideoConfiguration} videoConfig The 'video' field of the
  281. * MediaDecodingConfiguration.
  282. * @return {!Promise<boolean>} `true` when the stream can be displayed on a
  283. * Cast device.
  284. * @private
  285. */
  286. static async canCastDisplayType_(videoConfig) {
  287. if (!(window.cast &&
  288. cast.__platform__ && cast.__platform__.canDisplayType)) {
  289. shaka.log.warning('Expected cast APIs to be available! Falling back to ' +
  290. 'shaka.media.Capabilities.isTypeSupported() for type support.');
  291. return shaka.media.Capabilities.isTypeSupported(videoConfig.contentType);
  292. }
  293. let displayType = videoConfig.contentType;
  294. if (videoConfig.width && videoConfig.height) {
  295. // All Chromecast can support 720p videos
  296. if (videoConfig.width > 1280 && videoConfig.height > 720) {
  297. displayType +=
  298. `; width=${videoConfig.width}; height=${videoConfig.height}`;
  299. }
  300. }
  301. if (videoConfig.framerate) {
  302. // All Chromecast can support a framerate of 24, 25 or 30.
  303. const framerate = Math.round(videoConfig.framerate);
  304. if (framerate < 24 || framerate > 30) {
  305. displayType += `; framerate=${videoConfig.framerate}`;
  306. }
  307. }
  308. // Don't trust Closure types here. Although transferFunction is string or
  309. // undefined, we don't want to count on the input type. A switch statement
  310. // will, however, differentiate between null and undefined. So we default
  311. // to a blank string.
  312. const transferFunction = videoConfig.transferFunction || '';
  313. // Based on internal sources. Googlers, see go/cast-hdr-queries for source.
  314. switch (transferFunction) {
  315. // The empty case falls through to SDR.
  316. case '':
  317. // These are the only 3 values defined by MCap as of November 2024.
  318. case 'srgb':
  319. // https://en.wikipedia.org/wiki/Standard-dynamic-range_video
  320. // https://en.wikipedia.org/wiki/SRGB
  321. // https://en.wikipedia.org/wiki/Rec._709
  322. // This is SDR, standardized in BT 709.
  323. // The platform recognizes "eotf=bt709", but we can also omit it.
  324. break;
  325. case 'pq':
  326. // https://en.wikipedia.org/wiki/Perceptual_quantizer
  327. // This HDR transfer function is standardized as SMPTE ST 2084.
  328. displayType += '; eotf=smpte2084';
  329. break;
  330. case 'hlg':
  331. // https://en.wikipedia.org/wiki/Hybrid_log%E2%80%93gamma
  332. // This HDR transfer function is standardized as ARIB STD-B67.
  333. displayType += '; eotf=arib-std-b67';
  334. break;
  335. default:
  336. // An unrecognized transfer function. Reject this query.
  337. return false;
  338. }
  339. let result = false;
  340. if (displayType in shaka.polyfill.MediaCapabilities
  341. .memoizedCanDisplayTypeRequests_) {
  342. result = shaka.polyfill.MediaCapabilities
  343. .memoizedCanDisplayTypeRequests_[displayType];
  344. } else {
  345. result = await cast.__platform__.canDisplayType(displayType);
  346. shaka.polyfill.MediaCapabilities
  347. .memoizedCanDisplayTypeRequests_[displayType] = result;
  348. }
  349. return result;
  350. }
  351. };
  352. /**
  353. * A copy of the MediaCapabilities instance, to prevent Safari from
  354. * garbage-collecting the polyfilled method on it. We make it public and export
  355. * it to ensure that it is not stripped out by the compiler.
  356. *
  357. * @type {MediaCapabilities}
  358. * @export
  359. */
  360. shaka.polyfill.MediaCapabilities.originalMcap = null;
  361. /**
  362. * A cache that stores the canDisplayType result of calling
  363. * `cast.__platform__.canDisplayType`.
  364. *
  365. * @type {(Object<(!string), (!boolean)>)}
  366. * @export
  367. */
  368. shaka.polyfill.MediaCapabilities.memoizedCanDisplayTypeRequests_ = {};
  369. // Install at a lower priority than MediaSource polyfill, so that we have
  370. // MediaSource available first.
  371. shaka.polyfill.register(shaka.polyfill.MediaCapabilities.install, -1);