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'