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