/**
 * @file tech.js
 */

import Component from '../component' ;
import mergeOptions from '../utils/merge-options.js' ;
import * as Fn from '../utils/fn.js' ;
import log from '../utils/log.js' ;
import { createTimeRange } from '../utils/time-ranges.js' ;
import { bufferedPercent } from '../utils/buffer.js' ;
import MediaError de '../media-error.js' ;
import window from 'global/window' ;
import document from 'global/document' ;
import {isPlain} from '../utils/obj' ;
import * as TRACK_TYPES from '../tracks/track-types' ;
import {toTitleCase, toLowerCase} de '../utils/string-cases.js' ;
import vtt from 'videojs-vtt.js' ;
import * as Guid from '../utils/guid.js' ;

/**
 * Un objet contenant une structure comme : `{src : 'url', type : 'mimetype'}` ou une chaîne de caractères
 * qui ne contient que l'url src.
 * * `var SourceObject = {src : 'http://ex.com/video.mp4', type : 'video/mp4'};``
   * `var SourceString = 'http://example.com/some-video.mp4';`
 *
 * @typedef {Objet|chaîne} Tech~SourceObject
 *
 * @property {string} src
 *           L'url de la source
 *
 * @property {string} type
 *           Le type de mime de la source
 */

/**
 * Une fonction utilisée par {@link Tech} pour créer un nouveau {@link TextTrack}.
 *
 * @private
 *
 * @param {Tech} self
 *        Une instance de la classe Tech.
 *
 * @param {string} kind
 *        type de `TextTrack` (sous-titres, légendes, descriptions, chapitres ou métadonnées)
 *
 * @param {string} [label]
 *        Étiquette pour identifier la piste de texte
 *
 * @param {string} [langue]
 *        Abréviation linguistique à deux lettres
 *
 * @param {Objet} [options={}]
 *        Un objet avec des options supplémentaires de suivi de texte
 *
 * @return {TextTrack}
 *          La piste de texte qui a été créée.
 */
function createTrackHelper(self, kind, label, language, options = {}) {
  const tracks = self.textTracks() ;

  options.kind = kind ;

  if (label) {
    options.label = label ;
  }
  if (language) {
    options.language = language ;
  }
  options.tech = self ;

  const track = new TRACK_TYPES.ALL.text.TrackClass(options) ;

  tracks.addTrack(track) ;

  piste de retour ;
}

/**
 * Il s'agit de la classe de base pour les contrôleurs de la technologie de lecture des médias, tels que
 * {@link HTML5}
 *
 * @extends Component
 */
class Tech extends Component {

  /**
  * Créer une instance de ce Tech.
  *
  * @param {Objet} [options]
  *        La mémoire clé/valeur des options du lecteur.
  *
  * @param {Component~ReadyCallback} ready
  *        Fonction de rappel à appeler lorsque le Tech `HTML5` est prêt.
  */
  constructor(options = {}, ready = function() {}) {
    // nous ne voulons pas que la technologie signale automatiquement l'activité de l'utilisateur.
    // Ceci est fait manuellement dans addControlsListeners
    options.reportTouchActivity = false ;
    super(null, options, ready) ;

    this.onDurationChange_ = (e) => this.onDurationChange(e) ;
    this.trackProgress_ = (e) => this.trackProgress(e) ;
    this.trackCurrentTime_ = (e) => this.trackCurrentTime(e) ;
    this.stopTrackingCurrentTime_ = (e) => this.stopTrackingCurrentTime(e) ;
    this.disposeSourceHandler_ = (e) => this.disposeSourceHandler(e) ;

    this.queuedHanders_ = new Set() ;

    // vérifier si la source actuelle a été jouée ou non pour
    // mettre en œuvre une fonction jouée() très limitée
    this.hasStarted_ = false ;
    this.on('playing', function() {
      this.hasStarted_ = true ;
    }) ;
    this.on('loadstart', function() {
      this.hasStarted_ = false ;
    }) ;

    TRACK_TYPES.ALL.names.forEach((name) => {
      const props = TRACK_TYPES.ALL[name] ;

      if (options && options[props.getterName]) {
        this[props.privateName] = options[props.getterName] ;
      }
    }) ;

    // Suivre manuellement la progression dans les cas où le navigateur/la technologie ne la signale pas.
    if (!this.featuresProgressEvents) {
      this.manualProgressOn() ;
    }

    // Suivre manuellement les mises à jour temporelles dans les cas où le navigateur/la technologie ne les signale pas.
    if (!this.featuresTimeupdateEvents) {
      this.manualTimeUpdatesOn() ;
    }

    ['Text', 'Audio', 'Video'].forEach((track) => {
      if (options[`native${track}Tracks`] === false) {
        this[`featuresNative${track}Tracks`] = false ;
      }
    }) ;

    if (options.nativeCaptions === false || options.nativeTextTracks === false) {
      this.featuresNativeTextTracks = false ;
    } else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
      this.featuresNativeTextTracks = true ;
    }

    if (!this.featuresNativeTextTracks) {
      this.emulateTextTracks() ;
    }

    this.preloadTextTracks = options.preloadTextTracks !== false ;

    this.autoRemoteTextTracks_ = new TRACK_TYPES.ALL.text.ListClass() ;

    this.initTrackListeners() ;

    // Activation des événements de tapotement des composants uniquement si l'on n'utilise pas de contrôles natifs
    if (!options.nativeControlsForTouch) {
      this.emitTapEvents() ;
    }

    if (this.constructor) {
      this.name_ = this.constructor.name || 'Unknown Tech' ;
    }
  }

  /**
   * Une fonction spéciale pour déclencher le jeu de sources d'une manière qui permette au lecteur d'avoir accès à des informations sur les activités de l'entreprise
   * pour se redéclencher si le joueur ou la technologie ne sont pas encore prêts.
   *
   * @fires Tech#sourceset
   * @param {string} src La chaîne source au moment de la modification de la source.
   */
  triggerSourceset(src) {
    if (!this.isReady_) {
      // lors de la préparation initiale, nous devons déclencher l'ensemble des sources
      // 1ms après ready pour que le joueur puisse l'attendre.
      this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1)) ;
    }

    /**
     * Déclenché lorsque la source est définie sur le tech à l'origine de l'élément de média
     * pour recharger.
     *
     * @see {@link Player#event:sourceset}
     * @event Tech#sourceset
     * @type {EventTarget~Event}
     */
    this.trigger({
      src,
      type : "sourceset" (jeu de sources)
    }) ;
  }

  /* Retours pour les types d'événements non pris en charge
  ================================================================================ */

  /**
   * Polyfill l'événement `progress` pour les navigateurs qui ne le supportent pas nativement.
   *
   * @see {@link Tech#trackProgress}
   */
  manualProgressOn() {
    this.on('durationchange', this.onDurationChange_) ;

    this.manualProgress = true ;

    // Déclencher l'observation de la progression lorsqu'une source commence à se charger
    this.one('ready', this.trackProgress_) ;
  }

  /**
   * Désactiver le polyfill pour les événements `progress` qui a été créé dans
   * {@link Tech#manualProgressOn}
   */
  manualProgressOff() {
    this.manualProgress = false ;
    this.stopTrackingProgress() ;

    this.off('durationchange', this.onDurationChange_) ;
  }

  /**
   * Il est utilisé pour déclencher un événement `progress` lorsque le pourcentage mis en mémoire tampon change. Il
   * définit une fonction d'intervalle qui sera appelée toutes les 500 millisecondes pour vérifier si la fonction
   * le pourcentage final du tampon a changé.
   *
   * > Cette fonction est appelée par {@link Tech#manualProgressOn}
   *
   * @param {EventTarget~Event} event
   *        L'événement `ready` qui a provoqué l'exécution de cette opération.
   *
   * @listens Tech#ready
   * @fires Tech#progress
   */
  trackProgress(event) {
    this.stopTrackingProgress() ;
    this.progressInterval = this.setInterval(Fn.bind(this, function() {
      // Ne se déclenche pas si la quantité mise en mémoire tampon est supérieure à la dernière fois

      const numBufferedPercent = this.bufferedPercent() ;

      if (this.bufferedPercent_ !== numBufferedPercent) {
        /**
         * Voir {@link Player#progress}
         *
         * @event Tech#progress
         * @type {EventTarget~Event}
         */
        this.trigger('progress') ;
      }

      this.bufferedPercent_ = numBufferedPercent ;

      if (numBufferedPercent === 1) {
        this.stopTrackingProgress() ;
      }
    }), 500) ;
  }

  /**
   * Mettre à jour notre durée interne sur un événement `durationchange` en appelant
   * {@link Tech#duration}.
   *
   * @param {EventTarget~Event} event
   *        L'événement `durationchange` qui a provoqué l'exécution de cette opération.
   *
   * @listens Tech#durationchange
   */
  onDurationChange(event) {
    this.duration_ = this.duration() ;
  }

  /**
   * Récupère et crée un objet `TimeRange` pour la mise en mémoire tampon.
   *
   * @return {TimeRange}
   *         L'objet de plage horaire qui a été créé.
   */
  buffered() {
    return createTimeRange(0, 0) ;
  }

  /**
   * Obtenir le pourcentage de la vidéo en cours qui est actuellement mis en mémoire tampon.
   *
   * @return {number}
   *         Un nombre compris entre 0 et 1 qui représente le pourcentage décimal de la
   *         vidéo qui est mise en mémoire tampon.
   *
   */
  bufferedPercent() {
    return bufferedPercent(this.buffered(), this.duration_) ;
  }

  /**
   * Désactiver le polyfill pour les événements `progress` qui a été créé dans
   * {@link Tech#manualProgressOn}
   * Arrêtez de suivre manuellement les événements de progression en supprimant l'intervalle défini dans la section
   * {@link Tech#trackProgress}.
   */
  stopTrackingProgress() {
    this.clearInterval(this.progressInterval) ;
  }

  /**
   * Polyfill l'événement `timeupdate` pour les navigateurs qui ne le supportent pas.
   *
   * @see {@link Tech#trackCurrentTime}
   */
  manualTimeUpdatesOn() {
    this.manualTimeUpdates = true ;

    this.on('play', this.trackCurrentTime_) ;
    this.on('pause', this.stopTrackingCurrentTime_) ;
  }

  /**
   * Désactiver le polyfill pour les événements `timeupdate` qui a été créé en
   * {@link Tech#manualTimeUpdatesOn}
   */
  manualTimeUpdatesOff() {
    this.manualTimeUpdates = false ;
    this.stopTrackingCurrentTime() ;
    this.off('play', this.trackCurrentTime_) ;
    this.off('pause', this.stopTrackingCurrentTime_) ;
  }

  /**
   * Met en place une fonction d'intervalle pour suivre l'heure actuelle et déclencher `timeupdate` chaque fois que
   * 250 millisecondes.
   *
   * @listens Tech#play
   * @triggers Tech#timeupdate
   */
  trackCurrentTime() {
    if (this.currentTimeInterval) {
      this.stopTrackingCurrentTime() ;
    }
    this.currentTimeInterval = this.setInterval(function() {
      /**
       * Déclenché à un intervalle de 250 ms pour indiquer que le temps passe dans la vidéo.
       *
       * @event Tech#timeupdate
       * @type {EventTarget~Event}
       */
      this.trigger({ type : 'timeupdate', target : this, manuallyTriggered : true }) ;

    // 42 = 24 fps // 250 est ce que Webkit utilise // FF utilise 15
    }, 250) ;
  }

  /**
   * Arrêter la fonction d'intervalle créée dans {@link Tech#trackCurrentTime} pour que la fonction
   * l'événement `timeupdate` n'est plus déclenché.
   *
   * @listens {Tech#pause}
   */
  stopTrackingCurrentTime() {
    this.clearInterval(this.currentTimeInterval) ;

    // #1002 - si la vidéo se termine juste avant la prochaine mise à jour temporelle,
    // la barre de progression ne se rendra pas jusqu'à la fin
    this.trigger({ type : 'timeupdate', target : this, manuallyTriggered : true }) ;
  }

  /**
   * Désactive tous les polyfills d'événements, efface les `Tech`s {@link AudioTrackList},
   * {@link VideoTrackList}, et {@link TextTrackList}, et de disposer de cette Tech.
   *
   * @fires Composant#dispose
   */
  dispose() {

    // nettoyer toutes les pistes car nous ne pouvons pas les réutiliser entre les techniciens
    this.clearTracks(TRACK_TYPES.NORMAL.names) ;

    // Désactiver tout suivi manuel de la progression ou de la mise à jour de l'heure
    if (this.manualProgress) {
      this.manualProgressOff() ;
    }

    if (this.manualTimeUpdates) {
      this.manualTimeUpdatesOff() ;
    }

    super.dispose() ;
  }

  /**
   * Efface une seule `TrackList` ou un tableau de `TrackLists` avec leurs noms.
   *
   * > Remarque : Les techniciens qui n'ont pas de gestionnaire de sources devraient appeler cette fonction entre les sources pour `video`
   *         & pistes `audio`. Vous ne voulez pas les utiliser entre les pistes !
   *
   * @param {string[]|string} types
   *        Noms des listes de pistes à effacer, les noms valides sont `video`, `audio`, et
   *        `text`.
   */
  clearTracks(types) {
    types = [].concat(types) ;
    // nettoyer toutes les pistes car nous ne pouvons pas les réutiliser entre les techniciens
    types.forEach((type) => {
      const list = this[`${type}Tracks`]() || [] ;
      let i = list.length ;

      while (i--) {
        const track = list[i] ;

        if (type === 'text') {
          this.removeRemoteTextTrack(track) ;
        }
        list.removeTrack(track) ;
      }
    }) ;
  }

  /**
   * Supprimer tous les TextTracks ajoutés via addRemoteTextTrack qui sont
   * marqués pour le ramassage automatique des ordures
   */
  cleanupAutoTextTracks() {
    const list = this.autoRemoteTextTracks_ || [] ;
    let i = list.length ;

    while (i--) {
      const track = list[i] ;

      this.removeRemoteTextTrack(track) ;
    }
  }

  /**
   * Réinitialiser le tech, ce qui supprime toutes les sources et réinitialise l'état de préparation interne.
   *
   * @abstract
   */
  reset() {}

  /**
   * Obtenir la valeur de `crossOrigin` du tech.
   *
   * @abstract
   *
   * @see {Html5#crossOrigin}
   */
  crossOrigin() {}

  /**
   * Fixe la valeur de `crossOrigin` sur le tech.
   *
   * @abstract
   *
   * @param {string} crossOrigin la valeur de crossOrigin
   * @see {Html5#setCrossOrigin}
   */
  setCrossOrigin() {}

  /**
   * Obtenir ou définir une erreur sur le Tech.
   *
   * @param {MediaError} [err]
   *        Erreur à régler sur le Tech
   *
   * @return {MediaError|null}
   *         L'objet d'erreur actuel sur le tech, ou null s'il n'y en a pas.
   */
  error(err) {
    if (err !== undefined) {
      this.error_ = new MediaError(err) ;
      this.trigger('error') ;
    }
    return this.error_ ;
  }

  /**
   * Renvoie les `TimeRange`s qui ont été joués pour la source actuelle.
   *
   * > NOTE : Cette mise en œuvre est incomplète. Il ne suit pas la `TimeRange` jouée.
   *         Il vérifie uniquement si la source a été jouée ou non.
   *
   * @return {TimeRange}
   *         - Une plage de temps unique si la vidéo a été lue
   *         - Un ensemble vide de plages si ce n'est pas le cas.
   */
  played() {
    if (this.hasStarted_) {
      return createTimeRange(0, 0) ;
    }
    return createTimeRange() ;
  }

  /**
   * Démarrer la lecture
   *
   * @abstract
   *
   * @voir {Html5#play}
   */
  play() {}

  /**
   * Définir si nous nettoyons ou non
   *
   * @abstract
   *
   * @see {Html5#setScrubbing}
   */
  setScrubbing() {}

  /**
   * Obtenir si l'on frotte ou non
   *
   * @abstract
   *
   * @see {Html5#scrubbing}
   */
  scrubbing() {}

  /**
   * Provoque une mise à jour manuelle de l'heure si {@link Tech#manualTimeUpdatesOn} était
   * appelé précédemment.
   *
   * @fires Tech#timeupdate
   */
  setCurrentTime() {
    // améliorer la précision des mises à jour manuelles des heures de travail
    if (this.manualTimeUpdates) {
      /**
       * Un événement manuel `timeupdate`.
       *
       * @event Tech#timeupdate
       * @type {EventTarget~Event}
       */
      this.trigger({ type : 'timeupdate', target : this, manuallyTriggered : true }) ;
    }
  }

  /**
   * Activer les récepteurs pour {@link VideoTrackList}, {@link {AudioTrackList}, et
   * {@link TextTrackList} events.
   *
   * Cela ajoute {@link EventTarget~EventListeners} pour `addtrack`, et `removetrack`.
   *
   * @fires Tech#audiotrackchange
   * @fires Tech#videotrackchange
   * @fires Tech#texttrackchange
   */
  initTrackListeners() {
    /**
      * Déclenché lorsque des pistes sont ajoutées ou supprimées sur la Tech {@link AudioTrackList}
      *
      * @event Tech#audiotrackchange
      * @type {EventTarget~Event}
      */

    /**
      * Déclenché lorsque des pistes sont ajoutées ou supprimées dans la Tech {@link VideoTrackList}
      *
      * @event Tech#videotrackchange
      * @type {EventTarget~Event}
      */

    /**
      * Déclenché lorsque des pistes sont ajoutées ou supprimées dans la Tech {@link TextTrackList}
      *
      * @event Tech#texttrackchange
      * @type {EventTarget~Event}
      */
    TRACK_TYPES.NORMAL.names.forEach((name) => {
      const props = TRACK_TYPES.NORMAL[name] ;
      const trackListChanges = () => {
        this.trigger(`${nom}changement de piste`) ;
      };

      const tracks = this[props.getterName]() ;

      tracks.addEventListener('removetrack', trackListChanges) ;
      tracks.addEventListener('addtrack', trackListChanges) ;

      this.on('dispose', () => {
        tracks.removeEventListener('removetrack', trackListChanges) ;
        tracks.removeEventListener('addtrack', trackListChanges) ;
      }) ;
    }) ;
  }

  /**
   * Emuler les TextTracks en utilisant vtt.js si nécessaire
   *
   * @fires Tech#vttjsloaded
   * @fires Tech#vttjserror
   */
  addWebVttScript_() {
    if (window.WebVTT) {
      retour ;
    }

    // Initialement, Tech.el_ est un enfant d'un dummy-div attendre jusqu'à ce que le système Component
    // signale que la technologie est prête et que Tech.el_ fait partie du DOM
    // avant d'insérer le script WebVTT
    if (document.body.contains(this.el()))) {

      // chargement via require si disponible et si l'emplacement du script vtt.js n'a pas été transmis
      // Les constructions novtt transformeront l'appel à require ci-dessus en un objet vide
      // ce qui fera que cette vérification échouera toujours.
      if (!this.options_['vtt.js'] && isPlain(vtt) && Object.keys(vtt).length > 0) {
        this.trigger('vttjsloaded') ;
        retour ;
      }

      // charger vtt.js via l'option d'emplacement du script ou le cdn si aucun emplacement n'a été défini
      // transmis
      const script = document.createElement('script') ;

      script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js' ;
      script.onload = () => {
        /**
         * Déclenché lorsque le fichier vtt.js est chargé.
         *
         * @event Tech#vttjsloaded
         * @type {EventTarget~Event}
         */
        this.trigger('vttjsloaded') ;
      };
      script.onerror = () => {
        /**
         * Déclenché lorsque vtt.js n'a pas été chargé en raison d'une erreur
         *
         * @event Tech#vttjsloaded
         * @type {EventTarget~Event}
         */
        this.trigger('vttjserror') ;
      };
      this.on('dispose', () => {
        script.onload = null ;
        script.onerror = null ;
      }) ;
      // mais n'ont pas encore été chargées et nous les fixons à true avant l'injection de sorte que
      // nous n'écrasons pas le window.WebVTT injecté s'il se charge immédiatement
      window.WebVTT = true ;
      this.el().parentNode.appendChild(script) ;
    } else {
      this.ready(this.addWebVttScript_) ;
    }

  }

  /**
   * Emuler des pistes de texte
   *
   */
  emulateTextTracks() {
    const tracks = this.textTracks() ;
    const remoteTracks = this.remoteTextTracks() ;
    const handleAddTrack = (e) => tracks.addTrack(e.track) ;
    const handleRemoveTrack = (e) => tracks.removeTrack(e.track) ;

    remoteTracks.on('addtrack', handleAddTrack) ;
    remoteTracks.on('removetrack', handleRemoveTrack) ;

    this.addWebVttScript_() ;

    const updateDisplay = () => this.trigger('texttrackchange') ;

    const textTracksChanges = () => {
      updateDisplay() ;

      for (let i = 0 ; i < tracks.length ; i++) {
        const track = tracks[i] ;

        track.removeEventListener('cuechange', updateDisplay) ;
        if (track.mode === 'showing') {
          track.addEventListener('cuechange', updateDisplay) ;
        }
      }
    };

    textTracksChanges() ;
    tracks.addEventListener('change', textTracksChanges) ;
    tracks.addEventListener('addtrack', textTracksChanges) ;
    tracks.addEventListener('removetrack', textTracksChanges) ;

    this.on('dispose', function() {
      remoteTracks.off('addtrack', handleAddTrack) ;
      remoteTracks.off('removetrack', handleRemoveTrack) ;
      tracks.removeEventListener('change', textTracksChanges) ;
      tracks.removeEventListener('addtrack', textTracksChanges) ;
      tracks.removeEventListener('removetrack', textTracksChanges) ;

      for (let i = 0 ; i < tracks.length ; i++) {
        const track = tracks[i] ;

        track.removeEventListener('cuechange', updateDisplay) ;
      }
    }) ;
  }

  /**
   * Crée et renvoie un objet {@link TextTrack} distant.
   *
   * @param {string} kind
   *        type de `TextTrack` (sous-titres, légendes, descriptions, chapitres ou métadonnées)
   *
   * @param {string} [label]
   *        Étiquette pour identifier la piste de texte
   *
   * @param {string} [langue]
   *        Abréviation linguistique à deux lettres
   *
   * @return {TextTrack}
   *         La piste de texte qui est créée.
   */
  addTextTrack(kind, label, language) {
    if (!kind) {
      lancer une nouvelle erreur ('TextTrack kind is required but was not provided') ;
    }

    return createTrackHelper(this, kind, label, language) ;
  }

  /**
   * Créer un TextTrack émulé à utiliser par addRemoteTextTrack
   *
   * Elle est destinée à être surchargée par les classes qui héritent de
   * Tech afin de créer des TextTracks natifs ou personnalisés.
   *
   * @param {Objet} options
   *        L'objet doit contenir les options permettant d'initialiser la piste de texte.
   *
   * @param {string} [options.kind]
   *        type de `TextTrack` (sous-titres, légendes, descriptions, chapitres ou métadonnées).
   *
   * @param {string} [options.label].
   *        Étiquette pour identifier la piste de texte
   *
   * @param {string} [options.language]
   *        Abréviation linguistique de deux lettres.
   *
   * @return {HTMLTrackElement}
   *         L'élément de piste qui est créé.
   */
  createRemoteTextTrack(options) {
    const track = mergeOptions(options, {
      tech : this
    }) ;

    return new TRACK_TYPES.REMOTE.remoteTextEl.TrackClass(track) ;
  }

  /**
   * Crée un objet de piste de texte distant et renvoie un élément de piste html.
   *
   * > Remarque : Il peut s'agir d'un {@link HTMLTrackElement} émulé ou natif.
   *
   * @param {Objet} options
   *        Voir {@link Tech#createRemoteTextTrack} pour des propriétés plus détaillées.
   *
   * @param {boolean} [manualCleanup=true]
   *        - Si false : le TextTrack sera automatiquement supprimé de la vidéo
   *          chaque fois que la source change
   *        - Quand c'est vrai : Le TextTrack devra être nettoyé manuellement
   *
   * @return {HTMLTrackElement}
   *         Un élément de piste Html.
   *
   * @deprecated La fonctionnalité par défaut de cette fonction sera équivalente
   *             à "manualCleanup=false" à l'avenir. Le paramètre manualCleanup (nettoyage manuel)
   *             sont également supprimés.
   */
  addRemoteTextTrack(options = {}, manualCleanup) {
    const htmlTrackElement = this.createRemoteTextTrack(options) ;

    if (manualCleanup !== true && manualCleanup !== false) {
      // avertissement de dépréciation
      log.warn('Calling addRemoteTextTrack without explicitly setting the "manualCleanup" parameter to `true` is deprecated and default to `false` in future version of video.js') ;
      manualCleanup = true ;
    }

    // enregistrer HTMLTrackElement et TextTrack dans la liste distante
    this.remoteTextTrackEls().addTrackElement_(htmlTrackElement) ;
    this.remoteTextTracks().addTrack(htmlTrackElement.track) ;

    if (manualCleanup !== true) {
      // créer la TextTrackList si elle n'existe pas
      this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track)) ;
    }

    return htmlTrackElement ;
  }

  /**
   * Supprime une piste de texte distante de la `TextTrackList` distante.
   *
   * @param {TextTrack} track
   *        `TextTrack` à retirer de la `TextTrackList`
   */
  removeRemoteTextTrack(track) {
    const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track) ;

    // supprimer HTMLTrackElement et TextTrack de la liste distante
    this.remoteTextTrackEls().removeTrackElement_(trackElement) ;
    this.remoteTextTracks().removeTrack(track) ;
    this.autoRemoteTextTracks_.removeTrack(track) ;
  }

  /**
   * Obtient les mesures de qualité de lecture des médias disponibles, telles que spécifiées par la norme Media
   * Qualité de lecture API.
   *
   * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
   *
   * @return {Object}
   *         Un objet contenant les mesures de qualité de lecture des médias prises en charge
   *
   * @abstract
   */
  getVideoPlaybackQuality() {
    return {} ;
  }

  /**
   * Tentative de création d'une fenêtre vidéo flottante toujours au-dessus d'autres fenêtres
   * afin que les utilisateurs puissent continuer à consommer des médias tout en interagissant avec d'autres utilisateurs
   * des sites de contenu ou des applications sur leur appareil.
   *
   * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
   *
   * @return {Promesse|non défini}
   *         Une promesse avec une fenêtre d'image dans l'image si le navigateur le permet
   *         Promesses (ou l'une d'entre elles a été transmise en tant qu'option). Il renvoie une valeur non définie
   *         autrement.
   *
   * @abstract
   */
  requestPictureInPicture() {
    const PromiseClass = this.options_.Promise || window.Promise ;

    if (PromiseClass) {
      return PromiseClass.reject() ;
    }
  }

  /**
   * Méthode permettant de vérifier la valeur de la propriété "disablePictureInPicture" <video> .
   * La valeur par défaut est true, car elle doit être considérée comme désactivée si le technicien ne prend pas en charge les tuyaux
   *
   * @abstract
   */
  disablePictureInPicture() {
    retourner vrai ;
  }

  /**
   * Méthode permettant de définir ou d'annuler la propriété "disablePictureInPicture" de la vidéo <> .
   *
   * @abstract
   */
  setDisablePictureInPicture() {}

  /**
   * Une implémentation de secours de requestVideoFrameCallback utilisant requestAnimationFrame
   *
   * @param {fonction} cb
   * @return {number} request id
   */
  requestVideoFrameCallback(cb) {
    const id = Guid.newGUID() ;

    if (!this.isReady_ || this.paused()) {
      this.queuedHanders_.add(id) ;
      this.one('playing', () => {
        if (this.queuedHanders_.has(id)) {
          this.queuedHanders_.delete(id) ;
          cb() ;
        }
      }) ;
    } else {
      this.requestNamedAnimationFrame(id, cb) ;
    }

    retourner l'id ;
  }

  /**
   * Une implémentation de secours de cancelVideoFrameCallback
   *
   * @param {number} id id du rappel à annuler
   */
  cancelVideoFrameCallback(id) {
    if (this.queuedHanders_.has(id)) {
      this.queuedHanders_.delete(id) ;
    } else {
      this.cancelNamedAnimationFrame(id) ;
    }
  }

  /**
   * Une méthode pour créer un poster à partir d'un `Tech`.
   *
   * @abstract
   */
  setPoster() {}

  /**
   * Méthode permettant de vérifier la présence de l'attribut "playsinline" <video> .
   *
   * @abstract
   */
  playsinline() {}

  /**
   * Méthode permettant d'activer ou de désactiver l'attribut "playsinline" de la vidéo <> .
   *
   * @abstract
   */
  setPlaysinline() {}

  /**
   * Tentative de forcer le remplacement des pistes audio natives.
   *
   * @param {boolean} override - Si la valeur est fixée à true, l'audio natif sera remplacé,
   * sinon le son natif sera potentiellement utilisé.
   *
   * @abstract
   */
  overrideNativeAudioTracks() {}

  /**
   * Tentative de forcer le remplacement des pistes vidéo natives.
   *
   * @param {boolean} override - Si la valeur est fixée à true, la vidéo native sera remplacée,
   * dans le cas contraire, la vidéo native sera potentiellement utilisée.
   *
   * @abstract
   */
  overrideNativeVideoTracks() {}

  /*
   * Vérifie si le technicien peut prendre en charge le type de message donné.
   *
   * La technologie de base ne prend en charge aucun type, mais les gestionnaires de sources peuvent le faire
   * l'écraser.
   *
   * @param {string} type
   *         Le type d'image à vérifier pour la prise en charge
   *
   * @return {string}
   *         "probablement", "peut-être" ou chaîne vide
   *
   * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
   *
   * @abstract
   */
  canPlayType() {
    retourner '' ;
  }

  /**
   * Vérifier si le type est pris en charge par ce tech.
   *
   * La technologie de base ne prend en charge aucun type, mais les gestionnaires de sources peuvent le faire
   * l'écraser.
   *
   * @param {string} type
   *        Le type de support à vérifier
   * @return {string} Renvoie la réponse de l'élément vidéo natif
   */
  static canPlayType() {
    retourner '' ;
  }

  /**
   * Vérifier si le technicien peut prendre en charge la source donnée
   *
   * @param {Objet} srcObj
   *        L'objet source
   * @param {Objet} options
   *        Les options transmises au tech
   * @return {string} 'probablement', 'peut-être', ou '' (chaîne vide)
   */
  static canPlaySource(srcObj, options) {
    return Tech.canPlayType(srcObj.type) ;
  }

  /*
   * Retourne si l'argument est un Tech ou non.
   * On peut lui passer une classe comme `Html5` ou une instance comme `player.tech_`
   *
   * @param {Object} component
   *        L'élément à vérifier
   *
   * @return {boolean}
   *         Qu'il s'agisse d'une technologie ou non
   *         - Vrai s'il s'agit d'une technologie
   *         - Faux si ce n'est pas le cas
   */
  static isTech(component) {
    return component.prototype instanceof Tech ||
           composant instanceof Tech ||
           composant === Tech ;
  }

  /**
   * Enregistre une `Technologie` dans une liste partagée pour videojs.
   *
   * @param {string} name
   *        Nom de la "technologie" à enregistrer.
   *
   * @param {Objet} tech
   *        La classe `Tech` à inscrire.
   */
  static registerTech(name, tech) {
    if (!Tech.techs_) {
      Tech.techs_ = {} ;
    }

    if (!Tech.isTech(tech)) {
      lancer une nouvelle erreur (`Tech ${name} doit être un Tech`) ;
    }

    if (!Tech.canPlayType) {
      throw new Error('Les techniciens doivent avoir une méthode statique canPlayType sur eux') ;
    }
    if (!Tech.canPlaySource) {
      lancer une nouvelle erreur ('Les techniciens doivent avoir une méthode statique canPlaySource sur eux') ;
    }

    nom = toTitleCase(nom) ;

    Tech.techs_[name] = tech ;
    Tech.techs_[toLowerCase(name)] = tech ;
    if (name !== 'Tech') {
      // camel case le techName à utiliser dans techOrder
      Tech.defaultTechOrder_.push(name) ;
    }
    retour tech ;
  }

  /**
   * Obtenir un `Technicien` de la liste partagée par son nom.
   *
   * @param {string} name
   *        `camelCase` ou `TitleCase` nom de la Tech à obtenir
   *
   * @return {Tech|undefined}
   *         Le `Tech` ou undefined s'il n'y a pas de tech avec le nom demandé.
   */
  static getTech(name) {
    if (!name) {
      retour ;
    }

    if (Tech.techs_ && Tech.techs_[name]) {
      return Tech.techs_[nom] ;
    }

    nom = toTitleCase(nom) ;

    if (window && window.videojs && window.videojs[name]) {
      log.warn(`La technologie ${nom} a été ajoutée à l'objet videojs alors qu'elle devrait être enregistrée à l'aide de videojs.registerTech(nom, tech)`) ;
      return window.videojs[name] ;
    }
  }
}

/**
 * Obtenir la {@link VideoTrackList}
 *
 * @returns {VideoTrackList}
 * @method Tech.prototype.videoTracks
 */

/**
 * Obtenir la {@link AudioTrackList}
 *
 * @returns {AudioTrackList}
 * @method Tech.prototype.audioTracks
 */

/**
 * Obtenir la {@link TextTrackList}
 *
 * @returns {TextTrackList}
 * @method Tech.prototype.textTracks
 */

/**
 * Obtenir l'élément distant {@link TextTrackList}
 *
 * @returns {TextTrackList}
 * @méthode Tech.prototype.remoteTextTracks
 */

/**
 * Obtenir l'élément distant {@link HtmlTrackElementList}
 *
 * @returns {HtmlTrackElementList}
 * @method Tech.prototype.remoteTextTrackEls
 */

TRACK_TYPES.ALL.names.forEach(function(name) {
  const props = TRACK_TYPES.ALL[name] ;

  Tech.prototype[props.getterName] = function() {
    this[props.privateName] = this[props.privateName] || new props.ListClass() ;
    return this[props.privateName] ;
  };
}) ;

/**
 * Liste des pistes de texte associées
 *
 * @type {TextTrackList}
 * @private
 * @property Tech#textTracks_
 */

/**
 * Liste des pistes audio associées.
 *
 * @type {AudioTrackList}
 * @private
 * @property Tech#audioTracks_
 */

/**
 * Liste des pistes vidéo associées.
 *
 * @type {VideoTrackList}
 * @private
 * @property Tech#videoTracks_
 */

/**
 * Booléen indiquant si la `Tech` prend en charge le contrôle du volume.
 *
 * @type {boolean}
 * @défaut
 */
Tech.prototype.featuresVolumeControl = true ;

/**
 * Booléen indiquant si la `Tech` supporte le volume muting.
 *
 * @type {bolean}
 * @défaut
 */
Tech.prototype.featuresMuteControl = true ;

/**
 * Booléen indiquant si le `Tech` supporte le contrôle de redimensionnement plein écran.
 * Le redimensionnement des plugins à l'aide de la requête fullscreen recharge le plugin
 *
 * @type {boolean}
 * @défaut
 */
Tech.prototype.featuresFullscreenResize = false ;

/**
 * Booléen indiquant si la `Tech` prend en charge la modification de la vitesse à laquelle la vidéo est diffusée
 * joue. Exemples :
 *   - Régler le lecteur pour qu'il joue 2x (deux fois) plus vite
 *   - Régler le lecteur pour qu'il joue 0,5x (moitié) plus vite
 *
 * @type {boolean}
 * @défaut
 */
Tech.prototype.featuresPlaybackRate = false ;

/**
 * Booléen indiquant si la `Tech` supporte l'événement `progress`. Il s'agit actuellement
 * non déclenché par video-js-swf. Cela permettra de déterminer si
 * {@link Tech#manualProgressOn} doit être appelé.
 *
 * @type {boolean}
 * @défaut
 */
Tech.prototype.featuresProgressEvents = false ;

/**
 * Booléen indiquant si la `Tech` supporte l'événement `sourceset`.
 *
 * Un technicien doit mettre cette valeur à `true` et utiliser {@link Tech#triggerSourceset}
 * pour déclencher un {@link Tech#event:sourceset} au plus tôt après avoir obtenu
 * une nouvelle source.
 *
 * @type {boolean}
 * @défaut
 */
Tech.prototype.featuresSourceset = false ;

/**
 * Booléen indiquant si la `Tech` supporte l'événement `timeupdate`. Il s'agit actuellement
 * non déclenché par video-js-swf. Cela permettra de déterminer si
 * {@link Tech#manualTimeUpdates} doit être appelé.
 *
 * @type {boolean}
 * @défaut
 */
Tech.prototype.featuresTimeupdateEvents = false ;

/**
 * Booléen indiquant si la `Tech` supporte les `TextTrack`s natifs.
 * Cela nous aidera à intégrer les `TextTrack`s natifs si le navigateur les supporte.
 *
 * @type {boolean}
 * @défaut
 */
Tech.prototype.featuresNativeTextTracks = false ;

/**
 * Booléen indiquant si la `Tech` supporte `requestVideoFrameCallback`.
 *
 * @type {boolean}
 * @défaut
 */
Tech.prototype.featuresVideoFrameCallback = false ;

/**
 * Un mixin fonctionnel pour les techniciens qui veulent utiliser le modèle Source Handler.
 * Les gestionnaires de sources sont des scripts permettant de gérer des formats spécifiques.
 * Le modèle de gestionnaire de source est utilisé pour les formats adaptatifs (HLS, DASH) qui
 * charger manuellement des données vidéo et les introduire dans un tampon source (Media Source Extensions)
 * Exemple : `Tech.withSourceHandlers.call(MyTech);`
 *
 * @param {Tech} _Tech
 *        La technologie à laquelle ajouter des fonctions de gestion des sources.
 *
 * @mixes Tech~SourceHandlerAdditions
 */
Tech.withSourceHandlers = function(_Tech) {

  /**
   * Enregistrer un gestionnaire de sources
   *
   * @param {Fonction} handler
   *        La classe du gestionnaire de source
   *
   * @param {number} [index]
   *        Enregistrez-le à l'adresse suivante
   */
  _Tech.registerSourceHandler = function(handler, index) {
    let handlers = _Tech.sourceHandlers ;

    if (!handlers) {
      handlers = _Tech.sourceHandlers = [] ;
    }

    if (index === undefined) {
      // ajouter à la fin de la liste
      index = handlers.length ;
    }

    handlers.splice(index, 0, handler) ;
  };

  /**
   * Vérifie si le technicien peut prendre en charge le type donné. Vérifie également le
   * Techs sourceHandlers.
   *
   * @param {string} type
   *         Le type d'image à vérifier.
   *
   * @return {string}
   *         "probablement", "peut-être" ou '' (chaîne vide)
   */
  _Tech.canPlayType = function(type) {
    const handlers = _Tech.sourceHandlers || [] ;
    peuvent le faire ;

    for (let i = 0 ; i < handlers.length ; i++) {
      can = handlers[i].canPlayType(type) ;

      if (can) {
        peuvent être retournés ;
      }
    }

    retourner '' ;
  };

  /**
   * Renvoie le premier gestionnaire de source qui prend en charge la source.
   *
   * TODO : Réponse à la question : faut-il donner la priorité à "probablement" plutôt qu'à "peut-être" ?
   *
   * @param {Tech~SourceObject} source
   *        L'objet source
   *
   * @param {Objet} options
   *        Les options transmises au tech
   *
   * @return {SourceHandler|null}
   *          Le premier gestionnaire de source qui prend en charge la source ou null si
   *          aucun SourceHandler ne prend en charge la source
   */
  _Tech.selectSourceHandler = function(source, options) {
    const handlers = _Tech.sourceHandlers || [] ;
    peuvent le faire ;

    for (let i = 0 ; i < handlers.length ; i++) {
      can = handlers[i].canHandleSource(source, options) ;

      if (can) {
        return handlers[i] ;
      }
    }

    retourner null ;
  };

  /**
   * Vérifier si le technicien peut prendre en charge la source donnée.
   *
   * @param {Tech~SourceObject} srcObj
   *        L'objet source
   *
   * @param {Objet} options
   *        Les options transmises au tech
   *
   * @return {string}
   *         "probablement", "peut-être" ou '' (chaîne vide)
   */
  _Tech.canPlaySource = function(srcObj, options) {
    const sh = _Tech.selectSourceHandler(srcObj, options) ;

    if (sh) {
      return sh.canHandleSource(srcObj, options) ;
    }

    retourner '' ;
  };

  /**
   * Lors de l'utilisation d'un gestionnaire de source, préférez son implémentation de
   * toute fonction normalement assurée par le technicien.
   */
  const deferrable = [
    'cherchable',
    recherche",
    durée
  ] ;

  /**
   * Une enveloppe autour de {@link Tech#seekable} qui appellera un `SourceHandler`s seekable
   * si elle existe, avec un retour à la fonction "Techs seekable".
   *
   * @méthode _Tech.seekable
   */

  /**
   * Un wrapper autour de {@link Tech#duration} qui appellera un `SourceHandler`s duration
   * si elle existe, sinon il se rabattra sur la fonction de durée des techniciens.
   *
   * @method _Tech.duration
   */

  deferrable.forEach(function(fnName) {
    const originalFn = this[fnName] ;

    if (typeof originalFn !== 'function') {
      retour ;
    }

    this[fnName] = function() {
      if (this.sourceHandler_ && this.sourceHandler_[fnName]) {
        return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments) ;
      }
      return originalFn.apply(this, arguments) ;
    };
  }, _Tech.prototype) ;

  /**
   * Créer une fonction pour définir la source à l'aide d'un objet source
   * et les gestionnaires de sources.
   * Ne devrait jamais être appelé à moins qu'un gestionnaire de source n'ait été trouvé.
   *
   * @param {Tech~SourceObject} source
   *        Un objet source avec les clés src et type
   */
  _Tech.prototype.setSource = function(source) {
    let sh = _Tech.selectSourceHandler(source, this.options_) ;

    if (!sh) {
      // Revenir à un gestionnaire de source natif lorsque des sources non prises en charge sont
      // délibérément fixé
      if (_Tech.nativeSourceHandler) {
        sh = _Tech.nativeSourceHandler ;
      } else {
        log.error('Aucun gestionnaire de source n'a été trouvé pour la source actuelle.') ;
      }
    }

    // Se débarrasser de tout gestionnaire de source existant
    this.disposeSourceHandler() ;
    this.off('dispose', this.disposeSourceHandler_) ;

    if (sh !== _Tech.nativeSourceHandler) {
      this.currentSource_ = source ;
    }

    this.sourceHandler_ = sh.handleSource(source, this, this.options_) ;
    this.one('dispose', this.disposeSourceHandler_) ;
  };

  /**
   * Nettoyer tous les SourceHandlers et listeners existants lorsque le Tech est éliminé.
   *
   * @listens Tech#dispose
   */
  _Tech.prototype.disposeSourceHandler = function() {
    // si nous disposons d'une source et en obtenons une autre
    // nous chargeons alors quelque chose de nouveau
    // que d'effacer toutes nos traces actuelles
    if (this.currentSource_) {
      this.clearTracks(['audio', 'video']) ;
      this.currentSource_ = null ;
    }

    // toujours nettoyer les pistes de texte automatique
    this.cleanupAutoTextTracks() ;

    if (this.sourceHandler_) {

      if (this.sourceHandler_.dispose) {
        this.sourceHandler_.dispose() ;
      }

      this.sourceHandler_ = null ;
    }
  };

};

// La classe Tech de base doit être enregistrée en tant que composant. C'est le seul
// Tech qui peut être enregistré en tant que composant.
Component.registerComponent('Tech', Tech) ;
Tech.registerTech('Tech', Tech) ;

/**
 * Une liste de technologies qui devraient être ajoutées à techOrder sur les joueurs
 *
 * @private
 */
Tech.defaultTechOrder_ = [] ;

export default Tech ;