UNPKG

shaka-player

Version:
888 lines (781 loc) 27.6 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.ui.SeekBar'); goog.require('shaka.ads.Utils'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.net.NetworkingUtils'); goog.require('shaka.ui.Locales'); goog.require('shaka.ui.Localization'); goog.require('shaka.ui.RangeElement'); goog.require('shaka.ui.Utils'); goog.require('shaka.util.Dom'); goog.require('shaka.util.Error'); goog.require('shaka.util.Timer'); goog.requireType('shaka.ui.Controls'); /** * @extends {shaka.ui.RangeElement} * @implements {shaka.extern.IUISeekBar} * @final * @export */ shaka.ui.SeekBar = class extends shaka.ui.RangeElement { /** * @param {!HTMLElement} parent * @param {!shaka.ui.Controls} controls */ constructor(parent, controls) { super(parent, controls, [ 'shaka-seek-bar-container', ], [ 'shaka-seek-bar', 'shaka-no-propagation', 'shaka-show-controls-on-mouse-over', ]); /** @private {!HTMLElement} */ this.adMarkerContainer_ = shaka.util.Dom.createHTMLElement('div'); this.adMarkerContainer_.classList.add('shaka-ad-markers'); // Insert the ad markers container as a first child for proper // positioning. this.container.insertBefore( this.adMarkerContainer_, this.container.childNodes[0]); /** @private {!HTMLElement} */ this.chapterMarkerContainer_ = shaka.util.Dom.createHTMLElement('div'); this.chapterMarkerContainer_.classList.add('shaka-chapter-markers'); this.container.insertBefore( this.chapterMarkerContainer_, this.container.childNodes[0]); /** @private {!shaka.extern.UIConfiguration} */ this.config_ = this.controls.getConfig(); /** * This timer is used to introduce a delay between the user scrubbing across * the seek bar and the seek being sent to the player. * * @private {shaka.util.Timer} */ this.seekTimer_ = new shaka.util.Timer(() => { let newCurrentTime = this.getValue(); if (!this.player.isDynamic()) { if (newCurrentTime == this.video.duration) { newCurrentTime -= 0.001; } } this.video.currentTime = newCurrentTime; this.controls.hideContextMenus(); this.controls.hideSettingsMenus(); this.update(); }); /** * The timer is activated for live content and checks if * new ad breaks need to be marked in the current seek range. * * @private {shaka.util.Timer} */ this.adBreaksTimer_ = new shaka.util.Timer(() => { this.markAdBreaks_(); }); /** @private {shaka.util.Timer} */ this.chaptersTimer_ = new shaka.util.Timer(() => { this.markChapters_(); }); /** * When user is scrubbing the seek bar - we should pause the video - see * https://github.com/google/shaka-player/pull/2898#issuecomment-705229215 * but will conditionally pause or play the video after scrubbing * depending on its previous state * * @private {boolean} */ this.wasPlaying_ = false; /** @private {!HTMLElement} */ this.thumbnailContainer_ = shaka.util.Dom.createHTMLElement('div'); this.thumbnailContainer_.classList.add( 'shaka-player-ui-thumbnail-container'); /** @private {!HTMLElement} */ this.thumbnailImageContainer_ = shaka.util.Dom.createHTMLElement('div'); this.thumbnailImageContainer_.classList.add( 'shaka-player-ui-thumbnail-image-container'); /** @private {!HTMLImageElement} */ this.thumbnailImage_ = /** @type {!HTMLImageElement} */ ( shaka.util.Dom.createHTMLElement('img')); this.thumbnailImage_.classList.add('shaka-player-ui-thumbnail-image'); this.thumbnailImage_.draggable = false; this.thumbnailImageContainer_.appendChild(this.thumbnailImage_); /** @private {!HTMLElement} */ this.thumbnailTimeContainer_ = shaka.util.Dom.createHTMLElement('div'); this.thumbnailTimeContainer_.classList.add( 'shaka-player-ui-thumbnail-time-container'); /** @private {!HTMLElement} */ this.thumbnailTime_ = shaka.util.Dom.createHTMLElement('div'); this.thumbnailTime_.classList.add('shaka-player-ui-thumbnail-time'); this.thumbnailTimeContainer_.appendChild(this.thumbnailTime_); this.thumbnailContainer_.appendChild(this.thumbnailImageContainer_); this.thumbnailContainer_.appendChild(this.thumbnailTimeContainer_); this.container.appendChild(this.thumbnailContainer_); /** * @private {?shaka.extern.Thumbnail} */ this.lastThumbnail_ = null; /** * @private {?shaka.net.NetworkingEngine.PendingRequest} */ this.lastThumbnailPendingRequest_ = null; /** * True if the bar is moving due to touchscreen or keyboard events. * * @private {boolean} */ this.isMoving_ = false; /** * The timer is activated to hide the thumbnail. * * @private {shaka.util.Timer} */ this.hideThumbnailTimer_ = new shaka.util.Timer(() => { this.hideThumbnailTimeContainer_(); }); /** @private {!Array<!shaka.extern.AdCuePoint>} */ this.adCuePoints_ = []; this.eventManager.listen(this.bar, 'input', () => { this.controls.hideSettingsMenus(); }); this.eventManager.listenMulti( this.localization, [ shaka.ui.Localization.LOCALE_UPDATED, shaka.ui.Localization.LOCALE_CHANGED, ], () => { this.updateAriaLabel_(); }); this.eventManager.listen( this.adManager, shaka.ads.Utils.AD_STARTED, () => { if (!this.shouldBeDisplayed_()) { shaka.ui.Utils.setDisplay(this.container, false); } }); this.eventManager.listen( this.adManager, shaka.ads.Utils.AD_STOPPED, () => { if (this.shouldBeDisplayed_()) { shaka.ui.Utils.setDisplay(this.container, true); } }); this.eventManager.listen( this.adManager, shaka.ads.Utils.CUEPOINTS_CHANGED, () => { this.adCuePoints_ = this.controls.getAdCuePoints(); this.onAdCuePointsChanged_(); }); this.eventManager.listen( this.player, 'unloading', () => { this.adCuePoints_ = this.controls.getAdCuePoints(); this.onAdCuePointsChanged_(); if (this.lastThumbnailPendingRequest_) { this.lastThumbnailPendingRequest_.abort(); this.lastThumbnailPendingRequest_ = null; } this.lastThumbnail_ = null; this.hideThumbnailTimeContainer_(); }); this.eventManager.listen(this.bar, 'mousemove', (event) => { if (this.controls.anySettingsMenusAreOpen()) { this.hideThumbnailTimeContainer_(); return; } const value = this.getValueFromPosition(event.clientX); this.showThumbnailAtValue_(value); }); this.eventManager.listen(this.container, 'mouseleave', () => { this.hideThumbnailTimer_.stop(); this.hideThumbnailTimer_.tickNow(); }); this.eventManager.listen(this.controls, 'chaptersupdated', () => { this.markChapters_(); if (this.controls.getChapters().length > 0 && this.player.isDynamic()) { this.chaptersTimer_.tickEvery(/* seconds= */ 0.25); } }); // Initialize seek state and label. this.setValue(this.video.currentTime); this.update(); this.updateAriaLabel_(); if (this.ad) { // There was already an ad. shaka.ui.Utils.setDisplay(this.container, false); } } /** @override */ release() { this.seekTimer_?.stop(); this.seekTimer_ = null; this.adBreaksTimer_?.stop(); this.adBreaksTimer_ = null; this.chaptersTimer_?.stop(); this.chaptersTimer_ = null; super.release(); } /** * Called by the base class when user interaction with the input element * begins. * * @override */ onChangeStart() { this.wasPlaying_ = !this.video.paused; this.controls.setSeeking(true); this.video.pause(); this.hideThumbnailTimer_.stop(); this.isMoving_ = true; } /** * Update the video element's state to match the input element's state. * Called by the base class when the input element changes. * * @override */ onChange() { if (!this.video.duration) { // Can't seek yet. Ignore. return; } // Update the UI right away. this.update(); // We want to wait until the user has stopped moving the seek bar for a // little bit to reduce the number of times we ask the player to seek. // // To do this, we will start a timer that will fire in a little bit, but if // we see another seek bar change, we will cancel that timer and re-start // it. // // Calling |start| on an already pending timer will cancel the old request // and start the new one. this.seekTimer_.tickAfter(/* seconds= */ 0.125); if (!this.controls.anySettingsMenusAreOpen()) { this.showThumbnailAtValue_(this.getValue()); } else { this.hideThumbnailTimeContainer_(); } } /** * Called by the base class when user interaction with the input element * ends. * * @override */ onChangeEnd() { // They just let go of the seek bar, so cancel the timer and manually // call the event so that we can respond immediately. this.seekTimer_.tickNow(); this.controls.setSeeking(false); if (this.wasPlaying_) { this.video.play(); } if (this.isMoving_) { this.isMoving_ = false; this.hideThumbnailTimer_.stop(); this.hideThumbnailTimer_.tickAfter(/* seconds= */ 0.25); } } /** * @override */ isShowing() { // It is showing by default, so it is hidden if shaka-hidden is in the list. return !this.container.classList.contains('shaka-hidden'); } /** * Returns the range of buffered that contains 'time', or null if it does * not exist. * * @param {number} time * @return {?{start:number, end:number}} * @private */ getBufferedRangeForTime_(time) { const buffered = this.video.buffered; for (let i = 0; i < buffered.length; i++) { const start = buffered.start(i); const end = buffered.end(i); if (time >= start && time <= end) { return {start, end}; } } return null; } /** * @override */ update() { const colors = this.config_.seekBarColors; const currentTime = this.getValue(); const bufferedLength = this.video.buffered.length; let bufferedStart = 0; let bufferedEnd = 0; if (bufferedLength) { if (this.controls.isSeeking()) { // While the user drags, only paint the range that actually contains // the target position (if it exists). const r = this.getBufferedRangeForTime_(currentTime); if (r) { bufferedStart = r.start; bufferedEnd = r.end; } else { // Non-preloaded area: we do not buffered paint beyond the playhead. bufferedStart = currentTime; bufferedEnd = currentTime; } } else { bufferedStart = this.video.buffered.start(0); bufferedEnd = this.video.buffered.end(bufferedLength - 1); } } const seekRange = this.player.seekRange(); const seekRangeSize = seekRange.end - seekRange.start; this.setRange(seekRange.start, seekRange.end); if (!this.shouldBeDisplayed_()) { shaka.ui.Utils.setDisplay(this.container, false); } else { shaka.ui.Utils.setDisplay(this.container, true); const clampedBufferStart = Math.max(bufferedStart, seekRange.start); const clampedBufferEnd = Math.min(bufferedEnd, seekRange.end); const clampedCurrentTime = Math.min( Math.max(currentTime, seekRange.start), seekRange.end); const bufferStartDistance = clampedBufferStart - seekRange.start; const bufferEndDistance = clampedBufferEnd - seekRange.start; const playheadDistance = clampedCurrentTime - seekRange.start; // NOTE: the fallback to zero eliminates NaN. const bufferStartFraction = (bufferStartDistance / seekRangeSize) || 0; const bufferEndFraction = (bufferEndDistance / seekRangeSize) || 0; const playheadFraction = (playheadDistance / seekRangeSize) || 0; const unbufferedColor = this.config_.showUnbufferedStart ? colors.base : colors.played; const gradient = [ 'to right', this.makeColor_(unbufferedColor, bufferStartFraction), this.makeColor_(colors.played, bufferStartFraction), this.makeColor_(colors.played, playheadFraction), this.makeColor_(colors.buffered, playheadFraction), this.makeColor_(colors.buffered, bufferEndFraction), this.makeColor_(colors.base, bufferEndFraction), ]; this.container.style.background = 'linear-gradient(' + gradient.join(',') + ')'; } } /** * @private */ markAdBreaks_() { if (!this.adCuePoints_.length) { this.adMarkerContainer_.style.background = 'transparent'; this.adBreaksTimer_.stop(); return; } const seekRange = this.player.seekRange(); const seekRangeSize = seekRange.end - seekRange.start; const gradient = ['to right']; let pointsAsFractions = []; const adBreakColor = this.config_.seekBarColors.adBreaks; let postRollAd = false; for (const point of this.adCuePoints_) { // Post-roll ads are marked as starting at -1 in CS IMA ads. if (point.start == -1 && !point.end) { postRollAd = true; continue; } // Filter point within the seek range. For points with no endpoint // (client side ads) check that the start point is within range. if ((!point.end && point.start >= seekRange.start) || (typeof point.end == 'number' && point.end > seekRange.start)) { const startDist = Math.max(point.start, seekRange.start) - seekRange.start; const startFrac = (startDist / seekRangeSize) || 0; // For points with no endpoint assume a 1% length: not too much, // but enough to be visible on the timeline. let endFrac = startFrac + 0.01; if (point.end) { const endDist = point.end - seekRange.start; endFrac = (endDist / seekRangeSize) || 0; } pointsAsFractions.push({ start: startFrac, end: endFrac, }); } } pointsAsFractions = pointsAsFractions.sort((a, b) => { return a.start - b.start; }); for (const point of pointsAsFractions) { gradient.push(this.makeColor_('transparent', point.start)); gradient.push(this.makeColor_(adBreakColor, point.start)); gradient.push(this.makeColor_(adBreakColor, point.end)); gradient.push(this.makeColor_('transparent', point.end)); } if (postRollAd) { gradient.push(this.makeColor_('transparent', 0.99)); gradient.push(this.makeColor_(adBreakColor, 0.99)); } this.adMarkerContainer_.style.background = 'linear-gradient(' + gradient.join(',') + ')'; } /** * @private */ markChapters_() { const gradient = ['to right']; const chapterColor = this.config_.seekBarColors.chapters; const chapters = this.controls.getChapters(); if (!chapters.length || !chapterColor || chapterColor === 'transparent') { this.chapterMarkerContainer_.style.background = 'transparent'; this.chaptersTimer_?.stop(); return; } const seekRange = this.player.seekRange(); const seekRangeSize = seekRange.end - seekRange.start; const minSeekBarWindow = shaka.ui.SeekBar.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR_; if (seekRangeSize < minSeekBarWindow) { this.chapterMarkerContainer_.style.background = 'transparent'; this.chaptersTimer_?.stop(); return; } /** @type {!Set<number>} */ const points = new Set(); for (const chapter of chapters) { if (chapter.startTime < seekRange.start || chapter.startTime > seekRange.end) { continue; } if (chapter.startTime > seekRange.start) { points.add(chapter.startTime); } // We use minus 1 to avoid inaccuracies if (chapter.endTime && chapter.endTime < (seekRange.end - 1)) { points.add(chapter.endTime); } } if (!points.size) { this.chapterMarkerContainer_.style.background = 'transparent'; this.chaptersTimer_?.stop(); return; } const sortedPoints = Array.from(points).sort((a, b) => a - b); for (const point of sortedPoints) { const start = ((point - seekRange.start) / seekRangeSize) * 100 + '%'; const end = `calc(${start} + 2px)`; gradient.push(`transparent ${start}`); gradient.push(`${chapterColor} ${start}`); gradient.push(`${chapterColor} ${end}`); gradient.push(`transparent ${end}`); } this.chapterMarkerContainer_.style.background = 'linear-gradient(' + gradient.join(',') + ')'; } /** * @param {string} color * @param {number} fraction * @return {string} * @private */ makeColor_(color, fraction) { return color + ' ' + (fraction * 100) + '%'; } /** * @private */ onAdCuePointsChanged_() { const action = () => { this.markAdBreaks_(); const seekRange = this.player.seekRange(); const seekRangeSize = seekRange.end - seekRange.start; const minSeekBarWindow = shaka.ui.SeekBar.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR_; // Seek range keeps changing for live content and some of the known // ad breaks might not be in the seek range now, but get into // it later. // If we have a LIVE seekable content, keep checking for ad breaks // every second. if (this.player.isDynamic() && seekRangeSize > minSeekBarWindow) { this.adBreaksTimer_.tickEvery(/* seconds= */ 0.25); } }; action(); if (!this.player.isFullyLoaded()) { this.eventManager.listenOnce(this.player, 'loaded', action); } } /** * @return {boolean} * @private */ shouldBeDisplayed_() { // The seek bar should be hidden when the seek window's too small or // there's an ad playing. const seekRange = this.player.seekRange(); const seekRangeSize = seekRange.end - seekRange.start; if (this.player.isDynamic() && (seekRangeSize < shaka.ui.SeekBar.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR_ || !isFinite(seekRangeSize))) { return false; } return this.ad == null || !this.ad.isLinear(); } /** @private */ updateAriaLabel_() { this.bar.ariaLabel = this.localization.resolve(shaka.ui.Locales.Ids.SEEK); } /** * @param {number} value * @private */ showThumbnailAtValue_(value) { const min = parseFloat(this.bar.min); const max = parseFloat(this.bar.max); const rect = this.bar.getBoundingClientRect(); const thumbSize = 12; // @thumb-size in range_elements.less const scale = (rect.width - thumbSize) / (max - min); const position = (value - min) * scale + thumbSize / 2; this.showThumbnailAndTime_(position, value); } /** * @private */ async showThumbnailAndTime_(pixelPosition, value) { if (value < 0) { value = 0; } let isAdValue = false; if (this.adCuePoints_.length) { isAdValue = this.adCuePoints_.some((cuePoint) => { if (!cuePoint.end) { return false; } return value >= cuePoint.start && value <= cuePoint.end; }); } const seekRange = this.player.seekRange(); const playerValue = Math.max(Math.ceil(seekRange.start), Math.min(Math.floor(seekRange.end), value)); let time; if (this.player.isDynamic()) { const totalSeconds = seekRange.end - value; if (totalSeconds < 1) { time = this.localization.resolve(shaka.ui.Locales.Ids.LIVE); } else { time = '-' + this.timeFormatter_(totalSeconds); } } else { time = this.timeFormatter_(value); } const chapter = this.getChapter_(value); if (chapter) { this.thumbnailTime_.textContent = time + ' · ' + chapter.title; } else { this.thumbnailTime_.textContent = time; } const width = this.thumbnailContainer_.clientWidth; const leftPosition = Math.min(this.bar.offsetWidth - width, Math.max(0, pixelPosition - (width / 2))); this.thumbnailContainer_.style.left = leftPosition + 'px'; this.thumbnailContainer_.style.visibility = 'visible'; const hasImageTracks = this.player.getImageTracks().length > 0; const hasChapterThumbnails = chapter && chapter.images.length > 0; if (isAdValue || !(hasImageTracks || hasChapterThumbnails)) { this.thumbnailImageContainer_.style.display = 'none'; return; } this.thumbnailImageContainer_.style.display = ''; // Set the thumbnail height before getting the thumbnail because the // operation may take some time. if (!this.thumbnailImageContainer_.style.height) { let aspectRatio = 16 / 9; const videoTrack = this.player.getVideoTracks().find((t) => t.active); if (videoTrack && videoTrack.width && videoTrack.height) { aspectRatio = videoTrack.width / videoTrack.height; } const height = Math.floor(width / aspectRatio); this.thumbnailImageContainer_.style.height = height + 'px'; } let thumbnail; if (hasChapterThumbnails) { thumbnail = this.convertChapterToThumbnail_(chapter); } if (hasImageTracks) { thumbnail = await this.player.getThumbnails( /* trackId= */ null, playerValue) ?? thumbnail; } if (!thumbnail) { return; } if (thumbnail.width < thumbnail.height) { this.thumbnailImageContainer_.classList.add('portrait-thumbnail'); } else { this.thumbnailImageContainer_.classList.remove('portrait-thumbnail'); } let uri = thumbnail.uris[0].split('#xywh=')[0]; if (!this.lastThumbnail_ || uri !== this.lastThumbnail_.uris[0].split('#xywh=')[0] || thumbnail.startByte != this.lastThumbnail_.startByte || thumbnail.endByte != this.lastThumbnail_.endByte) { this.lastThumbnail_ = thumbnail; if (this.lastThumbnailPendingRequest_) { this.lastThumbnailPendingRequest_.abort(); this.lastThumbnailPendingRequest_ = null; } if (thumbnail.codecs == 'mjpg' || uri.startsWith('offline:')) { this.thumbnailImage_.src = shaka.ui.SeekBar.Transparent_Image_; try { const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT; const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT; const request = shaka.net.NetworkingUtils.createSegmentRequest( thumbnail.uris, thumbnail.startByte, thumbnail.endByte, this.player.getConfiguration().streaming.retryParameters); this.lastThumbnailPendingRequest_ = this.player.getNetworkingEngine() .request(requestType, request, {type}); const response = await this.lastThumbnailPendingRequest_.promise; this.lastThumbnailPendingRequest_ = null; uri = shaka.ui.Utils.getUriFromThumbnailResponse(thumbnail, response); } catch (error) { if (error.code == shaka.util.Error.Code.OPERATION_ABORTED) { return; } throw error; } } try { this.thumbnailImageContainer_.removeChild(this.thumbnailImage_); } catch (e) { // The image is not a child } this.thumbnailImage_ = /** @type {!HTMLImageElement} */ ( shaka.util.Dom.createHTMLElement('img')); this.thumbnailImage_.classList.add('shaka-player-ui-thumbnail-image'); this.thumbnailImage_.draggable = false; this.thumbnailImage_.src = uri; this.thumbnailImage_.onload = () => { if (uri.startsWith('blob:')) { URL.revokeObjectURL(uri); } }; this.thumbnailImageContainer_.insertBefore(this.thumbnailImage_, this.thumbnailImageContainer_.firstChild); } const widthImageContainer = this.thumbnailImageContainer_.clientWidth; const scale = widthImageContainer / thumbnail.width; if (thumbnail.imageHeight) { this.thumbnailImage_.height = thumbnail.imageHeight; } else if (!thumbnail.sprite) { this.thumbnailImage_.style.height = '100%'; this.thumbnailImage_.style.objectFit = 'contain'; } if (thumbnail.imageWidth) { this.thumbnailImage_.width = thumbnail.imageWidth; } else if (!thumbnail.sprite) { this.thumbnailImage_.style.width = '100%'; this.thumbnailImage_.style.objectFit = 'contain'; } if (!isNaN(scale) && isFinite(scale)) { this.thumbnailImage_.style.left = '-' + scale * thumbnail.positionX + 'px'; this.thumbnailImage_.style.top = '-' + scale * thumbnail.positionY + 'px'; this.thumbnailImage_.style.transform = 'scale(' + scale + ')'; this.thumbnailImage_.style.transformOrigin = 'left top'; } // Update container height const finalHeight = Math.floor(widthImageContainer * thumbnail.height / thumbnail.width); if (!isNaN(finalHeight) && isFinite(finalHeight)) { this.thumbnailImageContainer_.style.height = finalHeight + 'px'; } } /** * @private */ hideThumbnailTimeContainer_() { this.thumbnailContainer_.style.visibility = 'hidden'; } /** * @param {number} totalSeconds * @private */ timeFormatter_(totalSeconds) { return shaka.ui.Utils.buildTimeString(totalSeconds, totalSeconds >= 3600); } /** * @param {number} totalSeconds * @return {?shaka.extern.Chapter} * @private */ getChapter_(totalSeconds) { for (const chapter of this.controls.getChapters()) { if (chapter.startTime <= totalSeconds && chapter.endTime >= totalSeconds) { return chapter; } } return null; } /** * @param {?shaka.extern.Chapter} chapter * @return {?shaka.extern.Thumbnail} * @private */ convertChapterToThumbnail_(chapter) { if (!chapter || !chapter.images.length) { return null; } const image = chapter.images[0]; /** @type {shaka.extern.Thumbnail} */ const thumbnail = { segment: null, imageHeight: image.height || 0, imageWidth: image.width || 0, height: image.height || 0, positionX: 0, positionY: 0, startTime: chapter.startTime, duration: chapter.endTime - chapter.startTime, uris: [image.url], startByte: 0, endByte: null, width: image.width || 0, sprite: false, mimeType: '', codecs: '', }; return thumbnail; } }; /** * @const {string} * @private */ shaka.ui.SeekBar.Transparent_Image_ = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"/>'; /** * @const {number} * @private */ shaka.ui.SeekBar.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR_ = 5; // seconds /** * @implements {shaka.extern.IUISeekBar.Factory} * @export */ shaka.ui.SeekBar.Factory = class { /** * Creates a shaka.ui.SeekBar. Use this factory to register the default * SeekBar when needed * * @override */ create(rootElement, controls) { return new shaka.ui.SeekBar(rootElement, controls); } };