/**
* @file text-track.js
*/
import TextTrackCueList from './text-track-cue-list' ;
import * as Fn from '../utils/fn.js' ;
import {TextTrackKind, TextTrackMode} from './track-enums' ;
import log from '../utils/log.js' ;
import window from 'global/window' ;
import Track from './track.js' ;
import { isCrossOrigin } from '../utils/url.js' ;
import XHR de '@videojs/xhr' ;
import merge from '../utils/merge-options' ;
/**
* Prend le contenu d'un fichier webvtt et l'analyse en signaux
*
* @param {string} srcContent
* contenu du fichier webVTT
*
* @param {TextTrack} track
* Piste de texte à laquelle ajouter des repères. Les indices proviennent du srcContent.
*
* @private
*/
const parseCues = function(srcContent, track) {
const parser = new window.WebVTT.Parser(
fenêtre,
window.vttjs,
window.WebVTT.StringDecoder()
) ;
const errors = [] ;
parser.oncue = function(cue) {
track.addCue(cue) ;
};
parser.onparsingerror = function(error) {
errors.push(error) ;
};
parser.onflush = function() {
track.trigger({
type : "loadeddata",
cible : piste
}) ;
};
parser.parse(srcContent) ;
if (errors.length > 0) {
if (window.console && window.console.groupCollapsed) {
window.console.groupCollapsed(`Text Track parsing errors for ${track.src}`) ;
}
errors.forEach((error) => log.error(error)) ;
if (window.console && window.console.groupEnd) {
window.console.groupEnd() ;
}
}
parser.flush() ;
};
/**
* Charge un `TextTrack` à partir d'une url spécifiée.
*
* @param {string} src
* Url de chargement de la piste.
*
* @param {TextTrack} track
* Piste à laquelle ajouter des repères. Provient du contenu à la fin de `url`.
*
* @private
*/
const loadTrack = function(src, track) {
const opts = {
uri : src
};
const crossOrigin = isCrossOrigin(src) ;
if (crossOrigin) {
opts.cors = crossOrigin ;
}
const withCredentials = track.tech_.crossOrigin() === 'use-credentials' ;
if (withCredentials) {
opts.withCredentials = withCredentials ;
}
XHR(opts, Fn.bind(this, function(err, response, responseBody) {
if (err) {
return log.error(err, response) ;
}
track.loaded_ = true ;
// S'assurer que vttjs a été chargé, sinon attendre qu'il ait fini de se charger
// NOTE : ceci n'est utilisé que pour la construction de alt/video.novtt.js
if (typeof window.WebVTT !== 'function') {
if (track.tech_) {
// pour empêcher l'utilisation avant l'erreur eslint, nous définissons loadHandler
// en tant que let ici
track.tech_.any(['vttjsloaded', 'vttjserror'], (event) => {
if (event.type === 'vttjserror') {
log.error(`vttjs failed to load, stopping trying to process ${track.src}`) ;
retour ;
}
return parseCues(responseBody, track) ;
}) ;
}
} else {
parseCues(responseBody, track) ;
}
})) ;
};
/**
* Une représentation d'une seule `TextTrack`.
*
* @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
* @extends Track
*/
class TextTrack extends Track {
/**
* Créer une instance de cette classe.
*
* @param {Objet} options={}
* Objet de noms et de valeurs d'options
*
* @param {Tech} options.tech
* Une référence au technicien qui possède ce TextTrack.
*
* @param {TextTrack~Kind} [options.kind='subtitles']
* Un type de texte valide.
*
* @param {TextTrack~Mode} [options.mode='disabled']
* Un mode de suivi de texte valide.
*
* @param {string} [options.id='vjs_track_' + Guid.newGUID()]
* Un identifiant unique pour ce TextTrack.
*
* @param {string} [options.label='']
* L'étiquette du menu pour cette piste.
*
* @param {string} [options.language='']
* Un code linguistique valide à deux caractères.
*
* @param {string} [options.srclang='']
* Un code linguistique valide à deux caractères. Une alternative, mais dépourvue de priorité
* version de `options.language`
*
* @param {string} [options.src]
* Une url vers les indices TextTrack.
*
* @param {boolean} [options.default]
* Si cette piste doit être activée ou désactivée par défaut.
*/
constructor(options = {}) {
if (!options.tech) {
throw new Error('Un tech n'a pas été fourni.') ;
}
const settings = merge(options, {
genre : TextTrackKind[options.kind] || 'subtitles',
language : options.language || options.srclang || ''
}) ;
let mode = TextTrackMode[settings.mode] || 'disabled' ;
const default_ = settings.default ;
if (settings.kind === 'metadata' || settings.kind === 'chapters') {
mode = "caché" ;
}
super(settings) ;
this.tech_ = settings.tech ;
this.cues_ = [] ;
this.activeCues_ = [] ;
this.preload_ = this.tech_.preloadTextTracks !== false ;
const cues = new TextTrackCueList(this.cues_) ;
const activeCues = new TextTrackCueList(this.activeCues_) ;
let changed = false ;
this.timeupdateHandler = Fn.bind(this, function(event = {})) {
if (this.tech_.isDisposed()) {
retour ;
}
if (!this.tech_.isReady_) {
if (event.type !== 'timeupdate') {
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler) ;
}
retour ;
}
// Accès à this.activeCues pour les effets secondaires de sa propre mise à jour
// en raison de sa nature de fonction getter. Ne pas supprimer ou les signaux seront
// arrêter la mise à jour !
// Utiliser le setter pour empêcher la suppression de l'uglify (règle des pure_getters)
this.activeCues = this.activeCues ;
if (changed) {
this.trigger('cuechange') ;
changed = false ;
}
if (event.type !== 'timeupdate') {
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler) ;
}
}) ;
const disposeHandler = () => {
this.stopTracking() ;
};
this.tech_.one('dispose', disposeHandler) ;
if (mode !== 'disabled') {
this.startTracking() ;
}
Object.defineProperties(this, {
/**
* @membre de TextTrack
* @member {boolean} default
* Si cette piste a été activée ou désactivée par défaut. Ne peut être modifié après
* création.
* @instance
*
* en lecture seule
*/
par défaut : {
get() {
retour default_ ;
},
set() {}
},
/**
* @membre de TextTrack
* @member {string} mode
* Définit le mode de ce TextTrack à un {@link TextTrack~Mode} valide. Volonté
* n'est pas activée si elle est réglée sur un mode non valide.
* @instance
*
* @fires TextTrack#modechange
*/
mode : {
get() {
mode de retour ;
},
set(newMode) {
if (!TextTrackMode[newMode]) {
retour ;
}
if (mode === newMode) {
retour ;
}
mode = newMode ;
if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
// Chargement à la demande.
loadTrack(this.src, this) ;
}
this.stopTracking() ;
if (mode !== 'disabled') {
this.startTracking() ;
}
/**
* Un événement qui se déclenche lorsque le mode change sur cette piste. Cela permet
* la TextTrackList qui contient cette piste pour agir en conséquence.
*
* > Remarque : Cela ne fait pas partie de la spécification !
*
* @event TextTrack#modechange
* @type {EventTarget~Event}
*/
this.trigger('modechange') ;
}
},
/**
* @membre de TextTrack
* @member {TextTrackCueList} cues
* La liste de repères de la piste de texte pour cette TextTrack.
* @instance
*/
cues : {
get() {
if (!this.loaded_) {
retourner null ;
}
les indices de retour ;
},
set() {}
},
/**
* @membre de TextTrack
* @member {TextTrackCueList} activeCues
* La liste des repères de la piste de texte qui sont actuellement actifs pour cette piste de texte.
* @instance
*/
activeCues : {
get() {
if (!this.loaded_) {
retourner null ;
}
// rien à faire
if (this.cues.length === 0) {
return activeCues ;
}
const ct = this.tech_.currentTime() ;
const active = [] ;
for (let i = 0, l = this.cues.length ; i < l ; i++) {
const cue = this.cues[i] ;
if (cue.startTime <= ct && cue.endTime >= ct) {
active.push(cue) ;
} else if (cue.startTime === cue.endTime &&
cue.startTime <= ct &&
cue.startTime + 0.5 >= ct) {
active.push(cue) ;
}
}
changed = false ;
if (active.length !== this.activeCues_.length) {
changé = vrai ;
} else {
for (let i = 0 ; i < active.length ; i++) {
if (this.activeCues_.indexOf(active[i]) === -1) {
changé = vrai ;
}
}
}
this.activeCues_ = active ;
activeCues.setCues_(this.activeCues_) ;
return activeCues ;
},
// /!\N- Garder ce setter vide (voir le gestionnaire timeupdate ci-dessus)
set() {}
}
}) ;
if (settings.src) {
this.src = settings.src ;
if (!this.preload_) {
// Les pistes seront chargées à la demande.
// Agir comme si nous étions chargés à d'autres fins.
this.loaded_ = true ;
}
if (this.preload_ || (settings.kind !== 'subtitles' && settings.kind !== 'captions')) {
loadTrack(this.src, this) ;
}
} else {
this.loaded_ = true ;
}
}
startTracking() {
// Indices plus précis basés sur requestVideoFrameCallback avec un fallback requestAnimationFram
this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler) ;
// Écouter également timeupdate au cas où rVFC/rAF s'arrêterait (fenêtre en arrière-plan, audio dans video el)
this.tech_.on('timeupdate', this.timeupdateHandler) ;
}
stopTracking() {
if (this.rvf_) {
this.tech_.cancelVideoFrameCallback(this.rvf_) ;
this.rvf_ = undefined ;
}
this.tech_.off('timeupdate', this.timeupdateHandler) ;
}
/**
* Ajouter un repère à la liste interne des repères.
*
* @param {TextTrack~Cue} cue
* Le signal à ajouter à notre liste interne
*/
addCue(originalCue) {
let cue = originalCue ;
if (window.vttjs && !(originalCue instanceof window.vttjs.VTTCue)) {
cue = new window.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text) ;
for (const prop in originalCue) {
if ( !(prop in cue)) {
cue[prop] = originalCue[prop] ;
}
}
// s'assurer que `id` est bien copié
cue.id = originalCue.id ;
cue.originalCue_ = originalCue ;
}
const tracks = this.tech_.textTracks() ;
for (let i = 0 ; i < tracks.length ; i++) {
if (tracks[i] !== this) {
tracks[i].removeCue(cue) ;
}
}
this.cues_.push(cue) ;
this.cues.setCues_(this.cues_) ;
}
/**
* Retirer un indice de notre liste interne
*
* @param {TextTrack~Cue} removeCue
* L'indice à retirer de notre liste interne
*/
removeCue(removeCue) {
let i = this.cues_.length ;
while (i--) {
const cue = this.cues_[i] ;
if (cue === removeCue || (cue.originalCue_ && cue.originalCue_ === removeCue)) {
this.cues_.splice(i, 1) ;
this.cues.setCues_(this.cues_) ;
pause ;
}
}
}
}
/**
* cuechange - Un ou plusieurs repères de la piste sont devenus actifs ou ont cessé de l'être.
*/
TextTrack.prototype.allowedEvents_ = {
cuechange : 'cuechange'
};
exporter la piste de texte par défaut ;