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