Source: lib/util/platform.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.Platform');
  7. goog.require('shaka.drm.DrmUtils');
  8. goog.require('shaka.log');
  9. goog.require('shaka.util.Timer');
  10. /**
  11. * A wrapper for platform-specific functions.
  12. *
  13. * @final
  14. */
  15. shaka.util.Platform = class {
  16. /**
  17. * Check if the current platform supports media source. We assume that if
  18. * the current platform supports media source, then we can use media source
  19. * as per its design.
  20. *
  21. * @return {boolean}
  22. */
  23. static supportsMediaSource() {
  24. const mediaSource = window.ManagedMediaSource || window.MediaSource;
  25. // Browsers that lack a media source implementation will have no reference
  26. // to |window.MediaSource|. Platforms that we see having problematic media
  27. // source implementations will have this reference removed via a polyfill.
  28. if (!mediaSource) {
  29. return false;
  30. }
  31. // Some very old MediaSource implementations didn't have isTypeSupported.
  32. if (!mediaSource.isTypeSupported) {
  33. return false;
  34. }
  35. return true;
  36. }
  37. /**
  38. * Returns true if the media type is supported natively by the platform.
  39. *
  40. * @param {string} mimeType
  41. * @return {boolean}
  42. */
  43. static supportsMediaType(mimeType) {
  44. const video = shaka.util.Platform.anyMediaElement();
  45. return video.canPlayType(mimeType) != '';
  46. }
  47. /**
  48. * Check if the current platform is MS Edge.
  49. *
  50. * @return {boolean}
  51. */
  52. static isEdge() {
  53. // Legacy Edge contains "Edge/version".
  54. // Chromium-based Edge contains "Edg/version" (no "e").
  55. if (navigator.userAgent.match(/Edge?\//)) {
  56. return true;
  57. }
  58. return false;
  59. }
  60. /**
  61. * Check if the current platform is Legacy Edge.
  62. *
  63. * @return {boolean}
  64. */
  65. static isLegacyEdge() {
  66. // Legacy Edge contains "Edge/version".
  67. // Chromium-based Edge contains "Edg/version" (no "e").
  68. if (navigator.userAgent.match(/Edge\//)) {
  69. return true;
  70. }
  71. return false;
  72. }
  73. /**
  74. * Check if the current platform is MS IE.
  75. *
  76. * @return {boolean}
  77. */
  78. static isIE() {
  79. return shaka.util.Platform.userAgentContains_('Trident/');
  80. }
  81. /**
  82. * Check if the current platform is an Xbox One.
  83. *
  84. * @return {boolean}
  85. */
  86. static isXboxOne() {
  87. return shaka.util.Platform.userAgentContains_('Xbox One');
  88. }
  89. /**
  90. * Check if the current platform is a Tizen TV.
  91. *
  92. * @return {boolean}
  93. */
  94. static isTizen() {
  95. return shaka.util.Platform.userAgentContains_('Tizen');
  96. }
  97. /**
  98. * Check if the current platform is a Tizen 6 TV.
  99. *
  100. * @return {boolean}
  101. */
  102. static isTizen6() {
  103. return shaka.util.Platform.userAgentContains_('Tizen 6');
  104. }
  105. /**
  106. * Check if the current platform is a Tizen 5.0 TV.
  107. *
  108. * @return {boolean}
  109. */
  110. static isTizen5_0() {
  111. return shaka.util.Platform.userAgentContains_('Tizen 5.0');
  112. }
  113. /**
  114. * Check if the current platform is a Tizen 5 TV.
  115. *
  116. * @return {boolean}
  117. */
  118. static isTizen5() {
  119. return shaka.util.Platform.userAgentContains_('Tizen 5');
  120. }
  121. /**
  122. * Check if the current platform is a Tizen 4 TV.
  123. *
  124. * @return {boolean}
  125. */
  126. static isTizen4() {
  127. return shaka.util.Platform.userAgentContains_('Tizen 4');
  128. }
  129. /**
  130. * Check if the current platform is a Tizen 3 TV.
  131. *
  132. * @return {boolean}
  133. */
  134. static isTizen3() {
  135. return shaka.util.Platform.userAgentContains_('Tizen 3');
  136. }
  137. /**
  138. * Check if the current platform is a Tizen 2 TV.
  139. *
  140. * @return {boolean}
  141. */
  142. static isTizen2() {
  143. return shaka.util.Platform.userAgentContains_('Tizen 2');
  144. }
  145. /**
  146. * Check if the current platform is a WebOS.
  147. *
  148. * @return {boolean}
  149. */
  150. static isWebOS() {
  151. return shaka.util.Platform.userAgentContains_('Web0S');
  152. }
  153. /**
  154. * Check if the current platform is a WebOS 3.
  155. *
  156. * @return {boolean}
  157. */
  158. static isWebOS3() {
  159. // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string
  160. return shaka.util.Platform.isWebOS() &&
  161. shaka.util.Platform.chromeVersion() === 38;
  162. }
  163. /**
  164. * Check if the current platform is a WebOS 4.
  165. *
  166. * @return {boolean}
  167. */
  168. static isWebOS4() {
  169. // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string
  170. return shaka.util.Platform.isWebOS() &&
  171. shaka.util.Platform.chromeVersion() === 53;
  172. }
  173. /**
  174. * Check if the current platform is a WebOS 5.
  175. *
  176. * @return {boolean}
  177. */
  178. static isWebOS5() {
  179. // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string
  180. return shaka.util.Platform.isWebOS() &&
  181. shaka.util.Platform.chromeVersion() === 68;
  182. }
  183. /**
  184. * Check if the current platform is a WebOS 6.
  185. *
  186. * @return {boolean}
  187. */
  188. static isWebOS6() {
  189. // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string
  190. return shaka.util.Platform.isWebOS() &&
  191. shaka.util.Platform.chromeVersion() === 79;
  192. }
  193. /**
  194. * Check if the current platform is a Google Chromecast.
  195. *
  196. * @return {boolean}
  197. */
  198. static isChromecast() {
  199. const Platform = shaka.util.Platform;
  200. return Platform.userAgentContains_('CrKey') && !Platform.isVizio();
  201. }
  202. /**
  203. * Check if the current platform is a Google Chromecast without
  204. * Android or Fuchsia.
  205. *
  206. * @return {boolean}
  207. */
  208. static isOlderChromecast() {
  209. const Platform = shaka.util.Platform;
  210. return Platform.isChromecast() &&
  211. !Platform.isAndroid() && !Platform.isFuchsia();
  212. }
  213. /**
  214. * Check if the current platform is a Google Chromecast with Android
  215. * (i.e. Chromecast with GoogleTV).
  216. *
  217. * @return {boolean}
  218. */
  219. static isAndroidCastDevice() {
  220. const Platform = shaka.util.Platform;
  221. return Platform.isChromecast() && Platform.isAndroid();
  222. }
  223. /**
  224. * Check if the current platform is a Google Chromecast with Fuchsia
  225. * (i.e. Google Nest Hub).
  226. *
  227. * @return {boolean}
  228. */
  229. static isFuchsiaCastDevice() {
  230. const Platform = shaka.util.Platform;
  231. return Platform.isChromecast() && Platform.isFuchsia();
  232. }
  233. /**
  234. * Returns a major version number for Chrome, or Chromium-based browsers.
  235. *
  236. * For example:
  237. * - Chrome 106.0.5249.61 returns 106.
  238. * - Edge 106.0.1370.34 returns 106 (since this is based on Chromium).
  239. * - Safari returns null (since this is independent of Chromium).
  240. *
  241. * @return {?number} A major version number or null if not Chromium-based.
  242. */
  243. static chromeVersion() {
  244. if (!shaka.util.Platform.isChrome()) {
  245. return null;
  246. }
  247. // Looking for something like "Chrome/106.0.0.0".
  248. const match = navigator.userAgent.match(/Chrome\/(\d+)/);
  249. if (match) {
  250. return parseInt(match[1], /* base= */ 10);
  251. }
  252. return null;
  253. }
  254. /**
  255. * Check if the current platform is Google Chrome.
  256. *
  257. * @return {boolean}
  258. */
  259. static isChrome() {
  260. // The Edge Legacy user agent will also contain the "Chrome" keyword, so we
  261. // need to make sure this is not Edge Legacy.
  262. return shaka.util.Platform.userAgentContains_('Chrome') &&
  263. !shaka.util.Platform.isLegacyEdge();
  264. }
  265. /**
  266. * Check if the current platform is Firefox.
  267. *
  268. * @return {boolean}
  269. */
  270. static isFirefox() {
  271. return shaka.util.Platform.userAgentContains_('Firefox');
  272. }
  273. /**
  274. * Check if the current platform is from Apple.
  275. *
  276. * Returns true on all iOS browsers and on desktop Safari.
  277. *
  278. * Returns false for non-Safari browsers on macOS, which are independent of
  279. * Apple.
  280. *
  281. * @return {boolean}
  282. */
  283. static isApple() {
  284. return shaka.util.Platform.isAppleVendor_() &&
  285. (shaka.util.Platform.isMac() || shaka.util.Platform.isIOS());
  286. }
  287. /**
  288. * Check if the current platform is Playstation 5.
  289. *
  290. * Returns true on Playstation 5 browsers.
  291. *
  292. * Returns false for Playstation 5 browsers
  293. *
  294. * @return {boolean}
  295. */
  296. static isPS5() {
  297. return shaka.util.Platform.userAgentContains_('PlayStation 5');
  298. }
  299. /**
  300. * Check if the current platform is Playstation 4.
  301. * @return {boolean}
  302. */
  303. static isPS4() {
  304. return shaka.util.Platform.userAgentContains_('PlayStation 4');
  305. }
  306. /**
  307. * Check if the current platform is Hisense.
  308. * @return {boolean}
  309. */
  310. static isHisense() {
  311. return shaka.util.Platform.userAgentContains_('Hisense') ||
  312. shaka.util.Platform.userAgentContains_('VIDAA');
  313. }
  314. /**
  315. * Check if the current platform is Vizio TV.
  316. * @return {boolean}
  317. */
  318. static isVizio() {
  319. return shaka.util.Platform.userAgentContains_('VIZIO SmartCast');
  320. }
  321. /**
  322. * Check if the current platform is Orange.
  323. * @return {boolean}
  324. */
  325. static isOrange() {
  326. return shaka.util.Platform.userAgentContains_('SOPOpenBrowser');
  327. }
  328. /**
  329. * Check if the current platform is SkyQ STB.
  330. * @return {boolean}
  331. */
  332. static isSkyQ() {
  333. return shaka.util.Platform.userAgentContains_('Sky_STB');
  334. }
  335. /**
  336. * Check if the current platform is Deutsche Telecom Zenterio STB.
  337. * @return {boolean}
  338. */
  339. static isZenterio() {
  340. return shaka.util.Platform.userAgentContains_('DT_STB_BCM');
  341. }
  342. /**
  343. * Returns a major version number for Safari, or Webkit-based STBs,
  344. * or Safari-based iOS browsers.
  345. *
  346. * For example:
  347. * - Safari 13.0.4 on macOS returns 13.
  348. * - Safari on iOS 13.3.1 returns 13.
  349. * - Chrome on iOS 13.3.1 returns 13 (since this is based on Safari/WebKit).
  350. * - Chrome on macOS returns null (since this is independent of Apple).
  351. *
  352. * Returns null on Firefox on iOS, where this version information is not
  353. * available.
  354. *
  355. * @return {?number} A major version number or null if not iOS.
  356. */
  357. static safariVersion() {
  358. // All iOS browsers and desktop Safari will return true for isApple().
  359. if (!shaka.util.Platform.isApple() && !shaka.util.Platform.isWebkitSTB()) {
  360. return null;
  361. }
  362. // This works for iOS Safari and desktop Safari, which contain something
  363. // like "Version/13.0" indicating the major Safari or iOS version.
  364. let match = navigator.userAgent.match(/Version\/(\d+)/);
  365. if (match) {
  366. return parseInt(match[1], /* base= */ 10);
  367. }
  368. // This works for all other browsers on iOS, which contain something like
  369. // "OS 13_3" indicating the major & minor iOS version.
  370. match = navigator.userAgent.match(/OS (\d+)(?:_\d+)?/);
  371. if (match) {
  372. return parseInt(match[1], /* base= */ 10);
  373. }
  374. return null;
  375. }
  376. /**
  377. * Guesses if the platform is an Apple mobile one (iOS, iPad, iPod).
  378. * @return {boolean}
  379. */
  380. static isIOS() {
  381. if (/(?:iPhone|iPad|iPod)/.test(navigator.userAgent)) {
  382. // This is Android, iOS, or iPad < 13.
  383. return true;
  384. }
  385. // Starting with iOS 13 on iPad, the user agent string no longer has the
  386. // word "iPad" in it. It looks very similar to desktop Safari. This seems
  387. // to be intentional on Apple's part.
  388. // See: https://forums.developer.apple.com/thread/119186
  389. //
  390. // So if it's an Apple device with multi-touch support, assume it's a mobile
  391. // device. If some future iOS version starts masking their user agent on
  392. // both iPhone & iPad, this clause should still work. If a future
  393. // multi-touch desktop Mac is released, this will need some adjustment.
  394. //
  395. // As of January 2020, this is mainly used to adjust the default UI config
  396. // for mobile devices, so it's low risk if something changes to break this
  397. // detection.
  398. return shaka.util.Platform.isAppleVendor_() && navigator.maxTouchPoints > 1;
  399. }
  400. /**
  401. * Guesses if the platform is a mobile one.
  402. * @return {boolean}
  403. */
  404. static isMobile() {
  405. if (navigator.userAgentData) {
  406. return navigator.userAgentData.mobile;
  407. }
  408. return shaka.util.Platform.isIOS() || shaka.util.Platform.isAndroid();
  409. }
  410. /**
  411. * Return true if the platform is a Mac, regardless of the browser.
  412. *
  413. * @return {boolean}
  414. */
  415. static isMac() {
  416. // Try the newer standard first.
  417. if (navigator.userAgentData && navigator.userAgentData.platform) {
  418. return navigator.userAgentData.platform.toLowerCase() == 'macos';
  419. }
  420. // Fall back to the old API, with less strict matching.
  421. if (!navigator.platform) {
  422. return false;
  423. }
  424. return navigator.platform.toLowerCase().includes('mac');
  425. }
  426. /**
  427. * Return true if the platform is a VisionOS.
  428. *
  429. * @return {boolean}
  430. */
  431. static isVisionOS() {
  432. if (!shaka.util.Platform.isMac()) {
  433. return false;
  434. }
  435. if (!('xr' in navigator)) {
  436. return false;
  437. }
  438. return true;
  439. }
  440. /**
  441. * Checks is non-Apple STB based on Webkit.
  442. * @return {boolean}
  443. */
  444. static isWebkitSTB() {
  445. return shaka.util.Platform.isAppleVendor_() &&
  446. !shaka.util.Platform.isApple();
  447. }
  448. /**
  449. * Return true if the platform is a Windows, regardless of the browser.
  450. *
  451. * @return {boolean}
  452. */
  453. static isWindows() {
  454. // Try the newer standard first.
  455. if (navigator.userAgentData && navigator.userAgentData.platform) {
  456. return navigator.userAgentData.platform.toLowerCase() == 'windows';
  457. }
  458. // Fall back to the old API, with less strict matching.
  459. if (!navigator.platform) {
  460. return false;
  461. }
  462. return navigator.platform.toLowerCase().includes('win32');
  463. }
  464. /**
  465. * Return true if the platform is a Android, regardless of the browser.
  466. *
  467. * @return {boolean}
  468. */
  469. static isAndroid() {
  470. if (navigator.userAgentData && navigator.userAgentData.platform) {
  471. return navigator.userAgentData.platform.toLowerCase() == 'android';
  472. }
  473. return shaka.util.Platform.userAgentContains_('Android');
  474. }
  475. /**
  476. * Return true if the platform is a Fuchsia, regardless of the browser.
  477. *
  478. * @return {boolean}
  479. */
  480. static isFuchsia() {
  481. if (navigator.userAgentData && navigator.userAgentData.platform) {
  482. return navigator.userAgentData.platform.toLowerCase() == 'fuchsia';
  483. }
  484. return shaka.util.Platform.userAgentContains_('Fuchsia');
  485. }
  486. /**
  487. * Return true if the platform is controlled by a remote control.
  488. *
  489. * @return {boolean}
  490. */
  491. static isSmartTV() {
  492. const Platform = shaka.util.Platform;
  493. if (Platform.isTizen() || Platform.isWebOS() ||
  494. Platform.isXboxOne() || Platform.isPS4() ||
  495. Platform.isPS5() || Platform.isChromecast() ||
  496. Platform.isHisense() || Platform.isVizio() ||
  497. Platform.isWebkitSTB()) {
  498. return true;
  499. }
  500. return false;
  501. }
  502. /**
  503. * Check if the user agent contains a key. This is the best way we know of
  504. * right now to detect platforms. If there is a better way, please send a
  505. * PR.
  506. *
  507. * @param {string} key
  508. * @return {boolean}
  509. * @private
  510. */
  511. static userAgentContains_(key) {
  512. const userAgent = navigator.userAgent || '';
  513. return userAgent.includes(key);
  514. }
  515. /**
  516. * @return {boolean}
  517. * @private
  518. */
  519. static isAppleVendor_() {
  520. return (navigator.vendor || '').includes('Apple');
  521. }
  522. /**
  523. * For canPlayType queries, we just need any instance.
  524. *
  525. * First, use a cached element from a previous query.
  526. * Second, search the page for one.
  527. * Third, create a temporary one.
  528. *
  529. * Cached elements expire in one second so that they can be GC'd or removed.
  530. *
  531. * @return {!HTMLMediaElement}
  532. */
  533. static anyMediaElement() {
  534. const Platform = shaka.util.Platform;
  535. if (Platform.cachedMediaElement_) {
  536. return Platform.cachedMediaElement_;
  537. }
  538. if (!Platform.cacheExpirationTimer_) {
  539. Platform.cacheExpirationTimer_ = new shaka.util.Timer(() => {
  540. Platform.cachedMediaElement_ = null;
  541. });
  542. }
  543. Platform.cachedMediaElement_ = /** @type {HTMLMediaElement} */(
  544. document.getElementsByTagName('video')[0] ||
  545. document.getElementsByTagName('audio')[0]);
  546. if (!Platform.cachedMediaElement_) {
  547. Platform.cachedMediaElement_ = /** @type {!HTMLMediaElement} */(
  548. document.createElement('video'));
  549. }
  550. Platform.cacheExpirationTimer_.tickAfter(/* seconds= */ 1);
  551. return Platform.cachedMediaElement_;
  552. }
  553. /**
  554. * Returns true if the platform requires encryption information in all init
  555. * segments. For such platforms, MediaSourceEngine will attempt to work
  556. * around a lack of such info by inserting fake encryption information into
  557. * initialization segments.
  558. *
  559. * @param {?string} keySystem
  560. * @param {string} contentType
  561. * @return {boolean}
  562. * @see https://github.com/shaka-project/shaka-player/issues/2759
  563. */
  564. static requiresEncryptionInfoInAllInitSegments(keySystem, contentType) {
  565. const Platform = shaka.util.Platform;
  566. const isPlayReady = shaka.drm.DrmUtils.isPlayReadyKeySystem(keySystem);
  567. return (Platform.isApple() && contentType === 'audio') ||
  568. Platform.isTizen() || Platform.isXboxOne() || Platform.isOrange() ||
  569. (Platform.isEdge() && Platform.isWindows() && isPlayReady);
  570. }
  571. /**
  572. * @param {string} contentType
  573. * @return {boolean}
  574. */
  575. static requiresTfhdFix(contentType) {
  576. return shaka.util.Platform.isApple() && contentType === 'audio';
  577. }
  578. /**
  579. * Returns true if the platform requires AC-3 signalling in init
  580. * segments to be replaced with EC-3 signalling.
  581. * For such platforms, MediaSourceEngine will attempt to work
  582. * around it by inserting fake EC-3 signalling into
  583. * initialization segments.
  584. *
  585. * @return {boolean}
  586. */
  587. static requiresEC3InitSegments() {
  588. return shaka.util.Platform.isTizen3();
  589. }
  590. /**
  591. * Returns true if the platform supports SourceBuffer "sequence mode".
  592. *
  593. * @return {boolean}
  594. */
  595. static supportsSequenceMode() {
  596. const Platform = shaka.util.Platform;
  597. if (Platform.isTizen3() || Platform.isTizen2() ||
  598. Platform.isWebOS3() || Platform.isPS4() || Platform.isPS5()) {
  599. return false;
  600. }
  601. // See: https://bugs.webkit.org/show_bug.cgi?id=210341
  602. const safariVersion = Platform.safariVersion();
  603. if (Platform.isWebkitSTB() && safariVersion != null && safariVersion < 15) {
  604. return false;
  605. }
  606. return true;
  607. }
  608. /**
  609. * Returns if codec switching SMOOTH is known reliable device support.
  610. *
  611. * Some devices are known not to support <code>SourceBuffer.changeType</code>
  612. * well. These devices should use the reload strategy. If a device
  613. * reports that it supports <code<changeType</code> but supports it unreliably
  614. * it should be disallowed in this method.
  615. *
  616. * @return {boolean}
  617. */
  618. static supportsSmoothCodecSwitching() {
  619. const Platform = shaka.util.Platform;
  620. // All Tizen versions (up to Tizen 8) do not support SMOOTH so far.
  621. // webOS seems to support SMOOTH from webOS 22.
  622. if (Platform.isTizen() || Platform.isPS4() || Platform.isPS5() ||
  623. Platform.isWebOS6()) {
  624. return false;
  625. }
  626. // Older chromecasts without GoogleTV seem to not support SMOOTH properly.
  627. if (Platform.isOlderChromecast()) {
  628. return false;
  629. }
  630. // See: https://chromium-review.googlesource.com/c/chromium/src/+/4577759
  631. if (Platform.isWindows() && Platform.isEdge()) {
  632. return false;
  633. }
  634. return true;
  635. }
  636. /**
  637. * On some platforms, such as v1 Chromecasts, the act of seeking can take a
  638. * significant amount of time.
  639. *
  640. * @return {boolean}
  641. */
  642. static isSeekingSlow() {
  643. const Platform = shaka.util.Platform;
  644. if (Platform.isChromecast()) {
  645. if (Platform.isAndroidCastDevice()) {
  646. // Android-based Chromecasts are new enough to not be a problem.
  647. return false;
  648. } else {
  649. return true;
  650. }
  651. }
  652. return false;
  653. }
  654. /**
  655. * Detect the maximum resolution that the platform's hardware can handle.
  656. *
  657. * @return {!Promise<shaka.extern.Resolution>}
  658. */
  659. static async detectMaxHardwareResolution() {
  660. const Platform = shaka.util.Platform;
  661. /** @type {shaka.extern.Resolution} */
  662. const maxResolution = {
  663. width: Infinity,
  664. height: Infinity,
  665. };
  666. if (Platform.isChromecast()) {
  667. // In our tests, the original Chromecast seems to have trouble decoding
  668. // above 1080p. It would be a waste to select a higher res anyway, given
  669. // that the device only outputs 1080p to begin with.
  670. // Chromecast has an extension to query the device/display's resolution.
  671. const hasCanDisplayType = window.cast && cast.__platform__ &&
  672. cast.__platform__.canDisplayType;
  673. // Some hub devices can only do 720p. Default to that.
  674. maxResolution.width = 1280;
  675. maxResolution.height = 720;
  676. try {
  677. if (hasCanDisplayType && await cast.__platform__.canDisplayType(
  678. 'video/mp4; codecs="avc1.640028"; width=3840; height=2160')) {
  679. // The device and display can both do 4k. Assume a 4k limit.
  680. maxResolution.width = 3840;
  681. maxResolution.height = 2160;
  682. } else if (hasCanDisplayType && await cast.__platform__.canDisplayType(
  683. 'video/mp4; codecs="avc1.640028"; width=1920; height=1080')) {
  684. // Most Chromecasts can do 1080p.
  685. maxResolution.width = 1920;
  686. maxResolution.height = 1080;
  687. }
  688. } catch (error) {
  689. // This shouldn't generally happen. Log the error.
  690. shaka.log.alwaysError('Failed to check canDisplayType:', error);
  691. // Now ignore the error and let the 720p default stand.
  692. }
  693. } else if (Platform.isTizen()) {
  694. const devicePixelRatio = window.devicePixelRatio;
  695. maxResolution.width = window.screen.width * devicePixelRatio > 1920 ?
  696. 3840 : 1920;
  697. maxResolution.height = window.screen.height * devicePixelRatio > 1080 ?
  698. 2160 : 1080;
  699. try {
  700. if (webapis.systeminfo && webapis.systeminfo.getMaxVideoResolution) {
  701. const maxVideoResolution =
  702. webapis.systeminfo.getMaxVideoResolution();
  703. maxResolution.width = maxVideoResolution.width;
  704. maxResolution.height = maxVideoResolution.height;
  705. } else {
  706. if (webapis.productinfo.is8KPanelSupported &&
  707. webapis.productinfo.is8KPanelSupported()) {
  708. maxResolution.width = 7680;
  709. maxResolution.height = 4320;
  710. } else if (webapis.productinfo.isUdPanelSupported &&
  711. webapis.productinfo.isUdPanelSupported()) {
  712. maxResolution.width = 3840;
  713. maxResolution.height = 2160;
  714. }
  715. }
  716. } catch (e) {
  717. shaka.log.alwaysWarn('Tizen: Error detecting screen size, default ' +
  718. 'screen size 1920x1080.');
  719. }
  720. } else if (Platform.isWebOS()) {
  721. try {
  722. const deviceInfo =
  723. /** @type {{screenWidth: number, screenHeight: number}} */(
  724. JSON.parse(window.PalmSystem.deviceInfo));
  725. // WebOS has always been able to do 1080p. Assume a 1080p limit.
  726. maxResolution.width = Math.max(1920, deviceInfo['screenWidth']);
  727. maxResolution.height = Math.max(1080, deviceInfo['screenHeight']);
  728. } catch (e) {
  729. shaka.log.alwaysWarn('WebOS: Error detecting screen size, default ' +
  730. 'screen size 1920x1080.');
  731. maxResolution.width = 1920;
  732. maxResolution.height = 1080;
  733. }
  734. } else if (Platform.isHisense()) {
  735. let supports4k = null;
  736. if (window.Hisense_Get4KSupportState) {
  737. try {
  738. // eslint-disable-next-line new-cap
  739. supports4k = window.Hisense_Get4KSupportState();
  740. } catch (e) {
  741. shaka.log.debug('Hisense: Failed to get 4K support state', e);
  742. }
  743. }
  744. if (supports4k == null) {
  745. // If API is not there or not working for whatever reason, fallback to
  746. // user agent check, as it contains UHD or FHD info.
  747. supports4k = Platform.userAgentContains_('UHD');
  748. }
  749. if (supports4k) {
  750. maxResolution.width = 3840;
  751. maxResolution.height = 2160;
  752. } else {
  753. maxResolution.width = 1920;
  754. maxResolution.height = 1080;
  755. }
  756. } else if (Platform.isPS4() || Platform.isPS5()) {
  757. let supports4K = false;
  758. try {
  759. const result = await window.msdk.device.getDisplayInfo();
  760. supports4K = result.resolution === '4K';
  761. } catch (e) {
  762. try {
  763. const result = await window.msdk.device.getDisplayInfoImmediate();
  764. supports4K = result.resolution === '4K';
  765. } catch (e) {
  766. shaka.log.alwaysWarn(
  767. 'PlayStation: Failed to get the display info:', e);
  768. }
  769. }
  770. if (supports4K) {
  771. maxResolution.width = 3840;
  772. maxResolution.height = 2160;
  773. } else {
  774. maxResolution.width = 1920;
  775. maxResolution.height = 1080;
  776. }
  777. } else {
  778. // For Xbox and UWP apps.
  779. let winRT = undefined;
  780. try {
  781. // Try to access to WinRT for WebView, if it's not defined,
  782. // try to access to WinRT for WebView2, if it's not defined either,
  783. // let it throw.
  784. if (typeof Windows !== 'undefined') {
  785. winRT = Windows;
  786. } else {
  787. winRT = chrome.webview.hostObjects.sync.Windows;
  788. }
  789. } catch (e) {}
  790. if (winRT) {
  791. maxResolution.width = 1920;
  792. maxResolution.height = 1080;
  793. try {
  794. const protectionCapabilities =
  795. new winRT.Media.Protection.ProtectionCapabilities();
  796. const protectionResult =
  797. winRT.Media.Protection.ProtectionCapabilityResult;
  798. // isTypeSupported may return "maybe", which means the operation
  799. // is not completed. This means we need to retry
  800. // https://learn.microsoft.com/en-us/uwp/api/windows.media.protection.protectioncapabilityresult?view=winrt-22621
  801. let result = null;
  802. const type =
  803. 'video/mp4;codecs="hvc1,mp4a";features="decode-res-x=3840,' +
  804. 'decode-res-y=2160,decode-bitrate=20000,decode-fps=30,' +
  805. 'decode-bpc=10,display-res-x=3840,display-res-y=2160,' +
  806. 'display-bpc=8"';
  807. const keySystem = 'com.microsoft.playready.recommendation';
  808. do {
  809. result = protectionCapabilities.isTypeSupported(type, keySystem);
  810. } while (result === protectionResult.maybe);
  811. if (result === protectionResult.probably) {
  812. maxResolution.width = 3840;
  813. maxResolution.height = 2160;
  814. }
  815. } catch (e) {
  816. shaka.log.alwaysWarn('Xbox: Error detecting screen size, default ' +
  817. 'screen size 1920x1080.');
  818. }
  819. } else if (Platform.isXboxOne()) {
  820. maxResolution.width = 1920;
  821. maxResolution.height = 1080;
  822. shaka.log.alwaysWarn('Xbox: Error detecting screen size, default ' +
  823. 'screen size 1920x1080.');
  824. }
  825. }
  826. return maxResolution;
  827. }
  828. };
  829. /** @private {shaka.util.Timer} */
  830. shaka.util.Platform.cacheExpirationTimer_ = null;
  831. /** @private {HTMLMediaElement} */
  832. shaka.util.Platform.cachedMediaElement_ = null;