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:
parent
64e55f5492
commit
1179826cbc
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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) {
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user