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