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.Platform');
  11. /**
  12. * @summary A polyfill to provide navigator.mediaCapabilities on all browsers.
  13. * This is necessary for Tizen 3, Xbox One and possibly others we have yet to
  14. * discover.
  15. * @export
  16. */
  17. shaka.polyfill.MediaCapabilities = class {
  18. /**
  19. * Install the polyfill if needed.
  20. * @suppress {const}
  21. * @export
  22. */
  23. static install() {
  24. // Since MediaCapabilities implementation is buggy on the Chromecast
  25. // platform (see https://github.com/shaka-project/shaka-player/issues/4569),
  26. // we should always install polyfills on all Chromecast models.
  27. // TODO: re-evaluate MediaCapabilities in the future versions of Chromecast.
  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.isApple() ||
  50. shaka.util.Platform.isPS5() ||
  51. shaka.util.Platform.isPS4() ||
  52. shaka.util.Platform.isWebOS() ||
  53. shaka.util.Platform.isTizen() ||
  54. shaka.util.Platform.isChromecast() ||
  55. shaka.util.Platform.isEOS() ||
  56. shaka.util.Platform.isHisense()) {
  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. const res = {
  83. supported: false,
  84. powerEfficient: true,
  85. smooth: true,
  86. keySystemAccess: null,
  87. configuration: mediaDecodingConfig,
  88. };
  89. if (!mediaDecodingConfig) {
  90. return res;
  91. }
  92. const videoConfig = mediaDecodingConfig['video'];
  93. const audioConfig = mediaDecodingConfig['audio'];
  94. const Capabilities = shaka.media.Capabilities;
  95. if (mediaDecodingConfig.type == 'media-source') {
  96. if (!shaka.util.Platform.supportsMediaSource()) {
  97. return res;
  98. }
  99. // Use 'shaka.media.Capabilities.isTypeSupported'to check if
  100. // the stream is supported.
  101. // Cast platforms will additionally check canDisplayType(), which
  102. // accepts extended MIME type parameters.
  103. // See: https://github.com/shaka-project/shaka-player/issues/4726
  104. if (videoConfig) {
  105. let isSupported;
  106. if (shaka.util.Platform.isChromecast()) {
  107. isSupported =
  108. shaka.polyfill.MediaCapabilities.canCastDisplayType_(videoConfig);
  109. } else if (shaka.util.Platform.isTizen()) {
  110. let extendedType = videoConfig.contentType;
  111. if (videoConfig.width && videoConfig.height) {
  112. extendedType += `; width=${videoConfig.width}`;
  113. extendedType += `; height=${videoConfig.height}`;
  114. }
  115. if (videoConfig.framerate) {
  116. extendedType += `; framerate=${videoConfig.framerate}`;
  117. }
  118. if (videoConfig.bitrate) {
  119. extendedType += `; bitrate=${videoConfig.bitrate}`;
  120. }
  121. isSupported = Capabilities.isTypeSupported(extendedType);
  122. } else {
  123. isSupported = Capabilities.isTypeSupported(videoConfig.contentType);
  124. }
  125. if (!isSupported) {
  126. return res;
  127. }
  128. }
  129. if (audioConfig) {
  130. const contentType = audioConfig.contentType;
  131. const isSupported = Capabilities.isTypeSupported(contentType);
  132. if (!isSupported) {
  133. return res;
  134. }
  135. }
  136. } else if (mediaDecodingConfig.type == 'file') {
  137. if (videoConfig) {
  138. const contentType = videoConfig.contentType;
  139. const isSupported = shaka.util.Platform.supportsMediaType(contentType);
  140. if (!isSupported) {
  141. return res;
  142. }
  143. }
  144. if (audioConfig) {
  145. const contentType = audioConfig.contentType;
  146. const isSupported = shaka.util.Platform.supportsMediaType(contentType);
  147. if (!isSupported) {
  148. return res;
  149. }
  150. }
  151. } else {
  152. // Otherwise not supported.
  153. return res;
  154. }
  155. if (!mediaDecodingConfig.keySystemConfiguration) {
  156. // The variant is supported if it's unencrypted.
  157. res.supported = true;
  158. return Promise.resolve(res);
  159. } else {
  160. // Get the MediaKeySystemAccess for the key system.
  161. // Convert the MediaDecodingConfiguration object to a
  162. // MediaKeySystemConfiguration object.
  163. /** @type {MediaCapabilitiesKeySystemConfiguration} */
  164. const mediaCapkeySystemConfig =
  165. mediaDecodingConfig.keySystemConfiguration;
  166. const audioCapabilities = [];
  167. const videoCapabilities = [];
  168. if (mediaCapkeySystemConfig.audio) {
  169. const capability = {
  170. robustness: mediaCapkeySystemConfig.audio.robustness || '',
  171. contentType: mediaDecodingConfig.audio.contentType,
  172. };
  173. audioCapabilities.push(capability);
  174. }
  175. if (mediaCapkeySystemConfig.video) {
  176. const capability = {
  177. robustness: mediaCapkeySystemConfig.video.robustness || '',
  178. contentType: mediaDecodingConfig.video.contentType,
  179. };
  180. videoCapabilities.push(capability);
  181. }
  182. /** @type {MediaKeySystemConfiguration} */
  183. const mediaKeySystemConfig = {
  184. initDataTypes: [mediaCapkeySystemConfig.initDataType],
  185. distinctiveIdentifier: mediaCapkeySystemConfig.distinctiveIdentifier,
  186. persistentState: mediaCapkeySystemConfig.persistentState,
  187. sessionTypes: mediaCapkeySystemConfig.sessionTypes,
  188. };
  189. // Only add the audio video capabilities if they have valid data.
  190. // Otherwise the query will fail.
  191. if (audioCapabilities.length) {
  192. mediaKeySystemConfig.audioCapabilities = audioCapabilities;
  193. }
  194. if (videoCapabilities.length) {
  195. mediaKeySystemConfig.videoCapabilities = videoCapabilities;
  196. }
  197. const cacheKey = shaka.polyfill.MediaCapabilities
  198. .generateKeySystemCacheKey_(
  199. mediaDecodingConfig.video ?
  200. mediaDecodingConfig.video.contentType : '',
  201. mediaDecodingConfig.audio ?
  202. mediaDecodingConfig.audio.contentType : '',
  203. mediaDecodingConfig.keySystemConfiguration.keySystem);
  204. let keySystemAccess;
  205. try {
  206. if (cacheKey in shaka.polyfill.MediaCapabilities
  207. .memoizedMediaKeySystemAccessRequests_) {
  208. keySystemAccess = shaka.polyfill.MediaCapabilities
  209. .memoizedMediaKeySystemAccessRequests_[cacheKey];
  210. } else {
  211. keySystemAccess = await navigator.requestMediaKeySystemAccess(
  212. mediaCapkeySystemConfig.keySystem, [mediaKeySystemConfig]);
  213. shaka.polyfill.MediaCapabilities
  214. .memoizedMediaKeySystemAccessRequests_[cacheKey] =
  215. keySystemAccess;
  216. }
  217. } catch (e) {
  218. shaka.log.info('navigator.requestMediaKeySystemAccess failed.');
  219. }
  220. if (keySystemAccess) {
  221. res.supported = true;
  222. res.keySystemAccess = keySystemAccess;
  223. }
  224. }
  225. return res;
  226. }
  227. /**
  228. * Checks if the given media parameters of the video or audio streams are
  229. * supported by the Cast platform.
  230. * @param {!VideoConfiguration} videoConfig The 'video' field of the
  231. * MediaDecodingConfiguration.
  232. * @return {boolean} `true` when the stream can be displayed on a Cast device.
  233. * @private
  234. */
  235. static canCastDisplayType_(videoConfig) {
  236. if (!(window.cast &&
  237. cast.__platform__ && cast.__platform__.canDisplayType)) {
  238. shaka.log.warning('Expected cast APIs to be available! Falling back to ' +
  239. 'shaka.media.Capabilities.isTypeSupported() for type support.');
  240. return shaka.media.Capabilities.isTypeSupported(videoConfig.contentType);
  241. }
  242. let displayType = videoConfig.contentType;
  243. if (videoConfig.width && videoConfig.height) {
  244. displayType +=
  245. `; width=${videoConfig.width}; height=${videoConfig.height}`;
  246. }
  247. if (videoConfig.framerate) {
  248. displayType += `; framerate=${videoConfig.framerate}`;
  249. }
  250. if (videoConfig.transferFunction === 'pq') {
  251. // A "PQ" transfer function indicates this is an HDR-capable stream;
  252. // "smpte2084" is the published standard. We need to inform the platform
  253. // this query is specifically for HDR.
  254. displayType += '; eotf=smpte2084';
  255. }
  256. return cast.__platform__.canDisplayType(displayType);
  257. }
  258. /**
  259. * A method for generating a key for the MediaKeySystemAccessRequests cache.
  260. *
  261. * @param {!string} videoCodec
  262. * @param {!string} audioCodec
  263. * @param {!string} keySystem
  264. * @return {!string}
  265. * @private
  266. */
  267. static generateKeySystemCacheKey_(videoCodec, audioCodec, keySystem) {
  268. return `${videoCodec}#${audioCodec}#${keySystem}`;
  269. }
  270. };
  271. /**
  272. * A copy of the MediaCapabilities instance, to prevent Safari from
  273. * garbage-collecting the polyfilled method on it. We make it public and export
  274. * it to ensure that it is not stripped out by the compiler.
  275. *
  276. * @type {MediaCapabilities}
  277. * @export
  278. */
  279. shaka.polyfill.MediaCapabilities.originalMcap = null;
  280. /**
  281. * A cache that stores the MediaKeySystemAccess result of calling
  282. * `navigator.requestMediaKeySystemAccess` by a key combination of
  283. * video/audio codec and key system string.
  284. *
  285. * @type {(Object<(!string), (!MediaKeySystemAccess)>)}
  286. * @export
  287. */
  288. shaka.polyfill.MediaCapabilities.memoizedMediaKeySystemAccessRequests_ = {};
  289. // Install at a lower priority than MediaSource polyfill, so that we have
  290. // MediaSource available first.
  291. shaka.polyfill.register(shaka.polyfill.MediaCapabilities.install, -1);