mirror of
https://github.com/videojs/video.js.git
synced 2024-12-04 10:34:51 +02:00
fix(liveui): tweaks to prevent jitter (#6405)
This commit is contained in:
parent
1dd06a26c0
commit
668c7f44d6
@ -261,6 +261,11 @@ class SeekBar extends Slider {
|
|||||||
newTime = newTime - 0.1;
|
newTime = newTime - 0.1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
if (distance >= 0.99) {
|
||||||
|
liveTracker.seekToLiveEdge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const seekableStart = liveTracker.seekableStart();
|
const seekableStart = liveTracker.seekableStart();
|
||||||
const seekableEnd = liveTracker.liveCurrentTime();
|
const seekableEnd = liveTracker.liveCurrentTime();
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class SeekToLive extends Button {
|
|||||||
* Update the state of this button if we are at the live edge
|
* Update the state of this button if we are at the live edge
|
||||||
* or not
|
* or not
|
||||||
*/
|
*/
|
||||||
updateLiveEdgeStatus(e) {
|
updateLiveEdgeStatus() {
|
||||||
// default to live edge
|
// default to live edge
|
||||||
if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
|
if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
|
||||||
this.setAttribute('aria-disabled', true);
|
this.setAttribute('aria-disabled', true);
|
||||||
|
@ -1,19 +1,43 @@
|
|||||||
import Component from './component.js';
|
import Component from './component.js';
|
||||||
import median from './utils/median.js';
|
|
||||||
import mergeOptions from './utils/merge-options.js';
|
import mergeOptions from './utils/merge-options.js';
|
||||||
import document from 'global/document';
|
import document from 'global/document';
|
||||||
import * as browser from './utils/browser.js';
|
import * as browser from './utils/browser.js';
|
||||||
|
import window from 'global/window';
|
||||||
import * as Fn from './utils/fn.js';
|
import * as Fn from './utils/fn.js';
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
// Number of seconds of live window (seekableEnd - seekableStart) that
|
trackingThreshold: 30,
|
||||||
// a video needs to have before the liveui will be shown.
|
liveTolerance: 15
|
||||||
trackingThreshold: 30
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* track when we are at the live edge, and other helpers for live playback */
|
/*
|
||||||
|
track when we are at the live edge, and other helpers for live playback */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for checking live current time and determining when the player
|
||||||
|
* is at or behind the live edge.
|
||||||
|
*/
|
||||||
class LiveTracker extends Component {
|
class LiveTracker extends Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.trackingThreshold=30]
|
||||||
|
* Number of seconds of live window (seekableEnd - seekableStart) that
|
||||||
|
* media needs to have before the liveui will be shown.
|
||||||
|
*
|
||||||
|
* @param {number} [options.liveTolerance=15]
|
||||||
|
* Number of seconds behind live that we have to be
|
||||||
|
* before we will be considered non-live. Note that this will only
|
||||||
|
* be used when playing at the live edge. This allows large seekable end
|
||||||
|
* changes to not effect wether we are live or not.
|
||||||
|
*/
|
||||||
constructor(player, options) {
|
constructor(player, options) {
|
||||||
// LiveTracker does not need an element
|
// LiveTracker does not need an element
|
||||||
const options_ = mergeOptions(defaults, options, {createEl: false});
|
const options_ = mergeOptions(defaults, options, {createEl: false});
|
||||||
@ -32,6 +56,9 @@ class LiveTracker extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toggle tracking based on document visiblility
|
||||||
|
*/
|
||||||
handleVisibilityChange() {
|
handleVisibilityChange() {
|
||||||
if (this.player_.duration() !== Infinity) {
|
if (this.player_.duration() !== Infinity) {
|
||||||
return;
|
return;
|
||||||
@ -44,29 +71,11 @@ class LiveTracker extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isBehind_() {
|
/**
|
||||||
// don't report that we are behind until a timeupdate has been seen
|
* all the functionality for tracking when seek end changes
|
||||||
if (!this.timeupdateSeen_) {
|
* and for tracking how far past seek end we should be
|
||||||
return false;
|
*/
|
||||||
}
|
|
||||||
const liveCurrentTime = this.liveCurrentTime();
|
|
||||||
const currentTime = this.player_.currentTime();
|
|
||||||
|
|
||||||
// the live edge window is the amount of seconds away from live
|
|
||||||
// that a player can be, but still be considered live.
|
|
||||||
// we add 0.07 because the live tracking happens every 30ms
|
|
||||||
// and we want some wiggle room for short segment live playback
|
|
||||||
const liveEdgeWindow = (this.seekableIncrement_ * 2) + 0.07;
|
|
||||||
|
|
||||||
// on Android liveCurrentTime can bee Infinity, because seekableEnd
|
|
||||||
// can be Infinity, so we handle that case.
|
|
||||||
return liveCurrentTime !== Infinity && (liveCurrentTime - liveEdgeWindow) >= currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// all the functionality for tracking when seek end changes
|
|
||||||
// and for tracking how far past seek end we should be
|
|
||||||
trackLive_() {
|
trackLive_() {
|
||||||
this.pastSeekEnd_ = this.pastSeekEnd_;
|
|
||||||
const seekable = this.player_.seekable();
|
const seekable = this.player_.seekable();
|
||||||
|
|
||||||
// skip undefined seekable
|
// skip undefined seekable
|
||||||
@ -74,37 +83,33 @@ class LiveTracker extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSeekEnd = this.seekableEnd();
|
const newTime = Number(window.performance.now().toFixed(4));
|
||||||
|
const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
|
||||||
|
|
||||||
// we can only tell if we are behind live, when seekable changes
|
this.lastTime_ = newTime;
|
||||||
// once we detect that seekable has changed we check the new seek
|
|
||||||
// end against current time, with a fudge value of half a second.
|
|
||||||
if (newSeekEnd !== this.lastSeekEnd_) {
|
|
||||||
if (this.lastSeekEnd_) {
|
|
||||||
// we try to get the best fit value for the seeking increment
|
|
||||||
// variable from the last 12 values.
|
|
||||||
this.seekableIncrementList_ = this.seekableIncrementList_.slice(-11);
|
|
||||||
this.seekableIncrementList_.push(Math.abs(newSeekEnd - this.lastSeekEnd_));
|
|
||||||
if (this.seekableIncrementList_.length > 3) {
|
|
||||||
this.seekableIncrement_ = median(this.seekableIncrementList_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pastSeekEnd_ = 0;
|
this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
|
||||||
this.lastSeekEnd_ = newSeekEnd;
|
|
||||||
this.trigger('seekableendchange');
|
const liveCurrentTime = this.liveCurrentTime();
|
||||||
|
const currentTime = this.player_.currentTime();
|
||||||
|
|
||||||
|
// we are behind live if any are true
|
||||||
|
// 1. the player is paused
|
||||||
|
// 2. the user seeked to a location 2 seconds away from live
|
||||||
|
// 3. the difference between live and current time is greater
|
||||||
|
// liveTolerance which defaults to 15s
|
||||||
|
let isBehind = this.player_.paused() || this.seekedBehindLive_ ||
|
||||||
|
Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
|
||||||
|
|
||||||
|
// we cannot be behind if
|
||||||
|
// 1. until we have not seen a timeupdate yet
|
||||||
|
// 2. liveCurrentTime is Infinity, which happens on Android
|
||||||
|
if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
|
||||||
|
isBehind = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we should reset pastSeekEnd when the value
|
if (isBehind !== this.behindLiveEdge_) {
|
||||||
// is much higher than seeking increment.
|
this.behindLiveEdge_ = isBehind;
|
||||||
if (this.pastSeekEnd() > this.seekableIncrement_ * 1.5) {
|
|
||||||
this.pastSeekEnd_ = 0;
|
|
||||||
} else {
|
|
||||||
this.pastSeekEnd_ = this.pastSeekEnd() + 0.03;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isBehind_() !== this.behindLiveEdge()) {
|
|
||||||
this.behindLiveEdge_ = this.isBehind_();
|
|
||||||
this.trigger('liveedgechange');
|
this.trigger('liveedgechange');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,21 +148,41 @@ class LiveTracker extends Component {
|
|||||||
this.trackingInterval_ = this.setInterval(this.trackLive_, Fn.UPDATE_REFRESH_INTERVAL);
|
this.trackingInterval_ = this.setInterval(this.trackLive_, Fn.UPDATE_REFRESH_INTERVAL);
|
||||||
this.trackLive_();
|
this.trackLive_();
|
||||||
|
|
||||||
this.on(this.player_, 'play', this.trackLive_);
|
this.on(this.player_, ['play', 'pause'], this.trackLive_);
|
||||||
this.on(this.player_, 'pause', this.trackLive_);
|
|
||||||
|
|
||||||
// this is to prevent showing that we are not live
|
|
||||||
// before a video starts to play
|
|
||||||
if (!this.timeupdateSeen_) {
|
if (!this.timeupdateSeen_) {
|
||||||
this.one(this.player_, 'play', this.handlePlay);
|
this.one(this.player_, 'play', this.handlePlay);
|
||||||
this.handleTimeupdate = () => {
|
this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate);
|
||||||
this.timeupdateSeen_ = true;
|
} else {
|
||||||
this.handleTimeupdate = null;
|
this.on(this.player_, 'seeked', this.handleSeeked);
|
||||||
};
|
|
||||||
this.one(this.player_, 'timeupdate', this.handleTimeupdate);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle the first timeupdate on the player if it wasn't already playing
|
||||||
|
* when live tracker started tracking.
|
||||||
|
*/
|
||||||
|
handleFirstTimeupdate() {
|
||||||
|
this.timeupdateSeen_ = true;
|
||||||
|
this.on(this.player_, 'seeked', this.handleSeeked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep track of what time a seek starts, and listen for seeked
|
||||||
|
* to find where a seek ends.
|
||||||
|
*/
|
||||||
|
handleSeeked() {
|
||||||
|
const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
|
||||||
|
|
||||||
|
this.seekedBehindLive_ = this.skipNextSeeked_ ? false : timeDiff > 2;
|
||||||
|
this.skipNextSeeked_ = false;
|
||||||
|
this.trackLive_();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle the first play on the player, and make sure that we seek
|
||||||
|
* right to the live edge.
|
||||||
|
*/
|
||||||
handlePlay() {
|
handlePlay() {
|
||||||
this.one(this.player_, 'timeupdate', this.seekToLiveEdge);
|
this.one(this.player_, 'timeupdate', this.seekToLiveEdge);
|
||||||
}
|
}
|
||||||
@ -167,24 +192,22 @@ class LiveTracker extends Component {
|
|||||||
* their initial value.
|
* their initial value.
|
||||||
*/
|
*/
|
||||||
reset_() {
|
reset_() {
|
||||||
|
this.lastTime_ = -1;
|
||||||
this.pastSeekEnd_ = 0;
|
this.pastSeekEnd_ = 0;
|
||||||
this.lastSeekEnd_ = null;
|
this.lastSeekEnd_ = -1;
|
||||||
this.behindLiveEdge_ = null;
|
this.behindLiveEdge_ = true;
|
||||||
this.timeupdateSeen_ = false;
|
this.timeupdateSeen_ = false;
|
||||||
|
this.seekedBehindLive_ = false;
|
||||||
|
this.skipNextSeeked_ = false;
|
||||||
|
|
||||||
this.clearInterval(this.trackingInterval_);
|
this.clearInterval(this.trackingInterval_);
|
||||||
this.trackingInterval_ = null;
|
this.trackingInterval_ = null;
|
||||||
this.seekableIncrement_ = 12;
|
|
||||||
this.seekableIncrementList_ = [];
|
|
||||||
|
|
||||||
this.off(this.player_, 'play', this.trackLive_);
|
this.off(this.player_, ['play', 'pause'], this.trackLive_);
|
||||||
this.off(this.player_, 'pause', this.trackLive_);
|
this.off(this.player_, 'seeked', this.handleSeeked);
|
||||||
this.off(this.player_, 'play', this.handlePlay);
|
this.off(this.player_, 'play', this.handlePlay);
|
||||||
|
this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate);
|
||||||
this.off(this.player_, 'timeupdate', this.seekToLiveEdge);
|
this.off(this.player_, 'timeupdate', this.seekToLiveEdge);
|
||||||
if (this.handleTimeupdate) {
|
|
||||||
this.off(this.player_, 'timeupdate', this.handleTimeupdate);
|
|
||||||
this.handleTimeupdate = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -195,11 +218,15 @@ class LiveTracker extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.reset_();
|
this.reset_();
|
||||||
|
this.trigger('liveedgechange');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper to get the player seekable end
|
* A helper to get the player seekable end
|
||||||
* so that we don't have to null check everywhere
|
* so that we don't have to null check everywhere
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
* The furthest seekable end or Infinity.
|
||||||
*/
|
*/
|
||||||
seekableEnd() {
|
seekableEnd() {
|
||||||
const seekable = this.player_.seekable();
|
const seekable = this.player_.seekable();
|
||||||
@ -218,6 +245,9 @@ class LiveTracker extends Component {
|
|||||||
/**
|
/**
|
||||||
* A helper to get the player seekable start
|
* A helper to get the player seekable start
|
||||||
* so that we don't have to null check everywhere
|
* so that we don't have to null check everywhere
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
* The earliest seekable start or 0.
|
||||||
*/
|
*/
|
||||||
seekableStart() {
|
seekableStart() {
|
||||||
const seekable = this.player_.seekable();
|
const seekable = this.player_.seekable();
|
||||||
@ -234,7 +264,13 @@ class LiveTracker extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the live time window
|
* Get the live time window aka
|
||||||
|
* the amount of time between seekable start and
|
||||||
|
* live current time.
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
* The amount of seconds that are seekable in
|
||||||
|
* the live video.
|
||||||
*/
|
*/
|
||||||
liveWindow() {
|
liveWindow() {
|
||||||
const liveCurrentTime = this.liveCurrentTime();
|
const liveCurrentTime = this.liveCurrentTime();
|
||||||
@ -249,6 +285,9 @@ class LiveTracker extends Component {
|
|||||||
/**
|
/**
|
||||||
* Determines if the player is live, only checks if this component
|
* Determines if the player is live, only checks if this component
|
||||||
* is tracking live playback or not
|
* is tracking live playback or not
|
||||||
|
*
|
||||||
|
* @return {boolean}
|
||||||
|
* Wether liveTracker is tracking
|
||||||
*/
|
*/
|
||||||
isLive() {
|
isLive() {
|
||||||
return this.isTracking();
|
return this.isTracking();
|
||||||
@ -257,6 +296,9 @@ class LiveTracker extends Component {
|
|||||||
/**
|
/**
|
||||||
* Determines if currentTime is at the live edge and won't fall behind
|
* Determines if currentTime is at the live edge and won't fall behind
|
||||||
* on each seekableendchange
|
* on each seekableendchange
|
||||||
|
*
|
||||||
|
* @return {boolean}
|
||||||
|
* Wether playback is at the live edge
|
||||||
*/
|
*/
|
||||||
atLiveEdge() {
|
atLiveEdge() {
|
||||||
return !this.behindLiveEdge();
|
return !this.behindLiveEdge();
|
||||||
@ -264,26 +306,45 @@ class LiveTracker extends Component {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* get what we expect the live current time to be
|
* get what we expect the live current time to be
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
* The expected live current time
|
||||||
*/
|
*/
|
||||||
liveCurrentTime() {
|
liveCurrentTime() {
|
||||||
return this.pastSeekEnd() + this.seekableEnd();
|
return this.pastSeekEnd() + this.seekableEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns how far past seek end we expect current time to be
|
* The number of seconds that have occured after seekable end
|
||||||
|
* changed. This will be reset to 0 once seekable end changes.
|
||||||
|
*
|
||||||
|
* @return {number}
|
||||||
|
* Seconds past the current seekable end
|
||||||
*/
|
*/
|
||||||
pastSeekEnd() {
|
pastSeekEnd() {
|
||||||
|
const seekableEnd = this.seekableEnd();
|
||||||
|
|
||||||
|
if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
|
||||||
|
this.pastSeekEnd_ = 0;
|
||||||
|
}
|
||||||
|
this.lastSeekEnd_ = seekableEnd;
|
||||||
return this.pastSeekEnd_;
|
return this.pastSeekEnd_;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If we are currently behind the live edge, aka currentTime will be
|
* If we are currently behind the live edge, aka currentTime will be
|
||||||
* behind on a seekableendchange
|
* behind on a seekableendchange
|
||||||
|
*
|
||||||
|
* @return {boolean}
|
||||||
|
* If we are behind the live edge
|
||||||
*/
|
*/
|
||||||
behindLiveEdge() {
|
behindLiveEdge() {
|
||||||
return this.behindLiveEdge_;
|
return this.behindLiveEdge_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wether live tracker is currently tracking or not.
|
||||||
|
*/
|
||||||
isTracking() {
|
isTracking() {
|
||||||
return typeof this.trackingInterval_ === 'number';
|
return typeof this.trackingInterval_ === 'number';
|
||||||
}
|
}
|
||||||
@ -292,18 +353,21 @@ class LiveTracker extends Component {
|
|||||||
* Seek to the live edge if we are behind the live edge
|
* Seek to the live edge if we are behind the live edge
|
||||||
*/
|
*/
|
||||||
seekToLiveEdge() {
|
seekToLiveEdge() {
|
||||||
|
this.seekedBehindLive_ = false;
|
||||||
if (this.atLiveEdge()) {
|
if (this.atLiveEdge()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// skipNextSeeked_
|
||||||
|
this.skipNextSeeked_ = true;
|
||||||
this.player_.currentTime(this.liveCurrentTime());
|
this.player_.currentTime(this.liveCurrentTime());
|
||||||
|
|
||||||
if (this.player_.paused()) {
|
|
||||||
this.player_.play();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of liveTracker
|
||||||
|
*/
|
||||||
dispose() {
|
dispose() {
|
||||||
|
this.off(document, 'visibilitychange', this.handleVisibilityChange);
|
||||||
this.stopTracking();
|
this.stopTracking();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Computes the median of an array.
|
|
||||||
*
|
|
||||||
* @param {number[]} arr
|
|
||||||
* Input array of numbers.
|
|
||||||
*
|
|
||||||
* @return {number}
|
|
||||||
* Median value.
|
|
||||||
*/
|
|
||||||
const median = arr => {
|
|
||||||
const mid = Math.floor(arr.length / 2);
|
|
||||||
const sortedList = [...arr].sort((a, b) => a - b);
|
|
||||||
|
|
||||||
return arr.length % 2 !== 0 ? sortedList[mid] : (sortedList[mid - 1] + sortedList[mid]) / 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default median;
|
|
@ -69,7 +69,7 @@ QUnit.module('LiveTracker', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('starts/stop with durationchange and triggers liveedgechange', function(assert) {
|
QUnit.test('with durationchange and triggers liveedgechange', function(assert) {
|
||||||
let liveEdgeChange = 0;
|
let liveEdgeChange = 0;
|
||||||
|
|
||||||
this.liveTracker.on('liveedgechange', () => {
|
this.liveTracker.on('liveedgechange', () => {
|
||||||
@ -83,10 +83,11 @@ QUnit.module('LiveTracker', () => {
|
|||||||
|
|
||||||
this.player.duration(5);
|
this.player.duration(5);
|
||||||
assert.notOk(this.liveTracker.isTracking(), 'not started');
|
assert.notOk(this.liveTracker.isTracking(), 'not started');
|
||||||
|
assert.equal(liveEdgeChange, 2, 'liveedgechange fired when we stop tracking');
|
||||||
|
|
||||||
this.player.duration(Infinity);
|
this.player.duration(Infinity);
|
||||||
assert.ok(this.liveTracker.isTracking(), 'started');
|
assert.ok(this.liveTracker.isTracking(), 'started');
|
||||||
assert.equal(liveEdgeChange, 2, 'liveedgechange fired again');
|
assert.equal(liveEdgeChange, 3, 'liveedgechange fired again');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.module('tracking', {
|
QUnit.module('tracking', {
|
||||||
@ -97,14 +98,10 @@ QUnit.module('LiveTracker', () => {
|
|||||||
|
|
||||||
this.liveTracker = this.player.liveTracker;
|
this.liveTracker = this.player.liveTracker;
|
||||||
this.player.seekable = () => createTimeRanges(0, 30);
|
this.player.seekable = () => createTimeRanges(0, 30);
|
||||||
|
this.player.paused = () => false;
|
||||||
this.player.duration(Infinity);
|
this.player.duration(Infinity);
|
||||||
|
|
||||||
this.liveEdgeChanges = 0;
|
this.liveEdgeChanges = 0;
|
||||||
this.seekableEndChanges = 0;
|
|
||||||
|
|
||||||
this.liveTracker.on('seekableendchange', () => {
|
|
||||||
this.seekableEndChanges++;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.liveTracker.on('liveedgechange', () => {
|
this.liveTracker.on('liveedgechange', () => {
|
||||||
this.liveEdgeChanges++;
|
this.liveEdgeChanges++;
|
||||||
@ -117,8 +114,7 @@ QUnit.module('LiveTracker', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('Triggers liveedgechange when we fall behind and catch up', function(assert) {
|
QUnit.test('Triggers liveedgechange when we fall behind and catch up', function(assert) {
|
||||||
|
this.liveTracker.options_.liveTolerance = 6;
|
||||||
this.liveTracker.seekableIncrement_ = 6;
|
|
||||||
this.player.seekable = () => createTimeRanges(0, 20);
|
this.player.seekable = () => createTimeRanges(0, 20);
|
||||||
this.player.trigger('timeupdate');
|
this.player.trigger('timeupdate');
|
||||||
this.player.currentTime = () => 14;
|
this.player.currentTime = () => 14;
|
||||||
@ -129,31 +125,63 @@ QUnit.module('LiveTracker', () => {
|
|||||||
assert.equal(this.liveEdgeChanges, 1, 'should have one live edge change');
|
assert.equal(this.liveEdgeChanges, 1, 'should have one live edge change');
|
||||||
assert.ok(this.liveTracker.behindLiveEdge(), 'behind live edge');
|
assert.ok(this.liveTracker.behindLiveEdge(), 'behind live edge');
|
||||||
|
|
||||||
this.player.currentTime = () => 20;
|
this.player.currentTime = () => this.liveTracker.liveCurrentTime();
|
||||||
this.clock.tick(30);
|
this.clock.tick(30);
|
||||||
|
|
||||||
assert.equal(this.liveEdgeChanges, 2, 'should have two live edge change');
|
assert.equal(this.liveEdgeChanges, 2, 'should have two live edge change');
|
||||||
assert.ok(this.liveTracker.atLiveEdge(), 'at live edge');
|
assert.ok(this.liveTracker.atLiveEdge(), 'at live edge');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('is behindLiveEdge when paused', function(assert) {
|
||||||
|
this.liveTracker.options_.liveTolerance = 6;
|
||||||
|
this.player.seekable = () => createTimeRanges(0, 20);
|
||||||
|
this.player.trigger('timeupdate');
|
||||||
|
this.player.currentTime = () => 20;
|
||||||
|
this.clock.tick(1000);
|
||||||
|
|
||||||
|
assert.ok(this.liveTracker.atLiveEdge(), 'at live edge');
|
||||||
|
|
||||||
|
this.player.paused = () => true;
|
||||||
|
this.player.trigger('pause');
|
||||||
|
|
||||||
|
assert.equal(this.liveEdgeChanges, 1, 'should have one live edge change');
|
||||||
|
assert.ok(this.liveTracker.behindLiveEdge(), 'behindLiveEdge live edge');
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('is behindLiveEdge when seeking backwards', function(assert) {
|
||||||
|
this.liveTracker.options_.liveTolerance = 6;
|
||||||
|
this.player.seekable = () => createTimeRanges(0, 20);
|
||||||
|
this.player.trigger('timeupdate');
|
||||||
|
this.player.currentTime = () => 20;
|
||||||
|
this.clock.tick(1000);
|
||||||
|
|
||||||
|
assert.ok(this.liveTracker.atLiveEdge(), 'at live edge');
|
||||||
|
|
||||||
|
this.player.currentTime = () => 17;
|
||||||
|
this.player.trigger('seeked');
|
||||||
|
|
||||||
|
assert.equal(this.liveEdgeChanges, 1, 'should have one live edge change');
|
||||||
|
assert.ok(this.liveTracker.behindLiveEdge(), 'behindLiveEdge live edge');
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('pastSeekEnd should update when seekable changes', function(assert) {
|
QUnit.test('pastSeekEnd should update when seekable changes', function(assert) {
|
||||||
assert.strictEqual(this.liveTracker.liveCurrentTime(), 30.03, 'liveCurrentTime is now 30');
|
assert.strictEqual(this.liveTracker.liveCurrentTime(), 30, 'liveCurrentTime is now 30');
|
||||||
this.clock.tick(2010);
|
this.clock.tick(2010);
|
||||||
|
|
||||||
assert.ok(this.liveTracker.pastSeekEnd() > 2, 'pastSeekEnd should be over 2s');
|
assert.ok(this.liveTracker.pastSeekEnd() > 2, 'pastSeekEnd should be over 2s');
|
||||||
|
|
||||||
this.player.seekable = () => createTimeRanges(30, 61);
|
this.player.seekable = () => createTimeRanges(0, 2);
|
||||||
|
|
||||||
this.clock.tick(30);
|
this.clock.tick(30);
|
||||||
assert.strictEqual(this.liveTracker.pastSeekEnd(), 0.03, 'pastSeekEnd start at 0.03 again');
|
assert.strictEqual(this.liveTracker.pastSeekEnd(), 0.03, 'pastSeekEnd start at 0.03 again');
|
||||||
assert.strictEqual(this.liveTracker.liveCurrentTime(), 61.03, 'liveCurrentTime is now 2.03');
|
assert.strictEqual(this.liveTracker.liveCurrentTime(), 2.03, 'liveCurrentTime is now 2.03');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('seeks to live edge on seekableendchange', function(assert) {
|
QUnit.test('can seek to live edge', function(assert) {
|
||||||
this.player.trigger('timeupdate');
|
this.player.trigger('timeupdate');
|
||||||
|
|
||||||
this.player.seekable = () => createTimeRanges(0, 6);
|
this.player.seekable = () => createTimeRanges(0, 6);
|
||||||
this.liveTracker.seekableIncrement_ = 2;
|
this.liveTracker.options_.liveTolerance = 2;
|
||||||
let currentTime = 0;
|
let currentTime = 0;
|
||||||
|
|
||||||
this.player.currentTime = (ct) => {
|
this.player.currentTime = (ct) => {
|
||||||
@ -172,7 +200,7 @@ QUnit.module('LiveTracker', () => {
|
|||||||
assert.equal(currentTime, this.liveTracker.liveCurrentTime(), 'should have seeked to liveCurrentTime');
|
assert.equal(currentTime, this.liveTracker.liveCurrentTime(), 'should have seeked to liveCurrentTime');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('does not seek to to live edge if at live edge', function(assert) {
|
QUnit.test('does not seek to live edge if at live edge', function(assert) {
|
||||||
let pauseCalls = 0;
|
let pauseCalls = 0;
|
||||||
let playCalls = 0;
|
let playCalls = 0;
|
||||||
let currentTime = 0;
|
let currentTime = 0;
|
||||||
@ -196,13 +224,12 @@ QUnit.module('LiveTracker', () => {
|
|||||||
assert.notOk(this.player.hasClass('vjs-waiting'), 'player should not be waiting');
|
assert.notOk(this.player.hasClass('vjs-waiting'), 'player should not be waiting');
|
||||||
assert.equal(pauseCalls, 0, 'should not have called pause');
|
assert.equal(pauseCalls, 0, 'should not have called pause');
|
||||||
|
|
||||||
this.clock.tick(2000);
|
this.clock.tick(2010);
|
||||||
|
|
||||||
assert.ok(this.liveTracker.pastSeekEnd() > 2, 'pastSeekEnd should be over 2s');
|
assert.ok(this.liveTracker.pastSeekEnd() > 2, 'pastSeekEnd should be over 2s');
|
||||||
this.player.seekable = () => createTimeRanges(0, 2);
|
this.player.seekable = () => createTimeRanges(0, 2);
|
||||||
|
|
||||||
this.clock.tick(30);
|
this.clock.tick(30);
|
||||||
assert.equal(this.seekableEndChanges, 1, 'should be one seek end change');
|
|
||||||
assert.equal(currentTime, 0, 'should not have seeked to seekableEnd');
|
assert.equal(currentTime, 0, 'should not have seeked to seekableEnd');
|
||||||
assert.equal(playCalls, 0, 'should not have called play');
|
assert.equal(playCalls, 0, 'should not have called play');
|
||||||
});
|
});
|
||||||
@ -215,7 +242,6 @@ QUnit.module('LiveTracker', () => {
|
|||||||
assert.ok(this.liveTracker.seekToLiveEdge.notCalled, 'seekToLiveEdge was not called yet');
|
assert.ok(this.liveTracker.seekToLiveEdge.notCalled, 'seekToLiveEdge was not called yet');
|
||||||
|
|
||||||
this.player.trigger('play');
|
this.player.trigger('play');
|
||||||
this.player.trigger('playing');
|
|
||||||
assert.ok(this.liveTracker.seekToLiveEdge.notCalled, 'seekToLiveEdge was not called yet');
|
assert.ok(this.liveTracker.seekToLiveEdge.notCalled, 'seekToLiveEdge was not called yet');
|
||||||
|
|
||||||
this.player.trigger('timeupdate');
|
this.player.trigger('timeupdate');
|
||||||
|
@ -4,98 +4,89 @@ import sinon from 'sinon';
|
|||||||
import computedStyle from '../../src/js/utils/computed-style.js';
|
import computedStyle from '../../src/js/utils/computed-style.js';
|
||||||
import { createTimeRange } from '../../src/js/utils/time-ranges.js';
|
import { createTimeRange } from '../../src/js/utils/time-ranges.js';
|
||||||
|
|
||||||
QUnit.module('SeekToLive', () => {
|
QUnit.module('SeekToLive', {
|
||||||
QUnit.module('live with liveui', {
|
beforeEach() {
|
||||||
beforeEach() {
|
this.clock = sinon.useFakeTimers();
|
||||||
this.clock = sinon.useFakeTimers();
|
this.player = TestHelpers.makePlayer();
|
||||||
|
this.seekToLive = this.player.controlBar.seekToLive;
|
||||||
|
this.getComputedDisplay = () => {
|
||||||
|
return computedStyle(this.seekToLive.el(), 'display');
|
||||||
|
};
|
||||||
|
|
||||||
this.player = TestHelpers.makePlayer({liveui: true});
|
this.mockLiveui = () => {
|
||||||
this.seekToLive = this.player.controlBar.seekToLive;
|
this.player.paused = () => false;
|
||||||
|
this.player.hasStarted = () => true;
|
||||||
|
this.player.options_.liveui = true;
|
||||||
this.player.seekable = () => createTimeRange(0, 45);
|
this.player.seekable = () => createTimeRange(0, 45);
|
||||||
|
this.player.currentTime = () => this.player.liveTracker.liveCurrentTime();
|
||||||
this.getComputedDisplay = () => {
|
|
||||||
return computedStyle(this.seekToLive.el(), 'display');
|
|
||||||
};
|
|
||||||
|
|
||||||
// mock live state
|
|
||||||
this.player.duration(Infinity);
|
this.player.duration(Infinity);
|
||||||
},
|
};
|
||||||
afterEach() {
|
},
|
||||||
this.player.dispose();
|
afterEach() {
|
||||||
this.clock.restore();
|
this.player.dispose();
|
||||||
}
|
this.clock.restore();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
QUnit.test('at live edge if liveTracker says we are', function(assert) {
|
|
||||||
this.player.liveTracker.behindLiveEdge = () => false;
|
QUnit.test('liveui enabled, can switch between at and behind live edge ', function(assert) {
|
||||||
this.player.liveTracker.trigger('liveedgechange');
|
this.mockLiveui();
|
||||||
|
|
||||||
assert.ok(this.seekToLive.hasClass('vjs-at-live-edge'), 'has at live edge class');
|
assert.notEqual(this.getComputedDisplay(), 'none', 'is not hidden');
|
||||||
});
|
assert.ok(this.seekToLive.hasClass('vjs-at-live-edge'), 'has at live edge class');
|
||||||
|
|
||||||
QUnit.test('behind live edge if liveTracker says we are', function(assert) {
|
this.player.currentTime = () => 0;
|
||||||
this.player.liveTracker.behindLiveEdge = () => true;
|
this.player.seekable = () => createTimeRange(0, 38);
|
||||||
this.player.liveTracker.trigger('liveedgechange');
|
this.clock.tick(30);
|
||||||
|
|
||||||
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have live edge class');
|
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('switch to non live', function(assert) {
|
QUnit.test('liveui enabled can show/hide on durationchange', function(assert) {
|
||||||
this.player.duration(4);
|
// start out non-live
|
||||||
this.player.trigger('durationchange');
|
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
|
||||||
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
|
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
|
||||||
});
|
|
||||||
|
// switch to live
|
||||||
QUnit.module('live without liveui', {
|
this.mockLiveui();
|
||||||
beforeEach() {
|
|
||||||
this.clock = sinon.useFakeTimers();
|
assert.notEqual(this.getComputedDisplay(), 'none', 'is not hidden');
|
||||||
|
assert.ok(this.seekToLive.hasClass('vjs-at-live-edge'), 'has at live edge class');
|
||||||
this.player = TestHelpers.makePlayer();
|
|
||||||
this.seekToLive = this.player.controlBar.seekToLive;
|
// switch to non-live
|
||||||
this.player.seekable = () => createTimeRange(0, 45);
|
this.player.duration(20);
|
||||||
|
|
||||||
this.getComputedDisplay = () => {
|
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
|
||||||
return computedStyle(this.seekToLive.el(), 'display');
|
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
|
||||||
};
|
|
||||||
|
// back to live again.
|
||||||
// mock live state
|
this.mockLiveui();
|
||||||
this.player.duration(Infinity);
|
|
||||||
},
|
assert.notEqual(this.getComputedDisplay(), 'none', 'is not hidden');
|
||||||
afterEach() {
|
assert.ok(this.seekToLive.hasClass('vjs-at-live-edge'), 'has at live edge class');
|
||||||
this.player.dispose();
|
});
|
||||||
this.clock.restore();
|
|
||||||
}
|
QUnit.test('liveui disabled live window is never shown', function(assert) {
|
||||||
});
|
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
|
||||||
|
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
|
||||||
QUnit.test('should be hidden', function(assert) {
|
|
||||||
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
|
this.player.paused = () => false;
|
||||||
});
|
this.player.hasStarted = () => true;
|
||||||
|
this.player.currentTime = () => this.player.liveTracker.liveCurrentTime();
|
||||||
QUnit.module('not live', {
|
|
||||||
beforeEach() {
|
// liveui false, seekable range is good though
|
||||||
this.player = TestHelpers.makePlayer({liveui: true});
|
this.player.options_.liveui = false;
|
||||||
this.seekToLive = this.player.controlBar.seekToLive;
|
this.player.duration(Infinity);
|
||||||
|
|
||||||
this.getComputedDisplay = () => {
|
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
|
||||||
return computedStyle(this.seekToLive.el(), 'display');
|
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
|
||||||
};
|
|
||||||
},
|
this.player.duration(10);
|
||||||
afterEach() {
|
|
||||||
this.player.dispose();
|
// liveui false
|
||||||
}
|
this.player.options_.liveui = false;
|
||||||
});
|
this.player.seekable = () => createTimeRange(0, 29);
|
||||||
|
this.player.duration(Infinity);
|
||||||
QUnit.test('should not show or track', function(assert) {
|
|
||||||
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
|
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
|
||||||
});
|
assert.notOk(this.seekToLive.hasClass('vjs-at-live-edge'), 'does not have at live edge class');
|
||||||
|
|
||||||
QUnit.test('switch to live', function(assert) {
|
|
||||||
assert.equal(this.getComputedDisplay(), 'none', 'is hidden');
|
|
||||||
|
|
||||||
this.player.seekable = () => createTimeRange(0, 45);
|
|
||||||
this.player.duration(Infinity);
|
|
||||||
this.player.trigger('durationchange');
|
|
||||||
|
|
||||||
assert.notEqual(this.getComputedDisplay(), 'none', 'is not hidden');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
/* eslint-env qunit */
|
|
||||||
import median from '../../../src/js/utils/median.js';
|
|
||||||
|
|
||||||
QUnit.module('median');
|
|
||||||
|
|
||||||
QUnit.test('should compute the median', function(assert) {
|
|
||||||
let data;
|
|
||||||
let expected;
|
|
||||||
|
|
||||||
data = [2, 4, 5, 3, 8, 2];
|
|
||||||
expected = 3.5;
|
|
||||||
|
|
||||||
assert.equal(median(data), expected, 'median is correct for the first not sorted array');
|
|
||||||
|
|
||||||
data = [2, 4, 5, 3, 8, 2, 9];
|
|
||||||
expected = 4;
|
|
||||||
|
|
||||||
assert.equal(median(data), expected, 'median is correct for the second not sorted array');
|
|
||||||
|
|
||||||
data = [2, 2, 3, 4, 5, 8, 9];
|
|
||||||
expected = 4;
|
|
||||||
|
|
||||||
assert.equal(median(data), expected, 'median is correct for the sorted array');
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user