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.drm.DrmUtils');
  9. goog.require('shaka.media.Capabilities');
  10. goog.require('shaka.polyfill');
  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.isHisense() ||
  56. shaka.util.Platform.isVizio() ||
  57. shaka.util.Platform.isWebkitSTB()) {
  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. See this issue for details on the garbage-collection:
  71. // https://github.com/shaka-project/shaka-player/issues/3696#issuecomment-1009472718
  72. //
  73. // Using string notation is very important to avoid the compiler from
  74. // collapsing the property into a variable. See these two issues for more
  75. // details:
  76. // https://github.com/shaka-project/shaka-player/issues/8607
  77. // https://github.com/vercel/next.js/issues/78438#issuecomment-2879434319
  78. shaka.polyfill.MediaCapabilities['originalMcap'] =
  79. navigator.mediaCapabilities;
  80. navigator.mediaCapabilities.decodingInfo =
  81. shaka.polyfill.MediaCapabilities.decodingInfo_;
  82. }
  83. /**
  84. * @param {!MediaDecodingConfiguration} mediaDecodingConfig
  85. * @return {!Promise<!MediaCapabilitiesDecodingInfo>}
  86. * @private
  87. */
  88. static async decodingInfo_(mediaDecodingConfig) {
  89. /** @type {!MediaCapabilitiesDecodingInfo} */
  90. const res = {
  91. supported: false,
  92. powerEfficient: true,
  93. smooth: true,
  94. keySystemAccess: null,
  95. configuration: mediaDecodingConfig,
  96. };
  97. const videoConfig = mediaDecodingConfig['video'];
  98. const audioConfig = mediaDecodingConfig['audio'];
  99. if (mediaDecodingConfig.type == 'media-source') {
  100. if (!shaka.util.Platform.supportsMediaSource()) {
  101. return res;
  102. }
  103. if (videoConfig) {
  104. const isSupported =
  105. await shaka.polyfill.MediaCapabilities.checkVideoSupport_(
  106. videoConfig);
  107. if (!isSupported) {
  108. return res;
  109. }
  110. }
  111. if (audioConfig) {
  112. const isSupported =
  113. shaka.polyfill.MediaCapabilities.checkAudioSupport_(audioConfig);
  114. if (!isSupported) {
  115. return res;
  116. }
  117. }
  118. } else if (mediaDecodingConfig.type == 'file') {
  119. if (videoConfig) {
  120. const contentType = videoConfig.contentType;
  121. const isSupported = shaka.util.Platform.supportsMediaType(contentType);
  122. if (!isSupported) {
  123. return res;
  124. }
  125. }
  126. if (audioConfig) {
  127. const contentType = audioConfig.contentType;
  128. const isSupported = shaka.util.Platform.supportsMediaType(contentType);
  129. if (!isSupported) {
  130. return res;
  131. }
  132. }
  133. } else {
  134. // Otherwise not supported.
  135. return res;
  136. }
  137. if (!mediaDecodingConfig.keySystemConfiguration) {
  138. // The variant is supported if it's unencrypted.
  139. res.supported = true;
  140. return res;
  141. } else {
  142. const mcapKeySystemConfig = mediaDecodingConfig.keySystemConfiguration;
  143. const keySystemAccess =
  144. await shaka.polyfill.MediaCapabilities.checkDrmSupport_(
  145. videoConfig, audioConfig, mcapKeySystemConfig);
  146. if (keySystemAccess) {
  147. res.supported = true;
  148. res.keySystemAccess = keySystemAccess;
  149. }
  150. }
  151. return res;
  152. }
  153. /**
  154. * @param {!VideoConfiguration} videoConfig The 'video' field of the
  155. * MediaDecodingConfiguration.
  156. * @return {!Promise<boolean>}
  157. * @private
  158. */
  159. static async checkVideoSupport_(videoConfig) {
  160. // Use 'shaka.media.Capabilities.isTypeSupported' to check if
  161. // the stream is supported.
  162. // Cast platforms will additionally check canDisplayType(), which
  163. // accepts extended MIME type parameters.
  164. // See: https://github.com/shaka-project/shaka-player/issues/4726
  165. if (shaka.util.Platform.isChromecast()) {
  166. const isSupported =
  167. await shaka.polyfill.MediaCapabilities.canCastDisplayType_(
  168. videoConfig);
  169. return isSupported;
  170. }
  171. return shaka.media.Capabilities.isTypeSupported(videoConfig.contentType);
  172. }
  173. /**
  174. * @param {!AudioConfiguration} audioConfig The 'audio' field of the
  175. * MediaDecodingConfiguration.
  176. * @return {boolean}
  177. * @private
  178. */
  179. static checkAudioSupport_(audioConfig) {
  180. let extendedType = audioConfig.contentType;
  181. if (shaka.util.Platform.isChromecast() && audioConfig.spatialRendering) {
  182. extendedType += '; spatialRendering=true';
  183. }
  184. return shaka.media.Capabilities.isTypeSupported(extendedType);
  185. }
  186. /**
  187. * @param {VideoConfiguration} videoConfig The 'video' field of the
  188. * MediaDecodingConfiguration.
  189. * @param {AudioConfiguration} audioConfig The 'audio' field of the
  190. * MediaDecodingConfiguration.
  191. * @param {!MediaCapabilitiesKeySystemConfiguration} mcapKeySystemConfig The
  192. * 'keySystemConfiguration' field of the MediaDecodingConfiguration.
  193. * @return {Promise<MediaKeySystemAccess>}
  194. * @private
  195. */
  196. static async checkDrmSupport_(videoConfig, audioConfig, mcapKeySystemConfig) {
  197. const MimeUtils = shaka.util.MimeUtils;
  198. const audioCapabilities = [];
  199. const videoCapabilities = [];
  200. if (mcapKeySystemConfig.audio) {
  201. const capability = {
  202. robustness: mcapKeySystemConfig.audio.robustness || '',
  203. contentType: audioConfig.contentType,
  204. };
  205. // Some Tizen devices seem to misreport AC-3 support, but correctly
  206. // report EC-3 support. So query EC-3 as a fallback for AC-3.
  207. // See https://github.com/shaka-project/shaka-player/issues/2989 for
  208. // details.
  209. if (shaka.util.Platform.isTizen() &&
  210. audioConfig.contentType.includes('codecs="ac-3"')) {
  211. capability.contentType = 'audio/mp4; codecs="ec-3"';
  212. }
  213. if (mcapKeySystemConfig.audio.encryptionScheme) {
  214. capability.encryptionScheme =
  215. mcapKeySystemConfig.audio.encryptionScheme;
  216. }
  217. audioCapabilities.push(capability);
  218. }
  219. if (mcapKeySystemConfig.video) {
  220. const capability = {
  221. robustness: mcapKeySystemConfig.video.robustness || '',
  222. contentType: videoConfig.contentType,
  223. };
  224. if (mcapKeySystemConfig.video.encryptionScheme) {
  225. capability.encryptionScheme =
  226. mcapKeySystemConfig.video.encryptionScheme;
  227. }
  228. videoCapabilities.push(capability);
  229. }
  230. /** @type {MediaKeySystemConfiguration} */
  231. const mediaKeySystemConfig = {
  232. initDataTypes: [mcapKeySystemConfig.initDataType],
  233. distinctiveIdentifier: mcapKeySystemConfig.distinctiveIdentifier,
  234. persistentState: mcapKeySystemConfig.persistentState,
  235. sessionTypes: mcapKeySystemConfig.sessionTypes,
  236. };
  237. // Only add audio / video capabilities if they have valid data.
  238. // Otherwise the query will fail.
  239. if (audioCapabilities.length) {
  240. mediaKeySystemConfig.audioCapabilities = audioCapabilities;
  241. }
  242. if (videoCapabilities.length) {
  243. mediaKeySystemConfig.videoCapabilities = videoCapabilities;
  244. }
  245. const videoMimeType = videoConfig ? videoConfig.contentType : '';
  246. const audioMimeType = audioConfig ? audioConfig.contentType : '';
  247. const videoCodec = MimeUtils.getBasicType(videoMimeType) + ';' +
  248. MimeUtils.getCodecBase(videoMimeType);
  249. const audioCodec = MimeUtils.getBasicType(audioMimeType) + ';' +
  250. MimeUtils.getCodecBase(audioMimeType);
  251. const keySystem = mcapKeySystemConfig.keySystem;
  252. /** @type {MediaKeySystemAccess} */
  253. let keySystemAccess = null;
  254. try {
  255. if (shaka.drm.DrmUtils.hasMediaKeySystemAccess(
  256. videoCodec, audioCodec, keySystem)) {
  257. keySystemAccess = shaka.drm.DrmUtils.getMediaKeySystemAccess(
  258. videoCodec, audioCodec, keySystem);
  259. } else {
  260. keySystemAccess = await navigator.requestMediaKeySystemAccess(
  261. mcapKeySystemConfig.keySystem, [mediaKeySystemConfig]);
  262. shaka.drm.DrmUtils.setMediaKeySystemAccess(
  263. videoCodec, audioCodec, keySystem, keySystemAccess);
  264. }
  265. } catch (e) {
  266. shaka.log.info('navigator.requestMediaKeySystemAccess failed.');
  267. }
  268. return keySystemAccess;
  269. }
  270. /**
  271. * Checks if the given media parameters of the video or audio streams are
  272. * supported by the Cast platform.
  273. * @param {!VideoConfiguration} videoConfig The 'video' field of the
  274. * MediaDecodingConfiguration.
  275. * @return {!Promise<boolean>} `true` when the stream can be displayed on a
  276. * Cast device.
  277. * @private
  278. */
  279. static async canCastDisplayType_(videoConfig) {
  280. if (!(window.cast &&
  281. cast.__platform__ && cast.__platform__.canDisplayType)) {
  282. shaka.log.warning('Expected cast APIs to be available! Falling back to ' +
  283. 'shaka.media.Capabilities.isTypeSupported() for type support.');
  284. return shaka.media.Capabilities.isTypeSupported(videoConfig.contentType);
  285. }
  286. let displayType = videoConfig.contentType;
  287. if (videoConfig.width && videoConfig.height) {
  288. // All Chromecast can support 720p videos
  289. if (videoConfig.width > 1280 && videoConfig.height > 720) {
  290. displayType +=
  291. `; width=${videoConfig.width}; height=${videoConfig.height}`;
  292. }
  293. }
  294. if (videoConfig.framerate) {
  295. // All Chromecast can support a framerate of 24, 25 or 30.
  296. const framerate = Math.round(videoConfig.framerate);
  297. if (framerate < 24 || framerate > 30) {
  298. displayType += `; framerate=${videoConfig.framerate}`;
  299. }
  300. }
  301. // Don't trust Closure types here. Although transferFunction is string or
  302. // undefined, we don't want to count on the input type. A switch statement
  303. // will, however, differentiate between null and undefined. So we default
  304. // to a blank string.
  305. const transferFunction = videoConfig.transferFunction || '';
  306. // Based on internal sources. Googlers, see go/cast-hdr-queries for source.
  307. switch (transferFunction) {
  308. // The empty case falls through to SDR.
  309. case '':
  310. // These are the only 3 values defined by MCap as of November 2024.
  311. case 'srgb':
  312. // https://en.wikipedia.org/wiki/Standard-dynamic-range_video
  313. // https://en.wikipedia.org/wiki/SRGB
  314. // https://en.wikipedia.org/wiki/Rec._709
  315. // This is SDR, standardized in BT 709.
  316. // The platform recognizes "eotf=bt709", but we can also omit it.
  317. break;
  318. case 'pq':
  319. // https://en.wikipedia.org/wiki/Perceptual_quantizer
  320. // This HDR transfer function is standardized as SMPTE ST 2084.
  321. displayType += '; eotf=smpte2084';
  322. break;
  323. case 'hlg':
  324. // https://en.wikipedia.org/wiki/Hybrid_log%E2%80%93gamma
  325. // This HDR transfer function is standardized as ARIB STD-B67.
  326. displayType += '; eotf=arib-std-b67';
  327. break;
  328. default:
  329. // An unrecognized transfer function. Reject this query.
  330. return false;
  331. }
  332. let result = false;
  333. const memoizedCanDisplayTypeRequests =
  334. shaka.polyfill.MediaCapabilities.memoizedCanDisplayTypeRequests_;
  335. if (memoizedCanDisplayTypeRequests.has(displayType)) {
  336. result = memoizedCanDisplayTypeRequests.get(displayType);
  337. } else {
  338. result = await cast.__platform__.canDisplayType(displayType);
  339. memoizedCanDisplayTypeRequests.set(displayType, result);
  340. }
  341. return result;
  342. }
  343. };
  344. /**
  345. * A copy of the MediaCapabilities instance, to prevent Safari from
  346. * garbage-collecting the polyfilled method on it. We make it public and export
  347. * it to ensure that it is not stripped out by the compiler.
  348. *
  349. * @type {MediaCapabilities}
  350. * @export
  351. */
  352. shaka.polyfill.MediaCapabilities.originalMcap = null;
  353. /**
  354. * A cache that stores the canDisplayType result of calling
  355. * `cast.__platform__.canDisplayType`.
  356. *
  357. * @type {Map<string, boolean>}
  358. * @private
  359. */
  360. shaka.polyfill.MediaCapabilities.memoizedCanDisplayTypeRequests_ = new Map();
  361. // Install at a lower priority than MediaSource polyfill, so that we have
  362. // MediaSource available first.
  363. shaka.polyfill.register(shaka.polyfill.MediaCapabilities.install, -1);