mirror of
https://github.com/videojs/video.js.git
synced 2024-12-04 10:34:51 +02:00
feat: Add named requestAnimationFrame to prevent performance issues (#6627)
Make sure we don't create multiple rAFs particularly when in a background tab. Fixes #5937
This commit is contained in:
parent
4d9e1bcccc
commit
6e7cc75aaa
@ -13,6 +13,8 @@ import * as Guid from './utils/guid.js';
|
||||
import {toTitleCase, toLowerCase} from './utils/string-cases.js';
|
||||
import mergeOptions from './utils/merge-options.js';
|
||||
import computedStyle from './utils/computed-style';
|
||||
import Map from './utils/map.js';
|
||||
import Set from './utils/set.js';
|
||||
|
||||
/**
|
||||
* Base class for all UI Components.
|
||||
@ -100,38 +102,10 @@ class Component {
|
||||
this.childIndex_ = {};
|
||||
this.childNameIndex_ = {};
|
||||
|
||||
let SetSham;
|
||||
|
||||
if (!window.Set) {
|
||||
SetSham = class {
|
||||
constructor() {
|
||||
this.set_ = {};
|
||||
}
|
||||
has(key) {
|
||||
return key in this.set_;
|
||||
}
|
||||
delete(key) {
|
||||
const has = this.has(key);
|
||||
|
||||
delete this.set_[key];
|
||||
|
||||
return has;
|
||||
}
|
||||
add(key) {
|
||||
this.set_[key] = 1;
|
||||
return this;
|
||||
}
|
||||
forEach(callback, thisArg) {
|
||||
for (const key in this.set_) {
|
||||
callback.call(thisArg, key, key, this);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.setTimeoutIds_ = window.Set ? new Set() : new SetSham();
|
||||
this.setIntervalIds_ = window.Set ? new Set() : new SetSham();
|
||||
this.rafIds_ = window.Set ? new Set() : new SetSham();
|
||||
this.setTimeoutIds_ = new Set();
|
||||
this.setIntervalIds_ = new Set();
|
||||
this.rafIds_ = new Set();
|
||||
this.namedRafs_ = new Map();
|
||||
this.clearingTimersOnDispose_ = false;
|
||||
|
||||
// Add any child components in options
|
||||
@ -1529,6 +1503,53 @@ class Component {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request an animation frame, but only one named animation
|
||||
* frame will be queued. Another will never be added until
|
||||
* the previous one finishes.
|
||||
*
|
||||
* @param {string} name
|
||||
* The name to give this requestAnimationFrame
|
||||
*
|
||||
* @param {Component~GenericCallback} fn
|
||||
* A function that will be bound to this component and executed just
|
||||
* before the browser's next repaint.
|
||||
*/
|
||||
requestNamedAnimationFrame(name, fn) {
|
||||
if (this.namedRafs_.has(name)) {
|
||||
return;
|
||||
}
|
||||
this.clearTimersOnDispose_();
|
||||
|
||||
fn = Fn.bind(this, fn);
|
||||
|
||||
const id = this.requestAnimationFrame(() => {
|
||||
fn();
|
||||
if (this.namedRafs_.has(name)) {
|
||||
this.namedRafs_.delete(name);
|
||||
}
|
||||
});
|
||||
|
||||
this.namedRafs_.set(name, id);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a current named animation frame if it exists.
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the requestAnimationFrame to cancel.
|
||||
*/
|
||||
cancelNamedAnimationFrame(name) {
|
||||
if (!this.namedRafs_.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancelAnimationFrame(this.namedRafs_.get(name));
|
||||
this.namedRafs_.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a queued callback passed to {@link Component#requestAnimationFrame}
|
||||
* (rAF).
|
||||
@ -1578,11 +1599,15 @@ class Component {
|
||||
this.clearingTimersOnDispose_ = true;
|
||||
this.one('dispose', () => {
|
||||
[
|
||||
['namedRafs_', 'cancelNamedAnimationFrame'],
|
||||
['rafIds_', 'cancelAnimationFrame'],
|
||||
['setTimeoutIds_', 'clearTimeout'],
|
||||
['setIntervalIds_', 'clearInterval']
|
||||
].forEach(([idName, cancelName]) => {
|
||||
this[idName].forEach(this[cancelName], this);
|
||||
// for a `Set` key will actually be the value again
|
||||
// so forEach((val, val) =>` but for maps we want to use
|
||||
// the key.
|
||||
this[idName].forEach((val, key) => this[cancelName](key));
|
||||
});
|
||||
|
||||
this.clearingTimersOnDispose_ = false;
|
||||
|
@ -72,7 +72,7 @@ class LoadProgressBar extends Component {
|
||||
* @listens Player#progress
|
||||
*/
|
||||
update(event) {
|
||||
this.requestAnimationFrame(() => {
|
||||
this.requestNamedAnimationFrame('LoadProgressBar#update', () => {
|
||||
const liveTracker = this.player_.liveTracker;
|
||||
const buffered = this.player_.buffered();
|
||||
const duration = (liveTracker && liveTracker.isLive()) ? liveTracker.seekableEnd() : this.player_.duration();
|
||||
|
@ -133,7 +133,7 @@ class SeekBar extends Slider {
|
||||
update(event) {
|
||||
const percent = super.update();
|
||||
|
||||
this.requestAnimationFrame(() => {
|
||||
this.requestNamedAnimationFrame('SeekBar#update', () => {
|
||||
const currentTime = this.player_.ended() ?
|
||||
this.player_.duration() : this.getCurrentTime_();
|
||||
const liveTracker = this.player_.liveTracker;
|
||||
|
@ -128,12 +128,7 @@ class TimeTooltip extends Component {
|
||||
* for tooltips that need to do additional animations from the default
|
||||
*/
|
||||
updateTime(seekBarRect, seekBarPoint, time, cb) {
|
||||
// If there is an existing rAF ID, cancel it so we don't over-queue.
|
||||
if (this.rafId_) {
|
||||
this.cancelAnimationFrame(this.rafId_);
|
||||
}
|
||||
|
||||
this.rafId_ = this.requestAnimationFrame(() => {
|
||||
this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => {
|
||||
let content;
|
||||
const duration = this.player_.duration();
|
||||
|
||||
|
@ -81,7 +81,7 @@ class TimeDisplay extends Component {
|
||||
|
||||
this.formattedTime_ = time;
|
||||
|
||||
this.requestAnimationFrame(() => {
|
||||
this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => {
|
||||
if (!this.contentEl_) {
|
||||
return;
|
||||
}
|
||||
|
@ -249,7 +249,7 @@ class Slider extends Component {
|
||||
|
||||
this.progress_ = progress;
|
||||
|
||||
this.requestAnimationFrame(() => {
|
||||
this.requestNamedAnimationFrame('Slider#update', () => {
|
||||
// Set the new bar width or height
|
||||
const sizeKey = this.vertical() ? 'height' : 'width';
|
||||
|
||||
|
28
src/js/utils/map.js
Normal file
28
src/js/utils/map.js
Normal file
@ -0,0 +1,28 @@
|
||||
import window from 'global/window';
|
||||
|
||||
class MapSham {
|
||||
constructor() {
|
||||
this.map_ = {};
|
||||
}
|
||||
has(key) {
|
||||
return key in this.map_;
|
||||
}
|
||||
delete(key) {
|
||||
const has = this.has(key);
|
||||
|
||||
delete this.map_[key];
|
||||
|
||||
return has;
|
||||
}
|
||||
set(key, value) {
|
||||
this.set_[key] = value;
|
||||
return this;
|
||||
}
|
||||
forEach(callback, thisArg) {
|
||||
for (const key in this.map_) {
|
||||
callback.call(thisArg, this.map_[key], key, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default window.Map ? window.Map : MapSham;
|
28
src/js/utils/set.js
Normal file
28
src/js/utils/set.js
Normal file
@ -0,0 +1,28 @@
|
||||
import window from 'global/window';
|
||||
|
||||
class SetSham {
|
||||
constructor() {
|
||||
this.set_ = {};
|
||||
}
|
||||
has(key) {
|
||||
return key in this.set_;
|
||||
}
|
||||
delete(key) {
|
||||
const has = this.has(key);
|
||||
|
||||
delete this.set_[key];
|
||||
|
||||
return has;
|
||||
}
|
||||
add(key) {
|
||||
this.set_[key] = 1;
|
||||
return this;
|
||||
}
|
||||
forEach(callback, thisArg) {
|
||||
for (const key in this.set_) {
|
||||
callback.call(thisArg, key, key, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default window.Set ? window.Set : SetSham;
|
@ -952,7 +952,7 @@ QUnit.test('should provide interval methods that automatically get cleared on co
|
||||
assert.ok(intervalsFired === 5, 'Interval was cleared when component was disposed');
|
||||
});
|
||||
|
||||
QUnit.test('should provide *AnimationFrame methods that automatically get cleared on component disposal', function(assert) {
|
||||
QUnit.test('should provide a requestAnimationFrame method that is cleared on disposal', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
const oldRAF = window.requestAnimationFrame;
|
||||
const oldCAF = window.cancelAnimationFrame;
|
||||
@ -967,7 +967,7 @@ QUnit.test('should provide *AnimationFrame methods that automatically get cleare
|
||||
|
||||
const spyRAF = sinon.spy();
|
||||
|
||||
comp.requestAnimationFrame(spyRAF);
|
||||
comp.requestNamedAnimationFrame('testing', spyRAF);
|
||||
|
||||
assert.strictEqual(spyRAF.callCount, 0, 'rAF callback was not called immediately');
|
||||
this.clock.tick(1);
|
||||
@ -975,11 +975,11 @@ QUnit.test('should provide *AnimationFrame methods that automatically get cleare
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'rAF callback was not called after a second "repaint"');
|
||||
|
||||
comp.cancelAnimationFrame(comp.requestAnimationFrame(spyRAF));
|
||||
comp.cancelNamedAnimationFrame(comp.requestNamedAnimationFrame('testing', spyRAF));
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'second rAF callback was not called because it was cancelled');
|
||||
|
||||
comp.requestAnimationFrame(spyRAF);
|
||||
comp.requestNamedAnimationFrame('testing', spyRAF);
|
||||
comp.dispose();
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'third rAF callback was not called because the component was disposed');
|
||||
@ -988,7 +988,43 @@ QUnit.test('should provide *AnimationFrame methods that automatically get cleare
|
||||
window.cancelAnimationFrame = oldCAF;
|
||||
});
|
||||
|
||||
QUnit.test('*AnimationFrame methods fall back to timers if rAF not supported', function(assert) {
|
||||
QUnit.test('should provide a requestNamedAnimationFrame method that is cleared on disposal', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
const oldRAF = window.requestAnimationFrame;
|
||||
const oldCAF = window.cancelAnimationFrame;
|
||||
|
||||
// Stub the window.*AnimationFrame methods with window.setTimeout methods
|
||||
// so we can control when the callbacks are called via sinon's timer stubs.
|
||||
window.requestAnimationFrame = (fn) => window.setTimeout(fn, 1);
|
||||
window.cancelAnimationFrame = (id) => window.clearTimeout(id);
|
||||
|
||||
// Make sure the component thinks it supports rAF.
|
||||
comp.supportsRaf_ = true;
|
||||
|
||||
const spyRAF = sinon.spy();
|
||||
|
||||
comp.requestNamedAnimationFrame('testing', spyRAF);
|
||||
|
||||
assert.strictEqual(spyRAF.callCount, 0, 'rAF callback was not called immediately');
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'rAF callback was called after a "repaint"');
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'rAF callback was not called after a second "repaint"');
|
||||
|
||||
comp.cancelNamedAnimationFrame(comp.requestNamedAnimationFrame('testing', spyRAF));
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'second rAF callback was not called because it was cancelled');
|
||||
|
||||
comp.requestNamedAnimationFrame('testing', spyRAF);
|
||||
comp.dispose();
|
||||
this.clock.tick(1);
|
||||
assert.strictEqual(spyRAF.callCount, 1, 'third rAF callback was not called because the component was disposed');
|
||||
|
||||
window.requestAnimationFrame = oldRAF;
|
||||
window.cancelAnimationFrame = oldCAF;
|
||||
});
|
||||
|
||||
QUnit.test('requestAnimationFrame falls back to timers if rAF not supported', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
const oldRAF = window.requestAnimationFrame;
|
||||
const oldCAF = window.cancelAnimationFrame;
|
||||
@ -1030,7 +1066,7 @@ QUnit.test('setTimeout should remove dispose handler on trigger', function(asser
|
||||
comp.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('requestAnimationFrame should remove dispose handler on trigger', function(assert) {
|
||||
QUnit.test('requestNamedAnimationFrame should remove dispose handler on trigger', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
const oldRAF = window.requestAnimationFrame;
|
||||
const oldCAF = window.cancelAnimationFrame;
|
||||
@ -1045,13 +1081,15 @@ QUnit.test('requestAnimationFrame should remove dispose handler on trigger', fun
|
||||
|
||||
const spyRAF = sinon.spy();
|
||||
|
||||
comp.requestAnimationFrame(spyRAF);
|
||||
comp.requestNamedAnimationFrame('testFrame', spyRAF);
|
||||
|
||||
assert.equal(comp.rafIds_.size, 1, 'we got a new dispose handler');
|
||||
assert.equal(comp.rafIds_.size, 1, 'we got a new raf dispose handler');
|
||||
assert.equal(comp.namedRafs_.size, 1, 'we got a new named raf dispose handler');
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
assert.equal(comp.rafIds_.size, 0, 'we removed our dispose handle');
|
||||
assert.equal(comp.rafIds_.size, 0, 'we removed our raf dispose handle');
|
||||
assert.equal(comp.namedRafs_.size, 0, 'we removed our named raf dispose handle');
|
||||
|
||||
comp.dispose();
|
||||
|
||||
@ -1169,6 +1207,122 @@ QUnit.test('setInterval should be canceled on dispose', function(assert) {
|
||||
assert.equal(called, false, 'setInterval was never called');
|
||||
});
|
||||
|
||||
QUnit.test('requestNamedAnimationFrame should be canceled on dispose', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
let called = false;
|
||||
let clearName;
|
||||
const setName = comp.requestNamedAnimationFrame('testing', () => {
|
||||
called = true;
|
||||
});
|
||||
|
||||
const cancelNamedAnimationFrame = comp.cancelNamedAnimationFrame;
|
||||
|
||||
comp.cancelNamedAnimationFrame = (name) => {
|
||||
clearName = name;
|
||||
return cancelNamedAnimationFrame.call(comp, name);
|
||||
};
|
||||
|
||||
assert.equal(comp.namedRafs_.size, 1, 'we added a named raf');
|
||||
assert.equal(comp.rafIds_.size, 1, 'we added a raf id');
|
||||
|
||||
comp.dispose();
|
||||
|
||||
assert.equal(comp.namedRafs_.size, 0, 'we removed a named raf');
|
||||
assert.equal(comp.rafIds_.size, 0, 'we removed a raf id');
|
||||
assert.equal(clearName, setName, 'cancelNamedAnimationFrame was called');
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
assert.equal(called, false, 'requestNamedAnimationFrame was never called');
|
||||
});
|
||||
|
||||
QUnit.test('requestNamedAnimationFrame should only allow one raf of a specific name at a time', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
const calls = {
|
||||
one: 0,
|
||||
two: 0,
|
||||
three: 0
|
||||
};
|
||||
const cancelNames = [];
|
||||
const name = 'testing';
|
||||
const handlerOne = () => {
|
||||
assert.equal(comp.namedRafs_.size, 1, 'named raf still exists while function runs');
|
||||
assert.equal(comp.rafIds_.size, 0, 'raf id does not exist during run');
|
||||
|
||||
calls.one++;
|
||||
};
|
||||
const handlerTwo = () => {
|
||||
assert.equal(comp.namedRafs_.size, 1, 'named raf still exists while function runs');
|
||||
assert.equal(comp.rafIds_.size, 0, 'raf id does not exist during run');
|
||||
calls.two++;
|
||||
};
|
||||
const handlerThree = () => {
|
||||
assert.equal(comp.namedRafs_.size, 1, 'named raf still exists while function runs');
|
||||
assert.equal(comp.rafIds_.size, 0, 'raf id does not exist during run');
|
||||
calls.three++;
|
||||
};
|
||||
|
||||
const oldRAF = window.requestAnimationFrame;
|
||||
const oldCAF = window.cancelAnimationFrame;
|
||||
|
||||
// Make sure the component thinks it supports rAF.
|
||||
comp.supportsRaf_ = true;
|
||||
|
||||
// Stub the window.*AnimationFrame methods with window.setTimeout methods
|
||||
// so we can control when the callbacks are called via sinon's timer stubs.
|
||||
window.requestAnimationFrame = (fn) => window.setTimeout(fn, 1);
|
||||
window.cancelAnimationFrame = (id) => window.clearTimeout(id);
|
||||
|
||||
const cancelNamedAnimationFrame = comp.cancelNamedAnimationFrame;
|
||||
|
||||
comp.cancelNamedAnimationFrame = (_name) => {
|
||||
cancelNames.push(_name);
|
||||
return cancelNamedAnimationFrame.call(comp, _name);
|
||||
};
|
||||
|
||||
comp.requestNamedAnimationFrame(name, handlerOne);
|
||||
|
||||
assert.equal(comp.namedRafs_.size, 1, 'we added a named raf');
|
||||
assert.equal(comp.rafIds_.size, 1, 'we added a raf id');
|
||||
|
||||
comp.requestNamedAnimationFrame(name, handlerTwo);
|
||||
|
||||
assert.deepEqual(cancelNames, [], 'no named cancels');
|
||||
assert.equal(comp.namedRafs_.size, 1, 'still only one named raf');
|
||||
assert.equal(comp.rafIds_.size, 1, 'still only one raf id');
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
assert.equal(comp.namedRafs_.size, 0, 'we removed a named raf');
|
||||
assert.equal(comp.rafIds_.size, 0, 'we removed a raf id');
|
||||
assert.deepEqual(calls, {
|
||||
one: 1,
|
||||
two: 0,
|
||||
three: 0
|
||||
}, 'only handlerOne was called');
|
||||
|
||||
comp.requestNamedAnimationFrame(name, handlerOne);
|
||||
comp.requestNamedAnimationFrame(name, handlerTwo);
|
||||
comp.requestNamedAnimationFrame(name, handlerThree);
|
||||
|
||||
assert.deepEqual(cancelNames, [], 'no named cancels for testing');
|
||||
assert.equal(comp.namedRafs_.size, 1, 'only added one named raf');
|
||||
assert.equal(comp.rafIds_.size, 1, 'only added one named raf');
|
||||
|
||||
this.clock.tick(1);
|
||||
|
||||
assert.equal(comp.namedRafs_.size, 0, 'we removed a named raf');
|
||||
assert.equal(comp.rafIds_.size, 0, 'we removed a raf id');
|
||||
assert.deepEqual(calls, {
|
||||
one: 2,
|
||||
two: 0,
|
||||
three: 0
|
||||
}, 'only the handlerOne called');
|
||||
|
||||
window.requestAnimationFrame = oldRAF;
|
||||
window.cancelAnimationFrame = oldCAF;
|
||||
});
|
||||
|
||||
QUnit.test('$ and $$ functions', function(assert) {
|
||||
const comp = new Component(getFakePlayer());
|
||||
const contentEl = document.createElement('div');
|
||||
|
Loading…
Reference in New Issue
Block a user