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.Deprecate');
  10. goog.require('shaka.ads.AdManager');
  11. goog.require('shaka.cast.CastProxy');
  12. goog.require('shaka.log');
  13. goog.require('shaka.ui.AdCounter');
  14. goog.require('shaka.ui.AdPosition');
  15. goog.require('shaka.ui.BigPlayButton');
  16. goog.require('shaka.ui.ContextMenu');
  17. goog.require('shaka.ui.Locales');
  18. goog.require('shaka.ui.Localization');
  19. goog.require('shaka.ui.SeekBar');
  20. goog.require('shaka.ui.Utils');
  21. goog.require('shaka.util.Dom');
  22. goog.require('shaka.util.EventManager');
  23. goog.require('shaka.util.FakeEvent');
  24. goog.require('shaka.util.FakeEventTarget');
  25. goog.require('shaka.util.IDestroyable');
  26. goog.require('shaka.util.Timer');
  27. goog.requireType('shaka.Player');
  28. /**
  29. * A container for custom video controls.
  30. * @implements {shaka.util.IDestroyable}
  31. * @export
  32. */
  33. shaka.ui.Controls = class extends shaka.util.FakeEventTarget {
  34. /**
  35. * @param {!shaka.Player} player
  36. * @param {!HTMLElement} videoContainer
  37. * @param {!HTMLMediaElement} video
  38. * @param {shaka.extern.UIConfiguration} config
  39. */
  40. constructor(player, videoContainer, video, config) {
  41. super();
  42. /** @private {boolean} */
  43. this.enabled_ = true;
  44. /** @private {shaka.extern.UIConfiguration} */
  45. this.config_ = config;
  46. /** @private {shaka.cast.CastProxy} */
  47. this.castProxy_ = new shaka.cast.CastProxy(
  48. video, player, this.config_.castReceiverAppId);
  49. /** @private {boolean} */
  50. this.castAllowed_ = true;
  51. /** @private {HTMLMediaElement} */
  52. this.video_ = this.castProxy_.getVideo();
  53. /** @private {HTMLMediaElement} */
  54. this.localVideo_ = video;
  55. /** @private {shaka.Player} */
  56. this.player_ = this.castProxy_.getPlayer();
  57. /** @private {shaka.Player} */
  58. this.localPlayer_ = player;
  59. /** @private {!HTMLElement} */
  60. this.videoContainer_ = videoContainer;
  61. /** @private {shaka.extern.IAdManager} */
  62. this.adManager_ = this.player_.getAdManager();
  63. /** @private {?shaka.extern.IAd} */
  64. this.ad_ = null;
  65. /** @private {?shaka.extern.IUISeekBar} */
  66. this.seekBar_ = null;
  67. /** @private {boolean} */
  68. this.isSeeking_ = false;
  69. /** @private {!Array.<!HTMLElement>} */
  70. this.menus_ = [];
  71. /**
  72. * Individual controls which, when hovered or tab-focused, will force the
  73. * controls to be shown.
  74. * @private {!Array.<!Element>}
  75. */
  76. this.showOnHoverControls_ = [];
  77. /** @private {boolean} */
  78. this.recentMouseMovement_ = false;
  79. /**
  80. * This timer is used to detect when the user has stopped moving the mouse
  81. * and we should fade out the ui.
  82. *
  83. * @private {shaka.util.Timer}
  84. */
  85. this.mouseStillTimer_ = new shaka.util.Timer(() => {
  86. this.onMouseStill_();
  87. });
  88. /**
  89. * This timer is used to delay the fading of the UI.
  90. *
  91. * @private {shaka.util.Timer}
  92. */
  93. this.fadeControlsTimer_ = new shaka.util.Timer(() => {
  94. this.controlsContainer_.removeAttribute('shown');
  95. // If there's an overflow menu open, keep it this way for a couple of
  96. // seconds in case a user immediately initiates another mouse move to
  97. // interact with the menus. If that didn't happen, go ahead and hide
  98. // the menus.
  99. this.hideSettingsMenusTimer_.tickAfter(/* seconds= */ 2);
  100. });
  101. /**
  102. * This timer will be used to hide all settings menus. When the timer ticks
  103. * it will force all controls to invisible.
  104. *
  105. * Rather than calling the callback directly, |Controls| will always call it
  106. * through the timer to avoid conflicts.
  107. *
  108. * @private {shaka.util.Timer}
  109. */
  110. this.hideSettingsMenusTimer_ = new shaka.util.Timer(() => {
  111. for (const menu of this.menus_) {
  112. shaka.ui.Utils.setDisplay(menu, /* visible= */ false);
  113. }
  114. });
  115. /**
  116. * This timer is used to regularly update the time and seek range elements
  117. * so that we are communicating the current state as accurately as possibly.
  118. *
  119. * Unlike the other timers, this timer does not "own" the callback because
  120. * this timer is acting like a heartbeat.
  121. *
  122. * @private {shaka.util.Timer}
  123. */
  124. this.timeAndSeekRangeTimer_ = new shaka.util.Timer(() => {
  125. // Suppress timer-based updates if the controls are hidden.
  126. if (this.isOpaque()) {
  127. this.updateTimeAndSeekRange_();
  128. }
  129. });
  130. /** @private {?number} */
  131. this.lastTouchEventTime_ = null;
  132. /** @private {!Array.<!shaka.extern.IUIElement>} */
  133. this.elements_ = [];
  134. /** @private {shaka.ui.Localization} */
  135. this.localization_ = shaka.ui.Controls.createLocalization_();
  136. /** @private {shaka.util.EventManager} */
  137. this.eventManager_ = new shaka.util.EventManager();
  138. // Configure and create the layout of the controls
  139. this.configure(this.config_);
  140. this.addEventListeners_();
  141. /**
  142. * The pressed keys set is used to record which keys are currently pressed
  143. * down, so we can know what keys are pressed at the same time.
  144. * Used by the focusInsideOverflowMenu_() function.
  145. * @private {!Set.<string>}
  146. */
  147. this.pressedKeys_ = new Set();
  148. // We might've missed a caststatuschanged event from the proxy between
  149. // the controls creation and initializing. Run onCastStatusChange_()
  150. // to ensure we have the casting state right.
  151. this.onCastStatusChange_();
  152. // Start this timer after we are finished initializing everything,
  153. this.timeAndSeekRangeTimer_.tickEvery(/* seconds= */ 0.125);
  154. this.eventManager_.listen(this.localization_,
  155. shaka.ui.Localization.LOCALE_CHANGED, (e) => {
  156. const locale = e['locales'][0];
  157. this.adManager_.setLocale(locale);
  158. });
  159. }
  160. /**
  161. * @override
  162. * @export
  163. */
  164. async destroy() {
  165. if (document.pictureInPictureElement == this.localVideo_) {
  166. await document.exitPictureInPicture();
  167. }
  168. if (this.eventManager_) {
  169. this.eventManager_.release();
  170. this.eventManager_ = null;
  171. }
  172. if (this.mouseStillTimer_) {
  173. this.mouseStillTimer_.stop();
  174. this.mouseStillTimer_ = null;
  175. }
  176. if (this.fadeControlsTimer_) {
  177. this.fadeControlsTimer_.stop();
  178. this.fadeControlsTimer_ = null;
  179. }
  180. if (this.hideSettingsMenusTimer_) {
  181. this.hideSettingsMenusTimer_.stop();
  182. this.hideSettingsMenusTimer_ = null;
  183. }
  184. if (this.timeAndSeekRangeTimer_) {
  185. this.timeAndSeekRangeTimer_.stop();
  186. this.timeAndSeekRangeTimer_ = null;
  187. }
  188. // Important! Release all child elements before destroying the cast proxy
  189. // or player. This makes sure those destructions will not trigger event
  190. // listeners in the UI which would then invoke the cast proxy or player.
  191. this.releaseChildElements_();
  192. if (this.controlsContainer_) {
  193. this.videoContainer_.removeChild(this.controlsContainer_);
  194. this.controlsContainer_ = null;
  195. }
  196. if (this.castProxy_) {
  197. await this.castProxy_.destroy();
  198. this.castProxy_ = null;
  199. }
  200. if (this.localPlayer_) {
  201. await this.localPlayer_.destroy();
  202. this.localPlayer_ = null;
  203. }
  204. this.player_ = null;
  205. this.localVideo_ = null;
  206. this.video_ = null;
  207. this.localization_ = null;
  208. this.pressedKeys_.clear();
  209. // FakeEventTarget implements IReleasable
  210. super.release();
  211. }
  212. /** @private */
  213. releaseChildElements_() {
  214. for (const element of this.elements_) {
  215. element.release();
  216. }
  217. this.elements_ = [];
  218. }
  219. /**
  220. * @param {string} name
  221. * @param {!shaka.extern.IUIElement.Factory} factory
  222. * @export
  223. */
  224. static registerElement(name, factory) {
  225. shaka.ui.ControlsPanel.elementNamesToFactories_.set(name, factory);
  226. }
  227. /**
  228. * @param {!shaka.extern.IUISeekBar.Factory} factory
  229. * @export
  230. */
  231. static registerSeekBar(factory) {
  232. shaka.ui.ControlsPanel.seekBarFactory_ = factory;
  233. }
  234. /**
  235. * This allows the application to inhibit casting.
  236. *
  237. * @param {boolean} allow
  238. * @export
  239. */
  240. allowCast(allow) {
  241. this.castAllowed_ = allow;
  242. this.onCastStatusChange_();
  243. }
  244. /**
  245. * Used by the application to notify the controls that a load operation is
  246. * complete. This allows the controls to recalculate play/paused state, which
  247. * is important for platforms like Android where autoplay is disabled.
  248. * @export
  249. */
  250. loadComplete() {
  251. // If we are on Android or if autoplay is false, video.paused should be
  252. // true. Otherwise, video.paused is false and the content is autoplaying.
  253. this.onPlayStateChange_();
  254. }
  255. /**
  256. * @param {!shaka.extern.UIConfiguration} config
  257. * @export
  258. */
  259. configure(config) {
  260. this.config_ = config;
  261. this.castProxy_.changeReceiverId(config.castReceiverAppId);
  262. // Deconstruct the old layout if applicable
  263. if (this.seekBar_) {
  264. this.seekBar_ = null;
  265. }
  266. if (this.playButton_) {
  267. this.playButton_ = null;
  268. }
  269. if (this.contextMenu_) {
  270. this.contextMenu_ = null;
  271. }
  272. if (this.controlsContainer_) {
  273. shaka.util.Dom.removeAllChildren(this.controlsContainer_);
  274. this.releaseChildElements_();
  275. } else {
  276. this.addControlsContainer_();
  277. // The client-side ad container is only created once, and is never
  278. // re-created or uprooted in the DOM, even when the DOM is re-created,
  279. // since that seemingly breaks the IMA SDK.
  280. this.addClientAdContainer_();
  281. }
  282. // Create the new layout
  283. this.createDOM_();
  284. // Init the play state
  285. this.onPlayStateChange_();
  286. // Elements that should not propagate clicks (controls panel, menus)
  287. const noPropagationElements = this.videoContainer_.getElementsByClassName(
  288. 'shaka-no-propagation');
  289. for (const element of noPropagationElements) {
  290. const cb = (event) => event.stopPropagation();
  291. this.eventManager_.listen(element, 'click', cb);
  292. this.eventManager_.listen(element, 'dblclick', cb);
  293. }
  294. }
  295. /**
  296. * Enable or disable the custom controls. Enabling disables native
  297. * browser controls.
  298. *
  299. * @param {boolean} enabled
  300. * @export
  301. */
  302. setEnabledShakaControls(enabled) {
  303. this.enabled_ = enabled;
  304. if (enabled) {
  305. this.videoContainer_.setAttribute('shaka-controls', 'true');
  306. // If we're hiding native controls, make sure the video element itself is
  307. // not tab-navigable. Our custom controls will still be tab-navigable.
  308. this.localVideo_.tabIndex = -1;
  309. this.localVideo_.controls = false;
  310. } else {
  311. this.videoContainer_.removeAttribute('shaka-controls');
  312. }
  313. // The effects of play state changes are inhibited while showing native
  314. // browser controls. Recalculate that state now.
  315. this.onPlayStateChange_();
  316. }
  317. /**
  318. * Enable or disable native browser controls. Enabling disables shaka
  319. * controls.
  320. *
  321. * @param {boolean} enabled
  322. * @export
  323. */
  324. setEnabledNativeControls(enabled) {
  325. // If we enable the native controls, the element must be tab-navigable.
  326. // If we disable the native controls, we want to make sure that the video
  327. // element itself is not tab-navigable, so that the element is skipped over
  328. // when tabbing through the page.
  329. this.localVideo_.controls = enabled;
  330. this.localVideo_.tabIndex = enabled ? 0 : -1;
  331. if (enabled) {
  332. this.setEnabledShakaControls(false);
  333. }
  334. }
  335. /**
  336. * @export
  337. * @return {?shaka.extern.IAd}
  338. */
  339. getAd() {
  340. return this.ad_;
  341. }
  342. /**
  343. * @export
  344. * @return {shaka.cast.CastProxy}
  345. */
  346. getCastProxy() {
  347. return this.castProxy_;
  348. }
  349. /**
  350. * @return {shaka.ui.Localization}
  351. * @export
  352. */
  353. getLocalization() {
  354. return this.localization_;
  355. }
  356. /**
  357. * @return {!HTMLElement}
  358. * @export
  359. */
  360. getVideoContainer() {
  361. return this.videoContainer_;
  362. }
  363. /**
  364. * @return {HTMLMediaElement}
  365. * @export
  366. */
  367. getVideo() {
  368. return this.video_;
  369. }
  370. /**
  371. * @return {HTMLMediaElement}
  372. * @export
  373. */
  374. getLocalVideo() {
  375. return this.localVideo_;
  376. }
  377. /**
  378. * @return {shaka.Player}
  379. * @export
  380. */
  381. getPlayer() {
  382. return this.player_;
  383. }
  384. /**
  385. * @return {shaka.Player}
  386. * @export
  387. */
  388. getLocalPlayer() {
  389. return this.localPlayer_;
  390. }
  391. /**
  392. * @return {!HTMLElement}
  393. * @export
  394. */
  395. getControlsContainer() {
  396. goog.asserts.assert(
  397. this.controlsContainer_, 'No controls container after destruction!');
  398. return this.controlsContainer_;
  399. }
  400. /**
  401. * @return {!HTMLElement}
  402. * @export
  403. */
  404. getServerSideAdContainer() {
  405. return this.daiAdContainer_;
  406. }
  407. /**
  408. * @return {!HTMLElement}
  409. * @export
  410. */
  411. getClientSideAdContainer() {
  412. return this.clientAdContainer_;
  413. }
  414. /**
  415. * @return {!shaka.extern.UIConfiguration}
  416. * @export
  417. */
  418. getConfig() {
  419. return this.config_;
  420. }
  421. /**
  422. * @return {boolean}
  423. * @export
  424. */
  425. isSeeking() {
  426. return this.isSeeking_;
  427. }
  428. /**
  429. * @param {boolean} seeking
  430. * @export
  431. */
  432. setSeeking(seeking) {
  433. this.isSeeking_ = seeking;
  434. }
  435. /**
  436. * @return {boolean}
  437. * @export
  438. */
  439. isCastAllowed() {
  440. return this.castAllowed_;
  441. }
  442. /**
  443. * @return {number}
  444. * @export
  445. */
  446. getDisplayTime() {
  447. return this.seekBar_ ? this.seekBar_.getValue() : this.video_.currentTime;
  448. }
  449. /**
  450. * @param {?number} time
  451. * @export
  452. */
  453. setLastTouchEventTime(time) {
  454. this.lastTouchEventTime_ = time;
  455. }
  456. /**
  457. * @return {boolean}
  458. * @export
  459. */
  460. anySettingsMenusAreOpen() {
  461. return this.menus_.some(
  462. (menu) => !menu.classList.contains('shaka-hidden'));
  463. }
  464. /** @export */
  465. hideSettingsMenus() {
  466. this.hideSettingsMenusTimer_.tickNow();
  467. }
  468. /** @export */
  469. async toggleFullScreen() {
  470. if (document.fullscreenElement) {
  471. if (screen.orientation) {
  472. screen.orientation.unlock();
  473. }
  474. await document.exitFullscreen();
  475. } else {
  476. // If we are in PiP mode, leave PiP mode first.
  477. try {
  478. if (document.pictureInPictureElement) {
  479. await document.exitPictureInPicture();
  480. }
  481. await this.videoContainer_.requestFullscreen({navigationUI: 'hide'});
  482. if (this.config_.forceLandscapeOnFullscreen && screen.orientation) {
  483. try {
  484. // Locking to 'landscape' should let it be either
  485. // 'landscape-primary' or 'landscape-secondary' as appropriate.
  486. await screen.orientation.lock('landscape');
  487. } catch (error) {
  488. // If screen.orientation.lock does not work on a device, it will
  489. // be rejected with an error. Suppress that error.
  490. }
  491. }
  492. } catch (error) {
  493. this.dispatchEvent(new shaka.util.FakeEvent(
  494. 'error', (new Map()).set('detail', error)));
  495. }
  496. }
  497. }
  498. /** @export */
  499. showAdUI() {
  500. shaka.ui.Utils.setDisplay(this.adPanel_, true);
  501. shaka.ui.Utils.setDisplay(this.clientAdContainer_, true);
  502. this.controlsContainer_.setAttribute('ad-active', 'true');
  503. }
  504. /** @export */
  505. hideAdUI() {
  506. shaka.ui.Utils.setDisplay(this.adPanel_, false);
  507. shaka.ui.Utils.setDisplay(this.clientAdContainer_, false);
  508. this.controlsContainer_.removeAttribute('ad-active');
  509. }
  510. /**
  511. * Play or pause the current presentation.
  512. */
  513. playPausePresentation() {
  514. if (!this.enabled_) {
  515. return;
  516. }
  517. if (!this.video_.duration) {
  518. // Can't play yet. Ignore.
  519. return;
  520. }
  521. this.player_.cancelTrickPlay();
  522. if (this.presentationIsPaused()) {
  523. this.video_.play();
  524. } else {
  525. this.video_.pause();
  526. }
  527. }
  528. /**
  529. * Play or pause the current ad.
  530. */
  531. playPauseAd() {
  532. if (this.ad_ && this.ad_.isPaused()) {
  533. this.ad_.play();
  534. } else if (this.ad_) {
  535. this.ad_.pause();
  536. }
  537. }
  538. /**
  539. * Return true if the presentation is paused.
  540. *
  541. * @return {boolean}
  542. */
  543. presentationIsPaused() {
  544. // The video element is in a paused state while seeking, but we don't count
  545. // that.
  546. return this.video_.paused && !this.isSeeking();
  547. }
  548. /** @private */
  549. createDOM_() {
  550. this.videoContainer_.classList.add('shaka-video-container');
  551. this.localVideo_.classList.add('shaka-video');
  552. this.addScrimContainer_();
  553. if (this.config_.addBigPlayButton) {
  554. this.addPlayButton_();
  555. }
  556. if (this.config_.customContextMenu) {
  557. this.addContextMenu_();
  558. }
  559. if (!this.spinnerContainer_) {
  560. this.addBufferingSpinner_();
  561. }
  562. this.addDaiAdContainer_();
  563. this.addControlsButtonPanel_();
  564. this.menus_ = Array.from(
  565. this.videoContainer_.getElementsByClassName('shaka-settings-menu'));
  566. this.menus_.push(...Array.from(
  567. this.videoContainer_.getElementsByClassName('shaka-overflow-menu')));
  568. this.addSeekBar_();
  569. this.showOnHoverControls_ = Array.from(
  570. this.videoContainer_.getElementsByClassName(
  571. 'shaka-show-controls-on-mouse-over'));
  572. }
  573. /** @private */
  574. addControlsContainer_() {
  575. /** @private {HTMLElement} */
  576. this.controlsContainer_ = shaka.util.Dom.createHTMLElement('div');
  577. this.controlsContainer_.classList.add('shaka-controls-container');
  578. this.videoContainer_.appendChild(this.controlsContainer_);
  579. // Use our controls by default, without anyone calling
  580. // setEnabledShakaControls:
  581. this.videoContainer_.setAttribute('shaka-controls', 'true');
  582. this.eventManager_.listen(this.controlsContainer_, 'touchstart', (e) => {
  583. this.onContainerTouch_(e);
  584. }, {passive: false});
  585. this.eventManager_.listen(this.controlsContainer_, 'click', () => {
  586. this.onContainerClick_();
  587. });
  588. this.eventManager_.listen(this.controlsContainer_, 'dblclick', () => {
  589. if (this.config_.doubleClickForFullscreen && document.fullscreenEnabled) {
  590. this.toggleFullScreen();
  591. }
  592. });
  593. }
  594. /** @private */
  595. addPlayButton_() {
  596. const playButtonContainer = shaka.util.Dom.createHTMLElement('div');
  597. playButtonContainer.classList.add('shaka-play-button-container');
  598. this.controlsContainer_.appendChild(playButtonContainer);
  599. /** @private {shaka.ui.BigPlayButton} */
  600. this.playButton_ =
  601. new shaka.ui.BigPlayButton(playButtonContainer, this);
  602. this.elements_.push(this.playButton_);
  603. }
  604. /** @private */
  605. addContextMenu_() {
  606. /** @private {shaka.ui.ContextMenu} */
  607. this.contextMenu_ =
  608. new shaka.ui.ContextMenu(this.controlsButtonPanel_, this);
  609. this.elements_.push(this.contextMenu_);
  610. }
  611. /** @private */
  612. addScrimContainer_() {
  613. // This is the container that gets styled by CSS to have the
  614. // black gradient scrim at the end of the controls.
  615. const scrimContainer = shaka.util.Dom.createHTMLElement('div');
  616. scrimContainer.classList.add('shaka-scrim-container');
  617. this.controlsContainer_.appendChild(scrimContainer);
  618. }
  619. /** @private */
  620. addAdControls_() {
  621. /** @private {!HTMLElement} */
  622. this.adPanel_ = shaka.util.Dom.createHTMLElement('div');
  623. this.adPanel_.classList.add('shaka-ad-controls');
  624. const showAdPanel = this.ad_ != null && this.ad_.isLinear();
  625. shaka.ui.Utils.setDisplay(this.adPanel_, showAdPanel);
  626. this.bottomControls_.appendChild(this.adPanel_);
  627. const adPosition = new shaka.ui.AdPosition(this.adPanel_, this);
  628. this.elements_.push(adPosition);
  629. const adCounter = new shaka.ui.AdCounter(this.adPanel_, this);
  630. this.elements_.push(adCounter);
  631. }
  632. /** @private */
  633. addBufferingSpinner_() {
  634. /** @private {!HTMLElement} */
  635. this.spinnerContainer_ = shaka.util.Dom.createHTMLElement('div');
  636. this.spinnerContainer_.classList.add('shaka-spinner-container');
  637. this.videoContainer_.appendChild(this.spinnerContainer_);
  638. const spinner = shaka.util.Dom.createHTMLElement('div');
  639. spinner.classList.add('shaka-spinner');
  640. this.spinnerContainer_.appendChild(spinner);
  641. // Svg elements have to be created with the svg xml namespace.
  642. const xmlns = 'http://www.w3.org/2000/svg';
  643. const svg =
  644. /** @type {!HTMLElement} */(document.createElementNS(xmlns, 'svg'));
  645. svg.classList.add('shaka-spinner-svg');
  646. svg.setAttribute('viewBox', '0 0 30 30');
  647. spinner.appendChild(svg);
  648. // These coordinates are relative to the SVG viewBox above. This is
  649. // distinct from the actual display size in the page, since the "S" is for
  650. // "Scalable." The radius of 14.5 is so that the edges of the 1-px-wide
  651. // stroke will touch the edges of the viewBox.
  652. const spinnerCircle = document.createElementNS(xmlns, 'circle');
  653. spinnerCircle.classList.add('shaka-spinner-path');
  654. spinnerCircle.setAttribute('cx', '15');
  655. spinnerCircle.setAttribute('cy', '15');
  656. spinnerCircle.setAttribute('r', '14.5');
  657. spinnerCircle.setAttribute('fill', 'none');
  658. spinnerCircle.setAttribute('stroke-width', '1');
  659. spinnerCircle.setAttribute('stroke-miterlimit', '10');
  660. svg.appendChild(spinnerCircle);
  661. }
  662. /** @private */
  663. addControlsButtonPanel_() {
  664. /** @private {!HTMLElement} */
  665. this.bottomControls_ = shaka.util.Dom.createHTMLElement('div');
  666. this.bottomControls_.classList.add('shaka-bottom-controls');
  667. this.bottomControls_.classList.add('shaka-no-propagation');
  668. this.controlsContainer_.appendChild(this.bottomControls_);
  669. // Overflow menus are supposed to hide once you click elsewhere
  670. // on the page. The click event listener on window ensures that.
  671. // However, clicks on the bottom controls don't propagate to the container,
  672. // so we have to explicitly hide the menus onclick here.
  673. this.eventManager_.listen(this.bottomControls_, 'click', (e) => {
  674. // We explicitly deny this measure when clicking on buttons that
  675. // open submenus in the control panel.
  676. if (!e.target['closest']('.shaka-overflow-button')) {
  677. this.hideSettingsMenus();
  678. }
  679. });
  680. this.addAdControls_();
  681. /** @private {!HTMLElement} */
  682. this.controlsButtonPanel_ = shaka.util.Dom.createHTMLElement('div');
  683. this.controlsButtonPanel_.classList.add('shaka-controls-button-panel');
  684. this.controlsButtonPanel_.classList.add(
  685. 'shaka-show-controls-on-mouse-over');
  686. if (this.config_.enableTooltips) {
  687. this.controlsButtonPanel_.classList.add('shaka-tooltips-on');
  688. }
  689. this.bottomControls_.appendChild(this.controlsButtonPanel_);
  690. // Create the elements specified by controlPanelElements
  691. for (const name of this.config_.controlPanelElements) {
  692. if (shaka.ui.ControlsPanel.elementNamesToFactories_.get(name)) {
  693. const factory =
  694. shaka.ui.ControlsPanel.elementNamesToFactories_.get(name);
  695. const element = factory.create(this.controlsButtonPanel_, this);
  696. if (typeof element.release != 'function') {
  697. shaka.Deprecate.deprecateFeature(4,
  698. 'shaka.extern.IUIElement',
  699. 'Please update UI elements to have a release() method.');
  700. // This cast works around compiler strictness about the IUIElement
  701. // type being "@struct" (as ES6 classes are by default).
  702. const moddableElement = /** @type {Object} */(element);
  703. moddableElement['release'] = () => {
  704. if (moddableElement['destroy']) {
  705. moddableElement['destroy']();
  706. }
  707. };
  708. }
  709. this.elements_.push(element);
  710. } else {
  711. shaka.log.alwaysWarn('Unrecognized control panel element requested:',
  712. name);
  713. }
  714. }
  715. }
  716. /**
  717. * Adds a container for server side ad UI with IMA SDK.
  718. *
  719. * @private
  720. */
  721. addDaiAdContainer_() {
  722. /** @private {!HTMLElement} */
  723. this.daiAdContainer_ = shaka.util.Dom.createHTMLElement('div');
  724. this.daiAdContainer_.classList.add('shaka-server-side-ad-container');
  725. this.controlsContainer_.appendChild(this.daiAdContainer_);
  726. }
  727. /**
  728. * Adds a seekbar depending on the configuration.
  729. * By default an instance of shaka.ui.SeekBar is created
  730. * This behaviour can be overriden by providing a SeekBar factory using the
  731. * registerSeekBarFactory function.
  732. *
  733. * @private
  734. */
  735. addSeekBar_() {
  736. if (this.config_.addSeekBar) {
  737. this.seekBar_ = shaka.ui.ControlsPanel.seekBarFactory_.create(
  738. this.bottomControls_, this);
  739. this.elements_.push(this.seekBar_);
  740. } else {
  741. // Settings menus need to be positioned lower if the seekbar is absent.
  742. for (const menu of this.menus_) {
  743. menu.classList.add('shaka-low-position');
  744. }
  745. }
  746. }
  747. /**
  748. * Adds a container for client side ad UI with IMA SDK.
  749. *
  750. * @private
  751. */
  752. addClientAdContainer_() {
  753. /** @private {!HTMLElement} */
  754. this.clientAdContainer_ = shaka.util.Dom.createHTMLElement('div');
  755. this.clientAdContainer_.classList.add('shaka-client-side-ad-container');
  756. shaka.ui.Utils.setDisplay(this.clientAdContainer_, false);
  757. this.eventManager_.listen(this.clientAdContainer_, 'click', () => {
  758. this.onContainerClick_();
  759. });
  760. this.videoContainer_.appendChild(this.clientAdContainer_);
  761. }
  762. /**
  763. * Adds static event listeners. This should only add event listeners to
  764. * things that don't change (e.g. Player). Dynamic elements (e.g. controls)
  765. * should have their event listeners added when they are created.
  766. *
  767. * @private
  768. */
  769. addEventListeners_() {
  770. this.eventManager_.listen(this.player_, 'buffering', () => {
  771. this.onBufferingStateChange_();
  772. });
  773. // Set the initial state, as well.
  774. this.onBufferingStateChange_();
  775. // Listen for key down events to detect tab and enable outline
  776. // for focused elements.
  777. this.eventManager_.listen(window, 'keydown', (e) => {
  778. this.onWindowKeyDown_(/** @type {!KeyboardEvent} */(e));
  779. });
  780. // Listen for click events to dismiss the settings menus.
  781. this.eventManager_.listen(window, 'click', () => this.hideSettingsMenus());
  782. // Avoid having multiple submenus open at the same time.
  783. this.eventManager_.listen(
  784. this, 'submenuopen', () => {
  785. this.hideSettingsMenus();
  786. });
  787. this.eventManager_.listen(this.video_, 'play', () => {
  788. this.onPlayStateChange_();
  789. });
  790. this.eventManager_.listen(this.video_, 'pause', () => {
  791. this.onPlayStateChange_();
  792. });
  793. this.eventManager_.listen(this.videoContainer_, 'mousemove', (e) => {
  794. this.onMouseMove_(e);
  795. });
  796. this.eventManager_.listen(this.videoContainer_, 'touchmove', (e) => {
  797. this.onMouseMove_(e);
  798. }, {passive: true});
  799. this.eventManager_.listen(this.videoContainer_, 'touchend', (e) => {
  800. this.onMouseMove_(e);
  801. }, {passive: true});
  802. this.eventManager_.listen(this.videoContainer_, 'mouseleave', () => {
  803. this.onMouseLeave_();
  804. });
  805. this.eventManager_.listen(this.castProxy_, 'caststatuschanged', () => {
  806. this.onCastStatusChange_();
  807. });
  808. this.eventManager_.listen(this.videoContainer_, 'keydown', (e) => {
  809. this.onControlsKeyDown_(/** @type {!KeyboardEvent} */(e));
  810. });
  811. this.eventManager_.listen(this.videoContainer_, 'keyup', (e) => {
  812. this.onControlsKeyUp_(/** @type {!KeyboardEvent} */(e));
  813. });
  814. this.eventManager_.listen(
  815. this.adManager_, shaka.ads.AdManager.AD_STARTED, (e) => {
  816. this.ad_ = (/** @type {!Object} */ (e))['ad'];
  817. this.showAdUI();
  818. });
  819. this.eventManager_.listen(
  820. this.adManager_, shaka.ads.AdManager.AD_STOPPED, () => {
  821. this.ad_ = null;
  822. this.hideAdUI();
  823. });
  824. if (screen.orientation) {
  825. this.eventManager_.listen(screen.orientation, 'change', async () => {
  826. await this.onScreenRotation_();
  827. });
  828. }
  829. this.eventManager_.listen(document, 'fullscreenchange', () => {
  830. if (this.ad_) {
  831. this.ad_.resize(
  832. this.localVideo_.offsetWidth, this.localVideo_.offsetHeight);
  833. }
  834. });
  835. }
  836. /**
  837. * When a mobile device is rotated to landscape layout, and the video is
  838. * loaded, make the demo app go into fullscreen.
  839. * Similarly, exit fullscreen when the device is rotated to portrait layout.
  840. * @private
  841. */
  842. async onScreenRotation_() {
  843. if (!this.video_ ||
  844. this.video_.readyState == 0 ||
  845. this.castProxy_.isCasting() ||
  846. !this.config_.enableFullscreenOnRotation ||
  847. !document.fullscreenEnabled) {
  848. return;
  849. }
  850. if (screen.orientation.type.includes('landscape') &&
  851. !document.fullscreenElement) {
  852. await this.videoContainer_.requestFullscreen({navigationUI: 'hide'});
  853. } else if (screen.orientation.type.includes('portrait') &&
  854. document.fullscreenElement) {
  855. await document.exitFullscreen();
  856. }
  857. }
  858. /**
  859. * Hiding the cursor when the mouse stops moving seems to be the only
  860. * decent UX in fullscreen mode. Since we can't use pure CSS for that,
  861. * we use events both in and out of fullscreen mode.
  862. * Showing the control bar when a key is pressed, and hiding it after some
  863. * time.
  864. * @param {!Event} event
  865. * @private
  866. */
  867. onMouseMove_(event) {
  868. // Disable blue outline for focused elements for mouse navigation.
  869. if (event.type == 'mousemove') {
  870. this.controlsContainer_.classList.remove('shaka-keyboard-navigation');
  871. this.computeOpacity();
  872. }
  873. if (event.type == 'touchstart' || event.type == 'touchmove' ||
  874. event.type == 'touchend' || event.type == 'keyup') {
  875. this.lastTouchEventTime_ = Date.now();
  876. } else if (this.lastTouchEventTime_ + 1000 < Date.now()) {
  877. // It has been a while since the last touch event, this is probably a real
  878. // mouse moving, so treat it like a mouse.
  879. this.lastTouchEventTime_ = null;
  880. }
  881. // When there is a touch, we can get a 'mousemove' event after touch events.
  882. // This should be treated as part of the touch, which has already been
  883. // handled.
  884. if (this.lastTouchEventTime_ && event.type == 'mousemove') {
  885. return;
  886. }
  887. // Use the cursor specified in the CSS file.
  888. this.videoContainer_.style.cursor = '';
  889. this.recentMouseMovement_ = true;
  890. // Make sure we are not about to hide the settings menus and then force them
  891. // open.
  892. this.hideSettingsMenusTimer_.stop();
  893. if (!this.isOpaque()) {
  894. // Only update the time and seek range on mouse movement if it's the very
  895. // first movement and we're about to show the controls. Otherwise, the
  896. // seek bar will be updated much more rapidly during mouse movement. Do
  897. // this right before making it visible.
  898. this.updateTimeAndSeekRange_();
  899. this.computeOpacity();
  900. }
  901. // Hide the cursor when the mouse stops moving.
  902. // Only applies while the cursor is over the video container.
  903. this.mouseStillTimer_.stop();
  904. // Only start a timeout on 'touchend' or for 'mousemove' with no touch
  905. // events.
  906. if (event.type == 'touchend' ||
  907. event.type == 'keyup'|| !this.lastTouchEventTime_) {
  908. this.mouseStillTimer_.tickAfter(/* seconds= */ 3);
  909. }
  910. }
  911. /** @private */
  912. onMouseLeave_() {
  913. // We sometimes get 'mouseout' events with touches. Since we can never
  914. // leave the video element when touching, ignore.
  915. if (this.lastTouchEventTime_) {
  916. return;
  917. }
  918. // Stop the timer and invoke the callback now to hide the controls. If we
  919. // don't, the opacity style we set in onMouseMove_ will continue to override
  920. // the opacity in CSS and force the controls to stay visible.
  921. this.mouseStillTimer_.tickNow();
  922. }
  923. /**
  924. * This callback is for when we are pretty sure that the mouse has stopped
  925. * moving (aka the mouse is still). This method should only be called via
  926. * |mouseStillTimer_|. If this behaviour needs to be invoked directly, use
  927. * |mouseStillTimer_.tickNow()|.
  928. *
  929. * @private
  930. */
  931. onMouseStill_() {
  932. // Hide the cursor.
  933. this.videoContainer_.style.cursor = 'none';
  934. this.recentMouseMovement_ = false;
  935. this.computeOpacity();
  936. }
  937. /**
  938. * @return {boolean} true if any relevant elements are hovered.
  939. * @private
  940. */
  941. isHovered_() {
  942. if (!window.matchMedia('hover: hover').matches) {
  943. // This is primarily a touch-screen device, so the :hover query below
  944. // doesn't make sense. In spite of this, the :hover query on an element
  945. // can still return true on such a device after a touch ends.
  946. // See https://bit.ly/34dBORX for details.
  947. return false;
  948. }
  949. return this.showOnHoverControls_.some((element) => {
  950. return element.matches(':hover');
  951. });
  952. }
  953. /**
  954. * Recompute whether the controls should be shown or hidden.
  955. */
  956. computeOpacity() {
  957. const adIsPaused = this.ad_ ? this.ad_.isPaused() : false;
  958. const videoIsPaused = this.video_.paused && !this.isSeeking_;
  959. const keyboardNavigationMode = this.controlsContainer_.classList.contains(
  960. 'shaka-keyboard-navigation');
  961. // Keep showing the controls if the ad or video is paused, there has been
  962. // recent mouse movement, we're in keyboard navigation, or one of a special
  963. // class of elements is hovered.
  964. if (adIsPaused ||
  965. ((!this.ad_ || !this.ad_.isLinear()) && videoIsPaused) ||
  966. this.recentMouseMovement_ ||
  967. keyboardNavigationMode ||
  968. this.isHovered_()) {
  969. // Make sure the state is up-to-date before showing it.
  970. this.updateTimeAndSeekRange_();
  971. this.controlsContainer_.setAttribute('shown', 'true');
  972. this.fadeControlsTimer_.stop();
  973. } else {
  974. this.fadeControlsTimer_.tickAfter(/* seconds= */ this.config_.fadeDelay);
  975. }
  976. }
  977. /**
  978. * @param {!Event} event
  979. * @private
  980. */
  981. onContainerTouch_(event) {
  982. if (!this.video_.duration) {
  983. // Can't play yet. Ignore.
  984. return;
  985. }
  986. if (this.isOpaque()) {
  987. this.lastTouchEventTime_ = Date.now();
  988. // The controls are showing.
  989. // Let this event continue and become a click.
  990. } else {
  991. // The controls are hidden, so show them.
  992. this.onMouseMove_(event);
  993. // Stop this event from becoming a click event.
  994. event.cancelable && event.preventDefault();
  995. }
  996. }
  997. /** @private */
  998. onContainerClick_() {
  999. if (!this.enabled_) {
  1000. return;
  1001. }
  1002. if (this.anySettingsMenusAreOpen()) {
  1003. this.hideSettingsMenusTimer_.tickNow();
  1004. } else if (this.config_.singleClickForPlayAndPause) {
  1005. this.onPlayPauseClick_();
  1006. }
  1007. }
  1008. /** @private */
  1009. onPlayPauseClick_() {
  1010. if (this.ad_ && this.ad_.isLinear()) {
  1011. this.playPauseAd();
  1012. } else {
  1013. this.playPausePresentation();
  1014. }
  1015. }
  1016. /** @private */
  1017. onCastStatusChange_() {
  1018. const isCasting = this.castProxy_.isCasting();
  1019. this.dispatchEvent(new shaka.util.FakeEvent(
  1020. 'caststatuschanged', (new Map()).set('newStatus', isCasting)));
  1021. if (isCasting) {
  1022. this.controlsContainer_.setAttribute('casting', 'true');
  1023. } else {
  1024. this.controlsContainer_.removeAttribute('casting');
  1025. }
  1026. }
  1027. /** @private */
  1028. onPlayStateChange_() {
  1029. this.computeOpacity();
  1030. }
  1031. /**
  1032. * Support controls with keyboard inputs.
  1033. * @param {!KeyboardEvent} event
  1034. * @private
  1035. */
  1036. onControlsKeyDown_(event) {
  1037. const activeElement = document.activeElement;
  1038. const isVolumeBar = activeElement && activeElement.classList ?
  1039. activeElement.classList.contains('shaka-volume-bar') : false;
  1040. const isSeekBar = activeElement && activeElement.classList &&
  1041. activeElement.classList.contains('shaka-seek-bar');
  1042. // Show the control panel if it is on focus or any button is pressed.
  1043. if (this.controlsContainer_.contains(activeElement)) {
  1044. this.onMouseMove_(event);
  1045. }
  1046. if (!this.config_.enableKeyboardPlaybackControls) {
  1047. return;
  1048. }
  1049. switch (event.key) {
  1050. case 'ArrowLeft':
  1051. // If it's not focused on the volume bar, move the seek time backward
  1052. // for 5 sec. Otherwise, the volume will be adjusted automatically.
  1053. if (this.seekBar_ && !isVolumeBar) {
  1054. event.preventDefault();
  1055. this.seek_(this.seekBar_.getValue() - 5);
  1056. }
  1057. break;
  1058. case 'ArrowRight':
  1059. // If it's not focused on the volume bar, move the seek time forward
  1060. // for 5 sec. Otherwise, the volume will be adjusted automatically.
  1061. if (this.seekBar_ && !isVolumeBar) {
  1062. event.preventDefault();
  1063. this.seek_(this.seekBar_.getValue() + 5);
  1064. }
  1065. break;
  1066. // Jump to the beginning of the video's seek range.
  1067. case 'Home':
  1068. if (this.seekBar_) {
  1069. this.seek_(this.player_.seekRange().start);
  1070. }
  1071. break;
  1072. // Jump to the end of the video's seek range.
  1073. case 'End':
  1074. if (this.seekBar_) {
  1075. this.seek_(this.player_.seekRange().end);
  1076. }
  1077. break;
  1078. // Pause or play by pressing space on the seek bar.
  1079. case ' ':
  1080. if (isSeekBar) {
  1081. this.onPlayPauseClick_();
  1082. }
  1083. break;
  1084. }
  1085. }
  1086. /**
  1087. * Support controls with keyboard inputs.
  1088. * @param {!KeyboardEvent} event
  1089. * @private
  1090. */
  1091. onControlsKeyUp_(event) {
  1092. // When the key is released, remove it from the pressed keys set.
  1093. this.pressedKeys_.delete(event.key);
  1094. }
  1095. /**
  1096. * Called both as an event listener and directly by the controls to initialize
  1097. * the buffering state.
  1098. * @private
  1099. */
  1100. onBufferingStateChange_() {
  1101. if (!this.enabled_) {
  1102. return;
  1103. }
  1104. shaka.ui.Utils.setDisplay(
  1105. this.spinnerContainer_, this.player_.isBuffering());
  1106. }
  1107. /**
  1108. * @return {boolean}
  1109. * @export
  1110. */
  1111. isOpaque() {
  1112. if (!this.enabled_) {
  1113. return false;
  1114. }
  1115. return this.controlsContainer_.getAttribute('shown') != null ||
  1116. this.controlsContainer_.getAttribute('casting') != null;
  1117. }
  1118. /**
  1119. * Update the video's current time based on the keyboard operations.
  1120. *
  1121. * @param {number} currentTime
  1122. * @private
  1123. */
  1124. seek_(currentTime) {
  1125. goog.asserts.assert(
  1126. this.seekBar_, 'Caller of seek_ must check for seekBar_ first!');
  1127. this.seekBar_.changeTo(currentTime);
  1128. if (this.isOpaque()) {
  1129. // Only update the time and seek range if it's visible.
  1130. this.updateTimeAndSeekRange_();
  1131. }
  1132. }
  1133. /**
  1134. * Called when the seek range or current time need to be updated.
  1135. * @private
  1136. */
  1137. updateTimeAndSeekRange_() {
  1138. if (this.seekBar_) {
  1139. this.seekBar_.setValue(this.video_.currentTime);
  1140. this.seekBar_.update();
  1141. if (this.seekBar_.isShowing()) {
  1142. for (const menu of this.menus_) {
  1143. menu.classList.remove('shaka-low-position');
  1144. }
  1145. } else {
  1146. for (const menu of this.menus_) {
  1147. menu.classList.add('shaka-low-position');
  1148. }
  1149. }
  1150. }
  1151. this.dispatchEvent(new shaka.util.FakeEvent('timeandseekrangeupdated'));
  1152. }
  1153. /**
  1154. * Add behaviors for keyboard navigation.
  1155. * 1. Add blue outline for focused elements.
  1156. * 2. Allow exiting overflow settings menus by pressing Esc key.
  1157. * 3. When navigating on overflow settings menu by pressing Tab
  1158. * key or Shift+Tab keys keep the focus inside overflow menu.
  1159. *
  1160. * @param {!KeyboardEvent} event
  1161. * @private
  1162. */
  1163. onWindowKeyDown_(event) {
  1164. // Add the key to the pressed keys set when it's pressed.
  1165. this.pressedKeys_.add(event.key);
  1166. const anySettingsMenusAreOpen = this.anySettingsMenusAreOpen();
  1167. if (event.key == 'Tab') {
  1168. // Enable blue outline for focused elements for keyboard
  1169. // navigation.
  1170. this.controlsContainer_.classList.add('shaka-keyboard-navigation');
  1171. this.computeOpacity();
  1172. this.eventManager_.listen(window, 'mousedown', () => this.onMouseDown_());
  1173. }
  1174. // If escape key was pressed, close any open settings menus.
  1175. if (event.key == 'Escape') {
  1176. this.hideSettingsMenusTimer_.tickNow();
  1177. }
  1178. if (anySettingsMenusAreOpen && this.pressedKeys_.has('Tab')) {
  1179. // If Tab key or Shift+Tab keys are pressed when navigating through
  1180. // an overflow settings menu, keep the focus to loop inside the
  1181. // overflow menu.
  1182. this.keepFocusInMenu_(event);
  1183. }
  1184. }
  1185. /**
  1186. * When the user is using keyboard to navigate inside the overflow settings
  1187. * menu (pressing Tab key to go forward, or pressing Shift + Tab keys to go
  1188. * backward), make sure it's focused only on the elements of the overflow
  1189. * panel.
  1190. *
  1191. * This is called by onWindowKeyDown_() function, when there's a settings
  1192. * overflow menu open, and the Tab key / Shift+Tab keys are pressed.
  1193. *
  1194. * @param {!Event} event
  1195. * @private
  1196. */
  1197. keepFocusInMenu_(event) {
  1198. const openSettingsMenus = this.menus_.filter(
  1199. (menu) => !menu.classList.contains('shaka-hidden'));
  1200. if (!openSettingsMenus.length) {
  1201. // For example, this occurs when you hit escape to close the menu.
  1202. return;
  1203. }
  1204. const settingsMenu = openSettingsMenus[0];
  1205. if (settingsMenu.childNodes.length) {
  1206. // Get the first and the last displaying child element from the overflow
  1207. // menu.
  1208. let firstShownChild = settingsMenu.firstElementChild;
  1209. while (firstShownChild &&
  1210. firstShownChild.classList.contains('shaka-hidden')) {
  1211. firstShownChild = firstShownChild.nextElementSibling;
  1212. }
  1213. let lastShownChild = settingsMenu.lastElementChild;
  1214. while (lastShownChild &&
  1215. lastShownChild.classList.contains('shaka-hidden')) {
  1216. lastShownChild = lastShownChild.previousElementSibling;
  1217. }
  1218. const activeElement = document.activeElement;
  1219. // When only Tab key is pressed, navigate to the next elememnt.
  1220. // If it's currently focused on the last shown child element of the
  1221. // overflow menu, let the focus move to the first child element of the
  1222. // menu.
  1223. // When Tab + Shift keys are pressed at the same time, navigate to the
  1224. // previous element. If it's currently focused on the first shown child
  1225. // element of the overflow menu, let the focus move to the last child
  1226. // element of the menu.
  1227. if (this.pressedKeys_.has('Shift')) {
  1228. if (activeElement == firstShownChild) {
  1229. event.preventDefault();
  1230. lastShownChild.focus();
  1231. }
  1232. } else {
  1233. if (activeElement == lastShownChild) {
  1234. event.preventDefault();
  1235. firstShownChild.focus();
  1236. }
  1237. }
  1238. }
  1239. }
  1240. /**
  1241. * For keyboard navigation, we use blue borders to highlight the active
  1242. * element. If we detect that a mouse is being used, remove the blue border
  1243. * from the active element.
  1244. * @private
  1245. */
  1246. onMouseDown_() {
  1247. this.eventManager_.unlisten(window, 'mousedown');
  1248. }
  1249. /**
  1250. * Create a localization instance already pre-loaded with all the locales that
  1251. * we support.
  1252. *
  1253. * @return {!shaka.ui.Localization}
  1254. * @private
  1255. */
  1256. static createLocalization_() {
  1257. /** @type {string} */
  1258. const fallbackLocale = 'en';
  1259. /** @type {!shaka.ui.Localization} */
  1260. const localization = new shaka.ui.Localization(fallbackLocale);
  1261. shaka.ui.Locales.addTo(localization);
  1262. localization.changeLocale(navigator.languages || []);
  1263. return localization;
  1264. }
  1265. };
  1266. /**
  1267. * @event shaka.ui.Controls#CastStatusChangedEvent
  1268. * @description Fired upon receiving a 'caststatuschanged' event from
  1269. * the cast proxy.
  1270. * @property {string} type
  1271. * 'caststatuschanged'
  1272. * @property {boolean} newStatus
  1273. * The new status of the application. True for 'is casting' and
  1274. * false otherwise.
  1275. * @exportDoc
  1276. */
  1277. /**
  1278. * @event shaka.ui.Controls#SubMenuOpenEvent
  1279. * @description Fired when one of the overflow submenus is opened
  1280. * (e. g. language/resolution/subtitle selection).
  1281. * @property {string} type
  1282. * 'submenuopen'
  1283. * @exportDoc
  1284. */
  1285. /**
  1286. * @event shaka.ui.Controls#CaptionSelectionUpdatedEvent
  1287. * @description Fired when the captions/subtitles menu has finished updating.
  1288. * @property {string} type
  1289. * 'captionselectionupdated'
  1290. * @exportDoc
  1291. */
  1292. /**
  1293. * @event shaka.ui.Controls#ResolutionSelectionUpdatedEvent
  1294. * @description Fired when the resolution menu has finished updating.
  1295. * @property {string} type
  1296. * 'resolutionselectionupdated'
  1297. * @exportDoc
  1298. */
  1299. /**
  1300. * @event shaka.ui.Controls#LanguageSelectionUpdatedEvent
  1301. * @description Fired when the audio language menu has finished updating.
  1302. * @property {string} type
  1303. * 'languageselectionupdated'
  1304. * @exportDoc
  1305. */
  1306. /**
  1307. * @event shaka.ui.Controls#ErrorEvent
  1308. * @description Fired when something went wrong with the controls.
  1309. * @property {string} type
  1310. * 'error'
  1311. * @property {!shaka.util.Error} detail
  1312. * An object which contains details on the error. The error's 'category'
  1313. * and 'code' properties will identify the specific error that occurred.
  1314. * In an uncompiled build, you can also use the 'message' and 'stack'
  1315. * properties to debug.
  1316. * @exportDoc
  1317. */
  1318. /**
  1319. * @event shaka.ui.Controls#TimeAndSeekRangeUpdatedEvent
  1320. * @description Fired when the time and seek range elements have finished
  1321. * updating.
  1322. * @property {string} type
  1323. * 'timeandseekrangeupdated'
  1324. * @exportDoc
  1325. */
  1326. /**
  1327. * @event shaka.ui.Controls#UIUpdatedEvent
  1328. * @description Fired after a call to ui.configure() once the UI has finished
  1329. * updating.
  1330. * @property {string} type
  1331. * 'uiupdated'
  1332. * @exportDoc
  1333. */
  1334. /** @private {!Map.<string, !shaka.extern.IUIElement.Factory>} */
  1335. shaka.ui.ControlsPanel.elementNamesToFactories_ = new Map();
  1336. /** @private {?shaka.extern.IUISeekBar.Factory} */
  1337. shaka.ui.ControlsPanel.seekBarFactory_ = new shaka.ui.SeekBar.Factory();