/**
* @license
* Copyright 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.ui.SeekBar');
goog.require('shaka.ui.Constants');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.Localization');
goog.require('shaka.ui.RangeElement');
goog.require('shaka.ui.Utils');
goog.require('shaka.util.Timer');
/**
* @extends {shaka.ui.RangeElement}
* @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 {!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(() => {
this.video.currentTime = this.getValue();
});
/**
* When user is scrubbing the seek bar - we should pause the video - see https://git.io/JUhHG
* but will conditionally pause or play the video after scrubbing
* depending on its previous state
*
* @private {boolean}
*/
this.wasPlaying_ = false;
this.eventManager.listen(this.localization,
shaka.ui.Localization.LOCALE_UPDATED,
() => this.updateAriaLabel_());
this.eventManager.listen(this.localization,
shaka.ui.Localization.LOCALE_CHANGED,
() => this.updateAriaLabel_());
// Initialize seek state and label.
this.setValue(this.video.currentTime);
this.update();
this.updateAriaLabel_();
}
/** @override */
async destroy() {
if (this.seekTimer_) {
this.seekTimer_.stop();
this.seekTimer_ = null;
}
await super.destroy();
}
/**
* 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();
}
/**
* 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);
}
/**
* 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();
}
}
/** @return {boolean} */
isShowing() {
// It is showing by default, so it is hidden if shaka-hidden is in the list.
return !this.container.classList.contains('shaka-hidden');
}
/**
* Called by Controls on a timer to update the state of the seek bar.
* Also called internally when the user interacts with the input element.
*/
update() {
const colors = this.config_.seekBarColors;
const currentTime = this.getValue();
const bufferedLength = this.video.buffered.length;
const bufferedStart = bufferedLength ? this.video.buffered.start(0) : 0;
const bufferedEnd =
bufferedLength ? this.video.buffered.end(bufferedLength - 1) : 0;
const seekRange = this.player.seekRange();
const seekRangeSize = seekRange.end - seekRange.start;
this.setRange(seekRange.start, seekRange.end);
// Hide seekbar if the seek window is very small.
if (this.player.isLive() &&
seekRangeSize < shaka.ui.Constants.MIN_SEEK_WINDOW_TO_SHOW_SEEKBAR) {
shaka.ui.Utils.setDisplay(this.container, false);
} else {
shaka.ui.Utils.setDisplay(this.container, true);
if (bufferedLength == 0) {
this.container.style.background = colors.base;
} else {
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 makeColor = (color, fract) => color + ' ' + (fract * 100) + '%';
const gradient = [
'to right',
makeColor(unbufferedColor, bufferStartFraction),
makeColor(colors.played, bufferStartFraction),
makeColor(colors.played, playheadFraction),
makeColor(colors.buffered, playheadFraction),
makeColor(colors.buffered, bufferEndFraction),
makeColor(colors.base, bufferEndFraction),
];
this.container.style.background =
'linear-gradient(' + gradient.join(',') + ')';
}
}
}
/** @private */
updateAriaLabel_() {
this.bar.setAttribute(shaka.ui.Constants.ARIA_LABEL,
this.localization.resolve(shaka.ui.Locales.Ids.SEEK));
}
};