- 1 :
/**
- 2 :
* @file text-track.js
- 3 :
*/
- 4 :
import TextTrackCueList from './text-track-cue-list';
- 5 :
import * as Fn from '../utils/fn.js';
- 6 :
import {TextTrackKind, TextTrackMode} from './track-enums';
- 7 :
import log from '../utils/log.js';
- 8 :
import window from 'global/window';
- 9 :
import Track from './track.js';
- 10 :
import { isCrossOrigin } from '../utils/url.js';
- 11 :
import XHR from '@videojs/xhr';
- 12 :
import {merge} from '../utils/obj';
- 13 :
- 14 :
/**
- 15 :
* Takes a webvtt file contents and parses it into cues
- 16 :
*
- 17 :
* @param {string} srcContent
- 18 :
* webVTT file contents
- 19 :
*
- 20 :
* @param {TextTrack} track
- 21 :
* TextTrack to add cues to. Cues come from the srcContent.
- 22 :
*
- 23 :
* @private
- 24 :
*/
- 25 :
const parseCues = function(srcContent, track) {
- 26 :
const parser = new window.WebVTT.Parser(
- 27 :
window,
- 28 :
window.vttjs,
- 29 :
window.WebVTT.StringDecoder()
- 30 :
);
- 31 :
const errors = [];
- 32 :
- 33 :
parser.oncue = function(cue) {
- 34 :
track.addCue(cue);
- 35 :
};
- 36 :
- 37 :
parser.onparsingerror = function(error) {
- 38 :
errors.push(error);
- 39 :
};
- 40 :
- 41 :
parser.onflush = function() {
- 42 :
track.trigger({
- 43 :
type: 'loadeddata',
- 44 :
target: track
- 45 :
});
- 46 :
};
- 47 :
- 48 :
parser.parse(srcContent);
- 49 :
if (errors.length > 0) {
- 50 :
if (window.console && window.console.groupCollapsed) {
- 51 :
window.console.groupCollapsed(`Text Track parsing errors for ${track.src}`);
- 52 :
}
- 53 :
errors.forEach((error) => log.error(error));
- 54 :
if (window.console && window.console.groupEnd) {
- 55 :
window.console.groupEnd();
- 56 :
}
- 57 :
}
- 58 :
- 59 :
parser.flush();
- 60 :
};
- 61 :
- 62 :
/**
- 63 :
* Load a `TextTrack` from a specified url.
- 64 :
*
- 65 :
* @param {string} src
- 66 :
* Url to load track from.
- 67 :
*
- 68 :
* @param {TextTrack} track
- 69 :
* Track to add cues to. Comes from the content at the end of `url`.
- 70 :
*
- 71 :
* @private
- 72 :
*/
- 73 :
const loadTrack = function(src, track) {
- 74 :
const opts = {
- 75 :
uri: src
- 76 :
};
- 77 :
const crossOrigin = isCrossOrigin(src);
- 78 :
- 79 :
if (crossOrigin) {
- 80 :
opts.cors = crossOrigin;
- 81 :
}
- 82 :
- 83 :
const withCredentials = track.tech_.crossOrigin() === 'use-credentials';
- 84 :
- 85 :
if (withCredentials) {
- 86 :
opts.withCredentials = withCredentials;
- 87 :
}
- 88 :
- 89 :
XHR(opts, Fn.bind_(this, function(err, response, responseBody) {
- 90 :
if (err) {
- 91 :
return log.error(err, response);
- 92 :
}
- 93 :
- 94 :
track.loaded_ = true;
- 95 :
- 96 :
// Make sure that vttjs has loaded, otherwise, wait till it finished loading
- 97 :
// NOTE: this is only used for the alt/video.novtt.js build
- 98 :
if (typeof window.WebVTT !== 'function') {
- 99 :
if (track.tech_) {
- 100 :
// to prevent use before define eslint error, we define loadHandler
- 101 :
// as a let here
- 102 :
track.tech_.any(['vttjsloaded', 'vttjserror'], (event) => {
- 103 :
if (event.type === 'vttjserror') {
- 104 :
log.error(`vttjs failed to load, stopping trying to process ${track.src}`);
- 105 :
return;
- 106 :
}
- 107 :
return parseCues(responseBody, track);
- 108 :
});
- 109 :
}
- 110 :
} else {
- 111 :
parseCues(responseBody, track);
- 112 :
}
- 113 :
- 114 :
}));
- 115 :
};
- 116 :
- 117 :
/**
- 118 :
* A representation of a single `TextTrack`.
- 119 :
*
- 120 :
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
- 121 :
* @extends Track
- 122 :
*/
- 123 :
class TextTrack extends Track {
- 124 :
- 125 :
/**
- 126 :
* Create an instance of this class.
- 127 :
*
- 128 :
* @param {Object} options={}
- 129 :
* Object of option names and values
- 130 :
*
- 131 :
* @param { import('../tech/tech').default } options.tech
- 132 :
* A reference to the tech that owns this TextTrack.
- 133 :
*
- 134 :
* @param {TextTrack~Kind} [options.kind='subtitles']
- 135 :
* A valid text track kind.
- 136 :
*
- 137 :
* @param {TextTrack~Mode} [options.mode='disabled']
- 138 :
* A valid text track mode.
- 139 :
*
- 140 :
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- 141 :
* A unique id for this TextTrack.
- 142 :
*
- 143 :
* @param {string} [options.label='']
- 144 :
* The menu label for this track.
- 145 :
*
- 146 :
* @param {string} [options.language='']
- 147 :
* A valid two character language code.
- 148 :
*
- 149 :
* @param {string} [options.srclang='']
- 150 :
* A valid two character language code. An alternative, but deprioritized
- 151 :
* version of `options.language`
- 152 :
*
- 153 :
* @param {string} [options.src]
- 154 :
* A url to TextTrack cues.
- 155 :
*
- 156 :
* @param {boolean} [options.default]
- 157 :
* If this track should default to on or off.
- 158 :
*/
- 159 :
constructor(options = {}) {
- 160 :
if (!options.tech) {
- 161 :
throw new Error('A tech was not provided.');
- 162 :
}
- 163 :
- 164 :
const settings = merge(options, {
- 165 :
kind: TextTrackKind[options.kind] || 'subtitles',
- 166 :
language: options.language || options.srclang || ''
- 167 :
});
- 168 :
let mode = TextTrackMode[settings.mode] || 'disabled';
- 169 :
const default_ = settings.default;
- 170 :
- 171 :
if (settings.kind === 'metadata' || settings.kind === 'chapters') {
- 172 :
mode = 'hidden';
- 173 :
}
- 174 :
super(settings);
- 175 :
- 176 :
this.tech_ = settings.tech;
- 177 :
- 178 :
this.cues_ = [];
- 179 :
this.activeCues_ = [];
- 180 :
- 181 :
this.preload_ = this.tech_.preloadTextTracks !== false;
- 182 :
- 183 :
const cues = new TextTrackCueList(this.cues_);
- 184 :
const activeCues = new TextTrackCueList(this.activeCues_);
- 185 :
let changed = false;
- 186 :
- 187 :
this.timeupdateHandler = Fn.bind_(this, function(event = {}) {
- 188 :
if (this.tech_.isDisposed()) {
- 189 :
return;
- 190 :
}
- 191 :
- 192 :
if (!this.tech_.isReady_) {
- 193 :
if (event.type !== 'timeupdate') {
- 194 :
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- 195 :
}
- 196 :
- 197 :
return;
- 198 :
}
- 199 :
- 200 :
// Accessing this.activeCues for the side-effects of updating itself
- 201 :
// due to its nature as a getter function. Do not remove or cues will
- 202 :
// stop updating!
- 203 :
// Use the setter to prevent deletion from uglify (pure_getters rule)
- 204 :
this.activeCues = this.activeCues;
- 205 :
if (changed) {
- 206 :
this.trigger('cuechange');
- 207 :
changed = false;
- 208 :
}
- 209 :
- 210 :
if (event.type !== 'timeupdate') {
- 211 :
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- 212 :
}
- 213 :
- 214 :
});
- 215 :
- 216 :
const disposeHandler = () => {
- 217 :
this.stopTracking();
- 218 :
};
- 219 :
- 220 :
this.tech_.one('dispose', disposeHandler);
- 221 :
if (mode !== 'disabled') {
- 222 :
this.startTracking();
- 223 :
}
- 224 :
- 225 :
Object.defineProperties(this, {
- 226 :
/**
- 227 :
* @memberof TextTrack
- 228 :
* @member {boolean} default
- 229 :
* If this track was set to be on or off by default. Cannot be changed after
- 230 :
* creation.
- 231 :
* @instance
- 232 :
*
- 233 :
* @readonly
- 234 :
*/
- 235 :
default: {
- 236 :
get() {
- 237 :
return default_;
- 238 :
},
- 239 :
set() {}
- 240 :
},
- 241 :
- 242 :
/**
- 243 :
* @memberof TextTrack
- 244 :
* @member {string} mode
- 245 :
* Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will
- 246 :
* not be set if setting to an invalid mode.
- 247 :
* @instance
- 248 :
*
- 249 :
* @fires TextTrack#modechange
- 250 :
*/
- 251 :
mode: {
- 252 :
get() {
- 253 :
return mode;
- 254 :
},
- 255 :
set(newMode) {
- 256 :
if (!TextTrackMode[newMode]) {
- 257 :
return;
- 258 :
}
- 259 :
if (mode === newMode) {
- 260 :
return;
- 261 :
}
- 262 :
- 263 :
mode = newMode;
- 264 :
if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
- 265 :
// On-demand load.
- 266 :
loadTrack(this.src, this);
- 267 :
}
- 268 :
this.stopTracking();
- 269 :
- 270 :
if (mode !== 'disabled') {
- 271 :
this.startTracking();
- 272 :
}
- 273 :
/**
- 274 :
* An event that fires when mode changes on this track. This allows
- 275 :
* the TextTrackList that holds this track to act accordingly.
- 276 :
*
- 277 :
* > Note: This is not part of the spec!
- 278 :
*
- 279 :
* @event TextTrack#modechange
- 280 :
* @type {Event}
- 281 :
*/
- 282 :
this.trigger('modechange');
- 283 :
- 284 :
}
- 285 :
},
- 286 :
- 287 :
/**
- 288 :
* @memberof TextTrack
- 289 :
* @member {TextTrackCueList} cues
- 290 :
* The text track cue list for this TextTrack.
- 291 :
* @instance
- 292 :
*/
- 293 :
cues: {
- 294 :
get() {
- 295 :
if (!this.loaded_) {
- 296 :
return null;
- 297 :
}
- 298 :
- 299 :
return cues;
- 300 :
},
- 301 :
set() {}
- 302 :
},
- 303 :
- 304 :
/**
- 305 :
* @memberof TextTrack
- 306 :
* @member {TextTrackCueList} activeCues
- 307 :
* The list text track cues that are currently active for this TextTrack.
- 308 :
* @instance
- 309 :
*/
- 310 :
activeCues: {
- 311 :
get() {
- 312 :
if (!this.loaded_) {
- 313 :
return null;
- 314 :
}
- 315 :
- 316 :
// nothing to do
- 317 :
if (this.cues.length === 0) {
- 318 :
return activeCues;
- 319 :
}
- 320 :
- 321 :
const ct = this.tech_.currentTime();
- 322 :
const active = [];
- 323 :
- 324 :
for (let i = 0, l = this.cues.length; i < l; i++) {
- 325 :
const cue = this.cues[i];
- 326 :
- 327 :
if (cue.startTime <= ct && cue.endTime >= ct) {
- 328 :
active.push(cue);
- 329 :
}
- 330 :
}
- 331 :
- 332 :
changed = false;
- 333 :
- 334 :
if (active.length !== this.activeCues_.length) {
- 335 :
changed = true;
- 336 :
} else {
- 337 :
for (let i = 0; i < active.length; i++) {
- 338 :
if (this.activeCues_.indexOf(active[i]) === -1) {
- 339 :
changed = true;
- 340 :
}
- 341 :
}
- 342 :
}
- 343 :
- 344 :
this.activeCues_ = active;
- 345 :
activeCues.setCues_(this.activeCues_);
- 346 :
- 347 :
return activeCues;
- 348 :
},
- 349 :
- 350 :
// /!\ Keep this setter empty (see the timeupdate handler above)
- 351 :
set() {}
- 352 :
}
- 353 :
});
- 354 :
- 355 :
if (settings.src) {
- 356 :
this.src = settings.src;
- 357 :
if (!this.preload_) {
- 358 :
// Tracks will load on-demand.
- 359 :
// Act like we're loaded for other purposes.
- 360 :
this.loaded_ = true;
- 361 :
}
- 362 :
if (this.preload_ || (settings.kind !== 'subtitles' && settings.kind !== 'captions')) {
- 363 :
loadTrack(this.src, this);
- 364 :
}
- 365 :
} else {
- 366 :
this.loaded_ = true;
- 367 :
}
- 368 :
}
- 369 :
- 370 :
startTracking() {
- 371 :
// More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback
- 372 :
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- 373 :
// Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el)
- 374 :
this.tech_.on('timeupdate', this.timeupdateHandler);
- 375 :
}
- 376 :
- 377 :
stopTracking() {
- 378 :
if (this.rvf_) {
- 379 :
this.tech_.cancelVideoFrameCallback(this.rvf_);
- 380 :
this.rvf_ = undefined;
- 381 :
}
- 382 :
this.tech_.off('timeupdate', this.timeupdateHandler);
- 383 :
}
- 384 :
- 385 :
/**
- 386 :
* Add a cue to the internal list of cues.
- 387 :
*
- 388 :
* @param {TextTrack~Cue} cue
- 389 :
* The cue to add to our internal list
- 390 :
*/
- 391 :
addCue(originalCue) {
- 392 :
let cue = originalCue;
- 393 :
- 394 :
if (window.vttjs && !(originalCue instanceof window.vttjs.VTTCue)) {
- 395 :
cue = new window.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text);
- 396 :
- 397 :
for (const prop in originalCue) {
- 398 :
if (!(prop in cue)) {
- 399 :
cue[prop] = originalCue[prop];
- 400 :
}
- 401 :
}
- 402 :
- 403 :
// make sure that `id` is copied over
- 404 :
cue.id = originalCue.id;
- 405 :
cue.originalCue_ = originalCue;
- 406 :
}
- 407 :
- 408 :
const tracks = this.tech_.textTracks();
- 409 :
- 410 :
for (let i = 0; i < tracks.length; i++) {
- 411 :
if (tracks[i] !== this) {
- 412 :
tracks[i].removeCue(cue);
- 413 :
}
- 414 :
}
- 415 :
- 416 :
this.cues_.push(cue);
- 417 :
this.cues.setCues_(this.cues_);
- 418 :
}
- 419 :
- 420 :
/**
- 421 :
* Remove a cue from our internal list
- 422 :
*
- 423 :
* @param {TextTrack~Cue} removeCue
- 424 :
* The cue to remove from our internal list
- 425 :
*/
- 426 :
removeCue(removeCue) {
- 427 :
let i = this.cues_.length;
- 428 :
- 429 :
while (i--) {
- 430 :
const cue = this.cues_[i];
- 431 :
- 432 :
if (cue === removeCue || (cue.originalCue_ && cue.originalCue_ === removeCue)) {
- 433 :
this.cues_.splice(i, 1);
- 434 :
this.cues.setCues_(this.cues_);
- 435 :
break;
- 436 :
}
- 437 :
}
- 438 :
}
- 439 :
}
- 440 :
- 441 :
/**
- 442 :
* cuechange - One or more cues in the track have become active or stopped being active.
- 443 :
*/
- 444 :
TextTrack.prototype.allowedEvents_ = {
- 445 :
cuechange: 'cuechange'
- 446 :
};
- 447 :
- 448 :
export default TextTrack;