Source: ui/seek_bar.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.SeekBar');
  7. goog.require('shaka.ads.AdManager');
  8. goog.require('shaka.net.NetworkingEngine');
  9. goog.require('shaka.ui.Constants');
  10. goog.require('shaka.ui.Locales');
  11. goog.require('shaka.ui.Localization');
  12. goog.require('shaka.ui.RangeElement');
  13. goog.require('shaka.ui.Utils');
  14. goog.require('shaka.util.Dom');
  15. goog.require('shaka.util.Error');
  16. goog.require('shaka.util.Mp4Parser');
  17. goog.require('shaka.util.Networking');
  18. goog.require('shaka.util.Timer');
  19. goog.requireType('shaka.ui.Controls');
  20. /**
  21. * @extends {shaka.ui.RangeElement}
  22. * @implements {shaka.extern.IUISeekBar}
  23. * @final
  24. * @export
  25. */
  26. shaka.ui.SeekBar = class extends shaka.ui.RangeElement {
  27. /**
  28. * @param {!HTMLElement} parent
  29. * @param {!shaka.ui.Controls} controls
  30. */
  31. constructor(parent, controls) {
  32. super(parent, controls,
  33. [
  34. 'shaka-seek-bar-container',
  35. ],
  36. [
  37. 'shaka-seek-bar',
  38. 'shaka-no-propagation',
  39. 'shaka-show-controls-on-mouse-over',
  40. ]);
  41. /** @private {!HTMLElement} */
  42. this.adMarkerContainer_ = shaka.util.Dom.createHTMLElement('div');
  43. this.adMarkerContainer_.classList.add('shaka-ad-markers');
  44. // Insert the ad markers container as a first child for proper
  45. // positioning.
  46. this.container.insertBefore(
  47. this.adMarkerContainer_, this.container.childNodes[0]);
  48. /** @private {!shaka.extern.UIConfiguration} */
  49. this.config_ = this.controls.getConfig();
  50. /**
  51. * This timer is used to introduce a delay between the user scrubbing across
  52. * the seek bar and the seek being sent to the player.
  53. *
  54. * @private {shaka.util.Timer}
  55. */
  56. this.seekTimer_ = new shaka.util.Timer(() => {
  57. let newCurrentTime = this.getValue();
  58. if (!this.player.isLive()) {
  59. if (newCurrentTime == this.video.duration) {
  60. newCurrentTime -= 0.001;
  61. }
  62. }
  63. this.video.currentTime = newCurrentTime;
  64. });
  65. /**
  66. * The timer is activated for live content and checks if
  67. * new ad breaks need to be marked in the current seek range.
  68. *
  69. * @private {shaka.util.Timer}
  70. */
  71. this.adBreaksTimer_ = new shaka.util.Timer(() => {
  72. this.markAdBreaks_();
  73. });
  74. /**
  75. * When user is scrubbing the seek bar - we should pause the video - see
  76. * https://github.com/google/shaka-player/pull/2898#issuecomment-705229215
  77. * but will conditionally pause or play the video after scrubbing
  78. * depending on its previous state
  79. *
  80. * @private {boolean}
  81. */
  82. this.wasPlaying_ = false;
  83. /** @private {!HTMLElement} */
  84. this.thumbnailContainer_ = shaka.util.Dom.createHTMLElement('div');
  85. this.thumbnailContainer_.id = 'shaka-player-ui-thumbnail-container';
  86. /** @private {!HTMLImageElement} */
  87. this.thumbnailImage_ = /** @type {!HTMLImageElement} */ (
  88. shaka.util.Dom.createHTMLElement('img'));
  89. this.thumbnailImage_.id = 'shaka-player-ui-thumbnail-image';
  90. this.thumbnailImage_.draggable = false;
  91. /** @private {!HTMLElement} */
  92. this.thumbnailTime_ = shaka.util.Dom.createHTMLElement('div');
  93. this.thumbnailTime_.id = 'shaka-player-ui-thumbnail-time';
  94. this.thumbnailContainer_.appendChild(this.thumbnailImage_);
  95. this.thumbnailContainer_.appendChild(this.thumbnailTime_);
  96. this.container.appendChild(this.thumbnailContainer_);
  97. /**
  98. * @private {?shaka.extern.Thumbnail}
  99. */
  100. this.lastThumbnail_ = null;
  101. /**
  102. * @private {?shaka.net.NetworkingEngine.PendingRequest}
  103. */
  104. this.lastThumbnailPendingRequest_ = null;
  105. /**
  106. * True if the bar is moving due to touchscreen or keyboard events.
  107. *
  108. * @private {boolean}
  109. */
  110. this.isMoving_ = false;
  111. /**
  112. * The timer is activated to hide the thumbnail.
  113. *
  114. * @private {shaka.util.Timer}
  115. */
  116. this.hideThumbnailTimer_ = new shaka.util.Timer(() => {
  117. this.hideThumbnail_();
  118. });
  119. /** @private {!Array.<!shaka.extern.AdCuePoint>} */
  120. this.adCuePoints_ = [];
  121. this.eventManager.listen(this.localization,
  122. shaka.ui.Localization.LOCALE_UPDATED,
  123. () => this.updateAriaLabel_());
  124. this.eventManager.listen(this.localization,
  125. shaka.ui.Localization.LOCALE_CHANGED,
  126. () => this.updateAriaLabel_());
  127. this.eventManager.listen(
  128. this.adManager, shaka.ads.AdManager.AD_STARTED, () => {
  129. if (!this.shouldBeDisplayed_()) {
  130. shaka.ui.Utils.setDisplay(this.container, false);
  131. }
  132. });
  133. this.eventManager.listen(
  134. this.adManager, shaka.ads.AdManager.AD_STOPPED, () => {
  135. if (this.shouldBeDisplayed_()) {
  136. shaka.ui.Utils.setDisplay(this.container, true);
  137. }
  138. });
  139. this.eventManager.listen(
  140. this.adManager, shaka.ads.AdManager.CUEPOINTS_CHANGED, (e) => {
  141. this.adCuePoints_ = (e)['cuepoints'];
  142. this.onAdCuePointsChanged_();
  143. });
  144. this.eventManager.listen(
  145. this.player, 'unloading', () => {
  146. this.adCuePoints_ = [];
  147. this.onAdCuePointsChanged_();
  148. if (this.lastThumbnailPendingRequest_) {
  149. this.lastThumbnailPendingRequest_.abort();
  150. this.lastThumbnailPendingRequest_ = null;
  151. }
  152. this.lastThumbnail_ = null;
  153. this.hideThumbnail_();
  154. });
  155. this.eventManager.listen(this.bar, 'mousemove', (event) => {
  156. if (!this.player.getImageTracks().length) {
  157. this.hideThumbnail_();
  158. return;
  159. }
  160. const rect = this.bar.getBoundingClientRect();
  161. const min = parseFloat(this.bar.min);
  162. const max = parseFloat(this.bar.max);
  163. // Pixels from the left of the range element
  164. const mousePosition = event.clientX - rect.left;
  165. // Pixels per unit value of the range element.
  166. const scale = (max - min) / rect.width;
  167. // Mouse position in units, which may be outside the allowed range.
  168. const value = Math.round(min + scale * mousePosition);
  169. // Show Thumbnail
  170. this.showThumbnail_(mousePosition, value);
  171. });
  172. this.eventManager.listen(this.container, 'mouseleave', () => {
  173. this.hideThumbnailTimer_.stop();
  174. this.hideThumbnailTimer_.tickAfter(/* seconds= */ 0.25);
  175. });
  176. // Initialize seek state and label.
  177. this.setValue(this.video.currentTime);
  178. this.update();
  179. this.updateAriaLabel_();
  180. if (this.ad) {
  181. // There was already an ad.
  182. shaka.ui.Utils.setDisplay(this.container, false);
  183. }
  184. }
  185. /** @override */
  186. release() {
  187. if (this.seekTimer_) {
  188. this.seekTimer_.stop();
  189. this.seekTimer_ = null;
  190. this.adBreaksTimer_.stop();
  191. this.adBreaksTimer_ = null;
  192. }
  193. super.release();
  194. }
  195. /**
  196. * Called by the base class when user interaction with the input element
  197. * begins.
  198. *
  199. * @override
  200. */
  201. onChangeStart() {
  202. this.wasPlaying_ = !this.video.paused;
  203. this.controls.setSeeking(true);
  204. this.video.pause();
  205. this.hideThumbnailTimer_.stop();
  206. this.isMoving_ = true;
  207. }
  208. /**
  209. * Update the video element's state to match the input element's state.
  210. * Called by the base class when the input element changes.
  211. *
  212. * @override
  213. */
  214. onChange() {
  215. if (!this.video.duration) {
  216. // Can't seek yet. Ignore.
  217. return;
  218. }
  219. // Update the UI right away.
  220. this.update();
  221. // We want to wait until the user has stopped moving the seek bar for a
  222. // little bit to reduce the number of times we ask the player to seek.
  223. //
  224. // To do this, we will start a timer that will fire in a little bit, but if
  225. // we see another seek bar change, we will cancel that timer and re-start
  226. // it.
  227. //
  228. // Calling |start| on an already pending timer will cancel the old request
  229. // and start the new one.
  230. this.seekTimer_.tickAfter(/* seconds= */ 0.125);
  231. if (this.player.getImageTracks().length) {
  232. const min = parseFloat(this.bar.min);
  233. const max = parseFloat(this.bar.max);
  234. const rect = this.bar.getBoundingClientRect();
  235. const value = Math.round(this.getValue());
  236. const scale = (max - min) / rect.width;
  237. const position = (value - min) / scale;
  238. this.showThumbnail_(position, value);
  239. } else {
  240. this.hideThumbnail_();
  241. }
  242. }
  243. /**
  244. * Called by the base class when user interaction with the input element
  245. * ends.
  246. *
  247. * @override
  248. */
  249. onChangeEnd() {
  250. // They just let go of the seek bar, so cancel the timer and manually
  251. // call the event so that we can respond immediately.
  252. this.seekTimer_.tickNow();
  253. this.controls.setSeeking(false);
  254. if (this.wasPlaying_) {
  255. this.video.play();
  256. }
  257. if (this.isMoving_) {
  258. this.isMoving_ = false;
  259. this.hideThumbnailTimer_.stop();
  260. this.hideThumbnailTimer_.tickAfter(/* seconds= */ 0.25);
  261. }
  262. }
  263. /**
  264. * @override
  265. */
  266. isShowing() {
  267. // It is showing by default, so it is hidden if shaka-hidden is in the list.
  268. return !this.container.classList.contains('shaka-hidden');
  269. }
  270. /**
  271. * @override
  272. */
  273. update() {
  274. const colors = this.config_.seekBarColors;
  275. const currentTime = this.getValue();
  276. const bufferedLength = this.video.buffered.length;
  277. const bufferedStart = bufferedLength ? this.video.buffered.start(0) : 0;
  278. const bufferedEnd =
  279. bufferedLength ? this.video.buffered.end(bufferedLength - 1) : 0;
  280. const seekRange = this.player.seekRange();
  281. const seekRangeSize = seekRange.end - seekRange.start;
  282. this.setRange(seekRange.start, seekRange.end);
  283. if (!this.shouldBeDisplayed_()) {
  284. shaka.ui.Utils.setDisplay(this.container, false);
  285. } else {
  286. shaka.ui.Utils.setDisplay(this.container, true);
  287. if (bufferedLength == 0) {
  288. this.container.style.background = colors.base;
  289. } else {
  290. const clampedBufferStart = Math.max(bufferedStart, seekRange.start);
  291. const clampedBufferEnd = Math.min(bufferedEnd, seekRange.end);
  292. const clampedCurrentTime = Math.min(
  293. Math.max(currentTime, seekRange.start),
  294. seekRange.end);
  295. const bufferStartDistance = clampedBufferStart - seekRange.start;
  296. const bufferEndDistance = clampedBufferEnd - seekRange.start;
  297. const playheadDistance = clampedCurrentTime - seekRange.start;
  298. // NOTE: the fallback to zero eliminates NaN.
  299. const bufferStartFraction = (bufferStartDistance / seekRangeSize) || 0;
  300. const bufferEndFraction = (bufferEndDistance / seekRangeSize) || 0;
  301. const playheadFraction = (playheadDistance / seekRangeSize) || 0;
  302. const unbufferedColor =
  303. this.config_.showUnbufferedStart ? colors.base : colors.played;
  304. const gradient = [
  305. 'to right',
  306. this.makeColor_(unbufferedColor, bufferStartFraction),
  307. this.makeColor_(colors.played, bufferStartFraction),
  308. this.makeColor_(colors.played, playheadFraction),
  309. this.makeColor_(colors.buffered, playheadFraction),
  310. this.makeColor_(colors.buffered, bufferEndFraction),
  311. this.makeColor_(colors.base, bufferEndFraction),
  312. ];
  313. this.container.style.background =
  314. 'linear-gradient(' + gradient.join(',') + ')';
  315. }
  316. }
  317. }
  318. /**
  319. * @private
  320. */
  321. markAdBreaks_() {
  322. if (!this.adCuePoints_.length) {
  323. this.adMarkerContainer_.style.background = 'transparent';
  324. return;
  325. }
  326. const seekRange = this.player.seekRange();
  327. const seekRangeSize = seekRange.end - seekRange.start;
  328. const gradient = ['to right'];
  329. const pointsAsFractions = [];
  330. const adBreakColor = this.config_.seekBarColors.adBreaks;
  331. let postRollAd = false;
  332. for (const point of this.adCuePoints_) {
  333. // Post-roll ads are marked as starting at -1 in CS IMA ads.
  334. if (point.start == -1 && !point.end) {
  335. postRollAd = true;
  336. }
  337. // Filter point within the seek range. For points with no endpoint
  338. // (client side ads) check that the start point is within range.
  339. if (point.start >= seekRange.start && point.start < seekRange.end) {
  340. if (point.end && point.end > seekRange.end) {
  341. continue;
  342. }
  343. const startDist = point.start - seekRange.start;
  344. const startFrac = (startDist / seekRangeSize) || 0;
  345. // For points with no endpoint assume a 1% length: not too much,
  346. // but enough to be visible on the timeline.
  347. let endFrac = startFrac + 0.01;
  348. if (point.end) {
  349. const endDist = point.end - seekRange.start;
  350. endFrac = (endDist / seekRangeSize) || 0;
  351. }
  352. pointsAsFractions.push({
  353. start: startFrac,
  354. end: endFrac,
  355. });
  356. }
  357. }
  358. for (const point of pointsAsFractions) {
  359. gradient.push(this.makeColor_('transparent', point.start));
  360. gradient.push(this.makeColor_(adBreakColor, point.start));
  361. gradient.push(this.makeColor_(adBreakColor, point.end));
  362. gradient.push(this.makeColor_('transparent', point.end));
  363. }
  364. if (postRollAd) {
  365. gradient.push(this.makeColor_('transparent', 0.99));
  366. gradient.push(this.makeColor_(adBreakColor, 0.99));
  367. }
  368. this.adMarkerContainer_.style.background =
  369. 'linear-gradient(' + gradient.join(',') + ')';
  370. }
  371. /**
  372. * @param {string} color
  373. * @param {number} fract
  374. * @return {string}
  375. * @private
  376. */
  377. makeColor_(color, fract) {
  378. return color + ' ' + (fract * 100) + '%';
  379. }
  380. /**
  381. * @private
  382. */
  383. onAdCuePointsChanged_() {
  384. this.markAdBreaks_();
  385. const seekRange = this.player.seekRange();
  386. const seekRangeSize = seekRange.end - seekRange.start;
  387. const minSeekBarWindow = shaka.ui.Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR;
  388. // Seek range keeps changing for live content and some of the known
  389. // ad breaks might not be in the seek range now, but get into
  390. // it later.
  391. // If we have a LIVE seekable content, keep checking for ad breaks
  392. // every second.
  393. if (this.player.isLive() && seekRangeSize > minSeekBarWindow) {
  394. this.adBreaksTimer_.tickEvery(1);
  395. }
  396. }
  397. /**
  398. * @return {boolean}
  399. * @private
  400. */
  401. shouldBeDisplayed_() {
  402. // The seek bar should be hidden when the seek window's too small or
  403. // there's an ad playing.
  404. const seekRange = this.player.seekRange();
  405. const seekRangeSize = seekRange.end - seekRange.start;
  406. if (this.player.isLive() &&
  407. seekRangeSize < shaka.ui.Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR) {
  408. return false;
  409. }
  410. return this.ad == null || !this.ad.isLinear();
  411. }
  412. /** @private */
  413. updateAriaLabel_() {
  414. this.bar.ariaLabel = this.localization.resolve(shaka.ui.Locales.Ids.SEEK);
  415. }
  416. /**
  417. * @private
  418. */
  419. async showThumbnail_(pixelPosition, value) {
  420. const thumbnailTrack = this.getThumbnailTrack_();
  421. if (!thumbnailTrack) {
  422. this.hideThumbnail_();
  423. return;
  424. }
  425. if (value < 0) {
  426. value = 0;
  427. }
  428. const seekRange = this.player.seekRange();
  429. const playerValue = Math.max(Math.ceil(seekRange.start),
  430. Math.min(Math.floor(seekRange.end), value));
  431. const thumbnail =
  432. await this.player.getThumbnails(thumbnailTrack.id, playerValue);
  433. if (!thumbnail || !thumbnail.uris.length) {
  434. this.hideThumbnail_();
  435. return;
  436. }
  437. if (this.player.isLive()) {
  438. const totalSeconds = seekRange.end - value;
  439. if (totalSeconds < 1) {
  440. this.thumbnailTime_.textContent =
  441. this.localization.resolve(shaka.ui.Locales.Ids.LIVE);
  442. } else {
  443. this.thumbnailTime_.textContent =
  444. '-' + this.timeFormatter_(totalSeconds);
  445. }
  446. } else {
  447. this.thumbnailTime_.textContent = this.timeFormatter_(value);
  448. }
  449. const offsetTop = -10;
  450. const width = this.thumbnailContainer_.clientWidth;
  451. let height = Math.floor(width * 9 / 16);
  452. this.thumbnailContainer_.style.height = height + 'px';
  453. this.thumbnailContainer_.style.top = -(height - offsetTop) + 'px';
  454. const leftPosition = Math.min(this.bar.offsetWidth - width,
  455. Math.max(0, pixelPosition - (width / 2)));
  456. this.thumbnailContainer_.style.left = leftPosition + 'px';
  457. this.thumbnailContainer_.style.visibility = 'visible';
  458. let uri = thumbnail.uris[0].split('#xywh=')[0];
  459. if (!this.lastThumbnail_ ||
  460. uri !== this.lastThumbnail_.uris[0].split('#xywh=')[0] ||
  461. thumbnail.segment.getStartByte() !=
  462. this.lastThumbnail_.segment.getStartByte() ||
  463. thumbnail.segment.getEndByte() !=
  464. this.lastThumbnail_.segment.getEndByte()) {
  465. this.lastThumbnail_ = thumbnail;
  466. if (this.lastThumbnailPendingRequest_) {
  467. this.lastThumbnailPendingRequest_.abort();
  468. this.lastThumbnailPendingRequest_ = null;
  469. }
  470. if (thumbnailTrack.codecs == 'mjpg' || uri.startsWith('offline:')) {
  471. this.thumbnailImage_.src = shaka.ui.SeekBar.Transparent_Image_;
  472. try {
  473. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  474. const type =
  475. shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
  476. const request = shaka.util.Networking.createSegmentRequest(
  477. thumbnail.segment.getUris(),
  478. thumbnail.segment.getStartByte(),
  479. thumbnail.segment.getEndByte(),
  480. this.player.getConfiguration().streaming.retryParameters);
  481. this.lastThumbnailPendingRequest_ = this.player.getNetworkingEngine()
  482. .request(requestType, request, {type});
  483. const response = await this.lastThumbnailPendingRequest_.promise;
  484. this.lastThumbnailPendingRequest_ = null;
  485. if (thumbnailTrack.codecs == 'mjpg') {
  486. const parser = new shaka.util.Mp4Parser()
  487. .box('mdat', shaka.util.Mp4Parser.allData((data) => {
  488. const blob = new Blob([data], {type: 'image/jpeg'});
  489. uri = URL.createObjectURL(blob);
  490. }));
  491. parser.parse(response.data, /* partialOkay= */ false);
  492. } else {
  493. const mimeType = thumbnailTrack.mimeType || 'image/jpeg';
  494. const blob = new Blob([response.data], {type: mimeType});
  495. uri = URL.createObjectURL(blob);
  496. }
  497. } catch (error) {
  498. if (error.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  499. return;
  500. }
  501. throw error;
  502. }
  503. }
  504. try {
  505. this.thumbnailContainer_.removeChild(this.thumbnailImage_);
  506. } catch (e) {
  507. // The image is not a child
  508. }
  509. this.thumbnailImage_ = /** @type {!HTMLImageElement} */ (
  510. shaka.util.Dom.createHTMLElement('img'));
  511. this.thumbnailImage_.id = 'shaka-player-ui-thumbnail-image';
  512. this.thumbnailImage_.draggable = false;
  513. this.thumbnailImage_.src = uri;
  514. this.thumbnailImage_.onload = () => {
  515. if (uri.startsWith('blob:')) {
  516. URL.revokeObjectURL(uri);
  517. }
  518. };
  519. this.thumbnailContainer_.insertBefore(this.thumbnailImage_,
  520. this.thumbnailContainer_.firstChild);
  521. }
  522. const scale = width / thumbnail.width;
  523. if (thumbnail.imageHeight) {
  524. this.thumbnailImage_.height = thumbnail.imageHeight;
  525. } else if (!thumbnail.sprite) {
  526. this.thumbnailImage_.style.height = '100%';
  527. this.thumbnailImage_.style.objectFit = 'contain';
  528. }
  529. if (thumbnail.imageWidth) {
  530. this.thumbnailImage_.width = thumbnail.imageWidth;
  531. } else if (!thumbnail.sprite) {
  532. this.thumbnailImage_.style.width = '100%';
  533. this.thumbnailImage_.style.objectFit = 'contain';
  534. }
  535. this.thumbnailImage_.style.left = '-' + scale * thumbnail.positionX + 'px';
  536. this.thumbnailImage_.style.top = '-' + scale * thumbnail.positionY + 'px';
  537. this.thumbnailImage_.style.transform = 'scale(' + scale + ')';
  538. this.thumbnailImage_.style.transformOrigin = 'left top';
  539. // Update container height and top
  540. height = Math.floor(width * thumbnail.height / thumbnail.width);
  541. this.thumbnailContainer_.style.height = height + 'px';
  542. this.thumbnailContainer_.style.top = -(height - offsetTop) + 'px';
  543. }
  544. /**
  545. * @return {?shaka.extern.Track} The thumbnail track.
  546. * @private
  547. */
  548. getThumbnailTrack_() {
  549. const imageTracks = this.player.getImageTracks();
  550. if (!imageTracks.length) {
  551. return null;
  552. }
  553. const mimeTypesPreference = [
  554. 'image/avif',
  555. 'image/webp',
  556. 'image/jpeg',
  557. 'image/png',
  558. 'image/svg+xml',
  559. ];
  560. for (const mimeType of mimeTypesPreference) {
  561. const estimatedBandwidth = this.player.getStats().estimatedBandwidth;
  562. const bestOptions = imageTracks.filter((track) => {
  563. return track.mimeType.toLowerCase() === mimeType &&
  564. track.bandwidth < estimatedBandwidth * 0.01;
  565. }).sort((a, b) => {
  566. return b.bandwidth - a.bandwidth;
  567. });
  568. if (bestOptions && bestOptions.length) {
  569. return bestOptions[0];
  570. }
  571. }
  572. const mjpgTrack = imageTracks.find((track) => {
  573. return track.mimeType == 'application/mp4' && track.codecs == 'mjpg';
  574. });
  575. return mjpgTrack || imageTracks[0];
  576. }
  577. /**
  578. * @private
  579. */
  580. hideThumbnail_() {
  581. this.thumbnailContainer_.style.visibility = 'hidden';
  582. this.thumbnailTime_.textContent = '';
  583. }
  584. /**
  585. * @param {number} totalSeconds
  586. * @private
  587. */
  588. timeFormatter_(totalSeconds) {
  589. const secondsNumber = Math.round(totalSeconds);
  590. const hours = Math.floor(secondsNumber / 3600);
  591. let minutes = Math.floor((secondsNumber - (hours * 3600)) / 60);
  592. let seconds = secondsNumber - (hours * 3600) - (minutes * 60);
  593. if (seconds < 10) {
  594. seconds = '0' + seconds;
  595. }
  596. if (hours > 0) {
  597. if (minutes < 10) {
  598. minutes = '0' + minutes;
  599. }
  600. return hours + ':' + minutes + ':' + seconds;
  601. } else {
  602. return minutes + ':' + seconds;
  603. }
  604. }
  605. };
  606. /**
  607. * @const {string}
  608. * @private
  609. */
  610. shaka.ui.SeekBar.Transparent_Image_ =
  611. 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>';
  612. /**
  613. * @implements {shaka.extern.IUISeekBar.Factory}
  614. * @export
  615. */
  616. shaka.ui.SeekBar.Factory = class {
  617. /**
  618. * Creates a shaka.ui.SeekBar. Use this factory to register the default
  619. * SeekBar when needed
  620. *
  621. * @override
  622. */
  623. create(rootElement, controls) {
  624. return new shaka.ui.SeekBar(rootElement, controls);
  625. }
  626. };