1
0
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:
Brandon Casey 2020-03-30 17:27:45 -04:00 committed by GitHub
parent 1dd06a26c0
commit 668c7f44d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 276 additions and 231 deletions

View File

@ -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();

View File

@ -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);

View File

@ -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();
} }

View File

@ -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;

View File

@ -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');

View File

@ -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');
});
}); });

View File

@ -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');
});