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