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