summaryrefslogtreecommitdiff
path: root/javascript/videojs/src/js/control-bar/progress-control/seek-bar.js
diff options
context:
space:
mode:
Diffstat (limited to 'javascript/videojs/src/js/control-bar/progress-control/seek-bar.js')
-rw-r--r--javascript/videojs/src/js/control-bar/progress-control/seek-bar.js610
1 files changed, 610 insertions, 0 deletions
diff --git a/javascript/videojs/src/js/control-bar/progress-control/seek-bar.js b/javascript/videojs/src/js/control-bar/progress-control/seek-bar.js
new file mode 100644
index 0000000..bc7d83c
--- /dev/null
+++ b/javascript/videojs/src/js/control-bar/progress-control/seek-bar.js
@@ -0,0 +1,610 @@
+/**
+ * @file seek-bar.js
+ */
+import Slider from '../../slider/slider.js';
+import Component from '../../component.js';
+import {IS_IOS, IS_ANDROID} from '../../utils/browser.js';
+import * as Dom from '../../utils/dom.js';
+import * as Fn from '../../utils/fn.js';
+import {formatTime} from '../../utils/time.js';
+import {silencePromise} from '../../utils/promise';
+import {merge} from '../../utils/obj';
+import document from 'global/document';
+
+/** @import Player from '../../player' */
+
+import './load-progress-bar.js';
+import './play-progress-bar.js';
+import './mouse-time-display.js';
+
+/**
+ * Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
+ * as its `bar`.
+ *
+ * @extends Slider
+ */
+class SeekBar extends Slider {
+
+ /**
+ * Creates an instance of this class.
+ *
+ * @param {Player} player
+ * The `Player` that this class should be attached to.
+ *
+ * @param {Object} [options]
+ * The key/value store of player options.
+ * @param {number} [options.stepSeconds=5]
+ * The number of seconds to increment on keyboard control
+ * @param {number} [options.pageMultiplier=12]
+ * The multiplier of stepSeconds that PgUp/PgDown move the timeline.
+ */
+ constructor(player, options) {
+ options = merge(SeekBar.prototype.options_, options);
+
+ // Avoid mutating the prototype's `children` array by creating a copy
+ options.children = [...options.children];
+
+ const shouldDisableSeekWhileScrubbing =
+ (player.options_.disableSeekWhileScrubbingOnMobile && (IS_IOS || IS_ANDROID)) ||
+ (player.options_.disableSeekWhileScrubbingOnSTV);
+
+ // Add the TimeTooltip as a child if we are on desktop, or on mobile with `disableSeekWhileScrubbingOnMobile: true`
+ if ((!IS_IOS && !IS_ANDROID) || shouldDisableSeekWhileScrubbing) {
+ options.children.splice(1, 0, 'mouseTimeDisplay');
+ }
+
+ super(player, options);
+
+ this.shouldDisableSeekWhileScrubbing_ = shouldDisableSeekWhileScrubbing;
+ this.pendingSeekTime_ = null;
+
+ this.setEventHandlers_();
+ }
+
+ /**
+ * Sets the event handlers
+ *
+ * @private
+ */
+ setEventHandlers_() {
+ this.update_ = Fn.bind_(this, this.update);
+ this.update = Fn.throttle(this.update_, Fn.UPDATE_REFRESH_INTERVAL);
+
+ this.on(this.player_, ['durationchange', 'timeupdate'], this.update);
+ this.on(this.player_, ['ended'], this.update_);
+ if (this.player_.liveTracker) {
+ this.on(this.player_.liveTracker, 'liveedgechange', this.update);
+ }
+
+ // when playing, let's ensure we smoothly update the play progress bar
+ // via an interval
+ this.updateInterval = null;
+
+ this.enableIntervalHandler_ = (e) => this.enableInterval_(e);
+ this.disableIntervalHandler_ = (e) => this.disableInterval_(e);
+
+ this.on(this.player_, ['playing'], this.enableIntervalHandler_);
+
+ this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
+
+ // we don't need to update the play progress if the document is hidden,
+ // also, this causes the CPU to spike and eventually crash the page on IE11.
+ if ('hidden' in document && 'visibilityState' in document) {
+ this.on(document, 'visibilitychange', this.toggleVisibility_);
+ }
+ }
+
+ toggleVisibility_(e) {
+ if (document.visibilityState === 'hidden') {
+ this.cancelNamedAnimationFrame('SeekBar#update');
+ this.cancelNamedAnimationFrame('Slider#update');
+ this.disableInterval_(e);
+ } else {
+ if (!this.player_.ended() && !this.player_.paused()) {
+ this.enableInterval_();
+ }
+
+ // we just switched back to the page and someone may be looking, so, update ASAP
+ this.update();
+ }
+ }
+
+ enableInterval_() {
+ if (this.updateInterval) {
+ return;
+
+ }
+ this.updateInterval = this.setInterval(this.update, Fn.UPDATE_REFRESH_INTERVAL);
+ }
+
+ disableInterval_(e) {
+ if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
+ return;
+ }
+
+ if (!this.updateInterval) {
+ return;
+ }
+
+ this.clearInterval(this.updateInterval);
+ this.updateInterval = null;
+ }
+
+ /**
+ * Create the `Component`'s DOM element
+ *
+ * @return {Element}
+ * The element that was created.
+ */
+ createEl() {
+ return super.createEl('div', {
+ className: 'vjs-progress-holder'
+ }, {
+ 'aria-label': this.localize('Progress Bar')
+ });
+ }
+
+ /**
+ * This function updates the play progress bar and accessibility
+ * attributes to whatever is passed in.
+ *
+ * @param {Event} [event]
+ * The `timeupdate` or `ended` event that caused this to run.
+ *
+ * @listens Player#timeupdate
+ *
+ * @return {number}
+ * The current percent at a number from 0-1
+ */
+ update(event) {
+ // ignore updates while the tab is hidden
+ if (document.visibilityState === 'hidden') {
+ return;
+ }
+
+ const percent = super.update();
+
+ this.requestNamedAnimationFrame('SeekBar#update', () => {
+ const currentTime = this.player_.ended() ?
+ this.player_.duration() : this.getCurrentTime_();
+ const liveTracker = this.player_.liveTracker;
+ let duration = this.player_.duration();
+
+ if (liveTracker && liveTracker.isLive()) {
+ duration = this.player_.liveTracker.liveCurrentTime();
+ }
+
+ if (this.percent_ !== percent) {
+ // machine readable value of progress bar (percentage complete)
+ this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
+ this.percent_ = percent;
+ }
+
+ if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
+ // human readable value of progress bar (time complete)
+ this.el_.setAttribute(
+ 'aria-valuetext',
+ this.localize(
+ 'progress bar timing: currentTime={1} duration={2}',
+ [formatTime(currentTime, duration),
+ formatTime(duration, duration)],
+ '{1} of {2}'
+ )
+ );
+
+ this.currentTime_ = currentTime;
+ this.duration_ = duration;
+ }
+
+ // update the progress bar time tooltip with the current time
+ if (this.bar) {
+ this.bar.update(Dom.getBoundingClientRect(this.el()), this.getProgress(), event);
+ }
+ });
+
+ return percent;
+ }
+
+ /**
+ * Prevent liveThreshold from causing seeks to seem like they
+ * are not happening from a user perspective.
+ *
+ * @param {number} ct
+ * current time to seek to
+ */
+ userSeek_(ct) {
+ if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
+ this.player_.liveTracker.nextSeekedFromUser();
+ }
+
+ this.player_.currentTime(ct);
+ }
+
+ /**
+ * Get the value of current time but allows for smooth scrubbing,
+ * when player can't keep up.
+ *
+ * @return {number}
+ * The current time value to display
+ *
+ * @private
+ */
+ getCurrentTime_() {
+ return (this.player_.scrubbing()) ?
+ this.player_.getCache().currentTime :
+ this.player_.currentTime();
+ }
+
+ /**
+ * Getter and setter for pendingSeekTime.
+ * Ensures the value is clamped between 0 and duration.
+ *
+ * @param {number|null} [time] - Optional. The new pending seek time, can be a number or null.
+ * @return {number|null} - The current pending seek time.
+ */
+ pendingSeekTime(time) {
+ if (time !== undefined) {
+ if (time !== null) {
+ const duration = this.player_.duration();
+
+ this.pendingSeekTime_ = Math.max(0, Math.min(time, duration));
+ } else {
+ this.pendingSeekTime_ = null;
+ }
+ }
+ return this.pendingSeekTime_;
+ }
+
+ /**
+ * Get the percentage of media played so far.
+ *
+ * @return {number}
+ * The percentage of media played so far (0 to 1).
+ */
+ getPercent() {
+ // If we have a pending seek time, we are scrubbing on mobile and should set the slider percent
+ // to reflect the current scrub location.
+ if (this.pendingSeekTime() !== null) {
+ return this.pendingSeekTime() / this.player_.duration();
+ }
+
+ const currentTime = this.getCurrentTime_();
+ let percent;
+ const liveTracker = this.player_.liveTracker;
+
+ if (liveTracker && liveTracker.isLive()) {
+ percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();
+
+ // prevent the percent from changing at the live edge
+ if (liveTracker.atLiveEdge()) {
+ percent = 1;
+ }
+ } else {
+ percent = currentTime / this.player_.duration();
+ }
+
+ return percent;
+ }
+
+ /**
+ * Handle mouse down on seek bar
+ *
+ * @param {MouseEvent} event
+ * The `mousedown` event that caused this to run.
+ *
+ * @listens mousedown
+ */
+ handleMouseDown(event) {
+ if (!Dom.isSingleLeftClick(event)) {
+ return;
+ }
+
+ // Stop event propagation to prevent double fire in progress-control.js
+ event.stopPropagation();
+
+ this.videoWasPlaying = !this.player_.paused();
+
+ // Don't pause if we are on mobile and `disableSeekWhileScrubbingOnMobile: true`.
+ // In that case, playback should continue while the player scrubs to a new location.
+ if (!this.shouldDisableSeekWhileScrubbing_) {
+ this.player_.pause();
+ }
+
+ super.handleMouseDown(event);
+ }
+
+ /**
+ * Handle mouse move on seek bar
+ *
+ * @param {MouseEvent} event
+ * The `mousemove` event that caused this to run.
+ * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false
+ *
+ * @listens mousemove
+ */
+ handleMouseMove(event, mouseDown = false) {
+ if (!Dom.isSingleLeftClick(event) || isNaN(this.player_.duration())) {
+ return;
+ }
+
+ if (!mouseDown && !this.player_.scrubbing()) {
+ this.player_.scrubbing(true);
+ }
+
+ let newTime;
+ const distance = this.calculateDistance(event);
+ const liveTracker = this.player_.liveTracker;
+
+ if (!liveTracker || !liveTracker.isLive()) {
+ newTime = distance * this.player_.duration();
+
+ // Don't let video end while scrubbing.
+ if (newTime === this.player_.duration()) {
+ newTime = newTime - 0.1;
+ }
+ } else {
+
+ if (distance >= 0.99) {
+ liveTracker.seekToLiveEdge();
+ return;
+ }
+ const seekableStart = liveTracker.seekableStart();
+ const seekableEnd = liveTracker.liveCurrentTime();
+
+ newTime = seekableStart + (distance * liveTracker.liveWindow());
+
+ // Don't let video end while scrubbing.
+ if (newTime >= seekableEnd) {
+ newTime = seekableEnd;
+ }
+
+ // Compensate for precision differences so that currentTime is not less
+ // than seekable start
+ if (newTime <= seekableStart) {
+ newTime = seekableStart + 0.1;
+ }
+
+ // On android seekableEnd can be Infinity sometimes,
+ // this will cause newTime to be Infinity, which is
+ // not a valid currentTime.
+ if (newTime === Infinity) {
+ return;
+ }
+ }
+
+ // if on mobile and `disableSeekWhileScrubbingOnMobile: true`, keep track of the desired seek point but we won't initiate the seek until 'touchend'
+ if (this.shouldDisableSeekWhileScrubbing_) {
+ this.pendingSeekTime(newTime);
+ } else {
+ this.userSeek_(newTime);
+ }
+
+ if (this.player_.options_.enableSmoothSeeking) {
+ this.update();
+ }
+ }
+
+ enable() {
+ super.enable();
+ const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
+
+ if (!mouseTimeDisplay) {
+ return;
+ }
+
+ mouseTimeDisplay.show();
+ }
+
+ disable() {
+ super.disable();
+ const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
+
+ if (!mouseTimeDisplay) {
+ return;
+ }
+
+ mouseTimeDisplay.hide();
+ }
+
+ /**
+ * Handle mouse up on seek bar
+ *
+ * @param {MouseEvent} event
+ * The `mouseup` event that caused this to run.
+ *
+ * @listens mouseup
+ */
+ handleMouseUp(event) {
+ super.handleMouseUp(event);
+
+ // Stop event propagation to prevent double fire in progress-control.js
+ if (event) {
+ event.stopPropagation();
+ }
+ this.player_.scrubbing(false);
+
+ // If we have a pending seek time, then we have finished scrubbing on mobile and should initiate a seek.
+ if (this.pendingSeekTime() !== null) {
+ this.userSeek_(this.pendingSeekTime());
+
+ this.pendingSeekTime(null);
+ }
+
+ /**
+ * Trigger timeupdate because we're done seeking and the time has changed.
+ * This is particularly useful for if the player is paused to time the time displays.
+ *
+ * @event Tech#timeupdate
+ * @type {Event}
+ */
+ this.player_.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
+ if (this.videoWasPlaying) {
+ silencePromise(this.player_.play());
+ } else {
+ // We're done seeking and the time has changed.
+ // If the player is paused, make sure we display the correct time on the seek bar.
+ this.update_();
+ }
+ }
+
+ /**
+ * Handles pending seek time when `disableSeekWhileScrubbingOnSTV` is enabled.
+ *
+ * @param {number} stepAmount - The number of seconds to step (positive for forward, negative for backward).
+ */
+ handlePendingSeek_(stepAmount) {
+ if (!this.player_.paused()) {
+ this.player_.pause();
+ }
+
+ const currentPos = this.pendingSeekTime() !== null ?
+ this.pendingSeekTime() :
+ this.player_.currentTime();
+
+ this.pendingSeekTime(currentPos + stepAmount);
+ this.player_.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
+ }
+
+ /**
+ * Move more quickly fast forward for keyboard-only users
+ */
+ stepForward() {
+ // if `disableSeekWhileScrubbingOnSTV: true`, keep track of the desired seek point but we won't initiate the seek
+ if (this.shouldDisableSeekWhileScrubbing_) {
+ this.handlePendingSeek_(this.options().stepSeconds);
+ } else {
+ this.userSeek_(this.player_.currentTime() + this.options().stepSeconds);
+ }
+ }
+
+ /**
+ * Move more quickly rewind for keyboard-only users
+ */
+ stepBack() {
+ // if `disableSeekWhileScrubbingOnSTV: true`, keep track of the desired seek point but we won't initiate the seek
+ if (this.shouldDisableSeekWhileScrubbing_) {
+ this.handlePendingSeek_(-this.options().stepSeconds);
+ } else {
+ this.userSeek_(this.player_.currentTime() - this.options().stepSeconds);
+ }
+ }
+
+ /**
+ * Toggles the playback state of the player
+ * This gets called when enter or space is used on the seekbar
+ *
+ * @param {KeyboardEvent} event
+ * The `keydown` event that caused this function to be called
+ *
+ */
+ handleAction(event) {
+ if (this.pendingSeekTime() !== null) {
+ this.userSeek_(this.pendingSeekTime());
+ this.pendingSeekTime(null);
+ }
+ if (this.player_.paused()) {
+ this.player_.play();
+ } else {
+ this.player_.pause();
+ }
+ }
+
+ /**
+ * Called when this SeekBar has focus and a key gets pressed down.
+ * Supports the following keys:
+ *
+ * Space or Enter key fire a click event
+ * Home key moves to start of the timeline
+ * End key moves to end of the timeline
+ * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
+ * PageDown key moves back a larger step than ArrowDown
+ * PageUp key moves forward a large step
+ *
+ * @param {KeyboardEvent} event
+ * The `keydown` event that caused this function to be called.
+ *
+ * @listens keydown
+ */
+ handleKeyDown(event) {
+ const liveTracker = this.player_.liveTracker;
+
+ if (event.key === ' ' || event.key === 'Enter') {
+ event.preventDefault();
+ event.stopPropagation();
+ this.handleAction(event);
+ } else if (event.key === 'Home') {
+ event.preventDefault();
+ event.stopPropagation();
+ this.userSeek_(0);
+ } else if (event.key === 'End') {
+ event.preventDefault();
+ event.stopPropagation();
+ if (liveTracker && liveTracker.isLive()) {
+ this.userSeek_(liveTracker.liveCurrentTime());
+ } else {
+ this.userSeek_(this.player_.duration());
+ }
+ } else if (/^[0-9]$/.test(event.key)) {
+ event.preventDefault();
+ event.stopPropagation();
+ const gotoFraction = parseInt(event.key, 10) * 0.1;
+
+ if (liveTracker && liveTracker.isLive()) {
+ this.userSeek_(liveTracker.seekableStart() + (liveTracker.liveWindow() * gotoFraction));
+ } else {
+ this.userSeek_(this.player_.duration() * gotoFraction);
+ }
+ } else if (event.key === 'PageDown') {
+ event.preventDefault();
+ event.stopPropagation();
+ this.userSeek_(this.player_.currentTime() - (this.options().stepSeconds * this.options().pageMultiplier));
+ } else if (event.key === 'PageUp') {
+ event.preventDefault();
+ event.stopPropagation();
+ this.userSeek_(this.player_.currentTime() + (this.options().stepSeconds * this.options().pageMultiplier));
+ } else {
+ // Pass keydown handling up for unsupported keys
+ super.handleKeyDown(event);
+ }
+ }
+
+ dispose() {
+ this.disableInterval_();
+
+ this.off(this.player_, ['durationchange', 'timeupdate'], this.update);
+ this.off(this.player_, ['ended'], this.update_);
+ if (this.player_.liveTracker) {
+ this.off(this.player_.liveTracker, 'liveedgechange', this.update);
+ }
+
+ this.off(this.player_, ['playing'], this.enableIntervalHandler_);
+ this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
+
+ // we don't need to update the play progress if the document is hidden,
+ // also, this causes the CPU to spike and eventually crash the page on IE11.
+ if ('hidden' in document && 'visibilityState' in document) {
+ this.off(document, 'visibilitychange', this.toggleVisibility_);
+ }
+
+ super.dispose();
+ }
+}
+
+/**
+ * Default options for the `SeekBar`
+ *
+ * @type {Object}
+ * @private
+ */
+SeekBar.prototype.options_ = {
+ children: [
+ 'loadProgressBar',
+ 'playProgressBar'
+ ],
+ barName: 'playProgressBar',
+ stepSeconds: 5,
+ pageMultiplier: 12
+};
+
+Component.registerComponent('SeekBar', SeekBar);
+export default SeekBar;