diff --git a/src/js/tech/html5.js b/src/js/tech/html5.js
index 6283f8806..5dc860fe6 100644
--- a/src/js/tech/html5.js
+++ b/src/js/tech/html5.js
@@ -741,6 +741,32 @@ class Html5 extends Tech {
return this.el_.requestPictureInPicture();
}
+ /**
+ * Native requestVideoFrameCallback if supported by browser/tech, or fallback
+ *
+ * @param {function} cb function to call
+ * @return {number} id of request
+ */
+ requestVideoFrameCallback(cb) {
+ if (this.featuresVideoFrameCallback) {
+ return this.el_.requestVideoFrameCallback(cb);
+ }
+ return super.requestVideoFrameCallback(cb);
+ }
+
+ /**
+ * Native or fallback requestVideoFrameCallback
+ *
+ * @param {number} id request id to cancel
+ */
+ cancelVideoFrameCallback(id) {
+ if (this.featuresVideoFrameCallback) {
+ this.el_.cancelVideoFrameCallback(id);
+ } else {
+ super.cancelVideoFrameCallback(id);
+ }
+ }
+
/**
* A getter/setter for the `Html5` Tech's source object.
* > Note: Please use {@link Html5#setSource}
@@ -1299,6 +1325,13 @@ Html5.prototype.featuresProgressEvents = true;
*/
Html5.prototype.featuresTimeupdateEvents = true;
+/**
+ * Whether the HTML5 el supports `requestVideoFrameCallback`
+ *
+ * @type {boolean}
+ */
+Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback);
+
// HTML5 Feature detection and Device Fixes --------------------------------- //
let canPlayType;
diff --git a/src/js/tech/tech.js b/src/js/tech/tech.js
index 2489db552..b58b194b6 100644
--- a/src/js/tech/tech.js
+++ b/src/js/tech/tech.js
@@ -15,6 +15,7 @@ import {isPlain} from '../utils/obj';
import * as TRACK_TYPES from '../tracks/track-types';
import {toTitleCase, toLowerCase} from '../utils/string-cases.js';
import vtt from 'videojs-vtt.js';
+import * as Guid from '../utils/guid.js';
/**
* An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
@@ -103,6 +104,8 @@ class Tech extends Component {
this.stopTrackingCurrentTime_ = (e) => this.stopTrackingCurrentTime(e);
this.disposeSourceHandler_ = (e) => this.disposeSourceHandler(e);
+ this.queuedHanders_ = new Set();
+
// keep track of whether the current source has played at all to
// implement a very limited played()
this.hasStarted_ = false;
@@ -857,6 +860,43 @@ class Tech extends Component {
*/
setDisablePictureInPicture() {}
+ /**
+ * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame
+ *
+ * @param {function} cb
+ * @return {number} request id
+ */
+ requestVideoFrameCallback(cb) {
+ const id = Guid.newGUID();
+
+ if (this.paused()) {
+ this.queuedHanders_.add(id);
+ this.one('playing', () => {
+ if (this.queuedHanders_.has(id)) {
+ this.queuedHanders_.delete(id);
+ cb();
+ }
+ });
+ } else {
+ this.requestNamedAnimationFrame(id, cb);
+ }
+
+ return id;
+ }
+
+ /**
+ * A fallback implementation of cancelVideoFrameCallback
+ *
+ * @param {number} id id of callback to be cancelled
+ */
+ cancelVideoFrameCallback(id) {
+ if (this.queuedHanders_.has(id)) {
+ this.queuedHanders_.delete(id);
+ } else {
+ this.cancelNamedAnimationFrame(id);
+ }
+ }
+
/**
* A method to set a poster from a `Tech`.
*
@@ -1171,6 +1211,14 @@ Tech.prototype.featuresTimeupdateEvents = false;
*/
Tech.prototype.featuresNativeTextTracks = false;
+/**
+ * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`.
+ *
+ * @type {boolean}
+ * @default
+ */
+Tech.prototype.featuresVideoFrameCallback = false;
+
/**
* A functional mixin for techs that want to use the Source Handler pattern.
* Source handlers are scripts for handling specific formats.
diff --git a/src/js/tracks/text-track.js b/src/js/tracks/text-track.js
index e2fd0a30b..7cb02c899 100644
--- a/src/js/tracks/text-track.js
+++ b/src/js/tracks/text-track.js
@@ -183,11 +183,18 @@ class TextTrack extends Track {
const cues = new TextTrackCueList(this.cues_);
const activeCues = new TextTrackCueList(this.activeCues_);
let changed = false;
- const timeupdateHandler = Fn.bind(this, function() {
- if (!this.tech_.isReady_ || this.tech_.isDisposed()) {
- return;
+ this.timeupdateHandler = Fn.bind(this, function() {
+ if (this.tech_.isDisposed()) {
+ return;
}
+
+ if (!this.tech_.isReady_) {
+ this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
+
+ return;
+ }
+
// Accessing this.activeCues for the side-effects of updating itself
// due to its nature as a getter function. Do not remove or cues will
// stop updating!
@@ -197,15 +204,18 @@ class TextTrack extends Track {
this.trigger('cuechange');
changed = false;
}
+
+ this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
+
});
const disposeHandler = () => {
- this.tech_.off('timeupdate', timeupdateHandler);
+ this.stopTracking();
};
this.tech_.one('dispose', disposeHandler);
if (mode !== 'disabled') {
- this.tech_.on('timeupdate', timeupdateHandler);
+ this.startTracking();
}
Object.defineProperties(this, {
@@ -251,10 +261,10 @@ class TextTrack extends Track {
// On-demand load.
loadTrack(this.src, this);
}
- this.tech_.off('timeupdate', timeupdateHandler);
+ this.stopTracking();
if (mode !== 'disabled') {
- this.tech_.on('timeupdate', timeupdateHandler);
+ this.startTracking();
}
/**
* An event that fires when mode changes on this track. This allows
@@ -357,6 +367,17 @@ class TextTrack extends Track {
}
}
+ startTracking() {
+ this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
+ }
+
+ stopTracking() {
+ if (this.rvf_) {
+ this.tech_.cancelVideoFrameCallback(this.rvf_);
+ this.rvf_ = undefined;
+ }
+ }
+
/**
* Add a cue to the internal list of cues.
*
diff --git a/test/unit/tracks/text-track.test.js b/test/unit/tracks/text-track.test.js
index 02eb3b8f8..3b8ae9473 100644
--- a/test/unit/tracks/text-track.test.js
+++ b/test/unit/tracks/text-track.test.js
@@ -255,6 +255,7 @@ QUnit.test('can only remove one cue at a time', function(assert) {
QUnit.test('does not fire cuechange before Tech is ready', function(assert) {
const done = assert.async();
+ const clock = sinon.useFakeTimers();
const player = TestHelpers.makePlayer({techfaker: {autoReady: false}});
let changes = 0;
const tt = new TextTrack({
@@ -278,7 +279,7 @@ QUnit.test('does not fire cuechange before Tech is ready', function(assert) {
return 0;
};
- player.tech_.trigger('timeupdate');
+ player.tech_.trigger('playing');
assert.equal(changes, 0, 'a cuechange event is not triggered');
player.tech_.on('ready', function() {
@@ -286,15 +287,18 @@ QUnit.test('does not fire cuechange before Tech is ready', function(assert) {
return 0.2;
};
- player.tech_.trigger('timeupdate');
+ player.tech_.trigger('playing');
+ clock.tick(1);
assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange');
tt.off();
player.dispose();
+ clock.restore();
done();
});
player.tech_.triggerReady();
+ clock.tick(1);
});
QUnit.test('fires cuechange when cues become active and inactive', function(assert) {
@@ -321,7 +325,7 @@ QUnit.test('fires cuechange when cues become active and inactive', function(asse
return 2;
};
- player.tech_.trigger('timeupdate');
+ player.tech_.trigger('playing');
assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange');
@@ -329,7 +333,7 @@ QUnit.test('fires cuechange when cues become active and inactive', function(asse
return 7;
};
- player.tech_.trigger('timeupdate');
+ player.tech_.trigger('playing');
assert.equal(changes, 4, 'a cuechange event trigger addEventListener and oncuechange');
@@ -360,17 +364,18 @@ QUnit.test('enabled and disabled cuechange handler when changing mode to hidden'
player.tech_.currentTime = function() {
return 2;
};
- player.tech_.trigger('timeupdate');
+ player.tech_.trigger('playing');
assert.equal(changes, 1, 'a cuechange event trigger');
changes = 0;
+ // debugger;
tt.mode = 'disabled';
player.tech_.currentTime = function() {
return 7;
};
- player.tech_.trigger('timeupdate');
+ player.tech_.trigger('playing');
assert.equal(changes, 0, 'NO cuechange event trigger');
@@ -379,6 +384,7 @@ QUnit.test('enabled and disabled cuechange handler when changing mode to hidden'
});
QUnit.test('enabled and disabled cuechange handler when changing mode to showing', function(assert) {
+ const clock = sinon.useFakeTimers();
const player = TestHelpers.makePlayer();
let changes = 0;
const tt = new TextTrack({
@@ -401,7 +407,8 @@ QUnit.test('enabled and disabled cuechange handler when changing mode to showing
player.tech_.currentTime = function() {
return 2;
};
- player.tech_.trigger('timeupdate');
+ player.tech_.trigger('playing');
+ clock.tick(10);
assert.equal(changes, 1, 'a cuechange event trigger');
@@ -411,12 +418,13 @@ QUnit.test('enabled and disabled cuechange handler when changing mode to showing
player.tech_.currentTime = function() {
return 7;
};
- player.tech_.trigger('timeupdate');
+ player.tech_.trigger('playing');
assert.equal(changes, 0, 'NO cuechange event trigger');
tt.off();
player.dispose();
+ clock.restore();
});
QUnit.test('if preloadTextTracks is false, default tracks are not parsed until mode is showing', function(assert) {
diff --git a/test/unit/tracks/text-tracks.test.js b/test/unit/tracks/text-tracks.test.js
index a60da79ab..476cd9d1f 100644
--- a/test/unit/tracks/text-tracks.test.js
+++ b/test/unit/tracks/text-tracks.test.js
@@ -338,7 +338,7 @@ QUnit.test('no lang attribute on cue elements if one is provided', function(asse
player.tech_.textTracks().addTrack(tt);
player.currentTime(2);
- player.trigger('timeupdate');
+ player.tech_.trigger('playing');
assert.notOk(tt.activeCues[0].displayState.hasAttribute('lang'), 'no lang attribute should be set');
@@ -361,7 +361,7 @@ QUnit.test('set lang attribute on cue elements if one is provided', function(ass
player.tech_.textTracks().addTrack(tt);
player.currentTime(2);
- player.trigger('timeupdate');
+ player.tech_.trigger('playing');
assert.equal(tt.activeCues[0].displayState.getAttribute('lang'), 'en', 'the lang should be set to en');
@@ -404,7 +404,7 @@ QUnit.test('removes cuechange event when text track is hidden for emulated track
player.tech_.currentTime = function() {
return 3;
};
- player.tech_.trigger('timeupdate');
+ player.tech_.trigger('playing');
assert.equal(
numTextTrackChanges, 3,
'texttrackchange should be triggered once for the cuechange'