1
0
mirror of https://github.com/videojs/video.js.git synced 2024-12-12 11:15:04 +02:00

feat: Greater text track precision using requestVideoFrameCallback (#7633)

This commit is contained in:
mister-ben 2022-03-02 16:34:13 +01:00 committed by GitHub
parent 64e55f5492
commit 1179826cbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 128 additions and 18 deletions

View File

@ -741,6 +741,32 @@ class Html5 extends Tech {
return this.el_.requestPictureInPicture(); 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. * A getter/setter for the `Html5` Tech's source object.
* > Note: Please use {@link Html5#setSource} * > Note: Please use {@link Html5#setSource}
@ -1299,6 +1325,13 @@ Html5.prototype.featuresProgressEvents = true;
*/ */
Html5.prototype.featuresTimeupdateEvents = 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 --------------------------------- // // HTML5 Feature detection and Device Fixes --------------------------------- //
let canPlayType; let canPlayType;

View File

@ -15,6 +15,7 @@ import {isPlain} from '../utils/obj';
import * as TRACK_TYPES from '../tracks/track-types'; import * as TRACK_TYPES from '../tracks/track-types';
import {toTitleCase, toLowerCase} from '../utils/string-cases.js'; import {toTitleCase, toLowerCase} from '../utils/string-cases.js';
import vtt from 'videojs-vtt.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 * 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.stopTrackingCurrentTime_ = (e) => this.stopTrackingCurrentTime(e);
this.disposeSourceHandler_ = (e) => this.disposeSourceHandler(e); this.disposeSourceHandler_ = (e) => this.disposeSourceHandler(e);
this.queuedHanders_ = new Set();
// keep track of whether the current source has played at all to // keep track of whether the current source has played at all to
// implement a very limited played() // implement a very limited played()
this.hasStarted_ = false; this.hasStarted_ = false;
@ -857,6 +860,43 @@ class Tech extends Component {
*/ */
setDisablePictureInPicture() {} 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`. * A method to set a poster from a `Tech`.
* *
@ -1171,6 +1211,14 @@ Tech.prototype.featuresTimeupdateEvents = false;
*/ */
Tech.prototype.featuresNativeTextTracks = 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. * A functional mixin for techs that want to use the Source Handler pattern.
* Source handlers are scripts for handling specific formats. * Source handlers are scripts for handling specific formats.

View File

@ -183,11 +183,18 @@ class TextTrack extends Track {
const cues = new TextTrackCueList(this.cues_); const cues = new TextTrackCueList(this.cues_);
const activeCues = new TextTrackCueList(this.activeCues_); const activeCues = new TextTrackCueList(this.activeCues_);
let changed = false; 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 // Accessing this.activeCues for the side-effects of updating itself
// due to its nature as a getter function. Do not remove or cues will // due to its nature as a getter function. Do not remove or cues will
// stop updating! // stop updating!
@ -197,15 +204,18 @@ class TextTrack extends Track {
this.trigger('cuechange'); this.trigger('cuechange');
changed = false; changed = false;
} }
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
}); });
const disposeHandler = () => { const disposeHandler = () => {
this.tech_.off('timeupdate', timeupdateHandler); this.stopTracking();
}; };
this.tech_.one('dispose', disposeHandler); this.tech_.one('dispose', disposeHandler);
if (mode !== 'disabled') { if (mode !== 'disabled') {
this.tech_.on('timeupdate', timeupdateHandler); this.startTracking();
} }
Object.defineProperties(this, { Object.defineProperties(this, {
@ -251,10 +261,10 @@ class TextTrack extends Track {
// On-demand load. // On-demand load.
loadTrack(this.src, this); loadTrack(this.src, this);
} }
this.tech_.off('timeupdate', timeupdateHandler); this.stopTracking();
if (mode !== 'disabled') { if (mode !== 'disabled') {
this.tech_.on('timeupdate', timeupdateHandler); this.startTracking();
} }
/** /**
* An event that fires when mode changes on this track. This allows * 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. * Add a cue to the internal list of cues.
* *

View File

@ -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) { QUnit.test('does not fire cuechange before Tech is ready', function(assert) {
const done = assert.async(); const done = assert.async();
const clock = sinon.useFakeTimers();
const player = TestHelpers.makePlayer({techfaker: {autoReady: false}}); const player = TestHelpers.makePlayer({techfaker: {autoReady: false}});
let changes = 0; let changes = 0;
const tt = new TextTrack({ const tt = new TextTrack({
@ -278,7 +279,7 @@ QUnit.test('does not fire cuechange before Tech is ready', function(assert) {
return 0; return 0;
}; };
player.tech_.trigger('timeupdate'); player.tech_.trigger('playing');
assert.equal(changes, 0, 'a cuechange event is not triggered'); assert.equal(changes, 0, 'a cuechange event is not triggered');
player.tech_.on('ready', function() { player.tech_.on('ready', function() {
@ -286,15 +287,18 @@ QUnit.test('does not fire cuechange before Tech is ready', function(assert) {
return 0.2; 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'); assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange');
tt.off(); tt.off();
player.dispose(); player.dispose();
clock.restore();
done(); done();
}); });
player.tech_.triggerReady(); player.tech_.triggerReady();
clock.tick(1);
}); });
QUnit.test('fires cuechange when cues become active and inactive', function(assert) { 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; return 2;
}; };
player.tech_.trigger('timeupdate'); player.tech_.trigger('playing');
assert.equal(changes, 2, 'a cuechange event trigger addEventListener and oncuechange'); 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; return 7;
}; };
player.tech_.trigger('timeupdate'); player.tech_.trigger('playing');
assert.equal(changes, 4, 'a cuechange event trigger addEventListener and oncuechange'); 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() { player.tech_.currentTime = function() {
return 2; return 2;
}; };
player.tech_.trigger('timeupdate'); player.tech_.trigger('playing');
assert.equal(changes, 1, 'a cuechange event trigger'); assert.equal(changes, 1, 'a cuechange event trigger');
changes = 0; changes = 0;
// debugger;
tt.mode = 'disabled'; tt.mode = 'disabled';
player.tech_.currentTime = function() { player.tech_.currentTime = function() {
return 7; return 7;
}; };
player.tech_.trigger('timeupdate'); player.tech_.trigger('playing');
assert.equal(changes, 0, 'NO cuechange event trigger'); 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) { QUnit.test('enabled and disabled cuechange handler when changing mode to showing', function(assert) {
const clock = sinon.useFakeTimers();
const player = TestHelpers.makePlayer(); const player = TestHelpers.makePlayer();
let changes = 0; let changes = 0;
const tt = new TextTrack({ const tt = new TextTrack({
@ -401,7 +407,8 @@ QUnit.test('enabled and disabled cuechange handler when changing mode to showing
player.tech_.currentTime = function() { player.tech_.currentTime = function() {
return 2; return 2;
}; };
player.tech_.trigger('timeupdate'); player.tech_.trigger('playing');
clock.tick(10);
assert.equal(changes, 1, 'a cuechange event trigger'); 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() { player.tech_.currentTime = function() {
return 7; return 7;
}; };
player.tech_.trigger('timeupdate'); player.tech_.trigger('playing');
assert.equal(changes, 0, 'NO cuechange event trigger'); assert.equal(changes, 0, 'NO cuechange event trigger');
tt.off(); tt.off();
player.dispose(); player.dispose();
clock.restore();
}); });
QUnit.test('if preloadTextTracks is false, default tracks are not parsed until mode is showing', function(assert) { QUnit.test('if preloadTextTracks is false, default tracks are not parsed until mode is showing', function(assert) {

View File

@ -338,7 +338,7 @@ QUnit.test('no lang attribute on cue elements if one is provided', function(asse
player.tech_.textTracks().addTrack(tt); player.tech_.textTracks().addTrack(tt);
player.currentTime(2); player.currentTime(2);
player.trigger('timeupdate'); player.tech_.trigger('playing');
assert.notOk(tt.activeCues[0].displayState.hasAttribute('lang'), 'no lang attribute should be set'); 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.tech_.textTracks().addTrack(tt);
player.currentTime(2); 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'); 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() { player.tech_.currentTime = function() {
return 3; return 3;
}; };
player.tech_.trigger('timeupdate'); player.tech_.trigger('playing');
assert.equal( assert.equal(
numTextTrackChanges, 3, numTextTrackChanges, 3,
'texttrackchange should be triggered once for the cuechange' 'texttrackchange should be triggered once for the cuechange'