Source: ui/ui.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.Overlay');
  7. goog.provide('shaka.ui.Overlay.FailReasonCode');
  8. goog.provide('shaka.ui.Overlay.TrackLabelFormat');
  9. goog.require('goog.asserts');
  10. goog.require('shaka.Player');
  11. goog.require('shaka.device.DeviceFactory');
  12. goog.require('shaka.device.IDevice');
  13. goog.require('shaka.log');
  14. goog.require('shaka.polyfill');
  15. goog.require('shaka.ui.Controls');
  16. goog.require('shaka.ui.Watermark');
  17. goog.require('shaka.util.ConfigUtils');
  18. goog.require('shaka.util.Dom');
  19. goog.require('shaka.util.FakeEvent');
  20. goog.require('shaka.util.IDestroyable');
  21. /**
  22. * @implements {shaka.util.IDestroyable}
  23. * @export
  24. */
  25. shaka.ui.Overlay = class {
  26. /**
  27. * @param {!shaka.Player} player
  28. * @param {!HTMLElement} videoContainer
  29. * @param {!HTMLMediaElement} video
  30. * @param {?HTMLCanvasElement=} vrCanvas
  31. */
  32. constructor(player, videoContainer, video, vrCanvas = null) {
  33. /** @private {shaka.Player} */
  34. this.player_ = player;
  35. /** @private {HTMLElement} */
  36. this.videoContainer_ = videoContainer;
  37. /** @private {!shaka.extern.UIConfiguration} */
  38. this.config_ = this.defaultConfig_();
  39. // Get and configure cast app id.
  40. let castAppId = '';
  41. // Get and configure cast Android Receiver Compatibility
  42. let castAndroidReceiverCompatible = false;
  43. // Cast receiver id can be specified on either container or video.
  44. // It should not be provided on both. If it was, we will use the last
  45. // one we saw.
  46. if (videoContainer['dataset'] &&
  47. videoContainer['dataset']['shakaPlayerCastReceiverId']) {
  48. const dataSet = videoContainer['dataset'];
  49. castAppId = dataSet['shakaPlayerCastReceiverId'];
  50. castAndroidReceiverCompatible =
  51. dataSet['shakaPlayerCastAndroidReceiverCompatible'] === 'true';
  52. } else if (video['dataset'] &&
  53. video['dataset']['shakaPlayerCastReceiverId']) {
  54. const dataSet = video['dataset'];
  55. castAppId = dataSet['shakaPlayerCastReceiverId'];
  56. castAndroidReceiverCompatible =
  57. dataSet['shakaPlayerCastAndroidReceiverCompatible'] === 'true';
  58. }
  59. if (castAppId.length) {
  60. this.config_.castReceiverAppId = castAppId;
  61. this.config_.castAndroidReceiverCompatible =
  62. castAndroidReceiverCompatible;
  63. }
  64. // Make sure this container is discoverable and that the UI can be reached
  65. // through it.
  66. videoContainer['dataset']['shakaPlayerContainer'] = '';
  67. videoContainer['ui'] = this;
  68. // Tag the container for mobile platforms, to allow different styles.
  69. if (this.isMobile()) {
  70. videoContainer.classList.add('shaka-mobile');
  71. }
  72. /** @private {shaka.ui.Controls} */
  73. this.controls_ = new shaka.ui.Controls(
  74. player, videoContainer, video, vrCanvas, this.config_);
  75. // If the browser's native controls are disabled, use UI TextDisplayer.
  76. if (!video.controls) {
  77. player.setVideoContainer(videoContainer);
  78. }
  79. videoContainer['ui'] = this;
  80. video['ui'] = this;
  81. /** @private {shaka.ui.Watermark} */
  82. this.watermark_ = null;
  83. }
  84. /**
  85. * @param {boolean=} forceDisconnect If true, force the receiver app to shut
  86. * down by disconnecting. Does nothing if not connected.
  87. * @override
  88. * @export
  89. */
  90. async destroy(forceDisconnect = false) {
  91. if (this.controls_) {
  92. await this.controls_.destroy(forceDisconnect);
  93. }
  94. this.controls_ = null;
  95. if (this.player_) {
  96. await this.player_.destroy();
  97. }
  98. this.player_ = null;
  99. this.watermark_ = null;
  100. }
  101. /**
  102. * Detects if this is a mobile platform, in case you want to choose a
  103. * different UI configuration on mobile devices.
  104. *
  105. * @return {boolean}
  106. * @export
  107. */
  108. isMobile() {
  109. const device = shaka.device.DeviceFactory.getDevice();
  110. return device.getDeviceType() == shaka.device.IDevice.DeviceType.MOBILE;
  111. }
  112. /**
  113. * Detects if this is a smart tv platform, in case you want to choose a
  114. * different UI configuration on smart tv devices.
  115. *
  116. * @return {boolean}
  117. * @export
  118. */
  119. isSmartTV() {
  120. const device = shaka.device.DeviceFactory.getDevice();
  121. return device.getDeviceType() == shaka.device.IDevice.DeviceType.TV;
  122. }
  123. /**
  124. * @return {!shaka.extern.UIConfiguration}
  125. * @export
  126. */
  127. getConfiguration() {
  128. const ret = this.defaultConfig_();
  129. shaka.util.ConfigUtils.mergeConfigObjects(
  130. ret, this.config_, this.defaultConfig_(),
  131. /* overrides= */ {}, /* path= */ '');
  132. return ret;
  133. }
  134. /**
  135. * @param {string|!Object} config This should either be a field name or an
  136. * object following the form of {@link shaka.extern.UIConfiguration}, where
  137. * you may omit any field you do not wish to change.
  138. * @param {*=} value This should be provided if the previous parameter
  139. * was a string field name.
  140. * @export
  141. */
  142. configure(config, value) {
  143. goog.asserts.assert(typeof(config) == 'object' || arguments.length == 2,
  144. 'String configs should have values!');
  145. // ('fieldName', value) format
  146. if (arguments.length == 2 && typeof(config) == 'string') {
  147. config = shaka.util.ConfigUtils.convertToConfigObject(config, value);
  148. }
  149. goog.asserts.assert(typeof(config) == 'object', 'Should be an object!');
  150. const newConfig = /** @type {!shaka.extern.UIConfiguration} */(
  151. Object.assign({}, this.config_));
  152. shaka.util.ConfigUtils.mergeConfigObjects(
  153. newConfig, config, this.defaultConfig_(),
  154. /* overrides= */ {}, /* path= */ '');
  155. goog.asserts.assert(this.player_ != null, 'Should have a player!');
  156. const diff = shaka.util.ConfigUtils.getDifferenceFromConfigObjects(
  157. newConfig, this.config_);
  158. if (!Object.keys(diff).length) {
  159. // No changes
  160. return;
  161. }
  162. this.config_ = newConfig;
  163. this.controls_.configure(this.config_);
  164. this.controls_.dispatchEvent(new shaka.util.FakeEvent('uiupdated'));
  165. this.setupCastSenderUrl_();
  166. }
  167. /**
  168. * @return {shaka.ui.Controls}
  169. * @export
  170. */
  171. getControls() {
  172. return this.controls_;
  173. }
  174. /**
  175. * Enable or disable the custom controls.
  176. *
  177. * @param {boolean} enabled
  178. * @export
  179. */
  180. setEnabled(enabled) {
  181. this.controls_.setEnabledShakaControls(enabled);
  182. }
  183. /**
  184. * @param {string} text
  185. * @param {?shaka.ui.Watermark.Options=} options
  186. * @export
  187. */
  188. setTextWatermark(text, options) {
  189. if (text && !this.watermark_ && this.videoContainer_ && this.controls_) {
  190. this.watermark_ = new shaka.ui.Watermark(
  191. this.videoContainer_, this.controls_);
  192. }
  193. if (this.watermark_) {
  194. this.watermark_.setTextWatermark(text, options);
  195. }
  196. }
  197. /**
  198. * @export
  199. */
  200. removeWatermark() {
  201. if (this.watermark_) {
  202. this.watermark_.removeWatermark();
  203. }
  204. }
  205. /**
  206. * @return {!shaka.extern.UIConfiguration}
  207. * @private
  208. */
  209. defaultConfig_() {
  210. const controlPanelElements = [
  211. 'play_pause',
  212. 'skip_next',
  213. 'mute',
  214. 'volume',
  215. 'time_and_duration',
  216. 'spacer',
  217. 'overflow_menu',
  218. ];
  219. if (window.chrome) {
  220. controlPanelElements.push('cast');
  221. }
  222. // eslint-disable-next-line no-restricted-syntax
  223. if ('remote' in HTMLMediaElement.prototype) {
  224. controlPanelElements.push('remote');
  225. } else if (window.WebKitPlaybackTargetAvailabilityEvent) {
  226. controlPanelElements.push('airplay');
  227. }
  228. controlPanelElements.push('fullscreen');
  229. const config = {
  230. controlPanelElements,
  231. overflowMenuButtons: [
  232. 'captions',
  233. 'quality',
  234. 'language',
  235. 'chapter',
  236. 'picture_in_picture',
  237. 'playback_rate',
  238. 'recenter_vr',
  239. 'toggle_stereoscopic',
  240. 'save_video_frame',
  241. ],
  242. statisticsList: [
  243. 'width',
  244. 'height',
  245. 'currentCodecs',
  246. 'corruptedFrames',
  247. 'decodedFrames',
  248. 'droppedFrames',
  249. 'drmTimeSeconds',
  250. 'licenseTime',
  251. 'liveLatency',
  252. 'loadLatency',
  253. 'bufferingTime',
  254. 'manifestTimeSeconds',
  255. 'estimatedBandwidth',
  256. 'streamBandwidth',
  257. 'maxSegmentDuration',
  258. 'pauseTime',
  259. 'playTime',
  260. 'completionPercent',
  261. 'manifestSizeBytes',
  262. 'bytesDownloaded',
  263. 'nonFatalErrorCount',
  264. 'manifestPeriodCount',
  265. 'manifestGapCount',
  266. ],
  267. adStatisticsList: [
  268. 'loadTimes',
  269. 'averageLoadTime',
  270. 'started',
  271. 'overlayAds',
  272. 'playedCompletely',
  273. 'skipped',
  274. 'errors',
  275. ],
  276. contextMenuElements: [
  277. 'loop',
  278. 'picture_in_picture',
  279. 'save_video_frame',
  280. 'statistics',
  281. 'ad_statistics',
  282. ],
  283. playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
  284. fastForwardRates: [2, 4, 8, 1],
  285. rewindRates: [-1, -2, -4, -8],
  286. addSeekBar: true,
  287. addBigPlayButton: false,
  288. customContextMenu: false,
  289. castReceiverAppId: '',
  290. castAndroidReceiverCompatible: false,
  291. clearBufferOnQualityChange: true,
  292. showUnbufferedStart: false,
  293. seekBarColors: {
  294. base: 'rgba(255, 255, 255, 0.3)',
  295. buffered: 'rgba(255, 255, 255, 0.54)',
  296. played: 'rgb(255, 255, 255)',
  297. adBreaks: 'rgb(255, 204, 0)',
  298. },
  299. volumeBarColors: {
  300. base: 'rgba(255, 255, 255, 0.54)',
  301. level: 'rgb(255, 255, 255)',
  302. },
  303. qualityMarks: {
  304. '720': '',
  305. '1080': 'HD',
  306. '1440': '2K',
  307. '2160': '4K',
  308. '4320': '8K',
  309. },
  310. trackLabelFormat: shaka.ui.Overlay.TrackLabelFormat.LANGUAGE,
  311. textTrackLabelFormat: shaka.ui.Overlay.TrackLabelFormat.LANGUAGE,
  312. fadeDelay: 0,
  313. closeMenusDelay: 2,
  314. doubleClickForFullscreen: true,
  315. singleClickForPlayAndPause: true,
  316. enableKeyboardPlaybackControls: true,
  317. enableFullscreenOnRotation: true,
  318. forceLandscapeOnFullscreen: true,
  319. enableTooltips: true,
  320. keyboardSeekDistance: 5,
  321. keyboardLargeSeekDistance: 60,
  322. fullScreenElement: this.videoContainer_,
  323. preferDocumentPictureInPicture: true,
  324. showAudioChannelCountVariants: true,
  325. seekOnTaps: false,
  326. tapSeekDistance: 10,
  327. refreshTickInSeconds: 0.125,
  328. displayInVrMode: false,
  329. defaultVrProjectionMode: 'equirectangular',
  330. setupMediaSession: true,
  331. preferVideoFullScreenInVisionOS: false,
  332. showAudioCodec: true,
  333. showVideoCodec: true,
  334. castSenderUrl: 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js',
  335. };
  336. // On mobile, by default, hide the volume slide and the small play/pause
  337. // button and show the big play/pause button in the center.
  338. // This is in line with default styles in Chrome.
  339. if (this.isMobile()) {
  340. config.addBigPlayButton = true;
  341. config.singleClickForPlayAndPause = false;
  342. config.seekOnTaps = true;
  343. config.enableTooltips = false;
  344. config.doubleClickForFullscreen = false;
  345. const filterElements = [
  346. 'play_pause',
  347. 'skip_next',
  348. 'volume',
  349. ];
  350. config.controlPanelElements = config.controlPanelElements.filter(
  351. (name) => !filterElements.includes(name));
  352. config.overflowMenuButtons = config.overflowMenuButtons.filter(
  353. (name) => !filterElements.includes(name));
  354. config.contextMenuElements = config.contextMenuElements.filter(
  355. (name) => !filterElements.includes(name));
  356. }
  357. if (this.isSmartTV()) {
  358. config.addBigPlayButton = true;
  359. config.singleClickForPlayAndPause = false;
  360. config.enableTooltips = false;
  361. config.doubleClickForFullscreen = false;
  362. const filterElements = [
  363. 'play_pause',
  364. 'cast',
  365. 'remote',
  366. 'airplay',
  367. 'volume',
  368. 'save_video_frame',
  369. ];
  370. config.controlPanelElements = config.controlPanelElements.filter(
  371. (name) => !filterElements.includes(name));
  372. config.overflowMenuButtons = config.overflowMenuButtons.filter(
  373. (name) => !filterElements.includes(name));
  374. config.contextMenuElements = config.contextMenuElements.filter(
  375. (name) => !filterElements.includes(name));
  376. }
  377. return config;
  378. }
  379. /**
  380. * @private
  381. */
  382. setupCastSenderUrl_() {
  383. const castSenderUrl = this.config_.castSenderUrl;
  384. if (!castSenderUrl || !this.config_.castReceiverAppId ||
  385. !window.chrome || chrome.cast || this.isSmartTV()) {
  386. return;
  387. }
  388. let alreadyLoaded = false;
  389. for (const element of document.getElementsByTagName('script')) {
  390. const script = /** @type {HTMLScriptElement} **/(element);
  391. if (script.src === castSenderUrl) {
  392. alreadyLoaded = true;
  393. break;
  394. }
  395. }
  396. if (!alreadyLoaded) {
  397. const script =
  398. /** @type {HTMLScriptElement} **/(document.createElement('script'));
  399. script.src = castSenderUrl;
  400. script.defer = true;
  401. document.head.appendChild(script);
  402. }
  403. }
  404. /**
  405. * @private
  406. */
  407. static async scanPageForShakaElements_() {
  408. // Install built-in polyfills to patch browser incompatibilities.
  409. shaka.polyfill.installAll();
  410. // Check to see if the browser supports the basic APIs Shaka needs.
  411. if (!shaka.Player.isBrowserSupported()) {
  412. shaka.log.error('Shaka Player does not support this browser. ' +
  413. 'Please see https://tinyurl.com/y7s4j9tr for the list of ' +
  414. 'supported browsers.');
  415. // After scanning the page for elements, fire a special "loaded" event for
  416. // when the load fails. This will allow the page to react to the failure.
  417. shaka.ui.Overlay.dispatchLoadedEvent_('shaka-ui-load-failed',
  418. shaka.ui.Overlay.FailReasonCode.NO_BROWSER_SUPPORT);
  419. return;
  420. }
  421. // Look for elements marked 'data-shaka-player-container'
  422. // on the page. These will be used to create our default
  423. // UI.
  424. const containers = document.querySelectorAll(
  425. '[data-shaka-player-container]');
  426. // Look for elements marked 'data-shaka-player'. They will
  427. // either be used in our default UI or with native browser
  428. // controls.
  429. const videos = document.querySelectorAll(
  430. '[data-shaka-player]');
  431. // Look for elements marked 'data-shaka-player-canvas'
  432. // on the page. These will be used to create our default
  433. // UI.
  434. const canvases = document.querySelectorAll(
  435. '[data-shaka-player-canvas]');
  436. // Look for elements marked 'data-shaka-player-vr-canvas'
  437. // on the page. These will be used to create our default
  438. // UI.
  439. const vrCanvases = document.querySelectorAll(
  440. '[data-shaka-player-vr-canvas]');
  441. if (!videos.length && !containers.length) {
  442. // No elements have been tagged with shaka attributes.
  443. } else if (videos.length && !containers.length) {
  444. // Just the video elements were provided.
  445. for (const video of videos) {
  446. // If the app has already manually created a UI for this element,
  447. // don't create another one.
  448. if (video['ui']) {
  449. continue;
  450. }
  451. goog.asserts.assert(video.tagName.toLowerCase() == 'video',
  452. 'Should be a video element!');
  453. const container = document.createElement('div');
  454. const videoParent = video.parentElement;
  455. videoParent.replaceChild(container, video);
  456. container.appendChild(video);
  457. const {lcevcCanvas, vrCanvas} =
  458. shaka.ui.Overlay.findOrMakeSpecialCanvases_(
  459. container, canvases, vrCanvases);
  460. shaka.ui.Overlay.setupUIandAutoLoad_(
  461. container, video, lcevcCanvas, vrCanvas);
  462. }
  463. } else {
  464. for (const container of containers) {
  465. // If the app has already manually created a UI for this element,
  466. // don't create another one.
  467. if (container['ui']) {
  468. continue;
  469. }
  470. goog.asserts.assert(container.tagName.toLowerCase() == 'div',
  471. 'Container should be a div!');
  472. let currentVideo = null;
  473. for (const video of videos) {
  474. goog.asserts.assert(video.tagName.toLowerCase() == 'video',
  475. 'Should be a video element!');
  476. if (video.parentElement == container) {
  477. currentVideo = video;
  478. break;
  479. }
  480. }
  481. if (!currentVideo) {
  482. currentVideo = document.createElement('video');
  483. currentVideo.setAttribute('playsinline', '');
  484. container.appendChild(currentVideo);
  485. }
  486. const {lcevcCanvas, vrCanvas} =
  487. shaka.ui.Overlay.findOrMakeSpecialCanvases_(
  488. container, canvases, vrCanvases);
  489. try {
  490. // eslint-disable-next-line no-await-in-loop
  491. await shaka.ui.Overlay.setupUIandAutoLoad_(
  492. container, currentVideo, lcevcCanvas, vrCanvas);
  493. } catch (e) {
  494. // This can fail if, for example, not every player file has loaded.
  495. // Ad-block is a likely cause for this sort of failure.
  496. shaka.log.error('Error setting up Shaka Player', e);
  497. shaka.ui.Overlay.dispatchLoadedEvent_('shaka-ui-load-failed',
  498. shaka.ui.Overlay.FailReasonCode.PLAYER_FAILED_TO_LOAD);
  499. return;
  500. }
  501. }
  502. }
  503. // After scanning the page for elements, fire the "loaded" event. This will
  504. // let apps know they can use the UI library programmatically now, even if
  505. // they didn't have any Shaka-related elements declared in their HTML.
  506. shaka.ui.Overlay.dispatchLoadedEvent_('shaka-ui-loaded');
  507. }
  508. /**
  509. * @param {string} eventName
  510. * @param {shaka.ui.Overlay.FailReasonCode=} reasonCode
  511. * @private
  512. */
  513. static dispatchLoadedEvent_(eventName, reasonCode) {
  514. let detail = null;
  515. if (reasonCode != undefined) {
  516. detail = {
  517. 'reasonCode': reasonCode,
  518. };
  519. }
  520. const uiLoadedEvent = new CustomEvent(eventName, {detail});
  521. document.dispatchEvent(uiLoadedEvent);
  522. }
  523. /**
  524. * @param {!Element} container
  525. * @param {!Element} video
  526. * @param {!Element} lcevcCanvas
  527. * @param {?Element} vrCanvas
  528. * @private
  529. */
  530. static async setupUIandAutoLoad_(container, video, lcevcCanvas, vrCanvas) {
  531. // Create the UI
  532. const player = new shaka.Player();
  533. const ui = new shaka.ui.Overlay(player,
  534. shaka.util.Dom.asHTMLElement(container),
  535. shaka.util.Dom.asHTMLMediaElement(video),
  536. vrCanvas ? shaka.util.Dom.asHTMLCanvasElement(vrCanvas) : null);
  537. // Attach Canvas used for LCEVC Decoding
  538. player.attachCanvas(/** @type {HTMLCanvasElement} */(lcevcCanvas));
  539. if (shaka.util.Dom.asHTMLMediaElement(video).controls) {
  540. ui.getControls().setEnabledNativeControls(true);
  541. }
  542. // Get the source and load it
  543. // Source can be specified either on the video element:
  544. // <video src='foo.m2u8'></video>
  545. // or as a separate element inside the video element:
  546. // <video>
  547. // <source src='foo.m2u8'/>
  548. // </video>
  549. // It should not be specified on both.
  550. const urls = [];
  551. const src = video.getAttribute('src');
  552. if (src) {
  553. urls.push(src);
  554. video.removeAttribute('src');
  555. }
  556. for (const source of video.getElementsByTagName('source')) {
  557. urls.push(/** @type {!HTMLSourceElement} */ (source).src);
  558. video.removeChild(source);
  559. }
  560. await player.attach(shaka.util.Dom.asHTMLMediaElement(video));
  561. for (const url of urls) {
  562. try { // eslint-disable-next-line no-await-in-loop
  563. await ui.getControls().getPlayer().load(url);
  564. break;
  565. } catch (e) {
  566. shaka.log.error('Error auto-loading asset', e);
  567. }
  568. }
  569. }
  570. /**
  571. * @param {!Element} container
  572. * @param {!NodeList<!Element>} canvases
  573. * @param {!NodeList<!Element>} vrCanvases
  574. * @return {{lcevcCanvas: !Element, vrCanvas: ?Element}}
  575. * @private
  576. */
  577. static findOrMakeSpecialCanvases_(container, canvases, vrCanvases) {
  578. let lcevcCanvas = null;
  579. for (const canvas of canvases) {
  580. goog.asserts.assert(canvas.tagName.toLowerCase() == 'canvas',
  581. 'Should be a canvas element!');
  582. if (canvas.parentElement == container) {
  583. lcevcCanvas = canvas;
  584. break;
  585. }
  586. }
  587. if (!lcevcCanvas) {
  588. lcevcCanvas = document.createElement('canvas');
  589. lcevcCanvas.classList.add('shaka-canvas-container');
  590. container.appendChild(lcevcCanvas);
  591. }
  592. let vrCanvas = null;
  593. for (const canvas of vrCanvases) {
  594. goog.asserts.assert(canvas.tagName.toLowerCase() == 'canvas',
  595. 'Should be a canvas element!');
  596. if (canvas.parentElement == container) {
  597. vrCanvas = canvas;
  598. break;
  599. }
  600. }
  601. return {
  602. lcevcCanvas,
  603. vrCanvas,
  604. };
  605. }
  606. };
  607. /**
  608. * Describes what information should show up in labels for selecting audio
  609. * variants and text tracks.
  610. *
  611. * @enum {number}
  612. * @export
  613. */
  614. shaka.ui.Overlay.TrackLabelFormat = {
  615. 'LANGUAGE': 0,
  616. 'ROLE': 1,
  617. 'LANGUAGE_ROLE': 2,
  618. 'LABEL': 3,
  619. };
  620. /**
  621. * Describes the possible reasons that the UI might fail to load.
  622. *
  623. * @enum {number}
  624. * @export
  625. */
  626. shaka.ui.Overlay.FailReasonCode = {
  627. 'NO_BROWSER_SUPPORT': 0,
  628. 'PLAYER_FAILED_TO_LOAD': 1,
  629. };
  630. if (document.readyState == 'complete') {
  631. // Don't fire this event synchronously. In a compiled bundle, the "shaka"
  632. // namespace might not be exported to the window until after this point.
  633. (async () => {
  634. await Promise.resolve();
  635. shaka.ui.Overlay.scanPageForShakaElements_();
  636. })();
  637. } else {
  638. window.addEventListener('load', shaka.ui.Overlay.scanPageForShakaElements_);
  639. }