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