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 = 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.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. if (bufferedLength == 0) {
  299. this.container.style.background = colors.base;
  300. } else {
  301. const clampedBufferStart = Math.max(bufferedStart, seekRange.start);
  302. const clampedBufferEnd = Math.min(bufferedEnd, seekRange.end);
  303. const clampedCurrentTime = Math.min(
  304. Math.max(currentTime, seekRange.start),
  305. seekRange.end);
  306. const bufferStartDistance = clampedBufferStart - seekRange.start;
  307. const bufferEndDistance = clampedBufferEnd - seekRange.start;
  308. const playheadDistance = clampedCurrentTime - seekRange.start;
  309. // NOTE: the fallback to zero eliminates NaN.
  310. const bufferStartFraction = (bufferStartDistance / seekRangeSize) || 0;
  311. const bufferEndFraction = (bufferEndDistance / seekRangeSize) || 0;
  312. const playheadFraction = (playheadDistance / seekRangeSize) || 0;
  313. const unbufferedColor =
  314. this.config_.showUnbufferedStart ? colors.base : colors.played;
  315. const gradient = [
  316. 'to right',
  317. this.makeColor_(unbufferedColor, bufferStartFraction),
  318. this.makeColor_(colors.played, bufferStartFraction),
  319. this.makeColor_(colors.played, playheadFraction),
  320. this.makeColor_(colors.buffered, playheadFraction),
  321. this.makeColor_(colors.buffered, bufferEndFraction),
  322. this.makeColor_(colors.base, bufferEndFraction),
  323. ];
  324. this.container.style.background =
  325. 'linear-gradient(' + gradient.join(',') + ')';
  326. }
  327. }
  328. }
  329. /**
  330. * @private
  331. */
  332. markAdBreaks_() {
  333. if (!this.adCuePoints_.length) {
  334. this.adMarkerContainer_.style.background = 'transparent';
  335. this.adBreaksTimer_.stop();
  336. return;
  337. }
  338. const seekRange = this.player.seekRange();
  339. const seekRangeSize = seekRange.end - seekRange.start;
  340. const gradient = ['to right'];
  341. let pointsAsFractions = [];
  342. const adBreakColor = this.config_.seekBarColors.adBreaks;
  343. let postRollAd = false;
  344. for (const point of this.adCuePoints_) {
  345. // Post-roll ads are marked as starting at -1 in CS IMA ads.
  346. if (point.start == -1 && !point.end) {
  347. postRollAd = true;
  348. continue;
  349. }
  350. // Filter point within the seek range. For points with no endpoint
  351. // (client side ads) check that the start point is within range.
  352. if ((!point.end && point.start >= seekRange.start) ||
  353. (typeof point.end == 'number' && point.end > seekRange.start)) {
  354. const startDist =
  355. Math.max(point.start, seekRange.start) - seekRange.start;
  356. const startFrac = (startDist / seekRangeSize) || 0;
  357. // For points with no endpoint assume a 1% length: not too much,
  358. // but enough to be visible on the timeline.
  359. let endFrac = startFrac + 0.01;
  360. if (point.end) {
  361. const endDist = point.end - seekRange.start;
  362. endFrac = (endDist / seekRangeSize) || 0;
  363. }
  364. pointsAsFractions.push({
  365. start: startFrac,
  366. end: endFrac,
  367. });
  368. }
  369. }
  370. pointsAsFractions = pointsAsFractions.sort((a, b) => {
  371. return a.start - b.start;
  372. });
  373. for (const point of pointsAsFractions) {
  374. gradient.push(this.makeColor_('transparent', point.start));
  375. gradient.push(this.makeColor_(adBreakColor, point.start));
  376. gradient.push(this.makeColor_(adBreakColor, point.end));
  377. gradient.push(this.makeColor_('transparent', point.end));
  378. }
  379. if (postRollAd) {
  380. gradient.push(this.makeColor_('transparent', 0.99));
  381. gradient.push(this.makeColor_(adBreakColor, 0.99));
  382. }
  383. this.adMarkerContainer_.style.background =
  384. 'linear-gradient(' + gradient.join(',') + ')';
  385. }
  386. /**
  387. * @param {string} color
  388. * @param {number} fraction
  389. * @return {string}
  390. * @private
  391. */
  392. makeColor_(color, fraction) {
  393. return color + ' ' + (fraction * 100) + '%';
  394. }
  395. /**
  396. * @private
  397. */
  398. onAdCuePointsChanged_() {
  399. const action = () => {
  400. this.markAdBreaks_();
  401. const seekRange = this.player.seekRange();
  402. const seekRangeSize = seekRange.end - seekRange.start;
  403. const minSeekBarWindow =
  404. shaka.ui.Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR;
  405. // Seek range keeps changing for live content and some of the known
  406. // ad breaks might not be in the seek range now, but get into
  407. // it later.
  408. // If we have a LIVE seekable content, keep checking for ad breaks
  409. // every second.
  410. if (this.player.isLive() && seekRangeSize > minSeekBarWindow) {
  411. this.adBreaksTimer_.tickEvery(/* seconds= */ 0.25);
  412. }
  413. };
  414. if (this.player.isFullyLoaded()) {
  415. action();
  416. } else {
  417. this.eventManager.listenOnce(this.player, 'loaded', action);
  418. }
  419. }
  420. /**
  421. * @return {boolean}
  422. * @private
  423. */
  424. shouldBeDisplayed_() {
  425. // The seek bar should be hidden when the seek window's too small or
  426. // there's an ad playing.
  427. const seekRange = this.player.seekRange();
  428. const seekRangeSize = seekRange.end - seekRange.start;
  429. if (this.player.isLive() &&
  430. (seekRangeSize < shaka.ui.Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR ||
  431. !isFinite(seekRangeSize))) {
  432. return false;
  433. }
  434. return this.ad == null || !this.ad.isLinear();
  435. }
  436. /** @private */
  437. updateAriaLabel_() {
  438. this.bar.ariaLabel = this.localization.resolve(shaka.ui.Locales.Ids.SEEK);
  439. }
  440. /** @private */
  441. showTime_(pixelPosition, value) {
  442. const offsetTop = -10;
  443. const width = this.timeContainer_.clientWidth;
  444. const height = 20;
  445. this.timeContainer_.style.width = 'auto';
  446. this.timeContainer_.style.height = height + 'px';
  447. this.timeContainer_.style.top = -(height - offsetTop) + 'px';
  448. const leftPosition = Math.min(this.bar.offsetWidth - width,
  449. Math.max(0, pixelPosition - (width / 2)));
  450. this.timeContainer_.style.left = leftPosition + 'px';
  451. this.timeContainer_.style.right = '';
  452. this.timeContainer_.style.visibility = 'visible';
  453. const seekRange = this.player.seekRange();
  454. if (this.player.isLive()) {
  455. const totalSeconds = seekRange.end - value;
  456. if (totalSeconds < 1) {
  457. this.timeContainer_.textContent =
  458. this.localization.resolve(shaka.ui.Locales.Ids.LIVE);
  459. this.timeContainer_.style.left = '';
  460. this.timeContainer_.style.right = '0px';
  461. } else {
  462. this.timeContainer_.textContent =
  463. '-' + this.timeFormatter_(totalSeconds);
  464. }
  465. } else {
  466. const totalSeconds = value - seekRange.start;
  467. this.timeContainer_.textContent = this.timeFormatter_(totalSeconds);
  468. }
  469. }
  470. /**
  471. * @private
  472. */
  473. async showThumbnail_(pixelPosition, value) {
  474. if (value < 0) {
  475. value = 0;
  476. }
  477. const seekRange = this.player.seekRange();
  478. const playerValue = Math.max(Math.ceil(seekRange.start),
  479. Math.min(Math.floor(seekRange.end), value));
  480. if (this.player.isLive()) {
  481. const totalSeconds = seekRange.end - value;
  482. if (totalSeconds < 1) {
  483. this.thumbnailTime_.textContent =
  484. this.localization.resolve(shaka.ui.Locales.Ids.LIVE);
  485. } else {
  486. this.thumbnailTime_.textContent =
  487. '-' + this.timeFormatter_(totalSeconds);
  488. }
  489. } else {
  490. this.thumbnailTime_.textContent = this.timeFormatter_(value);
  491. }
  492. const thumbnail =
  493. await this.player.getThumbnails(/* trackId= */ null, playerValue);
  494. if (!thumbnail || !thumbnail.uris.length) {
  495. this.hideThumbnail_();
  496. return;
  497. }
  498. if (thumbnail.width < thumbnail.height) {
  499. this.thumbnailContainer_.classList.add('portrait-thumbnail');
  500. } else {
  501. this.thumbnailContainer_.classList.remove('portrait-thumbnail');
  502. }
  503. const offsetTop = -10;
  504. const width = this.thumbnailContainer_.clientWidth;
  505. let height = Math.floor(width * 9 / 16);
  506. this.thumbnailContainer_.style.height = height + 'px';
  507. this.thumbnailContainer_.style.top = -(height - offsetTop) + 'px';
  508. const leftPosition = Math.min(this.bar.offsetWidth - width,
  509. Math.max(0, pixelPosition - (width / 2)));
  510. this.thumbnailContainer_.style.left = leftPosition + 'px';
  511. this.thumbnailContainer_.style.visibility = 'visible';
  512. let uri = thumbnail.uris[0].split('#xywh=')[0];
  513. if (!this.lastThumbnail_ ||
  514. uri !== this.lastThumbnail_.uris[0].split('#xywh=')[0] ||
  515. thumbnail.segment.getStartByte() !=
  516. this.lastThumbnail_.segment.getStartByte() ||
  517. thumbnail.segment.getEndByte() !=
  518. this.lastThumbnail_.segment.getEndByte()) {
  519. this.lastThumbnail_ = thumbnail;
  520. if (this.lastThumbnailPendingRequest_) {
  521. this.lastThumbnailPendingRequest_.abort();
  522. this.lastThumbnailPendingRequest_ = null;
  523. }
  524. if (thumbnail.codecs == 'mjpg' || uri.startsWith('offline:')) {
  525. this.thumbnailImage_.src = shaka.ui.SeekBar.Transparent_Image_;
  526. try {
  527. const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  528. const type =
  529. shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
  530. const request = shaka.util.Networking.createSegmentRequest(
  531. thumbnail.segment.getUris(),
  532. thumbnail.segment.getStartByte(),
  533. thumbnail.segment.getEndByte(),
  534. this.player.getConfiguration().streaming.retryParameters);
  535. this.lastThumbnailPendingRequest_ = this.player.getNetworkingEngine()
  536. .request(requestType, request, {type});
  537. const response = await this.lastThumbnailPendingRequest_.promise;
  538. this.lastThumbnailPendingRequest_ = null;
  539. if (thumbnail.codecs == 'mjpg') {
  540. const parser = new shaka.util.Mp4Parser()
  541. .box('mdat', shaka.util.Mp4Parser.allData((data) => {
  542. const blob = new Blob([data], {type: 'image/jpeg'});
  543. uri = URL.createObjectURL(blob);
  544. }));
  545. parser.parse(response.data, /* partialOkay= */ false);
  546. } else {
  547. const mimeType = thumbnail.mimeType || 'image/jpeg';
  548. const blob = new Blob([response.data], {type: mimeType});
  549. uri = URL.createObjectURL(blob);
  550. }
  551. } catch (error) {
  552. if (error.code == shaka.util.Error.Code.OPERATION_ABORTED) {
  553. return;
  554. }
  555. throw error;
  556. }
  557. }
  558. try {
  559. this.thumbnailContainer_.removeChild(this.thumbnailImage_);
  560. } catch (e) {
  561. // The image is not a child
  562. }
  563. this.thumbnailImage_ = /** @type {!HTMLImageElement} */ (
  564. shaka.util.Dom.createHTMLElement('img'));
  565. this.thumbnailImage_.id = 'shaka-player-ui-thumbnail-image';
  566. this.thumbnailImage_.draggable = false;
  567. this.thumbnailImage_.src = uri;
  568. this.thumbnailImage_.onload = () => {
  569. if (uri.startsWith('blob:')) {
  570. URL.revokeObjectURL(uri);
  571. }
  572. };
  573. this.thumbnailContainer_.insertBefore(this.thumbnailImage_,
  574. this.thumbnailContainer_.firstChild);
  575. }
  576. const scale = width / thumbnail.width;
  577. if (thumbnail.imageHeight) {
  578. this.thumbnailImage_.height = thumbnail.imageHeight;
  579. } else if (!thumbnail.sprite) {
  580. this.thumbnailImage_.style.height = '100%';
  581. this.thumbnailImage_.style.objectFit = 'contain';
  582. }
  583. if (thumbnail.imageWidth) {
  584. this.thumbnailImage_.width = thumbnail.imageWidth;
  585. } else if (!thumbnail.sprite) {
  586. this.thumbnailImage_.style.width = '100%';
  587. this.thumbnailImage_.style.objectFit = 'contain';
  588. }
  589. this.thumbnailImage_.style.left = '-' + scale * thumbnail.positionX + 'px';
  590. this.thumbnailImage_.style.top = '-' + scale * thumbnail.positionY + 'px';
  591. this.thumbnailImage_.style.transform = 'scale(' + scale + ')';
  592. this.thumbnailImage_.style.transformOrigin = 'left top';
  593. // Update container height and top
  594. height = Math.floor(width * thumbnail.height / thumbnail.width);
  595. this.thumbnailContainer_.style.height = height + 'px';
  596. this.thumbnailContainer_.style.top = -(height - offsetTop) + 'px';
  597. }
  598. /**
  599. * @private
  600. */
  601. hideThumbnail_() {
  602. this.thumbnailContainer_.style.visibility = 'hidden';
  603. }
  604. /**
  605. * @private
  606. */
  607. hideTime_() {
  608. this.timeContainer_.style.visibility = 'hidden';
  609. }
  610. /**
  611. * @param {number} totalSeconds
  612. * @private
  613. */
  614. timeFormatter_(totalSeconds) {
  615. const secondsNumber = Math.round(totalSeconds);
  616. const hours = Math.floor(secondsNumber / 3600);
  617. let minutes = Math.floor((secondsNumber - (hours * 3600)) / 60);
  618. let seconds = secondsNumber - (hours * 3600) - (minutes * 60);
  619. if (seconds < 10) {
  620. seconds = '0' + seconds;
  621. }
  622. if (hours > 0) {
  623. if (minutes < 10) {
  624. minutes = '0' + minutes;
  625. }
  626. return hours + ':' + minutes + ':' + seconds;
  627. } else {
  628. return minutes + ':' + seconds;
  629. }
  630. }
  631. };
  632. /**
  633. * @const {string}
  634. * @private
  635. */
  636. shaka.ui.SeekBar.Transparent_Image_ =
  637. 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>';
  638. /**
  639. * @implements {shaka.extern.IUISeekBar.Factory}
  640. * @export
  641. */
  642. shaka.ui.SeekBar.Factory = class {
  643. /**
  644. * Creates a shaka.ui.SeekBar. Use this factory to register the default
  645. * SeekBar when needed
  646. *
  647. * @override
  648. */
  649. create(rootElement, controls) {
  650. return new shaka.ui.SeekBar(rootElement, controls);
  651. }
  652. };