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