Source: ui/controls.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.Controls');
  7. goog.provide('shaka.ui.ControlsPanel');
  8. goog.require('goog.asserts');
  9. goog.require('shaka.ads.Utils');
  10. goog.require('shaka.cast.CastProxy');
  11. goog.require('shaka.log');
  12. goog.require('shaka.ui.AdCounter');
  13. goog.require('shaka.ui.AdPosition');
  14. goog.require('shaka.ui.BigPlayButton');
  15. goog.require('shaka.ui.ContextMenu');
  16. goog.require('shaka.ui.HiddenFastForwardButton');
  17. goog.require('shaka.ui.HiddenRewindButton');
  18. goog.require('shaka.ui.Locales');
  19. goog.require('shaka.ui.Localization');
  20. goog.require('shaka.ui.SeekBar');
  21. goog.require('shaka.ui.SkipAdButton');
  22. goog.require('shaka.ui.Utils');
  23. goog.require('shaka.ui.VRManager');
  24. goog.require('shaka.util.Dom');
  25. goog.require('shaka.util.EventManager');
  26. goog.require('shaka.util.FakeEvent');
  27. goog.require('shaka.util.FakeEventTarget');
  28. goog.require('shaka.util.IDestroyable');
  29. goog.require('shaka.util.Platform');
  30. goog.require('shaka.util.Timer');
  31. goog.requireType('shaka.Player');
  32. /**
  33. * A container for custom video controls.
  34. * @implements {shaka.util.IDestroyable}
  35. * @export
  36. */
  37. shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
  38. /**
  39. * @param {!shaka.Player} player
  40. * @param {!HTMLElement} videoContainer
  41. * @param {!HTMLMediaElement} video
  42. * @param {?HTMLCanvasElement} vrCanvas
  43. * @param {shaka.extern.UIConfiguration} config
  44. */
  45. constructor(player, videoContainer, video, vrCanvas, config) {
  46. super();
  47. /** @private {boolean} */
  48. this.enabled_ = true;
  49. /** @private {shaka.extern.UIConfiguration} */
  50. this.config_ = config;
  51. /** @private {shaka.cast.CastProxy} */
  52. this.castProxy_ = new shaka.cast.CastProxy(
  53. video, player, this.config_.castReceiverAppId,
  54. this.config_.castAndroidReceiverCompatible);
  55. /** @private {boolean} */
  56. this.castAllowed_ = true;
  57. /** @private {HTMLMediaElement} */
  58. this.video_ = this.castProxy_.getVideo();
  59. /** @private {HTMLMediaElement} */
  60. this.localVideo_ = video;
  61. /** @private {shaka.Player} */
  62. this.player_ = this.castProxy_.getPlayer();
  63. /** @private {shaka.Player} */
  64. this.localPlayer_ = player;
  65. /** @private {!HTMLElement} */
  66. this.videoContainer_ = videoContainer;
  67. /** @private {?HTMLCanvasElement} */
  68. this.vrCanvas_ = vrCanvas;
  69. /** @private {shaka.extern.IAdManager} */
  70. this.adManager_ = this.player_.getAdManager();
  71. /** @private {?shaka.extern.IAd} */
  72. this.ad_ = null;
  73. /** @private {?shaka.extern.IUISeekBar} */
  74. this.seekBar_ = null;
  75. /** @private {boolean} */
  76. this.isSeeking_ = false;
  77. /** @private {!Array<!HTMLElement>} */
  78. this.menus_ = [];
  79. /**
  80. * Individual controls which, when hovered or tab-focused, will force the
  81. * controls to be shown.
  82. * @private {!Array<!Element>}
  83. */
  84. this.showOnHoverControls_ = [];
  85. /** @private {boolean} */
  86. this.recentMouseMovement_ = false;
  87. /**
  88. * This timer is used to detect when the user has stopped moving the mouse
  89. * and we should fade out the ui.
  90. *
  91. * @private {shaka.util.Timer}
  92. */
  93. this.mouseStillTimer_ = new shaka.util.Timer(() => {
  94. this.onMouseStill_();
  95. });
  96. /**
  97. * This timer is used to delay the fading of the UI.
  98. *
  99. * @private {shaka.util.Timer}
  100. */
  101. this.fadeControlsTimer_ = new shaka.util.Timer(() => {
  102. this.controlsContainer_.removeAttribute('shown');
  103. if (this.contextMenu_) {
  104. this.contextMenu_.closeMenu();
  105. }
  106. // If there's an overflow menu open, keep it this way for a couple of
  107. // seconds in case a user immediately initiates another mouse move to
  108. // interact with the menus. If that didn't happen, go ahead and hide
  109. // the menus.
  110. this.hideSettingsMenusTimer_.tickAfter(
  111. /* seconds= */ this.config_.closeMenusDelay);
  112. });
  113. /**
  114. * This timer will be used to hide all settings menus. When the timer ticks
  115. * it will force all controls to invisible.
  116. *
  117. * Rather than calling the callback directly, |Controls| will always call it
  118. * through the timer to avoid conflicts.
  119. *
  120. * @private {shaka.util.Timer}
  121. */
  122. this.hideSettingsMenusTimer_ = new shaka.util.Timer(() => {
  123. for (const menu of this.menus_) {
  124. shaka.ui.Utils.setDisplay(menu, /* visible= */ false);
  125. }
  126. });
  127. /**
  128. * This timer is used to regularly update the time and seek range elements
  129. * so that we are communicating the current state as accurately as possibly.
  130. *
  131. * Unlike the other timers, this timer does not "own" the callback because
  132. * this timer is acting like a heartbeat.
  133. *
  134. * @private {shaka.util.Timer}
  135. */
  136. this.timeAndSeekRangeTimer_ = new shaka.util.Timer(() => {
  137. // Suppress timer-based updates if the controls are hidden.
  138. if (this.isOpaque()) {
  139. this.updateTimeAndSeekRange_();
  140. }
  141. });
  142. /** @private {?number} */
  143. this.lastTouchEventTime_ = null;
  144. /** @private {!Array<!shaka.extern.IUIElement>} */
  145. this.elements_ = [];
  146. /** @private {shaka.ui.Localization} */
  147. this.localization_ = shaka.ui.Controls.createLocalization_();
  148. /** @private {shaka.util.EventManager} */
  149. this.eventManager_ = new shaka.util.EventManager();
  150. /** @private {?shaka.ui.VRManager} */
  151. this.vr_ = null;
  152. // Configure and create the layout of the controls
  153. this.configure(this.config_);
  154. this.addEventListeners_();
  155. this.setupMediaSession_();
  156. /**
  157. * The pressed keys set is used to record which keys are currently pressed
  158. * down, so we can know what keys are pressed at the same time.
  159. * Used by the focusInsideOverflowMenu_() function.
  160. * @private {!Set<string>}
  161. */
  162. this.pressedKeys_ = new Set();
  163. // We might've missed a caststatuschanged event from the proxy between
  164. // the controls creation and initializing. Run onCastStatusChange_()
  165. // to ensure we have the casting state right.
  166. this.onCastStatusChange_();
  167. // Start this timer after we are finished initializing everything,
  168. this.timeAndSeekRangeTimer_.tickEvery(this.config_.refreshTickInSeconds);
  169. this.eventManager_.listen(this.localization_,
  170. shaka.ui.Localization.LOCALE_CHANGED, (e) => {
  171. const locale = e['locales'][0];
  172. this.adManager_.setLocale(locale);
  173. this.videoContainer_.setAttribute('lang', locale);
  174. });
  175. this.adManager_.initInterstitial(
  176. this.getClientSideAdContainer(), this.localPlayer_, this.localVideo_);
  177. }
  178. /**
  179. * @override
  180. * @export
  181. */
  182. async destroy() {
  183. if (document.pictureInPictureElement == this.localVideo_) {
  184. await document.exitPictureInPicture();
  185. }
  186. if (this.eventManager_) {
  187. this.eventManager_.release();
  188. this.eventManager_ = null;
  189. }
  190. if (this.mouseStillTimer_) {
  191. this.mouseStillTimer_.stop();
  192. this.mouseStillTimer_ = null;
  193. }
  194. if (this.fadeControlsTimer_) {
  195. this.fadeControlsTimer_.stop();
  196. this.fadeControlsTimer_ = null;
  197. }
  198. if (this.hideSettingsMenusTimer_) {
  199. this.hideSettingsMenusTimer_.stop();
  200. this.hideSettingsMenusTimer_ = null;
  201. }
  202. if (this.timeAndSeekRangeTimer_) {
  203. this.timeAndSeekRangeTimer_.stop();
  204. this.timeAndSeekRangeTimer_ = null;
  205. }
  206. if (this.vr_) {
  207. this.vr_.release();
  208. this.vr_ = null;
  209. }
  210. // Important! Release all child elements before destroying the cast proxy
  211. // or player. This makes sure those destructions will not trigger event
  212. // listeners in the UI which would then invoke the cast proxy or player.
  213. this.releaseChildElements_();
  214. if (this.controlsContainer_) {
  215. this.videoContainer_.removeChild(this.controlsContainer_);
  216. this.controlsContainer_ = null;
  217. }
  218. if (this.castProxy_) {
  219. await this.castProxy_.destroy();
  220. this.castProxy_ = null;
  221. }
  222. if (this.spinnerContainer_) {
  223. this.videoContainer_.removeChild(this.spinnerContainer_);
  224. this.spinnerContainer_ = null;
  225. }
  226. if (this.clientAdContainer_) {
  227. this.videoContainer_.removeChild(this.clientAdContainer_);
  228. this.clientAdContainer_ = null;
  229. }
  230. if (this.localPlayer_) {
  231. await this.localPlayer_.destroy();
  232. this.localPlayer_ = null;
  233. }
  234. this.player_ = null;
  235. this.localVideo_ = null;
  236. this.video_ = null;
  237. this.localization_ = null;
  238. this.pressedKeys_.clear();
  239. this.removeMediaSession_();
  240. // FakeEventTarget implements IReleasable
  241. super.release();
  242. }
  243. /** @private */
  244. releaseChildElements_() {
  245. for (const element of this.elements_) {
  246. element.release();
  247. }
  248. this.elements_ = [];
  249. }
  250. /**
  251. * @param {string} name
  252. * @param {!shaka.extern.IUIElement.Factory} factory
  253. * @export
  254. */
  255. static registerElement(name, factory) {
  256. shaka.ui.ControlsPanel.elementNamesToFactories_.set(name, factory);
  257. }
  258. /**
  259. * @param {!shaka.extern.IUISeekBar.Factory} factory
  260. * @export
  261. */
  262. static registerSeekBar(factory) {
  263. shaka.ui.ControlsPanel.seekBarFactory_ = factory;
  264. }
  265. /**
  266. * This allows the application to inhibit casting.
  267. *
  268. * @param {boolean} allow
  269. * @export
  270. */
  271. allowCast(allow) {
  272. this.castAllowed_ = allow;
  273. this.onCastStatusChange_();
  274. }
  275. /**
  276. * Used by the application to notify the controls that a load operation is
  277. * complete. This allows the controls to recalculate play/paused state, which
  278. * is important for platforms like Android where autoplay is disabled.
  279. * @export
  280. */
  281. loadComplete() {
  282. // If we are on Android or if autoplay is false, video.paused should be
  283. // true. Otherwise, video.paused is false and the content is autoplaying.
  284. this.onPlayStateChange_();
  285. }
  286. /**
  287. * @param {!shaka.extern.UIConfiguration} config
  288. * @export
  289. */
  290. configure(config) {
  291. this.config_ = config;
  292. this.castProxy_.changeReceiverId(config.castReceiverAppId,
  293. config.castAndroidReceiverCompatible);
  294. // Deconstruct the old layout if applicable
  295. if (this.seekBar_) {
  296. this.seekBar_ = null;
  297. }
  298. if (this.playButton_) {
  299. this.playButton_ = null;
  300. }
  301. if (this.contextMenu_) {
  302. this.contextMenu_ = null;
  303. }
  304. if (this.vr_) {
  305. this.vr_.configure(config);
  306. }
  307. if (this.controlsContainer_) {
  308. shaka.util.Dom.removeAllChildren(this.controlsContainer_);
  309. this.releaseChildElements_();
  310. } else {
  311. this.addControlsContainer_();
  312. // The client-side ad container is only created once, and is never
  313. // re-created or uprooted in the DOM, even when the DOM is re-created,
  314. // since that seemingly breaks the IMA SDK.
  315. this.addClientAdContainer_();
  316. goog.asserts.assert(
  317. this.controlsContainer_, 'Should have a controlsContainer_!');
  318. goog.asserts.assert(this.localVideo_, 'Should have a localVideo_!');
  319. goog.asserts.assert(this.player_, 'Should have a player_!');
  320. this.vr_ = new shaka.ui.VRManager(this.controlsContainer_, this.vrCanvas_,
  321. this.localVideo_, this.player_, this.config_);
  322. }
  323. // Create the new layout
  324. this.createDOM_();
  325. // Init the play state
  326. this.onPlayStateChange_();
  327. // Elements that should not propagate clicks (controls panel, menus)
  328. const noPropagationElements = this.videoContainer_.getElementsByClassName(
  329. 'shaka-no-propagation');
  330. for (const element of noPropagationElements) {
  331. const cb = (event) => event.stopPropagation();
  332. this.eventManager_.listen(element, 'click', cb);
  333. this.eventManager_.listen(element, 'dblclick', cb);
  334. if (navigator.maxTouchPoints > 0) {
  335. const touchCb = (event) => {
  336. if (!this.isOpaque()) {
  337. return;
  338. }
  339. event.stopPropagation();
  340. };
  341. this.eventManager_.listen(element, 'touchend', touchCb);
  342. }
  343. }
  344. }
  345. /**
  346. * Enable or disable the custom controls. Enabling disables native
  347. * browser controls.
  348. *
  349. * @param {boolean} enabled
  350. * @export
  351. */
  352. setEnabledShakaControls(enabled) {
  353. this.enabled_ = enabled;
  354. if (enabled) {
  355. this.videoContainer_.setAttribute('shaka-controls', 'true');
  356. // If we're hiding native controls, make sure the video element itself is
  357. // not tab-navigable. Our custom controls will still be tab-navigable.
  358. this.localVideo_.tabIndex = -1;
  359. this.localVideo_.controls = false;
  360. } else {
  361. this.videoContainer_.removeAttribute('shaka-controls');
  362. }
  363. // The effects of play state changes are inhibited while showing native
  364. // browser controls. Recalculate that state now.
  365. this.onPlayStateChange_();
  366. }
  367. /**
  368. * Enable or disable native browser controls. Enabling disables shaka
  369. * controls.
  370. *
  371. * @param {boolean} enabled
  372. * @export
  373. */
  374. setEnabledNativeControls(enabled) {
  375. // If we enable the native controls, the element must be tab-navigable.
  376. // If we disable the native controls, we want to make sure that the video
  377. // element itself is not tab-navigable, so that the element is skipped over
  378. // when tabbing through the page.
  379. this.localVideo_.controls = enabled;
  380. this.localVideo_.tabIndex = enabled ? 0 : -1;
  381. if (enabled) {
  382. this.setEnabledShakaControls(false);
  383. }
  384. }
  385. /**
  386. * @export
  387. * @return {?shaka.extern.IAd}
  388. */
  389. getAd() {
  390. return this.ad_;
  391. }
  392. /**
  393. * @export
  394. * @return {shaka.cast.CastProxy}
  395. */
  396. getCastProxy() {
  397. return this.castProxy_;
  398. }
  399. /**
  400. * @return {shaka.ui.Localization}
  401. * @export
  402. */
  403. getLocalization() {
  404. return this.localization_;
  405. }
  406. /**
  407. * @return {!HTMLElement}
  408. * @export
  409. */
  410. getVideoContainer() {
  411. return this.videoContainer_;
  412. }
  413. /**
  414. * @return {HTMLMediaElement}
  415. * @export
  416. */
  417. getVideo() {
  418. return this.video_;
  419. }
  420. /**
  421. * @return {HTMLMediaElement}
  422. * @export
  423. */
  424. getLocalVideo() {
  425. return this.localVideo_;
  426. }
  427. /**
  428. * @return {shaka.Player}
  429. * @export
  430. */
  431. getPlayer() {
  432. return this.player_;
  433. }
  434. /**
  435. * @return {shaka.Player}
  436. * @export
  437. */
  438. getLocalPlayer() {
  439. return this.localPlayer_;
  440. }
  441. /**
  442. * @return {!HTMLElement}
  443. * @export
  444. */
  445. getControlsContainer() {
  446. goog.asserts.assert(
  447. this.controlsContainer_, 'No controls container after destruction!');
  448. return this.controlsContainer_;
  449. }
  450. /**
  451. * @return {!HTMLElement}
  452. * @export
  453. */
  454. getServerSideAdContainer() {
  455. return this.daiAdContainer_;
  456. }
  457. /**
  458. * @return {!HTMLElement}
  459. * @export
  460. */
  461. getClientSideAdContainer() {
  462. goog.asserts.assert(
  463. this.clientAdContainer_, 'No client ad container after destruction!');
  464. return this.clientAdContainer_;
  465. }
  466. /**
  467. * @return {!shaka.extern.UIConfiguration}
  468. * @export
  469. */
  470. getConfig() {
  471. return this.config_;
  472. }
  473. /**
  474. * @return {boolean}
  475. * @export
  476. */
  477. isSeeking() {
  478. return this.isSeeking_;
  479. }
  480. /**
  481. * @param {boolean} seeking
  482. * @export
  483. */
  484. setSeeking(seeking) {
  485. this.isSeeking_ = seeking;
  486. if (seeking) {
  487. this.mouseStillTimer_.stop();
  488. } else {
  489. this.mouseStillTimer_.tickAfter(/* seconds= */ 3);
  490. }
  491. }
  492. /**
  493. * @return {boolean}
  494. * @export
  495. */
  496. isCastAllowed() {
  497. return this.castAllowed_;
  498. }
  499. /**
  500. * @return {number}
  501. * @export
  502. */
  503. getDisplayTime() {
  504. return this.seekBar_ ? this.seekBar_.getValue() : this.video_.currentTime;
  505. }
  506. /**
  507. * @param {?number} time
  508. * @export
  509. */
  510. setLastTouchEventTime(time) {
  511. this.lastTouchEventTime_ = time;
  512. }
  513. /**
  514. * @return {boolean}
  515. * @export
  516. */
  517. anySettingsMenusAreOpen() {
  518. return this.menus_.some(
  519. (menu) => !menu.classList.contains('shaka-hidden'));
  520. }
  521. /** @export */
  522. hideSettingsMenus() {
  523. this.hideSettingsMenusTimer_.tickNow();
  524. }
  525. /**
  526. * @return {boolean}
  527. * @private
  528. */
  529. shouldUseDocumentFullscreen_() {
  530. if (!document.fullscreenEnabled) {
  531. return false;
  532. }
  533. // When the preferVideoFullScreenInVisionOS configuration value applies,
  534. // we avoid using document fullscreen, even if it is available.
  535. const video = /** @type {HTMLVideoElement} */(this.localVideo_);
  536. if (video.webkitSupportsFullscreen) {
  537. if (this.config_.preferVideoFullScreenInVisionOS &&
  538. shaka.util.Platform.isVisionOS()) {
  539. return false;
  540. }
  541. }
  542. return true;
  543. }
  544. /**
  545. * @return {boolean}
  546. * @export
  547. */
  548. isFullScreenSupported() {
  549. if (this.shouldUseDocumentFullscreen_()) {
  550. return true;
  551. }
  552. if (!this.ad_ || !this.ad_.isUsingAnotherMediaElement()) {
  553. const video = /** @type {HTMLVideoElement} */(this.localVideo_);
  554. if (video.webkitSupportsFullscreen) {
  555. return true;
  556. }
  557. }
  558. return false;
  559. }
  560. /**
  561. * @return {boolean}
  562. * @export
  563. */
  564. isFullScreenEnabled() {
  565. if (this.shouldUseDocumentFullscreen_()) {
  566. return !!document.fullscreenElement;
  567. }
  568. const video = /** @type {HTMLVideoElement} */(this.localVideo_);
  569. if (video.webkitSupportsFullscreen) {
  570. return video.webkitDisplayingFullscreen;
  571. }
  572. return false;
  573. }
  574. /** @private */
  575. async enterFullScreen_() {
  576. try {
  577. if (this.shouldUseDocumentFullscreen_()) {
  578. if (document.pictureInPictureElement) {
  579. await document.exitPictureInPicture();
  580. }
  581. const fullScreenElement = this.config_.fullScreenElement;
  582. await fullScreenElement.requestFullscreen({navigationUI: 'hide'});
  583. if (this.config_.forceLandscapeOnFullscreen && screen.orientation) {
  584. // Locking to 'landscape' should let it be either
  585. // 'landscape-primary' or 'landscape-secondary' as appropriate.
  586. // We ignore errors from this specific call, since it creates noise
  587. // on desktop otherwise.
  588. try {
  589. await screen.orientation.lock('landscape');
  590. } catch (error) {}
  591. }
  592. } else {
  593. const video = /** @type {HTMLVideoElement} */(this.localVideo_);
  594. if (video.webkitSupportsFullscreen) {
  595. video.webkitEnterFullscreen();
  596. }
  597. }
  598. } catch (error) {
  599. // Entering fullscreen can fail without user interaction.
  600. this.dispatchEvent(new shaka.util.FakeEvent(
  601. 'error', (new Map()).set('detail', error)));
  602. }
  603. }
  604. /** @private */
  605. async exitFullScreen_() {
  606. if (this.shouldUseDocumentFullscreen_()) {
  607. if (screen.orientation) {
  608. screen.orientation.unlock();
  609. }
  610. await document.exitFullscreen();
  611. } else {
  612. const video = /** @type {HTMLVideoElement} */(this.localVideo_);
  613. if (video.webkitSupportsFullscreen) {
  614. video.webkitExitFullscreen();
  615. }
  616. }
  617. }
  618. /** @export */
  619. async toggleFullScreen() {
  620. if (this.isFullScreenEnabled()) {
  621. await this.exitFullScreen_();
  622. } else {
  623. await this.enterFullScreen_();
  624. }
  625. }
  626. /**
  627. * @return {boolean}
  628. * @export
  629. */
  630. isPiPAllowed() {
  631. if (this.castProxy_.isCasting()) {
  632. return false;
  633. }
  634. if ('documentPictureInPicture' in window &&
  635. this.config_.preferDocumentPictureInPicture) {
  636. const video = /** @type {HTMLVideoElement} */(this.localVideo_);
  637. return !video.disablePictureInPicture;
  638. }
  639. if (document.pictureInPictureEnabled) {
  640. const video = /** @type {HTMLVideoElement} */(this.localVideo_);
  641. return !video.disablePictureInPicture;
  642. }
  643. return false;
  644. }
  645. /**
  646. * @return {boolean}
  647. * @export
  648. */
  649. isPiPEnabled() {
  650. if ('documentPictureInPicture' in window &&
  651. this.config_.preferDocumentPictureInPicture) {
  652. return !!window.documentPictureInPicture.window;
  653. } else {
  654. return !!document.pictureInPictureElement;
  655. }
  656. }
  657. /** @export */
  658. async togglePiP() {
  659. try {
  660. if ('documentPictureInPicture' in window &&
  661. this.config_.preferDocumentPictureInPicture) {
  662. await this.toggleDocumentPictureInPicture_();
  663. } else if (!document.pictureInPictureElement) {
  664. // If you were fullscreen, leave fullscreen first.
  665. if (document.fullscreenElement) {
  666. document.exitFullscreen();
  667. }
  668. const video = /** @type {HTMLVideoElement} */(this.localVideo_);
  669. await video.requestPictureInPicture();
  670. } else {
  671. await document.exitPictureInPicture();
  672. }
  673. } catch (error) {
  674. this.dispatchEvent(new shaka.util.FakeEvent(
  675. 'error', (new Map()).set('detail', error)));
  676. }
  677. }
  678. /**
  679. * The Document Picture-in-Picture API makes it possible to open an
  680. * always-on-top window that can be populated with arbitrary HTML content.
  681. * https://developer.chrome.com/docs/web-platform/document-picture-in-picture
  682. * @private
  683. */
  684. async toggleDocumentPictureInPicture_() {
  685. // Close Picture-in-Picture window if any.
  686. if (window.documentPictureInPicture.window) {
  687. window.documentPictureInPicture.window.close();
  688. return;
  689. }
  690. // Open a Picture-in-Picture window.
  691. const pipPlayer = this.videoContainer_;
  692. const rectPipPlayer = pipPlayer.getBoundingClientRect();
  693. const pipWindow = await window.documentPictureInPicture.requestWindow({
  694. width: rectPipPlayer.width,
  695. height: rectPipPlayer.height,
  696. });
  697. // Copy style sheets to the Picture-in-Picture window.
  698. this.copyStyleSheetsToWindow_(pipWindow);
  699. // Add placeholder for the player.
  700. const parentPlayer = pipPlayer.parentNode || document.body;
  701. const placeholder = this.videoContainer_.cloneNode(true);
  702. placeholder.style.visibility = 'hidden';
  703. placeholder.style.height = getComputedStyle(pipPlayer).height;
  704. parentPlayer.appendChild(placeholder);
  705. // Make sure player fits in the Picture-in-Picture window.
  706. const styles = document.createElement('style');
  707. styles.append(`[data-shaka-player-container] {
  708. width: 100% !important; max-height: 100%}`);
  709. pipWindow.document.head.append(styles);
  710. // Move player to the Picture-in-Picture window.
  711. pipWindow.document.body.append(pipPlayer);
  712. // Listen for the PiP closing event to move the player back.
  713. this.eventManager_.listenOnce(pipWindow, 'pagehide', () => {
  714. placeholder.replaceWith(/** @type {!Node} */(pipPlayer));
  715. });
  716. }
  717. /** @private */
  718. copyStyleSheetsToWindow_(win) {
  719. const styleSheets = /** @type {!Iterable<*>} */(document.styleSheets);
  720. const allCSS = [...styleSheets]
  721. .map((sheet) => {
  722. try {
  723. return [...sheet.cssRules].map((rule) => rule.cssText).join('');
  724. } catch (e) {
  725. const link = /** @type {!HTMLLinkElement} */(
  726. document.createElement('link'));
  727. link.rel = 'stylesheet';
  728. link.type = sheet.type;
  729. link.media = sheet.media;
  730. link.href = sheet.href;
  731. win.document.head.appendChild(link);
  732. }
  733. return '';
  734. })
  735. .filter(Boolean)
  736. .join('\n');
  737. const style = document.createElement('style');
  738. style.textContent = allCSS;
  739. win.document.head.appendChild(style);
  740. }
  741. /** @export */
  742. showAdUI() {
  743. shaka.ui.Utils.setDisplay(this.adPanel_, true);
  744. shaka.ui.Utils.setDisplay(this.clientAdContainer_, true);
  745. if (this.ad_.hasCustomClick()) {
  746. this.controlsContainer_.setAttribute('ad-active', 'true');
  747. } else {
  748. this.controlsContainer_.removeAttribute('ad-active');
  749. }
  750. }
  751. /** @export */
  752. hideAdUI() {
  753. shaka.ui.Utils.setDisplay(this.adPanel_, false);
  754. shaka.ui.Utils.setDisplay(this.clientAdContainer_, false);
  755. this.controlsContainer_.removeAttribute('ad-active');
  756. }
  757. /**
  758. * Play or pause the current presentation.
  759. */
  760. playPausePresentation() {
  761. if (!this.enabled_) {
  762. return;
  763. }
  764. if (this.ad_) {
  765. this.playPauseAd();
  766. if (this.ad_.isLinear()) {
  767. return;
  768. }
  769. }
  770. if (!this.video_.duration) {
  771. // Can't play yet. Ignore.
  772. return;
  773. }
  774. if (this.presentationIsPaused()) {
  775. // If we are at the end, go back to the beginning.
  776. if (this.player_.isEnded()) {
  777. this.video_.currentTime = this.player_.seekRange().start;
  778. }
  779. this.video_.play();
  780. } else {
  781. this.video_.pause();
  782. }
  783. }
  784. /**
  785. * Play or pause the current ad.
  786. */
  787. playPauseAd() {
  788. if (this.ad_ && this.ad_.isPaused()) {
  789. this.ad_.play();
  790. } else if (this.ad_) {
  791. this.ad_.pause();
  792. }
  793. }
  794. /**
  795. * Return true if the presentation is paused.
  796. *
  797. * @return {boolean}
  798. */
  799. presentationIsPaused() {
  800. // The video element is in a paused state while seeking, but we don't count
  801. // that.
  802. return this.video_.paused && !this.isSeeking();
  803. }
  804. /** @private */
  805. createDOM_() {
  806. this.videoContainer_.classList.add('shaka-video-container');
  807. this.localVideo_.classList.add('shaka-video');
  808. this.addScrimContainer_();
  809. if (this.config_.addBigPlayButton) {
  810. this.addPlayButton_();
  811. }
  812. if (this.config_.customContextMenu) {
  813. this.addContextMenu_();
  814. }
  815. if (!this.spinnerContainer_) {
  816. this.addBufferingSpinner_();
  817. }
  818. if (this.config_.seekOnTaps) {
  819. this.addFastForwardButtonOnControlsContainer_();
  820. this.addRewindButtonOnControlsContainer_();
  821. }
  822. this.addDaiAdContainer_();
  823. this.addControlsButtonPanel_();
  824. this.menus_ = Array.from(
  825. this.videoContainer_.getElementsByClassName('shaka-settings-menu'));
  826. this.menus_.push(...Array.from(
  827. this.videoContainer_.getElementsByClassName('shaka-overflow-menu')));
  828. this.addSeekBar_();
  829. this.showOnHoverControls_ = Array.from(
  830. this.videoContainer_.getElementsByClassName(
  831. 'shaka-show-controls-on-mouse-over'));
  832. }
  833. /** @private */
  834. addControlsContainer_() {
  835. /** @private {HTMLElement} */
  836. this.controlsContainer_ = shaka.util.Dom.createHTMLElement('div');
  837. this.controlsContainer_.classList.add('shaka-controls-container');
  838. this.videoContainer_.appendChild(this.controlsContainer_);
  839. // Use our controls by default, without anyone calling
  840. // setEnabledShakaControls:
  841. this.videoContainer_.setAttribute('shaka-controls', 'true');
  842. this.eventManager_.listen(this.controlsContainer_, 'touchend', (e) => {
  843. this.onContainerTouch_(e);
  844. });
  845. this.eventManager_.listen(this.controlsContainer_, 'click', () => {
  846. this.onContainerClick();
  847. });
  848. this.eventManager_.listen(this.controlsContainer_, 'dblclick', () => {
  849. if (this.config_.doubleClickForFullscreen &&
  850. this.isFullScreenSupported()) {
  851. this.toggleFullScreen();
  852. }
  853. });
  854. }
  855. /** @private */
  856. addPlayButton_() {
  857. const playButtonContainer = shaka.util.Dom.createHTMLElement('div');
  858. playButtonContainer.classList.add('shaka-play-button-container');
  859. this.controlsContainer_.appendChild(playButtonContainer);
  860. /** @private {shaka.ui.BigPlayButton} */
  861. this.playButton_ =
  862. new shaka.ui.BigPlayButton(playButtonContainer, this);
  863. this.elements_.push(this.playButton_);
  864. }
  865. /** @private */
  866. addContextMenu_() {
  867. /** @private {shaka.ui.ContextMenu} */
  868. this.contextMenu_ =
  869. new shaka.ui.ContextMenu(this.controlsButtonPanel_, this);
  870. this.elements_.push(this.contextMenu_);
  871. }
  872. /** @private */
  873. addScrimContainer_() {
  874. // This is the container that gets styled by CSS to have the
  875. // black gradient scrim at the end of the controls.
  876. const scrimContainer = shaka.util.Dom.createHTMLElement('div');
  877. scrimContainer.classList.add('shaka-scrim-container');
  878. this.controlsContainer_.appendChild(scrimContainer);
  879. }
  880. /** @private */
  881. addAdControls_() {
  882. /** @private {!HTMLElement} */
  883. this.adPanel_ = shaka.util.Dom.createHTMLElement('div');
  884. this.adPanel_.classList.add('shaka-ad-controls');
  885. const showAdPanel = this.ad_ != null && this.ad_.isLinear();
  886. shaka.ui.Utils.setDisplay(this.adPanel_, showAdPanel);
  887. this.bottomControls_.appendChild(this.adPanel_);
  888. const adPosition = new shaka.ui.AdPosition(this.adPanel_, this);
  889. this.elements_.push(adPosition);
  890. const adCounter = new shaka.ui.AdCounter(this.adPanel_, this);
  891. this.elements_.push(adCounter);
  892. const skipButton = new shaka.ui.SkipAdButton(this.adPanel_, this);
  893. this.elements_.push(skipButton);
  894. }
  895. /** @private */
  896. addBufferingSpinner_() {
  897. /** @private {HTMLElement} */
  898. this.spinnerContainer_ = shaka.util.Dom.createHTMLElement('div');
  899. this.spinnerContainer_.classList.add('shaka-spinner-container');
  900. this.videoContainer_.appendChild(this.spinnerContainer_);
  901. const spinner = shaka.util.Dom.createHTMLElement('div');
  902. spinner.classList.add('shaka-spinner');
  903. this.spinnerContainer_.appendChild(spinner);
  904. // Svg elements have to be created with the svg xml namespace.
  905. const xmlns = 'http://www.w3.org/2000/svg';
  906. const svg =
  907. /** @type {!HTMLElement} */(document.createElementNS(xmlns, 'svg'));
  908. svg.classList.add('shaka-spinner-svg');
  909. svg.setAttribute('viewBox', '0 0 30 30');
  910. spinner.appendChild(svg);
  911. // These coordinates are relative to the SVG viewBox above. This is
  912. // distinct from the actual display size in the page, since the "S" is for
  913. // "Scalable." The radius of 14.5 is so that the edges of the 1-px-wide
  914. // stroke will touch the edges of the viewBox.
  915. const spinnerCircle = document.createElementNS(xmlns, 'circle');
  916. spinnerCircle.classList.add('shaka-spinner-path');
  917. spinnerCircle.setAttribute('cx', '15');
  918. spinnerCircle.setAttribute('cy', '15');
  919. spinnerCircle.setAttribute('r', '14.5');
  920. spinnerCircle.setAttribute('fill', 'none');
  921. spinnerCircle.setAttribute('stroke-width', '1');
  922. spinnerCircle.setAttribute('stroke-miterlimit', '10');
  923. svg.appendChild(spinnerCircle);
  924. }
  925. /**
  926. * Add fast-forward button on Controls container for moving video some
  927. * seconds ahead when the video is tapped more than once, video seeks ahead
  928. * some seconds for every extra tap.
  929. * @private
  930. */
  931. addFastForwardButtonOnControlsContainer_() {
  932. const hiddenFastForwardContainer = shaka.util.Dom.createHTMLElement('div');
  933. hiddenFastForwardContainer.classList.add(
  934. 'shaka-hidden-fast-forward-container');
  935. this.controlsContainer_.appendChild(hiddenFastForwardContainer);
  936. /** @private {shaka.ui.HiddenFastForwardButton} */
  937. this.hiddenFastForwardButton_ =
  938. new shaka.ui.HiddenFastForwardButton(hiddenFastForwardContainer, this);
  939. this.elements_.push(this.hiddenFastForwardButton_);
  940. }
  941. /**
  942. * Add Rewind button on Controls container for moving video some seconds
  943. * behind when the video is tapped more than once, video seeks behind some
  944. * seconds for every extra tap.
  945. * @private
  946. */
  947. addRewindButtonOnControlsContainer_() {
  948. const hiddenRewindContainer = shaka.util.Dom.createHTMLElement('div');
  949. hiddenRewindContainer.classList.add(
  950. 'shaka-hidden-rewind-container');
  951. this.controlsContainer_.appendChild(hiddenRewindContainer);
  952. /** @private {shaka.ui.HiddenRewindButton} */
  953. this.hiddenRewindButton_ =
  954. new shaka.ui.HiddenRewindButton(hiddenRewindContainer, this);
  955. this.elements_.push(this.hiddenRewindButton_);
  956. }
  957. /** @private */
  958. addControlsButtonPanel_() {
  959. /** @private {!HTMLElement} */
  960. this.bottomControls_ = shaka.util.Dom.createHTMLElement('div');
  961. this.bottomControls_.classList.add('shaka-bottom-controls');
  962. this.bottomControls_.classList.add('shaka-no-propagation');
  963. this.controlsContainer_.appendChild(this.bottomControls_);
  964. // Overflow menus are supposed to hide once you click elsewhere
  965. // on the page. The click event listener on window ensures that.
  966. // However, clicks on the bottom controls don't propagate to the container,
  967. // so we have to explicitly hide the menus onclick here.
  968. this.eventManager_.listen(this.bottomControls_, 'click', (e) => {
  969. // We explicitly deny this measure when clicking on buttons that
  970. // open submenus in the control panel.
  971. if (!e.target['closest']('.shaka-overflow-button')) {
  972. this.hideSettingsMenus();
  973. }
  974. });
  975. this.addAdControls_();
  976. /** @private {!HTMLElement} */
  977. this.controlsButtonPanel_ = shaka.util.Dom.createHTMLElement('div');
  978. this.controlsButtonPanel_.classList.add('shaka-controls-button-panel');
  979. this.controlsButtonPanel_.classList.add(
  980. 'shaka-show-controls-on-mouse-over');
  981. if (this.config_.enableTooltips) {
  982. this.controlsButtonPanel_.classList.add('shaka-tooltips-on');
  983. }
  984. this.bottomControls_.appendChild(this.controlsButtonPanel_);
  985. // Create the elements specified by controlPanelElements
  986. for (const name of this.config_.controlPanelElements) {
  987. if (shaka.ui.ControlsPanel.elementNamesToFactories_.get(name)) {
  988. const factory =
  989. shaka.ui.ControlsPanel.elementNamesToFactories_.get(name);
  990. const element = factory.create(this.controlsButtonPanel_, this);
  991. this.elements_.push(element);
  992. } else {
  993. shaka.log.alwaysWarn('Unrecognized control panel element requested:',
  994. name);
  995. }
  996. }
  997. }
  998. /**
  999. * Adds a container for server side ad UI with IMA SDK.
  1000. *
  1001. * @private
  1002. */
  1003. addDaiAdContainer_() {
  1004. /** @private {!HTMLElement} */
  1005. this.daiAdContainer_ = shaka.util.Dom.createHTMLElement('div');
  1006. this.daiAdContainer_.classList.add('shaka-server-side-ad-container');
  1007. this.controlsContainer_.appendChild(this.daiAdContainer_);
  1008. }
  1009. /**
  1010. * Adds a seekbar depending on the configuration.
  1011. * By default an instance of shaka.ui.SeekBar is created
  1012. * This behaviour can be overridden by providing a SeekBar factory using the
  1013. * registerSeekBarFactory function.
  1014. *
  1015. * @private
  1016. */
  1017. addSeekBar_() {
  1018. if (this.config_.addSeekBar) {
  1019. this.seekBar_ = shaka.ui.ControlsPanel.seekBarFactory_.create(
  1020. this.bottomControls_, this);
  1021. this.elements_.push(this.seekBar_);
  1022. } else {
  1023. // Settings menus need to be positioned lower if the seekbar is absent.
  1024. for (const menu of this.menus_) {
  1025. menu.classList.add('shaka-low-position');
  1026. }
  1027. }
  1028. }
  1029. /**
  1030. * Adds a container for client side ad UI with IMA SDK.
  1031. *
  1032. * @private
  1033. */
  1034. addClientAdContainer_() {
  1035. /** @private {HTMLElement} */
  1036. this.clientAdContainer_ = shaka.util.Dom.createHTMLElement('div');
  1037. this.clientAdContainer_.classList.add('shaka-client-side-ad-container');
  1038. shaka.ui.Utils.setDisplay(this.clientAdContainer_, false);
  1039. this.eventManager_.listen(this.clientAdContainer_, 'click', () => {
  1040. this.onContainerClick();
  1041. });
  1042. this.videoContainer_.appendChild(this.clientAdContainer_);
  1043. }
  1044. /**
  1045. * Adds static event listeners. This should only add event listeners to
  1046. * things that don't change (e.g. Player). Dynamic elements (e.g. controls)
  1047. * should have their event listeners added when they are created.
  1048. *
  1049. * @private
  1050. */
  1051. addEventListeners_() {
  1052. this.eventManager_.listen(this.player_, 'buffering', () => {
  1053. this.onBufferingStateChange_();
  1054. });
  1055. // Set the initial state, as well.
  1056. this.onBufferingStateChange_();
  1057. // Listen for key down events to detect tab and enable outline
  1058. // for focused elements.
  1059. this.eventManager_.listen(window, 'keydown', (e) => {
  1060. this.onWindowKeyDown_(/** @type {!KeyboardEvent} */(e));
  1061. });
  1062. // Listen for click events to dismiss the settings menus.
  1063. this.eventManager_.listen(window, 'click', () => this.hideSettingsMenus());
  1064. // Avoid having multiple submenus open at the same time.
  1065. this.eventManager_.listen(
  1066. this, 'submenuopen', () => {
  1067. this.hideSettingsMenus();
  1068. });
  1069. this.eventManager_.listen(this.video_, 'play', () => {
  1070. this.onPlayStateChange_();
  1071. });
  1072. this.eventManager_.listen(this.video_, 'pause', () => {
  1073. this.onPlayStateChange_();
  1074. });
  1075. this.eventManager_.listen(this.videoContainer_, 'mousemove', (e) => {
  1076. this.onMouseMove_(e);
  1077. });
  1078. this.eventManager_.listen(this.videoContainer_, 'touchmove', (e) => {
  1079. this.onMouseMove_(e);
  1080. }, {passive: true});
  1081. this.eventManager_.listen(this.videoContainer_, 'touchend', (e) => {
  1082. this.onMouseMove_(e);
  1083. }, {passive: true});
  1084. this.eventManager_.listen(this.videoContainer_, 'mouseleave', () => {
  1085. this.onMouseLeave_();
  1086. });
  1087. this.eventManager_.listen(this.castProxy_, 'caststatuschanged', () => {
  1088. this.onCastStatusChange_();
  1089. });
  1090. this.eventManager_.listen(this.vr_, 'vrstatuschanged', () => {
  1091. this.dispatchEvent(new shaka.util.FakeEvent('vrstatuschanged'));
  1092. });
  1093. this.eventManager_.listen(this.videoContainer_, 'keydown', (e) => {
  1094. this.onControlsKeyDown_(/** @type {!KeyboardEvent} */(e));
  1095. });
  1096. this.eventManager_.listen(this.videoContainer_, 'keyup', (e) => {
  1097. this.onControlsKeyUp_(/** @type {!KeyboardEvent} */(e));
  1098. });
  1099. this.eventManager_.listen(
  1100. this.adManager_, shaka.ads.Utils.AD_STARTED, (e) => {
  1101. this.ad_ = (/** @type {!Object} */ (e))['ad'];
  1102. this.showAdUI();
  1103. this.onBufferingStateChange_();
  1104. });
  1105. this.eventManager_.listen(
  1106. this.adManager_, shaka.ads.Utils.AD_STOPPED, () => {
  1107. this.ad_ = null;
  1108. this.hideAdUI();
  1109. this.onBufferingStateChange_();
  1110. });
  1111. if (screen.orientation) {
  1112. this.eventManager_.listen(screen.orientation, 'change', async () => {
  1113. await this.onScreenRotation_();
  1114. });
  1115. }
  1116. }
  1117. /**
  1118. * @private
  1119. */
  1120. setupMediaSession_() {
  1121. if (!this.config_.setupMediaSession || !navigator.mediaSession) {
  1122. return;
  1123. }
  1124. const addMediaSessionHandler = (type, callback) => {
  1125. try {
  1126. navigator.mediaSession.setActionHandler(type, (details) => {
  1127. callback(details);
  1128. });
  1129. } catch (error) {
  1130. shaka.log.debug(
  1131. `The "${type}" media session action is not supported.`);
  1132. }
  1133. };
  1134. const updatePositionState = () => {
  1135. if (this.ad_ && this.ad_.isLinear()) {
  1136. clearPositionState();
  1137. return;
  1138. }
  1139. const seekRange = this.player_.seekRange();
  1140. let duration = seekRange.end - seekRange.start;
  1141. const position = parseFloat(
  1142. (this.video_.currentTime - seekRange.start).toFixed(2));
  1143. if (this.player_.isLive() && Math.abs(duration - position) < 1) {
  1144. // Positive infinity indicates media without a defined end, such as a
  1145. // live stream.
  1146. duration = Infinity;
  1147. }
  1148. try {
  1149. navigator.mediaSession.setPositionState({
  1150. duration: Math.max(0, duration),
  1151. playbackRate: this.video_.playbackRate,
  1152. position: Math.max(0, position),
  1153. });
  1154. } catch (error) {
  1155. shaka.log.v2(
  1156. 'setPositionState in media session is not supported.');
  1157. }
  1158. };
  1159. const clearPositionState = () => {
  1160. try {
  1161. navigator.mediaSession.setPositionState();
  1162. } catch (error) {
  1163. shaka.log.v2(
  1164. 'setPositionState in media session is not supported.');
  1165. }
  1166. };
  1167. const commonHandler = (details) => {
  1168. const keyboardSeekDistance = this.config_.keyboardSeekDistance;
  1169. switch (details.action) {
  1170. case 'pause':
  1171. this.playPausePresentation();
  1172. break;
  1173. case 'play':
  1174. this.playPausePresentation();
  1175. break;
  1176. case 'seekbackward':
  1177. if (details.seekOffset && !isFinite(details.seekOffset)) {
  1178. break;
  1179. }
  1180. if (!this.ad_ || !this.ad_.isLinear()) {
  1181. this.seek_(this.seekBar_.getValue() -
  1182. (details.seekOffset || keyboardSeekDistance));
  1183. }
  1184. break;
  1185. case 'seekforward':
  1186. if (details.seekOffset && !isFinite(details.seekOffset)) {
  1187. break;
  1188. }
  1189. if (!this.ad_ || !this.ad_.isLinear()) {
  1190. this.seek_(this.seekBar_.getValue() +
  1191. (details.seekOffset || keyboardSeekDistance));
  1192. }
  1193. break;
  1194. case 'seekto':
  1195. if (details.seekTime && !isFinite(details.seekTime)) {
  1196. break;
  1197. }
  1198. if (!this.ad_ || !this.ad_.isLinear()) {
  1199. this.seek_(this.player_.seekRange().start + details.seekTime);
  1200. }
  1201. break;
  1202. case 'stop':
  1203. this.player_.unload();
  1204. break;
  1205. case 'enterpictureinpicture':
  1206. if (!this.ad_ || !this.ad_.isLinear()) {
  1207. this.togglePiP();
  1208. }
  1209. break;
  1210. }
  1211. };
  1212. addMediaSessionHandler('pause', commonHandler);
  1213. addMediaSessionHandler('play', commonHandler);
  1214. addMediaSessionHandler('seekbackward', commonHandler);
  1215. addMediaSessionHandler('seekforward', commonHandler);
  1216. addMediaSessionHandler('seekto', commonHandler);
  1217. addMediaSessionHandler('stop', commonHandler);
  1218. if ('documentPictureInPicture' in window ||
  1219. document.pictureInPictureEnabled) {
  1220. addMediaSessionHandler('enterpictureinpicture', commonHandler);
  1221. }
  1222. const playerLoaded = () => {
  1223. if (this.player_.isLive() || this.player_.seekRange().start != 0) {
  1224. updatePositionState();
  1225. this.eventManager_.listen(
  1226. this.video_, 'timeupdate', updatePositionState);
  1227. } else {
  1228. clearPositionState();
  1229. }
  1230. };
  1231. const playerUnloading = () => {
  1232. this.eventManager_.unlisten(
  1233. this.video_, 'timeupdate', updatePositionState);
  1234. };
  1235. if (this.player_.isFullyLoaded()) {
  1236. playerLoaded();
  1237. }
  1238. this.eventManager_.listen(this.player_, 'loaded', playerLoaded);
  1239. this.eventManager_.listen(this.player_, 'unloading', playerUnloading);
  1240. this.eventManager_.listen(this.player_, 'metadata', (event) => {
  1241. const payload = event['payload'];
  1242. if (!payload) {
  1243. return;
  1244. }
  1245. let title;
  1246. if (payload['key'] == 'TIT2' && payload['data']) {
  1247. title = payload['data'];
  1248. }
  1249. let imageUrl;
  1250. if (payload['key'] == 'APIC' && payload['mimeType'] == '-->') {
  1251. imageUrl = payload['data'];
  1252. }
  1253. if (title) {
  1254. let metadata = {
  1255. title: title,
  1256. artwork: [],
  1257. };
  1258. if (navigator.mediaSession.metadata) {
  1259. metadata = navigator.mediaSession.metadata;
  1260. metadata.title = title;
  1261. }
  1262. navigator.mediaSession.metadata = new MediaMetadata(metadata);
  1263. }
  1264. if (imageUrl) {
  1265. const video = /** @type {HTMLVideoElement} */ (this.localVideo_);
  1266. if (imageUrl != video.poster) {
  1267. video.poster = imageUrl;
  1268. }
  1269. let metadata = {
  1270. title: '',
  1271. artwork: [{src: imageUrl}],
  1272. };
  1273. if (navigator.mediaSession.metadata) {
  1274. metadata = navigator.mediaSession.metadata;
  1275. metadata.artwork = [{src: imageUrl}];
  1276. }
  1277. navigator.mediaSession.metadata = new MediaMetadata(metadata);
  1278. }
  1279. });
  1280. }
  1281. /**
  1282. * @private
  1283. */
  1284. removeMediaSession_() {
  1285. if (!this.config_.setupMediaSession || !navigator.mediaSession) {
  1286. return;
  1287. }
  1288. try {
  1289. navigator.mediaSession.setPositionState();
  1290. } catch (error) {}
  1291. const disableMediaSessionHandler = (type) => {
  1292. try {
  1293. navigator.mediaSession.setActionHandler(type, null);
  1294. } catch (error) {}
  1295. };
  1296. disableMediaSessionHandler('pause');
  1297. disableMediaSessionHandler('play');
  1298. disableMediaSessionHandler('seekbackward');
  1299. disableMediaSessionHandler('seekforward');
  1300. disableMediaSessionHandler('seekto');
  1301. disableMediaSessionHandler('stop');
  1302. disableMediaSessionHandler('enterpictureinpicture');
  1303. }
  1304. /**
  1305. * When a mobile device is rotated to landscape layout, and the video is
  1306. * loaded, make the demo app go into fullscreen.
  1307. * Similarly, exit fullscreen when the device is rotated to portrait layout.
  1308. * @private
  1309. */
  1310. async onScreenRotation_() {
  1311. if (!this.video_ ||
  1312. this.video_.readyState == 0 ||
  1313. this.castProxy_.isCasting() ||
  1314. !this.config_.enableFullscreenOnRotation ||
  1315. !this.isFullScreenSupported()) {
  1316. return;
  1317. }
  1318. if (screen.orientation.type.includes('landscape') &&
  1319. !this.isFullScreenEnabled()) {
  1320. await this.enterFullScreen_();
  1321. } else if (screen.orientation.type.includes('portrait') &&
  1322. this.isFullScreenEnabled()) {
  1323. await this.exitFullScreen_();
  1324. }
  1325. }
  1326. /**
  1327. * Hiding the cursor when the mouse stops moving seems to be the only
  1328. * decent UX in fullscreen mode. Since we can't use pure CSS for that,
  1329. * we use events both in and out of fullscreen mode.
  1330. * Showing the control bar when a key is pressed, and hiding it after some
  1331. * time.
  1332. * @param {!Event} event
  1333. * @private
  1334. */
  1335. onMouseMove_(event) {
  1336. // Disable blue outline for focused elements for mouse navigation.
  1337. if (event.type == 'mousemove') {
  1338. this.controlsContainer_.classList.remove('shaka-keyboard-navigation');
  1339. this.computeOpacity();
  1340. }
  1341. if (event.type == 'touchstart' || event.type == 'touchmove' ||
  1342. event.type == 'touchend' || event.type == 'keyup') {
  1343. this.lastTouchEventTime_ = Date.now();
  1344. } else if (this.lastTouchEventTime_ + 1000 < Date.now()) {
  1345. // It has been a while since the last touch event, this is probably a real
  1346. // mouse moving, so treat it like a mouse.
  1347. this.lastTouchEventTime_ = null;
  1348. }
  1349. // When there is a touch, we can get a 'mousemove' event after touch events.
  1350. // This should be treated as part of the touch, which has already been
  1351. // handled.
  1352. if (this.lastTouchEventTime_ && event.type == 'mousemove') {
  1353. return;
  1354. }
  1355. // Use the cursor specified in the CSS file.
  1356. this.videoContainer_.classList.remove('no-cursor');
  1357. this.recentMouseMovement_ = true;
  1358. // Make sure we are not about to hide the settings menus and then force them
  1359. // open.
  1360. this.hideSettingsMenusTimer_.stop();
  1361. if (!this.isOpaque()) {
  1362. // Only update the time and seek range on mouse movement if it's the very
  1363. // first movement and we're about to show the controls. Otherwise, the
  1364. // seek bar will be updated much more rapidly during mouse movement. Do
  1365. // this right before making it visible.
  1366. this.updateTimeAndSeekRange_();
  1367. this.computeOpacity();
  1368. }
  1369. // Hide the cursor when the mouse stops moving.
  1370. // Only applies while the cursor is over the video container.
  1371. this.mouseStillTimer_.stop();
  1372. // Only start a timeout on 'touchend' or for 'mousemove' with no touch
  1373. // events.
  1374. if (event.type == 'touchend' ||
  1375. event.type == 'keyup'|| !this.lastTouchEventTime_) {
  1376. this.mouseStillTimer_.tickAfter(/* seconds= */ 3);
  1377. }
  1378. }
  1379. /** @private */
  1380. onMouseLeave_() {
  1381. // We sometimes get 'mouseout' events with touches. Since we can never
  1382. // leave the video element when touching, ignore.
  1383. if (this.lastTouchEventTime_) {
  1384. return;
  1385. }
  1386. // Stop the timer and invoke the callback now to hide the controls. If we
  1387. // don't, the opacity style we set in onMouseMove_ will continue to override
  1388. // the opacity in CSS and force the controls to stay visible.
  1389. this.mouseStillTimer_.tickNow();
  1390. }
  1391. /**
  1392. * This callback is for when we are pretty sure that the mouse has stopped
  1393. * moving (aka the mouse is still). This method should only be called via
  1394. * |mouseStillTimer_|. If this behaviour needs to be invoked directly, use
  1395. * |mouseStillTimer_.tickNow()|.
  1396. *
  1397. * @private
  1398. */
  1399. onMouseStill_() {
  1400. // Hide the cursor.
  1401. this.videoContainer_.classList.add('no-cursor');
  1402. this.recentMouseMovement_ = false;
  1403. this.computeOpacity();
  1404. }
  1405. /**
  1406. * @return {boolean} true if any relevant elements are hovered.
  1407. * @private
  1408. */
  1409. isHovered_() {
  1410. if (!window.matchMedia('hover: hover').matches) {
  1411. // This is primarily a touch-screen device, so the :hover query below
  1412. // doesn't make sense. In spite of this, the :hover query on an element
  1413. // can still return true on such a device after a touch ends.
  1414. // See https://bit.ly/34dBORX for details.
  1415. return false;
  1416. }
  1417. return this.showOnHoverControls_.some((element) => {
  1418. return element.matches(':hover');
  1419. });
  1420. }
  1421. /**
  1422. * Recompute whether the controls should be shown or hidden.
  1423. */
  1424. computeOpacity() {
  1425. const adIsPaused = this.ad_ ? this.ad_.isPaused() : false;
  1426. const videoIsPaused = this.video_.paused && !this.isSeeking_;
  1427. const keyboardNavigationMode = this.controlsContainer_.classList.contains(
  1428. 'shaka-keyboard-navigation');
  1429. // Keep showing the controls if the ad or video is paused, there has been
  1430. // recent mouse movement, we're in keyboard navigation, or one of a special
  1431. // class of elements is hovered.
  1432. if (adIsPaused ||
  1433. ((!this.ad_ || !this.ad_.isLinear()) && videoIsPaused) ||
  1434. this.recentMouseMovement_ ||
  1435. keyboardNavigationMode ||
  1436. this.isHovered_()) {
  1437. // Make sure the state is up-to-date before showing it.
  1438. this.updateTimeAndSeekRange_();
  1439. this.controlsContainer_.setAttribute('shown', 'true');
  1440. this.fadeControlsTimer_.stop();
  1441. } else {
  1442. this.fadeControlsTimer_.tickAfter(/* seconds= */ this.config_.fadeDelay);
  1443. }
  1444. }
  1445. /**
  1446. * @param {!Event} event
  1447. * @private
  1448. */
  1449. onContainerTouch_(event) {
  1450. if (!this.video_.duration) {
  1451. // Can't play yet. Ignore.
  1452. return;
  1453. }
  1454. if (this.isOpaque()) {
  1455. this.lastTouchEventTime_ = Date.now();
  1456. // The controls are showing.
  1457. // Let this event continue and become a click.
  1458. } else {
  1459. // The controls are hidden, so show them.
  1460. this.onMouseMove_(event);
  1461. // Stop this event from becoming a click event.
  1462. event.cancelable && event.preventDefault();
  1463. }
  1464. }
  1465. /**
  1466. * Manage the container click.
  1467. */
  1468. onContainerClick() {
  1469. if (!this.enabled_ || this.isPlayingVR()) {
  1470. return;
  1471. }
  1472. if (this.anySettingsMenusAreOpen()) {
  1473. this.hideSettingsMenusTimer_.tickNow();
  1474. } else if (this.config_.singleClickForPlayAndPause) {
  1475. this.playPausePresentation();
  1476. }
  1477. }
  1478. /** @private */
  1479. onCastStatusChange_() {
  1480. const isCasting = this.castProxy_.isCasting();
  1481. this.dispatchEvent(new shaka.util.FakeEvent(
  1482. 'caststatuschanged', (new Map()).set('newStatus', isCasting)));
  1483. if (isCasting) {
  1484. this.controlsContainer_.setAttribute('casting', 'true');
  1485. } else {
  1486. this.controlsContainer_.removeAttribute('casting');
  1487. }
  1488. }
  1489. /** @private */
  1490. onPlayStateChange_() {
  1491. this.computeOpacity();
  1492. }
  1493. /**
  1494. * Support controls with keyboard inputs.
  1495. * @param {!KeyboardEvent} event
  1496. * @private
  1497. */
  1498. onControlsKeyDown_(event) {
  1499. const activeElement = document.activeElement;
  1500. const isVolumeBar = activeElement && activeElement.classList ?
  1501. activeElement.classList.contains('shaka-volume-bar') : false;
  1502. const isSeekBar = activeElement && activeElement.classList &&
  1503. activeElement.classList.contains('shaka-seek-bar');
  1504. // Show the control panel if it is on focus or any button is pressed.
  1505. if (this.controlsContainer_.contains(activeElement)) {
  1506. this.onMouseMove_(event);
  1507. }
  1508. if (!this.config_.enableKeyboardPlaybackControls) {
  1509. return;
  1510. }
  1511. const keyboardSeekDistance = this.config_.keyboardSeekDistance;
  1512. const keyboardLargeSeekDistance = this.config_.keyboardLargeSeekDistance;
  1513. switch (event.key) {
  1514. case 'ArrowLeft':
  1515. // If it's not focused on the volume bar, move the seek time backward
  1516. // for a few sec. Otherwise, the volume will be adjusted automatically.
  1517. if (this.seekBar_ && isSeekBar && !isVolumeBar &&
  1518. keyboardSeekDistance > 0) {
  1519. event.preventDefault();
  1520. this.seek_(this.seekBar_.getValue() - keyboardSeekDistance);
  1521. }
  1522. break;
  1523. case 'ArrowRight':
  1524. // If it's not focused on the volume bar, move the seek time forward
  1525. // for a few sec. Otherwise, the volume will be adjusted automatically.
  1526. if (this.seekBar_ && isSeekBar && !isVolumeBar &&
  1527. keyboardSeekDistance > 0) {
  1528. event.preventDefault();
  1529. this.seek_(this.seekBar_.getValue() + keyboardSeekDistance);
  1530. }
  1531. break;
  1532. case 'PageDown':
  1533. // PageDown is like ArrowLeft, but has a larger jump distance, and does
  1534. // nothing to volume.
  1535. if (this.seekBar_ && isSeekBar && keyboardSeekDistance > 0) {
  1536. event.preventDefault();
  1537. this.seek_(this.seekBar_.getValue() - keyboardLargeSeekDistance);
  1538. }
  1539. break;
  1540. case 'PageUp':
  1541. // PageDown is like ArrowRight, but has a larger jump distance, and does
  1542. // nothing to volume.
  1543. if (this.seekBar_ && isSeekBar && keyboardSeekDistance > 0) {
  1544. event.preventDefault();
  1545. this.seek_(this.seekBar_.getValue() + keyboardLargeSeekDistance);
  1546. }
  1547. break;
  1548. // Jump to the beginning of the video's seek range.
  1549. case 'Home':
  1550. if (this.seekBar_) {
  1551. this.seek_(this.player_.seekRange().start);
  1552. }
  1553. break;
  1554. // Jump to the end of the video's seek range.
  1555. case 'End':
  1556. if (this.seekBar_) {
  1557. this.seek_(this.player_.seekRange().end);
  1558. }
  1559. break;
  1560. case 'f':
  1561. if (this.isFullScreenSupported()) {
  1562. this.toggleFullScreen();
  1563. }
  1564. break;
  1565. case 'm':
  1566. if (this.ad_ && this.ad_.isLinear()) {
  1567. this.ad_.setMuted(!this.ad_.isMuted());
  1568. } else {
  1569. this.localVideo_.muted = !this.localVideo_.muted;
  1570. }
  1571. break;
  1572. case 'p':
  1573. if (this.isPiPAllowed()) {
  1574. this.togglePiP();
  1575. }
  1576. break;
  1577. // Pause or play by pressing space on the seek bar.
  1578. case ' ':
  1579. if (isSeekBar) {
  1580. this.playPausePresentation();
  1581. }
  1582. break;
  1583. }
  1584. }
  1585. /**
  1586. * Support controls with keyboard inputs.
  1587. * @param {!KeyboardEvent} event
  1588. * @private
  1589. */
  1590. onControlsKeyUp_(event) {
  1591. // When the key is released, remove it from the pressed keys set.
  1592. this.pressedKeys_.delete(event.key);
  1593. }
  1594. /**
  1595. * Called both as an event listener and directly by the controls to initialize
  1596. * the buffering state.
  1597. * @private
  1598. */
  1599. onBufferingStateChange_() {
  1600. if (!this.enabled_) {
  1601. return;
  1602. }
  1603. if (this.ad_ && this.ad_.isClientRendering() && this.ad_.isLinear()) {
  1604. shaka.ui.Utils.setDisplay(this.spinnerContainer_, false);
  1605. return;
  1606. }
  1607. shaka.ui.Utils.setDisplay(
  1608. this.spinnerContainer_, this.player_.isBuffering());
  1609. }
  1610. /**
  1611. * @return {boolean}
  1612. * @export
  1613. */
  1614. isOpaque() {
  1615. if (!this.enabled_) {
  1616. return false;
  1617. }
  1618. return this.controlsContainer_.getAttribute('shown') != null ||
  1619. this.controlsContainer_.getAttribute('casting') != null;
  1620. }
  1621. /**
  1622. * Update the video's current time based on the keyboard operations.
  1623. *
  1624. * @param {number} currentTime
  1625. * @private
  1626. */
  1627. seek_(currentTime) {
  1628. goog.asserts.assert(
  1629. this.seekBar_, 'Caller of seek_ must check for seekBar_ first!');
  1630. this.video_.currentTime = currentTime;
  1631. this.updateTimeAndSeekRange_();
  1632. }
  1633. /**
  1634. * Called when the seek range or current time need to be updated.
  1635. * @private
  1636. */
  1637. updateTimeAndSeekRange_() {
  1638. if (this.seekBar_) {
  1639. this.seekBar_.setValue(this.video_.currentTime);
  1640. this.seekBar_.update();
  1641. if (this.seekBar_.isShowing()) {
  1642. for (const menu of this.menus_) {
  1643. menu.classList.remove('shaka-low-position');
  1644. }
  1645. } else {
  1646. for (const menu of this.menus_) {
  1647. menu.classList.add('shaka-low-position');
  1648. }
  1649. }
  1650. }
  1651. this.dispatchEvent(new shaka.util.FakeEvent('timeandseekrangeupdated'));
  1652. }
  1653. /**
  1654. * Add behaviors for keyboard navigation.
  1655. * 1. Add blue outline for focused elements.
  1656. * 2. Allow exiting overflow settings menus by pressing Esc key.
  1657. * 3. When navigating on overflow settings menu by pressing Tab
  1658. * key or Shift+Tab keys keep the focus inside overflow menu.
  1659. *
  1660. * @param {!KeyboardEvent} event
  1661. * @private
  1662. */
  1663. onWindowKeyDown_(event) {
  1664. // Add the key to the pressed keys set when it's pressed.
  1665. this.pressedKeys_.add(event.key);
  1666. const anySettingsMenusAreOpen = this.anySettingsMenusAreOpen();
  1667. if (event.key == 'Tab') {
  1668. // Enable blue outline for focused elements for keyboard
  1669. // navigation.
  1670. this.controlsContainer_.classList.add('shaka-keyboard-navigation');
  1671. this.computeOpacity();
  1672. this.eventManager_.listen(window, 'mousedown', () => this.onMouseDown_());
  1673. }
  1674. // If escape key was pressed, close any open settings menus.
  1675. if (event.key == 'Escape') {
  1676. this.hideSettingsMenusTimer_.tickNow();
  1677. }
  1678. if (anySettingsMenusAreOpen && this.pressedKeys_.has('Tab')) {
  1679. // If Tab key or Shift+Tab keys are pressed when navigating through
  1680. // an overflow settings menu, keep the focus to loop inside the
  1681. // overflow menu.
  1682. this.keepFocusInMenu_(event);
  1683. }
  1684. }
  1685. /**
  1686. * When the user is using keyboard to navigate inside the overflow settings
  1687. * menu (pressing Tab key to go forward, or pressing Shift + Tab keys to go
  1688. * backward), make sure it's focused only on the elements of the overflow
  1689. * panel.
  1690. *
  1691. * This is called by onWindowKeyDown_() function, when there's a settings
  1692. * overflow menu open, and the Tab key / Shift+Tab keys are pressed.
  1693. *
  1694. * @param {!Event} event
  1695. * @private
  1696. */
  1697. keepFocusInMenu_(event) {
  1698. const openSettingsMenus = this.menus_.filter(
  1699. (menu) => !menu.classList.contains('shaka-hidden'));
  1700. if (!openSettingsMenus.length) {
  1701. // For example, this occurs when you hit escape to close the menu.
  1702. return;
  1703. }
  1704. const settingsMenu = openSettingsMenus[0];
  1705. if (settingsMenu.childNodes.length) {
  1706. // Get the first and the last displaying child element from the overflow
  1707. // menu.
  1708. let firstShownChild = settingsMenu.firstElementChild;
  1709. while (firstShownChild &&
  1710. firstShownChild.classList.contains('shaka-hidden')) {
  1711. firstShownChild = firstShownChild.nextElementSibling;
  1712. }
  1713. let lastShownChild = settingsMenu.lastElementChild;
  1714. while (lastShownChild &&
  1715. lastShownChild.classList.contains('shaka-hidden')) {
  1716. lastShownChild = lastShownChild.previousElementSibling;
  1717. }
  1718. const activeElement = document.activeElement;
  1719. // When only Tab key is pressed, navigate to the next element.
  1720. // If it's currently focused on the last shown child element of the
  1721. // overflow menu, let the focus move to the first child element of the
  1722. // menu.
  1723. // When Tab + Shift keys are pressed at the same time, navigate to the
  1724. // previous element. If it's currently focused on the first shown child
  1725. // element of the overflow menu, let the focus move to the last child
  1726. // element of the menu.
  1727. if (this.pressedKeys_.has('Shift')) {
  1728. if (activeElement == firstShownChild) {
  1729. event.preventDefault();
  1730. lastShownChild.focus();
  1731. }
  1732. } else {
  1733. if (activeElement == lastShownChild) {
  1734. event.preventDefault();
  1735. firstShownChild.focus();
  1736. }
  1737. }
  1738. }
  1739. }
  1740. /**
  1741. * For keyboard navigation, we use blue borders to highlight the active
  1742. * element. If we detect that a mouse is being used, remove the blue border
  1743. * from the active element.
  1744. * @private
  1745. */
  1746. onMouseDown_() {
  1747. this.eventManager_.unlisten(window, 'mousedown');
  1748. }
  1749. /**
  1750. * @export
  1751. */
  1752. showUI() {
  1753. const event = new Event('mousemove', {bubbles: false, cancelable: false});
  1754. this.onMouseMove_(event);
  1755. }
  1756. /**
  1757. * @export
  1758. */
  1759. hideUI() {
  1760. // Stop the timer and invoke the callback now to hide the controls. If we
  1761. // don't, the opacity style we set in onMouseMove_ will continue to override
  1762. // the opacity in CSS and force the controls to stay visible.
  1763. this.mouseStillTimer_.tickNow();
  1764. }
  1765. /**
  1766. * @return {shaka.ui.VRManager}
  1767. */
  1768. getVR() {
  1769. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1770. return this.vr_;
  1771. }
  1772. /**
  1773. * Returns if a VR is capable.
  1774. *
  1775. * @return {boolean}
  1776. * @export
  1777. */
  1778. canPlayVR() {
  1779. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1780. return this.vr_.canPlayVR();
  1781. }
  1782. /**
  1783. * Returns if a VR is supported.
  1784. *
  1785. * @return {boolean}
  1786. * @export
  1787. */
  1788. isPlayingVR() {
  1789. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1790. return this.vr_.isPlayingVR();
  1791. }
  1792. /**
  1793. * Reset VR view.
  1794. */
  1795. resetVR() {
  1796. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1797. this.vr_.reset();
  1798. }
  1799. /**
  1800. * Get the angle of the north.
  1801. *
  1802. * @return {?number}
  1803. * @export
  1804. */
  1805. getVRNorth() {
  1806. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1807. return this.vr_.getNorth();
  1808. }
  1809. /**
  1810. * Returns the angle of the current field of view displayed in degrees.
  1811. *
  1812. * @return {?number}
  1813. * @export
  1814. */
  1815. getVRFieldOfView() {
  1816. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1817. return this.vr_.getFieldOfView();
  1818. }
  1819. /**
  1820. * Changing the field of view increases or decreases the portion of the video
  1821. * that is viewed at one time. If the field of view is decreased, a small
  1822. * part of the video will be seen, but with more detail. If the field of view
  1823. * is increased, a larger part of the video will be seen, but with less
  1824. * detail.
  1825. *
  1826. * @param {number} fieldOfView In degrees
  1827. * @export
  1828. */
  1829. setVRFieldOfView(fieldOfView) {
  1830. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1831. this.vr_.setFieldOfView(fieldOfView);
  1832. }
  1833. /**
  1834. * Toggle stereoscopic mode.
  1835. *
  1836. * @export
  1837. */
  1838. toggleStereoscopicMode() {
  1839. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1840. this.vr_.toggleStereoscopicMode();
  1841. }
  1842. /**
  1843. * Returns true if stereoscopic mode is enabled.
  1844. *
  1845. * @return {boolean}
  1846. */
  1847. isStereoscopicModeEnabled() {
  1848. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1849. return this.vr_.isStereoscopicModeEnabled();
  1850. }
  1851. /**
  1852. * Increment the yaw in X angle in degrees.
  1853. *
  1854. * @param {number} angle In degrees
  1855. * @export
  1856. */
  1857. incrementYaw(angle) {
  1858. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1859. this.vr_.incrementYaw(angle);
  1860. }
  1861. /**
  1862. * Increment the pitch in X angle in degrees.
  1863. *
  1864. * @param {number} angle In degrees
  1865. * @export
  1866. */
  1867. incrementPitch(angle) {
  1868. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1869. this.vr_.incrementPitch(angle);
  1870. }
  1871. /**
  1872. * Increment the roll in X angle in degrees.
  1873. *
  1874. * @param {number} angle In degrees
  1875. * @export
  1876. */
  1877. incrementRoll(angle) {
  1878. goog.asserts.assert(this.vr_ != null, 'Should have a VR manager!');
  1879. this.vr_.incrementRoll(angle);
  1880. }
  1881. /**
  1882. * Create a localization instance already pre-loaded with all the locales that
  1883. * we support.
  1884. *
  1885. * @return {!shaka.ui.Localization}
  1886. * @private
  1887. */
  1888. static createLocalization_() {
  1889. /** @type {string} */
  1890. const fallbackLocale = 'en';
  1891. /** @type {!shaka.ui.Localization} */
  1892. const localization = new shaka.ui.Localization(fallbackLocale);
  1893. shaka.ui.Locales.addTo(localization);
  1894. localization.changeLocale(navigator.languages || []);
  1895. return localization;
  1896. }
  1897. };
  1898. /**
  1899. * @event shaka.ui.Controls#CastStatusChangedEvent
  1900. * @description Fired upon receiving a 'caststatuschanged' event from
  1901. * the cast proxy.
  1902. * @property {string} type
  1903. * 'caststatuschanged'
  1904. * @property {boolean} newStatus
  1905. * The new status of the application. True for 'is casting' and
  1906. * false otherwise.
  1907. * @exportDoc
  1908. */
  1909. /**
  1910. * @event shaka.ui.Controls#VRStatusChangedEvent
  1911. * @description Fired when VR status change
  1912. * @property {string} type
  1913. * 'vrstatuschanged'
  1914. * @exportDoc
  1915. */
  1916. /**
  1917. * @event shaka.ui.Controls#SubMenuOpenEvent
  1918. * @description Fired when one of the overflow submenus is opened
  1919. * (e. g. language/resolution/subtitle selection).
  1920. * @property {string} type
  1921. * 'submenuopen'
  1922. * @exportDoc
  1923. */
  1924. /**
  1925. * @event shaka.ui.Controls#CaptionSelectionUpdatedEvent
  1926. * @description Fired when the captions/subtitles menu has finished updating.
  1927. * @property {string} type
  1928. * 'captionselectionupdated'
  1929. * @exportDoc
  1930. */
  1931. /**
  1932. * @event shaka.ui.Controls#ResolutionSelectionUpdatedEvent
  1933. * @description Fired when the resolution menu has finished updating.
  1934. * @property {string} type
  1935. * 'resolutionselectionupdated'
  1936. * @exportDoc
  1937. */
  1938. /**
  1939. * @event shaka.ui.Controls#LanguageSelectionUpdatedEvent
  1940. * @description Fired when the audio language menu has finished updating.
  1941. * @property {string} type
  1942. * 'languageselectionupdated'
  1943. * @exportDoc
  1944. */
  1945. /**
  1946. * @event shaka.ui.Controls#ErrorEvent
  1947. * @description Fired when something went wrong with the controls.
  1948. * @property {string} type
  1949. * 'error'
  1950. * @property {!shaka.util.Error} detail
  1951. * An object which contains details on the error. The error's 'category'
  1952. * and 'code' properties will identify the specific error that occurred.
  1953. * In an uncompiled build, you can also use the 'message' and 'stack'
  1954. * properties to debug.
  1955. * @exportDoc
  1956. */
  1957. /**
  1958. * @event shaka.ui.Controls#TimeAndSeekRangeUpdatedEvent
  1959. * @description Fired when the time and seek range elements have finished
  1960. * updating.
  1961. * @property {string} type
  1962. * 'timeandseekrangeupdated'
  1963. * @exportDoc
  1964. */
  1965. /**
  1966. * @event shaka.ui.Controls#UIUpdatedEvent
  1967. * @description Fired after a call to ui.configure() once the UI has finished
  1968. * updating.
  1969. * @property {string} type
  1970. * 'uiupdated'
  1971. * @exportDoc
  1972. */
  1973. /** @private {!Map<string, !shaka.extern.IUIElement.Factory>} */
  1974. shaka.ui.ControlsPanel.elementNamesToFactories_ = new Map();
  1975. /** @private {?shaka.extern.IUISeekBar.Factory} */
  1976. shaka.ui.ControlsPanel.seekBarFactory_ = new shaka.ui.SeekBar.Factory();