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