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.util.Timer');
  8. /**
  9. * A wrapper for platform-specific functions.
  10. *
  11. * @final
  12. */
  13. shaka.util.Platform = class {
  14. /**
  15. * Check if the current platform supports media source. We assume that if
  16. * the current platform supports media source, then we can use media source
  17. * as per its design.
  18. *
  19. * @return {boolean}
  20. */
  21. static supportsMediaSource() {
  22. const mediaSource = window.ManagedMediaSource || window.MediaSource;
  23. // Browsers that lack a media source implementation will have no reference
  24. // to |window.MediaSource|. Platforms that we see having problematic media
  25. // source implementations will have this reference removed via a polyfill.
  26. if (!mediaSource) {
  27. return false;
  28. }
  29. // Some very old MediaSource implementations didn't have isTypeSupported.
  30. if (!mediaSource.isTypeSupported) {
  31. return false;
  32. }
  33. return true;
  34. }
  35. /**
  36. * Returns true if the media type is supported natively by the platform.
  37. *
  38. * @param {string} mimeType
  39. * @return {boolean}
  40. */
  41. static supportsMediaType(mimeType) {
  42. const video = shaka.util.Platform.anyMediaElement();
  43. return video.canPlayType(mimeType) != '';
  44. }
  45. /**
  46. * Check if the current platform is MS Edge.
  47. *
  48. * @return {boolean}
  49. */
  50. static isEdge() {
  51. // Legacy Edge contains "Edge/version".
  52. // Chromium-based Edge contains "Edg/version" (no "e").
  53. if (navigator.userAgent.match(/Edge?\//)) {
  54. return true;
  55. }
  56. return false;
  57. }
  58. /**
  59. * Check if the current platform is Legacy Edge.
  60. *
  61. * @return {boolean}
  62. */
  63. static isLegacyEdge() {
  64. // Legacy Edge contains "Edge/version".
  65. // Chromium-based Edge contains "Edg/version" (no "e").
  66. if (navigator.userAgent.match(/Edge\//)) {
  67. return true;
  68. }
  69. return false;
  70. }
  71. /**
  72. * Check if the current platform is MS IE.
  73. *
  74. * @return {boolean}
  75. */
  76. static isIE() {
  77. return shaka.util.Platform.userAgentContains_('Trident/');
  78. }
  79. /**
  80. * Check if the current platform is an Xbox One.
  81. *
  82. * @return {boolean}
  83. */
  84. static isXboxOne() {
  85. return shaka.util.Platform.userAgentContains_('Xbox One');
  86. }
  87. /**
  88. * Check if the current platform is a Tizen TV.
  89. *
  90. * @return {boolean}
  91. */
  92. static isTizen() {
  93. return shaka.util.Platform.userAgentContains_('Tizen');
  94. }
  95. /**
  96. * Check if the current platform is a Tizen 6 TV.
  97. *
  98. * @return {boolean}
  99. */
  100. static isTizen6() {
  101. return shaka.util.Platform.userAgentContains_('Tizen 6');
  102. }
  103. /**
  104. * Check if the current platform is a Tizen 5.0 TV.
  105. *
  106. * @return {boolean}
  107. */
  108. static isTizen5_0() {
  109. return shaka.util.Platform.userAgentContains_('Tizen 5.0');
  110. }
  111. /**
  112. * Check if the current platform is a Tizen 5 TV.
  113. *
  114. * @return {boolean}
  115. */
  116. static isTizen5() {
  117. return shaka.util.Platform.userAgentContains_('Tizen 5');
  118. }
  119. /**
  120. * Check if the current platform is a Tizen 4 TV.
  121. *
  122. * @return {boolean}
  123. */
  124. static isTizen4() {
  125. return shaka.util.Platform.userAgentContains_('Tizen 4');
  126. }
  127. /**
  128. * Check if the current platform is a Tizen 3 TV.
  129. *
  130. * @return {boolean}
  131. */
  132. static isTizen3() {
  133. return shaka.util.Platform.userAgentContains_('Tizen 3');
  134. }
  135. /**
  136. * Check if the current platform is a Tizen 2 TV.
  137. *
  138. * @return {boolean}
  139. */
  140. static isTizen2() {
  141. return shaka.util.Platform.userAgentContains_('Tizen 2');
  142. }
  143. /**
  144. * Check if the current platform is a WebOS.
  145. *
  146. * @return {boolean}
  147. */
  148. static isWebOS() {
  149. return shaka.util.Platform.userAgentContains_('Web0S');
  150. }
  151. /**
  152. * Check if the current platform is a WebOS 3.
  153. *
  154. * @return {boolean}
  155. */
  156. static isWebOS3() {
  157. // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string
  158. return shaka.util.Platform.isWebOS() &&
  159. shaka.util.Platform.chromeVersion() === 38;
  160. }
  161. /**
  162. * Check if the current platform is a WebOS 4.
  163. *
  164. * @return {boolean}
  165. */
  166. static isWebOS4() {
  167. // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string
  168. return shaka.util.Platform.isWebOS() &&
  169. shaka.util.Platform.chromeVersion() === 53;
  170. }
  171. /**
  172. * Check if the current platform is a WebOS 5.
  173. *
  174. * @return {boolean}
  175. */
  176. static isWebOS5() {
  177. // See: https://webostv.developer.lge.com/develop/specifications/web-api-and-web-engine#useragent-string
  178. return shaka.util.Platform.isWebOS() &&
  179. shaka.util.Platform.chromeVersion() === 68;
  180. }
  181. /**
  182. * Check if the current platform is a Google Chromecast.
  183. *
  184. * @return {boolean}
  185. */
  186. static isChromecast() {
  187. return shaka.util.Platform.userAgentContains_('CrKey');
  188. }
  189. /**
  190. * Check if the current platform is a Google Chromecast with Android
  191. * (i.e. Chromecast with GoogleTV).
  192. *
  193. * @return {boolean}
  194. */
  195. static isAndroidCastDevice() {
  196. const Platform = shaka.util.Platform;
  197. return Platform.isChromecast() && Platform.isAndroid();
  198. }
  199. /**
  200. * Check if the current platform is a Google Chromecast with Fuchsia
  201. * (i.e. Google Nest Hub).
  202. *
  203. * @return {boolean}
  204. */
  205. static isFuchsiaCastDevice() {
  206. const Platform = shaka.util.Platform;
  207. return Platform.isChromecast() && Platform.isFuchsia();
  208. }
  209. /**
  210. * Returns a major version number for Chrome, or Chromium-based browsers.
  211. *
  212. * For example:
  213. * - Chrome 106.0.5249.61 returns 106.
  214. * - Edge 106.0.1370.34 returns 106 (since this is based on Chromium).
  215. * - Safari returns null (since this is independent of Chromium).
  216. *
  217. * @return {?number} A major version number or null if not Chromium-based.
  218. */
  219. static chromeVersion() {
  220. if (!shaka.util.Platform.isChrome()) {
  221. return null;
  222. }
  223. // Looking for something like "Chrome/106.0.0.0".
  224. const match = navigator.userAgent.match(/Chrome\/(\d+)/);
  225. if (match) {
  226. return parseInt(match[1], /* base= */ 10);
  227. }
  228. return null;
  229. }
  230. /**
  231. * Check if the current platform is Google Chrome.
  232. *
  233. * @return {boolean}
  234. */
  235. static isChrome() {
  236. // The Edge Legacy user agent will also contain the "Chrome" keyword, so we
  237. // need to make sure this is not Edge Legacy.
  238. return shaka.util.Platform.userAgentContains_('Chrome') &&
  239. !shaka.util.Platform.isLegacyEdge();
  240. }
  241. /**
  242. * Check if the current platform is Firefox.
  243. *
  244. * @return {boolean}
  245. */
  246. static isFirefox() {
  247. return shaka.util.Platform.userAgentContains_('Firefox');
  248. }
  249. /**
  250. * Check if the current platform is from Apple.
  251. *
  252. * Returns true on all iOS browsers and on desktop Safari.
  253. *
  254. * Returns false for non-Safari browsers on macOS, which are independent of
  255. * Apple.
  256. *
  257. * @return {boolean}
  258. */
  259. static isApple() {
  260. return !!navigator.vendor && navigator.vendor.includes('Apple') &&
  261. !shaka.util.Platform.isTizen() &&
  262. !shaka.util.Platform.isEOS() &&
  263. !shaka.util.Platform.isAPL() &&
  264. !shaka.util.Platform.isVirginMedia() &&
  265. !shaka.util.Platform.isOrange() &&
  266. !shaka.util.Platform.isPS4() &&
  267. !shaka.util.Platform.isAmazonFireTV() &&
  268. !shaka.util.Platform.isWPE();
  269. }
  270. /**
  271. * Check if the current platform is Playstation 5.
  272. *
  273. * Returns true on Playstation 5 browsers.
  274. *
  275. * Returns false for Playstation 5 browsers
  276. *
  277. * @return {boolean}
  278. */
  279. static isPS5() {
  280. return shaka.util.Platform.userAgentContains_('PlayStation 5');
  281. }
  282. /**
  283. * Check if the current platform is Playstation 4.
  284. */
  285. static isPS4() {
  286. return shaka.util.Platform.userAgentContains_('PlayStation 4');
  287. }
  288. /**
  289. * Check if the current platform is Hisense.
  290. */
  291. static isHisense() {
  292. return shaka.util.Platform.userAgentContains_('Hisense') ||
  293. shaka.util.Platform.userAgentContains_('VIDAA');
  294. }
  295. /**
  296. * Check if the current platform is Virgin Media device.
  297. */
  298. static isVirginMedia() {
  299. return shaka.util.Platform.userAgentContains_('VirginMedia');
  300. }
  301. /**
  302. * Check if the current platform is Orange.
  303. */
  304. static isOrange() {
  305. return shaka.util.Platform.userAgentContains_('SOPOpenBrowser');
  306. }
  307. /**
  308. * Check if the current platform is Amazon Fire TV.
  309. * https://developer.amazon.com/docs/fire-tv/identify-amazon-fire-tv-devices.html
  310. *
  311. * @return {boolean}
  312. */
  313. static isAmazonFireTV() {
  314. return shaka.util.Platform.userAgentContains_('AFT');
  315. }
  316. /**
  317. * Check if the current platform is Comcast X1.
  318. * @return {boolean}
  319. */
  320. static isWPE() {
  321. return shaka.util.Platform.userAgentContains_('WPE');
  322. }
  323. /**
  324. * Returns a major version number for Safari, or Safari-based iOS browsers.
  325. *
  326. * For example:
  327. * - Safari 13.0.4 on macOS returns 13.
  328. * - Safari on iOS 13.3.1 returns 13.
  329. * - Chrome on iOS 13.3.1 returns 13 (since this is based on Safari/WebKit).
  330. * - Chrome on macOS returns null (since this is independent of Apple).
  331. *
  332. * Returns null on Firefox on iOS, where this version information is not
  333. * available.
  334. *
  335. * @return {?number} A major version number or null if not iOS.
  336. */
  337. static safariVersion() {
  338. // All iOS browsers and desktop Safari will return true for isApple().
  339. if (!shaka.util.Platform.isApple()) {
  340. return null;
  341. }
  342. // This works for iOS Safari and desktop Safari, which contain something
  343. // like "Version/13.0" indicating the major Safari or iOS version.
  344. let match = navigator.userAgent.match(/Version\/(\d+)/);
  345. if (match) {
  346. return parseInt(match[1], /* base= */ 10);
  347. }
  348. // This works for all other browsers on iOS, which contain something like
  349. // "OS 13_3" indicating the major & minor iOS version.
  350. match = navigator.userAgent.match(/OS (\d+)(?:_\d+)?/);
  351. if (match) {
  352. return parseInt(match[1], /* base= */ 10);
  353. }
  354. return null;
  355. }
  356. /**
  357. * Check if the current platform is Apple Safari
  358. * or Safari-based iOS browsers.
  359. *
  360. * @return {boolean}
  361. */
  362. static isSafari() {
  363. return !!shaka.util.Platform.safariVersion();
  364. }
  365. /**
  366. * Check if the current platform is an EOS set-top box.
  367. *
  368. * @return {boolean}
  369. */
  370. static isEOS() {
  371. return shaka.util.Platform.userAgentContains_('PC=EOS');
  372. }
  373. /**
  374. * Check if the current platform is an APL set-top box.
  375. *
  376. * @return {boolean}
  377. */
  378. static isAPL() {
  379. return shaka.util.Platform.userAgentContains_('PC=APL');
  380. }
  381. /**
  382. * Guesses if the platform is a mobile one (iOS or Android).
  383. *
  384. * @return {boolean}
  385. */
  386. static isMobile() {
  387. if (/(?:iPhone|iPad|iPod|Android)/.test(navigator.userAgent)) {
  388. // This is Android, iOS, or iPad < 13.
  389. return true;
  390. }
  391. // Starting with iOS 13 on iPad, the user agent string no longer has the
  392. // word "iPad" in it. It looks very similar to desktop Safari. This seems
  393. // to be intentional on Apple's part.
  394. // See: https://forums.developer.apple.com/thread/119186
  395. //
  396. // So if it's an Apple device with multi-touch support, assume it's a mobile
  397. // device. If some future iOS version starts masking their user agent on
  398. // both iPhone & iPad, this clause should still work. If a future
  399. // multi-touch desktop Mac is released, this will need some adjustment.
  400. //
  401. // As of January 2020, this is mainly used to adjust the default UI config
  402. // for mobile devices, so it's low risk if something changes to break this
  403. // detection.
  404. return shaka.util.Platform.isApple() && navigator.maxTouchPoints > 1;
  405. }
  406. /**
  407. * Return true if the platform is a Mac, regardless of the browser.
  408. *
  409. * @return {boolean}
  410. */
  411. static isMac() {
  412. // Try the newer standard first.
  413. if (navigator.userAgentData && navigator.userAgentData.platform) {
  414. return navigator.userAgentData.platform.toLowerCase() == 'macos';
  415. }
  416. // Fall back to the old API, with less strict matching.
  417. if (!navigator.platform) {
  418. return false;
  419. }
  420. return navigator.platform.toLowerCase().includes('mac');
  421. }
  422. /**
  423. * Return true if the platform is a Windows, regardless of the browser.
  424. *
  425. * @return {boolean}
  426. */
  427. static isWindows() {
  428. // Try the newer standard first.
  429. if (navigator.userAgentData && navigator.userAgentData.platform) {
  430. return navigator.userAgentData.platform.toLowerCase() == 'windows';
  431. }
  432. // Fall back to the old API, with less strict matching.
  433. if (!navigator.platform) {
  434. return false;
  435. }
  436. return navigator.platform.toLowerCase().includes('windows');
  437. }
  438. /**
  439. * Return true if the platform is a Android, regardless of the browser.
  440. *
  441. * @return {boolean}
  442. */
  443. static isAndroid() {
  444. return shaka.util.Platform.userAgentContains_('Android');
  445. }
  446. /**
  447. * Return true if the platform is a Fuchsia, regardless of the browser.
  448. *
  449. * @return {boolean}
  450. */
  451. static isFuchsia() {
  452. return shaka.util.Platform.userAgentContains_('Fuchsia');
  453. }
  454. /**
  455. * Return true if the platform is controlled by a remote control.
  456. *
  457. * @return {boolean}
  458. */
  459. static isSmartTV() {
  460. const Platform = shaka.util.Platform;
  461. if (Platform.isTizen() || Platform.isWebOS() ||
  462. Platform.isXboxOne() || Platform.isPS4() ||
  463. Platform.isPS5() || Platform.isAmazonFireTV() ||
  464. Platform.isEOS() || Platform.isAPL() ||
  465. Platform.isVirginMedia() || Platform.isOrange() ||
  466. Platform.isWPE() || Platform.isChromecast() ||
  467. Platform.isHisense()) {
  468. return true;
  469. }
  470. return false;
  471. }
  472. /**
  473. * Check if the user agent contains a key. This is the best way we know of
  474. * right now to detect platforms. If there is a better way, please send a
  475. * PR.
  476. *
  477. * @param {string} key
  478. * @return {boolean}
  479. * @private
  480. */
  481. static userAgentContains_(key) {
  482. const userAgent = navigator.userAgent || '';
  483. return userAgent.includes(key);
  484. }
  485. /**
  486. * For canPlayType queries, we just need any instance.
  487. *
  488. * First, use a cached element from a previous query.
  489. * Second, search the page for one.
  490. * Third, create a temporary one.
  491. *
  492. * Cached elements expire in one second so that they can be GC'd or removed.
  493. *
  494. * @return {!HTMLMediaElement}
  495. */
  496. static anyMediaElement() {
  497. const Platform = shaka.util.Platform;
  498. if (Platform.cachedMediaElement_) {
  499. return Platform.cachedMediaElement_;
  500. }
  501. if (!Platform.cacheExpirationTimer_) {
  502. Platform.cacheExpirationTimer_ = new shaka.util.Timer(() => {
  503. Platform.cachedMediaElement_ = null;
  504. });
  505. }
  506. Platform.cachedMediaElement_ = /** @type {HTMLMediaElement} */(
  507. document.getElementsByTagName('video')[0] ||
  508. document.getElementsByTagName('audio')[0]);
  509. if (!Platform.cachedMediaElement_) {
  510. Platform.cachedMediaElement_ = /** @type {!HTMLMediaElement} */(
  511. document.createElement('video'));
  512. }
  513. Platform.cacheExpirationTimer_.tickAfter(/* seconds= */ 1);
  514. return Platform.cachedMediaElement_;
  515. }
  516. /**
  517. * Returns true if the platform requires encryption information in all init
  518. * segments. For such platforms, MediaSourceEngine will attempt to work
  519. * around a lack of such info by inserting fake encryption information into
  520. * initialization segments.
  521. *
  522. * @return {boolean}
  523. * @see https://github.com/shaka-project/shaka-player/issues/2759
  524. */
  525. static requiresEncryptionInfoInAllInitSegments() {
  526. const Platform = shaka.util.Platform;
  527. return Platform.isTizen() || Platform.isXboxOne() || Platform.isOrange();
  528. }
  529. /**
  530. * Returns true if the platform supports SourceBuffer "sequence mode".
  531. *
  532. * @return {boolean}
  533. */
  534. static supportsSequenceMode() {
  535. const Platform = shaka.util.Platform;
  536. if (Platform.isTizen3() || Platform.isTizen2() ||
  537. Platform.isWebOS3() || Platform.isPS4()) {
  538. return false;
  539. }
  540. return true;
  541. }
  542. /**
  543. * Returns if codec switching SMOOTH is known reliable device support.
  544. *
  545. * Some devices are known not to support `MediaSource.changeType`
  546. * well. These devices should use the reload strategy. If a device
  547. * reports that it supports `changeType` but support it reliabley
  548. * it should be added to this list.
  549. *
  550. * @return {boolean}
  551. */
  552. static supportsSmoothCodecSwitching() {
  553. const Platform = shaka.util.Platform;
  554. if (Platform.isTizen2() || Platform.isTizen3() || Platform.isTizen4() ||
  555. Platform.isTizen5() || Platform.isTizen6() || Platform.isWebOS3() ||
  556. Platform.isWebOS4() || Platform.isWebOS5()) {
  557. return false;
  558. }
  559. // Older chromecasts without GoogleTV seem to not support SMOOTH properly.
  560. if (Platform.isChromecast() && !Platform.isAndroidCastDevice() &&
  561. !Platform.isFuchsiaCastDevice()) {
  562. return false;
  563. }
  564. // See: https://chromium-review.googlesource.com/c/chromium/src/+/4577759
  565. if (Platform.isWindows() && Platform.isEdge()) {
  566. return false;
  567. }
  568. return true;
  569. }
  570. /**
  571. * Returns true if MediaKeys is polyfilled
  572. *
  573. * @return {boolean}
  574. */
  575. static isMediaKeysPolyfilled() {
  576. if (window.shakaMediaKeysPolyfill) {
  577. return true;
  578. }
  579. return false;
  580. }
  581. };
  582. /** @private {shaka.util.Timer} */
  583. shaka.util.Platform.cacheExpirationTimer_ = null;
  584. /** @private {HTMLMediaElement} */
  585. shaka.util.Platform.cachedMediaElement_ = null;