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