diff --git a/src/js/control-bar/progress-control/progress-control.js b/src/js/control-bar/progress-control/progress-control.js index 82f840665d..8b5bdc9f15 100644 --- a/src/js/control-bar/progress-control/progress-control.js +++ b/src/js/control-bar/progress-control/progress-control.js @@ -56,25 +56,34 @@ class ProgressControl extends Component { handleMouseMove(event) { const seekBar = this.getChild('seekBar'); - if (seekBar) { - const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); - const seekBarEl = seekBar.el(); - const seekBarRect = Dom.getBoundingClientRect(seekBarEl); - let seekBarPoint = Dom.getPointerPosition(seekBarEl, event).x; - - // The default skin has a gap on either side of the `SeekBar`. This means - // that it's possible to trigger this behavior outside the boundaries of - // the `SeekBar`. This ensures we stay within it at all times. - if (seekBarPoint > 1) { - seekBarPoint = 1; - } else if (seekBarPoint < 0) { - seekBarPoint = 0; - } - - if (mouseTimeDisplay) { - mouseTimeDisplay.update(seekBarRect, seekBarPoint); - } + if (!seekBar) { + return; } + + const playProgressBar = seekBar.getChild('playProgressBar'); + const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); + + if (!playProgressBar && !mouseTimeDisplay) { + return; + } + + const seekBarEl = seekBar.el(); + const seekBarRect = Dom.getBoundingClientRect(seekBarEl); + let seekBarPoint = Dom.getPointerPosition(seekBarEl, event).x; + + // The default skin has a gap on either side of the `SeekBar`. This means + // that it's possible to trigger this behavior outside the boundaries of + // the `SeekBar`. This ensures we stay within it at all times. + seekBarPoint = Math.max(0, Math.min(1, seekBarPoint)); + + if (mouseTimeDisplay) { + mouseTimeDisplay.update(seekBarRect, seekBarPoint); + } + + if (playProgressBar) { + playProgressBar.update(seekBarRect, seekBar.percent_); + } + } /** diff --git a/src/js/control-bar/progress-control/seek-bar.js b/src/js/control-bar/progress-control/seek-bar.js index ea19ff76a3..8ffc3ca5f0 100644 --- a/src/js/control-bar/progress-control/seek-bar.js +++ b/src/js/control-bar/progress-control/seek-bar.js @@ -54,9 +54,7 @@ class SeekBar extends Slider { setEventHandlers_() { this.update = Fn.throttle(Fn.bind(this, this.update), UPDATE_REFRESH_INTERVAL); - this.on(this.player_, 'timeupdate', this.update); - this.on(this.player_, 'ended', this.handleEnded); - this.on(this.player_, 'durationchange', this.update); + this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update); if (this.player_.liveTracker) { this.on(this.player_.liveTracker, 'liveedgechange', this.update); } @@ -83,16 +81,16 @@ class SeekBar extends Slider { this.enableInterval_(); // we just switched back to the page and someone may be looking, so, update ASAP - this.requestAnimationFrame(this.update); + this.update(); } } enableInterval_() { - this.clearInterval(this.updateInterval); + if (this.updateInterval) { + return; - this.updateInterval = this.setInterval(() =>{ - this.requestAnimationFrame(this.update); - }, UPDATE_REFRESH_INTERVAL); + } + this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL); } disableInterval_(e) { @@ -100,7 +98,12 @@ class SeekBar extends Slider { return; } + if (!this.updateInterval) { + return; + } + this.clearInterval(this.updateInterval); + this.updateInterval = null; } /** @@ -130,31 +133,36 @@ class SeekBar extends Slider { * @private */ update_(currentTime, percent) { - const liveTracker = this.player_.liveTracker; - let duration = this.player_.duration(); + this.requestAnimationFrame(() => { + const liveTracker = this.player_.liveTracker; + let duration = this.player_.duration(); - if (liveTracker && liveTracker.isLive()) { - duration = this.player_.liveTracker.liveCurrentTime(); - } + if (liveTracker && liveTracker.isLive()) { + duration = this.player_.liveTracker.liveCurrentTime(); + } - // machine readable value of progress bar (percentage complete) - this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2)); - - // 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}' - ) - ); - - // Update the `PlayProgressBar`. - if (this.bar) { - this.bar.update(Dom.getBoundingClientRect(this.el_), percent); - } + 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; + } + }); } /** @@ -169,15 +177,11 @@ class SeekBar extends Slider { * The current percent at a number from 0-1 */ update(event) { - // if the offsetParent is null, then this element is hidden, in which case - // we don't need to update it. - if (this.el().offsetParent === null) { - return; - } - const percent = super.update(); + const currentTime = this.player_.ended() ? + this.player.duration() : this.getCurrentTime_(); - this.update_(this.getCurrentTime_(), percent); + this.update_(currentTime, percent); return percent; } @@ -196,19 +200,6 @@ class SeekBar extends Slider { this.player_.currentTime(); } - /** - * We want the seek bar to be full on ended - * no matter what the actual internal values are. so we force it. - * - * @param {EventTarget~Event} [event] - * The `timeupdate` or `ended` event that caused this to run. - * - * @listens Player#ended - */ - handleEnded(event) { - this.update_(this.player_.duration(), 1); - } - /** * Get the percentage of media played so far. * @@ -231,7 +222,7 @@ class SeekBar extends Slider { percent = currentTime / this.player_.duration(); } - return percent >= 1 ? 1 : (percent || 0); + return percent; } /** diff --git a/src/js/control-bar/progress-control/time-tooltip.js b/src/js/control-bar/progress-control/time-tooltip.js index e336fdc0b5..9da2fb7b7b 100644 --- a/src/js/control-bar/progress-control/time-tooltip.js +++ b/src/js/control-bar/progress-control/time-tooltip.js @@ -5,6 +5,7 @@ import Component from '../../component'; import * as Dom from '../../utils/dom.js'; import formatTime from '../../utils/format-time.js'; import * as Fn from '../../utils/fn.js'; +import * as browser from '../../utils/browser.js'; /** * Time tooltips display a time above the progress bar. @@ -24,6 +25,9 @@ class TimeTooltip extends Component { */ constructor(player, options) { super(player, options); + if (browser.IS_IOS || browser.IS_ANDROID) { + this.hide(); + } this.update = Fn.throttle(Fn.bind(this, this.update), Fn.UPDATE_REFRESH_INTERVAL); } diff --git a/src/js/control-bar/time-controls/current-time-display.js b/src/js/control-bar/time-controls/current-time-display.js index f2fdea203d..3eec4b1556 100644 --- a/src/js/control-bar/time-controls/current-time-display.js +++ b/src/js/control-bar/time-controls/current-time-display.js @@ -11,20 +11,6 @@ import Component from '../../component.js'; */ class CurrentTimeDisplay extends TimeDisplay { - /** - * 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. - */ - constructor(player, options) { - super(player, options); - this.on(player, 'ended', this.handleEnded); - } - /** * Builds the default DOM `className`. * @@ -45,28 +31,16 @@ class CurrentTimeDisplay extends TimeDisplay { */ updateContent(event) { // Allows for smooth scrubbing, when player can't keep up. - const time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); - - this.updateFormattedTime_(time); - } + let time; - /** - * When the player fires ended there should be no time left. Sadly - * this is not always the case, lets make it seem like that is the case - * for users. - * - * @param {EventTarget~Event} [event] - * The `ended` event that caused this to run. - * - * @listens Player#ended - */ - handleEnded(event) { - if (!this.player_.duration()) { - return; + if (this.player_.ended()) { + time = this.player_.duration(); + } else { + time = (this.player_.scrubbing()) ? this.player_.getCache().currentTime : this.player_.currentTime(); } - this.updateFormattedTime_(this.player_.duration()); - } + this.updateTextNode_(time); + } } /** diff --git a/src/js/control-bar/time-controls/duration-display.js b/src/js/control-bar/time-controls/duration-display.js index f8c24b155d..63af3fe6ab 100644 --- a/src/js/control-bar/time-controls/duration-display.js +++ b/src/js/control-bar/time-controls/duration-display.js @@ -36,7 +36,7 @@ class DurationDisplay extends TimeDisplay { // Also listen for timeupdate (in the parent) and loadedmetadata because removing those // listeners could have broken dependent applications/libraries. These // can likely be removed for 7.0. - this.on(player, 'loadedmetadata', this.throttledUpdateContent); + this.on(player, 'loadedmetadata', this.updateContent); } /** @@ -63,10 +63,7 @@ class DurationDisplay extends TimeDisplay { updateContent(event) { const duration = this.player_.duration(); - if (this.duration_ !== duration) { - this.duration_ = duration; - this.updateFormattedTime_(duration); - } + this.updateTextNode_(duration); } } diff --git a/src/js/control-bar/time-controls/remaining-time-display.js b/src/js/control-bar/time-controls/remaining-time-display.js index 68294dbf25..77995a07c5 100644 --- a/src/js/control-bar/time-controls/remaining-time-display.js +++ b/src/js/control-bar/time-controls/remaining-time-display.js @@ -23,8 +23,7 @@ class RemainingTimeDisplay extends TimeDisplay { */ constructor(player, options) { super(player, options); - this.on(player, 'durationchange', this.throttledUpdateContent); - this.on(player, 'ended', this.handleEnded); + this.on(player, 'durationchange', this.updateContent); } /** @@ -64,30 +63,19 @@ class RemainingTimeDisplay extends TimeDisplay { return; } + let time; + // @deprecated We should only use remainingTimeDisplay // as of video.js 7 - if (this.player_.remainingTimeDisplay) { - this.updateFormattedTime_(this.player_.remainingTimeDisplay()); + if (this.player_.ended()) { + time = 0; + } else if (this.player_.remainingTimeDisplay) { + time = this.player_.remainingTimeDisplay(); } else { - this.updateFormattedTime_(this.player_.remainingTime()); + time = this.player_.remainingTime(); } - } - /** - * When the player fires ended there should be no time left. Sadly - * this is not always the case, lets make it seem like that is the case - * for users. - * - * @param {EventTarget~Event} [event] - * The `ended` event that caused this to run. - * - * @listens Player#ended - */ - handleEnded(event) { - if (!this.player_.duration()) { - return; - } - this.updateFormattedTime_(0); + this.updateTextNode_(time); } } diff --git a/src/js/control-bar/time-controls/time-display.js b/src/js/control-bar/time-controls/time-display.js index 6440408472..e104c8d0b6 100644 --- a/src/js/control-bar/time-controls/time-display.js +++ b/src/js/control-bar/time-controls/time-display.js @@ -4,7 +4,6 @@ import document from 'global/document'; import Component from '../../component.js'; import * as Dom from '../../utils/dom.js'; -import {bind, throttle, UPDATE_REFRESH_INTERVAL} from '../../utils/fn.js'; import formatTime from '../../utils/format-time.js'; /** @@ -25,8 +24,9 @@ class TimeDisplay extends Component { */ constructor(player, options) { super(player, options); - this.throttledUpdateContent = throttle(bind(this, this.updateContent), UPDATE_REFRESH_INTERVAL); - this.on(player, 'timeupdate', this.throttledUpdateContent); + + this.on(player, ['timeupdate', 'ended'], this.updateContent); + this.updateTextNode_(); } /** @@ -54,7 +54,6 @@ class TimeDisplay extends Component { 'role': 'presentation' }); - this.updateTextNode_(); el.appendChild(this.contentEl_); return el; } @@ -67,57 +66,40 @@ class TimeDisplay extends Component { } /** - * Updates the "remaining time" text node with new content using the - * contents of the `formattedTime_` property. + * Updates the time display text node with a new time + * + * @param {number} [time=0] the time to update to * * @private */ - updateTextNode_() { - if (!this.contentEl_) { + updateTextNode_(time = 0) { + time = formatTime(time); + + if (this.formattedTime_ === time) { return; } - while (this.contentEl_.firstChild) { - this.contentEl_.removeChild(this.contentEl_.firstChild); - } + this.formattedTime_ = time; - this.textNode_ = document.createTextNode(this.formattedTime_ || this.formatTime_(0)); - this.contentEl_.appendChild(this.textNode_); - } + this.requestAnimationFrame(() => { + if (!this.contentEl_) { + return; + } - /** - * Generates a formatted time for this component to use in display. - * - * @param {number} time - * A numeric time, in seconds. - * - * @return {string} - * A formatted time - * - * @private - */ - formatTime_(time) { - return formatTime(time); - } + const oldNode = this.textNode_; - /** - * Updates the time display text node if it has what was passed in changed - * the formatted time. - * - * @param {number} time - * The time to update to - * - * @private - */ - updateFormattedTime_(time) { - const formattedTime = this.formatTime_(time); + this.textNode_ = document.createTextNode(this.formattedTime_); - if (formattedTime === this.formattedTime_) { - return; - } + if (!this.textNode_) { + return; + } - this.formattedTime_ = formattedTime; - this.requestAnimationFrame(this.updateTextNode_); + if (oldNode) { + this.contentEl_.replaceChild(this.textNode_, oldNode); + } else { + this.contentEl_.appendChild(this.textNode_); + } + }); } /** diff --git a/src/js/slider/slider.js b/src/js/slider/slider.js index cac295eab3..61f53d6356 100644 --- a/src/js/slider/slider.js +++ b/src/js/slider/slider.js @@ -234,40 +234,27 @@ class Slider extends Component { // In VolumeBar init we have a setTimeout for update that pops and update // to the end of the execution stack. The player is destroyed before then // update will cause an error - if (!this.el_) { + // If there's no bar... + if (!this.el_ || !this.bar) { return; } - // If scrubbing, we could use a cached value to make the handle keep up - // with the user's mouse. On HTML5 browsers scrubbing is really smooth, but - // some flash players are slow, so we might want to utilize this later. - // var progress = (this.player_.scrubbing()) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration(); - let progress = this.getPercent(); - const bar = this.bar; + // clamp progress between 0 and 1 + // and only round to four decimal places, as we round to two below + const percent = Number(this.getPercent()) || 0; + const progress = Math.max(0, Math.min(1, percent)).toFixed(4); - // If there's no bar... - if (!bar) { + if (progress === this.progress_) { return; } - // Protect against no duration and other division issues - if (typeof progress !== 'number' || - progress !== progress || - progress < 0 || - progress === Infinity) { - progress = 0; - } - - // Convert to a percentage for setting - const percentage = (progress * 100).toFixed(2) + '%'; - const style = bar.el().style; + this.progress_ = progress; // Set the new bar width or height const sizeKey = this.vertical() ? 'height' : 'width'; - if (style[sizeKey] !== percentage) { - style[sizeKey] = percentage; - } + // Convert to a percentage for css value + this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%'; return progress; }