diff --git a/src/js/control-bar/progress-control/load-progress-bar.js b/src/js/control-bar/progress-control/load-progress-bar.js index 019d757c5d..59e357739d 100644 --- a/src/js/control-bar/progress-control/load-progress-bar.js +++ b/src/js/control-bar/progress-control/load-progress-bar.js @@ -3,6 +3,11 @@ */ import Component from '../../component.js'; import * as Dom from '../../utils/dom.js'; +import clamp from '../../utils/clamp'; +import document from 'global/document'; + +// get the percent width of a time compared to the total end +const percentify = (time, end) => clamp((time / end) * 100, 0, 100).toFixed(2) + '%'; /** * Shows loading progress @@ -33,14 +38,27 @@ class LoadProgressBar extends Component { * The element that was created. */ createEl() { - return super.createEl('div', { - className: 'vjs-load-progress', - innerHTML: `${this.localize('Loaded')}: 0%` + const el = super.createEl('div', {className: 'vjs-load-progress'}); + const wrapper = Dom.createEl('span', {className: 'vjs-control-text'}); + const loadedText = Dom.createEl('span', {textContent: this.localize('Loaded')}); + const separator = document.createTextNode(': '); + + this.percentageEl_ = Dom.createEl('span', { + className: 'vjs-control-text-loaded-percentage', + textContent: '0%' }); + + el.appendChild(wrapper); + wrapper.appendChild(loadedText); + wrapper.appendChild(separator); + wrapper.appendChild(this.percentageEl_); + + return el; } dispose() { this.partEls_ = null; + this.percentageEl_ = null; super.dispose(); } @@ -54,56 +72,53 @@ class LoadProgressBar extends Component { * @listens Player#progress */ update(event) { - const liveTracker = this.player_.liveTracker; - const buffered = this.player_.buffered(); - const duration = (liveTracker && liveTracker.isLive()) ? liveTracker.seekableEnd() : this.player_.duration(); - const bufferedEnd = this.player_.bufferedEnd(); - const children = this.partEls_; - const controlTextPercentage = this.$('.vjs-control-text-loaded-percentage'); - - // get the percent width of a time compared to the total end - const percentify = function(time, end, rounded) { - // no NaN - let percent = (time / end) || 0; - - percent = (percent >= 1 ? 1 : percent) * 100; - - if (rounded) { - percent = percent.toFixed(2); + this.requestAnimationFrame(() => { + const liveTracker = this.player_.liveTracker; + const buffered = this.player_.buffered(); + const duration = (liveTracker && liveTracker.isLive()) ? liveTracker.seekableEnd() : this.player_.duration(); + const bufferedEnd = this.player_.bufferedEnd(); + const children = this.partEls_; + const percent = percentify(bufferedEnd, duration); + + if (this.percent_ !== percent) { + // update the width of the progress bar + this.el_.style.width = percent; + // update the control-text + Dom.textContent(this.percentageEl_, percent); + this.percent_ = percent; } - return percent + '%'; - }; + // add child elements to represent the individual buffered time ranges + for (let i = 0; i < buffered.length; i++) { + const start = buffered.start(i); + const end = buffered.end(i); + let part = children[i]; - // update the width of the progress bar - this.el_.style.width = percentify(bufferedEnd, duration); + if (!part) { + part = this.el_.appendChild(Dom.createEl()); + children[i] = part; + } - // update the control-text - Dom.textContent(controlTextPercentage, percentify(bufferedEnd, duration, true)); + // only update if changed + if (part.dataset.start === start && part.dataset.end === end) { + continue; + } - // add child elements to represent the individual buffered time ranges - for (let i = 0; i < buffered.length; i++) { - const start = buffered.start(i); - const end = buffered.end(i); - let part = children[i]; + part.dataset.start = start; + part.dataset.end = end; - if (!part) { - part = this.el_.appendChild(Dom.createEl()); - children[i] = part; + // set the percent based on the width of the progress bar (bufferedEnd) + part.style.left = percentify(start, bufferedEnd); + part.style.width = percentify(end - start, bufferedEnd); } - // set the percent based on the width of the progress bar (bufferedEnd) - part.style.left = percentify(start, bufferedEnd); - part.style.width = percentify(end - start, bufferedEnd); - } - - // remove unused buffered range elements - for (let i = children.length; i > buffered.length; i--) { - this.el_.removeChild(children[i - 1]); - } - children.length = buffered.length; + // remove unused buffered range elements + for (let i = children.length; i > buffered.length; i--) { + this.el_.removeChild(children[i - 1]); + } + children.length = buffered.length; + }); } - } Component.registerComponent('LoadProgressBar', LoadProgressBar); diff --git a/src/js/control-bar/progress-control/progress-control.js b/src/js/control-bar/progress-control/progress-control.js index 82f840665d..63536b1988 100644 --- a/src/js/control-bar/progress-control/progress-control.js +++ b/src/js/control-bar/progress-control/progress-control.js @@ -3,6 +3,7 @@ */ import Component from '../../component.js'; import * as Dom from '../../utils/dom.js'; +import clamp from '../../utils/clamp.js'; import {bind, throttle, UPDATE_REFRESH_INTERVAL} from '../../utils/fn.js'; import './seek-bar.js'; @@ -56,25 +57,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 = clamp(0, 1, seekBarPoint); + + if (mouseTimeDisplay) { + mouseTimeDisplay.update(seekBarRect, seekBarPoint); + } + + if (playProgressBar) { + playProgressBar.update(seekBarRect, seekBar.getProgress()); + } + } /** diff --git a/src/js/control-bar/progress-control/seek-bar.js b/src/js/control-bar/progress-control/seek-bar.js index ea19ff76a3..28e52771d5 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; } /** @@ -121,45 +124,6 @@ class SeekBar extends Slider { * This function updates the play progress bar and accessibility * attributes to whatever is passed in. * - * @param {number} currentTime - * The currentTime value that should be used for accessibility - * - * @param {number} percent - * The percentage as a decimal that the bar should be filled from 0-1. - * - * @private - */ - update_(currentTime, percent) { - const liveTracker = this.player_.liveTracker; - let duration = this.player_.duration(); - - 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); - } - } - - /** - * Update the seek bar's UI. - * * @param {EventTarget~Event} [event] * The `timeupdate` or `ended` event that caused this to run. * @@ -169,15 +133,41 @@ 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(); - this.update_(this.getCurrentTime_(), percent); + this.requestAnimationFrame(() => { + 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; + } + }); + return percent; } @@ -196,19 +186,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 +208,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/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..b33e9f9dbf 100644 --- a/src/js/slider/slider.js +++ b/src/js/slider/slider.js @@ -5,6 +5,7 @@ import Component from '../component.js'; import * as Dom from '../utils/dom.js'; import {assign} from '../utils/obj'; import {IS_CHROME} from '../utils/browser.js'; +import clamp from '../utils/clamp.js'; import keycode from 'keycode'; /** @@ -230,48 +231,46 @@ class Slider extends Component { * number from 0 to 1. */ update() { - // 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 progress = this.getProgress(); - // 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'; + this.requestAnimationFrame(() => { + // 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; } + /** + * Get the percentage of the bar that should be filled + * but clamped and rounded. + * + * @return {number} + * percentage filled that the slider is + */ + getProgress() { + return clamp(this.getPercent(), 0, 1).toFixed(4); + } + /** * Calculate distance for slider * diff --git a/src/js/utils/clamp.js b/src/js/utils/clamp.js new file mode 100644 index 0000000000..f4b1e52ee0 --- /dev/null +++ b/src/js/utils/clamp.js @@ -0,0 +1,21 @@ +/** + * Keep a number between a min and a max value + * + * @param {number} number + * The number to clamp + * + * @param {number} min + * The minimum value + * @param {number} max + * The maximum value + * + * @return {number} + * the clamped number + */ +const clamp = function(number, min, max) { + number = Number(number); + + return Math.min(max, Math.max(min, isNaN(number) ? min : number)); +}; + +export default clamp; diff --git a/test/unit/tech/tech-faker.js b/test/unit/tech/tech-faker.js index 0606b5d468..7c6891a635 100644 --- a/test/unit/tech/tech-faker.js +++ b/test/unit/tech/tech-faker.js @@ -152,6 +152,9 @@ class TechFaker extends Tech { controls() { return false; } + ended() { + return false; + } // Support everything except for "video/unsupported-format" static isSupported() {