/**
 * fichier player.js
 */
// Sous-classes Composant
import Component from './component.js' ;

import {version} de '../../package.json' ;
import document from 'global/document' ;
import window from 'global/window' ;
import evented de './mixins/evented' ;
import {isEvented, addEventedCallback} from './mixins/evented' ;
import * as Events from './utils/events.js' ;
import * as Dom from './utils/dom.js' ;
import * as Fn from './utils/fn.js' ;
import * as Guid from './utils/guid.js' ;
import * as browser from './utils/browser.js' ;
import {IE_VERSION, IS_CHROME, IS_WINDOWS} from './utils/browser.js' ;
import log, { createLogger } from './utils/log.js' ;
import {toTitleCase, titleCaseEquals} from './utils/string-cases.js' ;
import { createTimeRange } from './utils/time-ranges.js' ;
import { bufferedPercent } from './utils/buffer.js' ;
import * as stylesheet from './utils/stylesheet.js' ;
import FullscreenApi de './fullscreen-api.js' ;
import MediaError de './media-error.js' ;
import safeParseTuple from 'safe-json-parse/tuple' ;
import {assign} from './utils/obj' ;
import mergeOptions from './utils/merge-options.js' ;
import {silencePromise, isPromise} de './utils/promesse' ;
import textTrackConverter from './tracks/text-track-list-converter.js' ;
import ModalDialog from './modal-dialog' ;
import Tech from './tech/tech.js' ;
import * as middleware from './tech/middleware.js' ;
import {ALL as TRACK_TYPES} from './tracks/track-types' ;
import filterSource de './utils/filter-source' ;
import {getMimetype, findMimetype} de './utils/mimetypes' ;
import {hooks} de './utils/hooks' ;
import {isObject} de './utils/obj' ;
import keycode from 'keycode' ;

// Les importations suivantes ne sont utilisées que pour garantir que les modules correspondants
// sont toujours inclus dans le paquet video.js. L'importation des modules
// exécutez-les et ils s'enregistreront avec video.js.
import './tech/loader.js' ;
import './poster-image.js' ;
import './tracks/text-track-display.js' ;
import './loading-spinner.js' ;
import './big-play-button.js' ;
import './close-button.js' ;
import './control-bar/control-bar.js' ;
import './error-display.js' ;
import './tracks/text-track-settings.js' ;
import './resize-manager.js' ;
import './live-tracker.js' ;

// Importer la technologie Html5, au moins pour éliminer la balise vidéo originale.
import './tech/html5.js' ;

// Les événements techniques suivants sont simplement redéclenchés
// sur le joueur lorsqu'ils se produisent
const TECH_EVENTS_RETRIGGER = [
  /**
   * Déclenché lorsque l'agent utilisateur est en train de télécharger des données multimédias.
   *
   * @event Player#progress
   * @type {EventTarget~Event}
   */
  /**
   * Redéclenche l'événement `progress` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechProgress_ (méthode du joueur)
   * @fires Player#progress
   * @listens Tech#progrès
   */
  'progrès',

  /**
   * Se déclenche lorsque le chargement d'un fichier audio/vidéo est interrompu.
   *
   * @event Player#abort
   * @type {EventTarget~Event}
   */
  /**
   * Redéclenche l'événement `abort` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechAbort_ (méthode du joueur)
   * @fires Player#abort
   * @listens Tech#abort
   */
  'abort',

  /**
   * Se déclenche lorsque le navigateur ne reçoit intentionnellement pas de données multimédias.
   *
   * @event Player#suspend
   * @type {EventTarget~Event}
   */
  /**
   * Redéclenche l'événement `suspend` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechSuspend_ (méthode du joueur)
   * @fires Player#suspend
   * @listens Tech#suspend
   */
  suspendre",

  /**
   * Se déclenche lorsque la liste de lecture actuelle est vide.
   *
   * @event Player#emptied
   * @type {EventTarget~Event}
   */
  /**
   * Redéclenche l'événement `emptied` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechEmptied_ (méthode du joueur)
   * @fires Player#emptied
   * @listens Tech#emptied
   */
  vide",
  /**
   * Se déclenche lorsque le navigateur tente d'obtenir des données multimédias, mais que celles-ci ne sont pas disponibles.
   *
   * @event Player#stalled
   * @type {EventTarget~Event}
   */
  /**
   * Redéclenche l'événement `stalled` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechStalled_ (méthode du joueur)
   * @fires Player#stalled
   * @listens Tech#stalled
   */
  "bloqué",

  /**
   * Se déclenche lorsque le navigateur a chargé les métadonnées de l'audio/vidéo.
   *
   * @event Player#loadedmetadata
   * @type {EventTarget~Event}
   */
  /**
   * Redéclenche l'événement `loadedmetadata` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechLoadedmetadata_ (méthode du joueur)
   * @fires Player#loadedmetadata
   * @listens Tech#loadedmetadata
   */
  'loadedmetadata',

  /**
   * Se déclenche lorsque le navigateur a chargé l'image actuelle de l'audio/vidéo.
   *
   * @event Player#loadeddata
   * @type {event}
   */
  /**
   * Redéclenche l'événement `loadeddata` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechLoaddeddata_ (méthode du joueur)
   * @fires Player#loadeddata
   * @listens Tech#loadeddata
   */
  'loadeddata',

  /**
   * Se déclenche lorsque la position de lecture actuelle a changé.
   *
   * @event Player#timeupdate
   * @type {event}
   */
  /**
   * Redéclenche l'événement `timeupdate` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechTimeUpdate_ (méthode du joueur)
   * @fires Player#timeupdate
   * @listens Tech#timeupdate
   */
  'timeupdate',

  /**
   * Se déclenche lorsque les dimensions intrinsèques de la vidéo changent
   *
   * @event Player#resize
   * @type {event}
   */
  /**
   * Redéclenche l'événement `resize` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechResize_ (méthode du joueur)
   * @fires Player#resize
   * @listens Tech#resize
   */
  'redimensionner',

  /**
   * Se déclenche lorsque le volume a été modifié
   *
   * @event Player#volumechange
   * @type {event}
   */
  /**
   * Redéclenche l'événement `volumechange` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Player#handleTechVolumechange_ (méthode du joueur)
   * @fires Player#volumechange
   * @listens Tech#volumechange
   */
  'volumechange',

  /**
   * Se déclenche lorsque la piste de texte a été modifiée
   *
   * @event Player#texttrackchange
   * @type {event}
   */
  /**
   * Redéclenche l'événement `texttrackchange` qui a été déclenché par le {@link Tech}.
   *
   * @private
   * @method Joueur#manipulerTexttrackchange_
   * @fires Player#texttrackchange
   * @listens Tech#texttrackchange
   */
  'texttrackchange' (changement de texte)
] ;

// événements à mettre en file d'attente lorsque la vitesse de lecture est nulle
// il s'agit d'un hachage dans le seul but de mettre en correspondance les noms d'événements sans majuscule
// aux noms de fonctions en majuscules
const TECH_EVENTS_QUEUE = {
  canplay : canPlay",
  peut jouer : canPlayThrough",
  jouer : jouer",
  recherchée : cherché
};

const BREAKPOINT_ORDER = [
  minuscule",
  xsmall",
  petit",
  moyen",
  grand",
  xlarge",
  énorme
] ;

const BREAKPOINT_CLASSES = {} ;

// grep : vjs-layout-tiny
// grep : vjs-layout-x-small
// grep : vjs-layout-small
// grep : vjs-layout-medium
// grep : vjs-layout-large
// grep : vjs-layout-x-large
// grep : vjs-layout-huge
BREAKPOINT_ORDER.forEach(k => {
  const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k ;

  BREAKPOINT_CLASSES[k] = `vjs-layout-${v}` ;
}) ;

const DEFAULT_BREAKPOINTS = {
  minuscule : 210,
  xsmall : 320,
  petit : 425,
  moyen : 768,
  grand : 1440,
  xlarge : 2560,
  énorme : L'infini
};

/**
 * Une instance de la classe `Player` est créée lorsque l'une des méthodes d'installation de Video.js
 * sont utilisées pour initialiser une vidéo.
 *
 * Une fois qu'une instance a été créée, il est possible d'y accéder globalement de deux manières :
 * 1. En appelant `videojs('example_video_1');`
 * 2. En l'utilisant directement via `videojs.players.example_video_1;`
 *
 * @extends Component
 */
class Player extends Component {

  /**
   * Créer une instance de cette classe.
   *
   * @param {Element} tag
   *        L'élément DOM de la vidéo originale utilisé pour configurer les options.
   *
   * @param {Objet} [options]
   *        Objet contenant les noms et les valeurs des options.
   *
   * @param {Component~ReadyCallback} [ready]
   *        Fonction de rappel de l'état prêt.
   */
  constructor(tag, options, ready) {
    // S'assurer que l'identifiant de l'étiquette existe
    tag.id = tag.id || options.id || `vjs_video_${Guid.newGUID()}` ;

    // Définir les options
    // L'argument options remplace les options définies dans la balise vidéo
    // qui remplace les options définies globalement.
    // Cette dernière partie coïncide avec l'ordre de chargement
    // (la balise doit exister avant le lecteur)
    options = assign(Player.getTagSettings(tag), options) ;

    // Retarder l'initialisation des enfants parce que nous devons mettre en place
    // les propriétés du joueur en premier, et ne peut pas utiliser `this` avant `super()`
    options.initChildren = false ;

    // Idem pour la création de l'élément
    options.createEl = false ;

    // ne pas mélanger automatiquement le mélange de l'événement
    options.evented = false ;

    // nous ne voulons pas que le lecteur signale une activité tactile sur lui-même
    // voir enableTouchActivity dans le composant
    options.reportTouchActivity = false ;

    // Si la langue n'est pas définie, l'attribut lang le plus proche est obtenu
    if (!options.language) {
      if (typeof tag.closest === 'function') {
        const closest = tag.closest('[lang]') ;

        if (closest && closest.getAttribute) {
          options.language = closest.getAttribute('lang') ;
        }
      } else {
        let element = tag ;

        while (element && element.nodeType === 1) {
          if (Dom.getAttributes(element).hasOwnProperty('lang')) {
            options.language = element.getAttribute('lang') ;
            pause ;
          }
          element = element.parentNode ;
        }
      }
    }

    // Exécuter le composant de base en l'initialisant avec les nouvelles options
    super(null, options, ready) ;

    // Créer des méthodes liées pour les auditeurs de documents.
    this.boundDocumentFullscreenChange_ = (e) => this.documentFullscreenChange_(e) ;
    this.boundFullWindowOnEscKey_ = (e) => this.fullWindowOnEscKey(e) ;

    this.boundUpdateStyleEl_ = (e) => this.updateStyleEl_(e) ;
    this.boundApplyInitTime_ = (e) => this.applyInitTime_(e) ;
    this.boundUpdateCurrentBreakpoint_ = (e) => this.updateCurrentBreakpoint_(e) ;

    this.boundHandleTechClick_ = (e) => this.handleTechClick_(e) ;
    this.boundHandleTechDoubleClick_ = (e) => this.handleTechDoubleClick_(e) ;
    this.boundHandleTechTouchStart_ = (e) => this.handleTechTouchStart_(e) ;
    this.boundHandleTechTouchMove_ = (e) => this.handleTechTouchMove_(e) ;
    this.boundHandleTechTouchEnd_ = (e) => this.handleTechTouchEnd_(e) ;
    this.boundHandleTechTap_ = (e) => this.handleTechTap_(e) ;

    // la valeur par défaut de isFullscreen_ est false
    this.isFullscreen_ = false ;

    // créer un logger
    this.log = createLogger(this.id_) ;

    // Conserver notre propre référence à l'api fullscreen pour qu'elle puisse être simulée dans les tests
    this.fsApi_ = FullscreenApi ;

    // Trace le changement d'affiche par un technicien
    this.isPosterFromTech_ = false ;

    // Contient les informations de rappel qui sont mises en file d'attente lorsque la vitesse de lecture est nulle
    // et une recherche est en cours
    this.queuedCallbacks_ = [] ;

    // Désactiver l'accès à l'API car nous chargeons une nouvelle technologie qui pourrait se charger de manière asynchrone
    this.isReady_ = false ;

    // État d'initialisation hasStarted_
    this.hasStarted_ = false ;

    // État d'initialisation userActive_
    this.userActive_ = false ;

    // Init debugEnabled_
    this.debugEnabled_ = false ;

    // Init state audioOnlyMode_ (état d'activation)
    this.audioOnlyMode_ = false ;

    // État d'initialisation audioPosterMode_
    this.audioPosterMode_ = false ;

    // État d'initialisation audioOnlyCache_
    this.audioOnlyCache_ = {
      playerHeight : null,
      hiddenChildren : []
    };

    // si l'objet global de l'option a été accidentellement emporté par
    // quelqu'un, se retirer plus tôt avec une erreur d'information
    if (!this.options_ ||
        !this.options_.techOrder ||
        !this.options_.techOrder.length) {
      lancer une nouvelle erreur ('No techOrder specified. Avez-vous écrasé ' +
                      'videojs.options au lieu de simplement changer le ' +
                      'properties you want to override?') ;
    }

    // Conserve la balise originale utilisée pour définir les options
    this.tag = tag ;

    // Stocke les attributs de la balise utilisés pour restaurer l'élément html5
    this.tagAttributes = tag && Dom.getAttributes(tag) ;

    // Mise à jour de la langue actuelle
    this.language(this.options_.language) ;

    // Mise à jour des langues prises en charge
    if (options.languages) {
      // Normaliser les langues des options du lecteur en minuscules
      const languagesToLower = {} ;

      Object.getOwnPropertyNames(options.languages).forEach(function(name) {
        languagesToLower[name.toLowerCase()] = options.languages[name] ;
      }) ;
      this.languages_ = languagesToLower ;
    } else {
      this.languages_ = Player.prototype.options_.languages ;
    }

    this.resetCache_() ;

    // Affiche fixe
    this.poster_ = options.poster || '' ;

    // Définir les contrôles
    this.controls_ = !!options.controls ;

    // Les paramètres originaux de la balise sont stockés dans les options
    // à retirer immédiatement pour que les contrôles natifs ne clignotent pas.
    // Peut être réactivé par la technologie HTML5 si nativeControlsForTouch est vrai
    tag.controls = false ;
    tag.removeAttribute('controls') ;

    this.changingSrc_ = false ;
    this.playCallbacks_ = [] ;
    this.playTerminatedQueue_ = [] ;

    // l'attribut remplace l'option
    if (tag.hasAttribute('autoplay')) {
      this.autoplay(true) ;
    } else {
      // sinon utiliser le setter pour valider et
      // définir la valeur correcte.
      this.autoplay(this.options_.autoplay) ;
    }

    // vérifier les plugins
    if (options.plugins) {
      Object.keys(options.plugins).forEach((name) => {
        if (typeof this[name] !== 'function') {
          throw new Error(`le plugin "${nom}" n'existe pas`) ;
        }
      }) ;
    }

    /*
     * Stocker l'état interne de l'épuration
     *
     * @private
     * @return {Boolean} True si l'utilisateur est en train d'épurer
     */
    this.scrubbing_ = false ;

    this.el_ = this.createEl() ;

    // Faire de cet objet un objet événementiel et utiliser `el_` comme bus d'événements.
    evented(this, {eventBusKey : 'el_'}) ;

    // écouter les gestionnaires de changement d'écran du document et du lecteur afin de recevoir ces événements
    // avant qu'un utilisateur ne les reçoive, afin que nous puissions mettre à jour isFullscreen de manière appropriée.
    // s'assurer que nous écoutons les événements fullscreenchange avant tout le reste pour s'assurer que
    // notre méthode isFullscreen est mise à jour correctement pour les composants internes comme pour les composants externes.
    if (this.fsApi_.requestFullscreen) {
      Events.on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_) ;
      this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_) ;
    }

    if (this.fluid_) {
      this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_) ;
    }
    // Nous voulons également transmettre les options du lecteur d'origine à chaque composant et plugin
    // afin qu'ils n'aient pas besoin de retourner dans le lecteur pour trouver des options plus tard.
    // Nous devons également faire une autre copie de this.options_ pour ne pas nous retrouver avec
    // une boucle infinie.
    const playerOptionsCopy = mergeOptions(this.options_) ;

    // Chargement des plugins
    if (options.plugins) {
      Object.keys(options.plugins).forEach((name) => {
        this[name](options.plugins[name]) ;
      }) ;
    }

    // Activer le mode débogage pour déclencher l'événement debugon pour tous les plugins.
    if (options.debug) {
      this.debug(true) ;
    }

    this.options_.playerOptions = playerOptionsCopy ;

    this.middleware_ = [] ;

    this.playbackRates(options.playbackRates) ;

    this.initChildren() ;

    // Définir isAudio en fonction de l'utilisation ou non d'une balise audio
    this.isAudio(tag.nodeName.toLowerCase() === 'audio') ;

    // Mise à jour des contrôles className. Impossible de le faire lorsque les contrôles sont initiaux
    // set parce que l'élément n'existe pas encore.
    if (this.controls()) {
      this.addClass('vjs-controls-enabled') ;
    } else {
      this.addClass('vjs-controls-disabled') ;
    }

    // Définir l'étiquette ARIA et le rôle de la région en fonction du type de joueur
    this.el_.setAttribute('role', 'region') ;
    if (this.isAudio()) {
      this.el_.setAttribute('aria-label', this.localize('Audio Player')) ;
    } else {
      this.el_.setAttribute('aria-label', this.localize('Video Player')) ;
    }

    if (this.isAudio()) {
      this.addClass('vjs-audio') ;
    }

    if (this.flexNotSupported_()) {
      this.addClass('vjs-no-flex') ;
    }

    // TODO : Rendez cela plus intelligent. Basculer l'état de l'utilisateur entre le toucher et la souris
    // en utilisant des événements, puisque les appareils peuvent avoir à la fois des événements tactiles et des événements souris.
    // TODO : Cette vérification doit être répétée lorsque la fenêtre passe d'un moniteur à l'autre
    // (Voir https://github.com/videojs/video.js/issues/5683)
    if (browser.TOUCH_ENABLED) {
      this.addClass('vjs-touch-enabled') ;
    }

    // Safari iOS ne gère pas le survol de l'image
    if (!browser.IS_IOS) {
      this.addClass('vjs-workinghover') ;
    }

    // Rendre le joueur facilement trouvable par son identifiant
    Player.players[this.id_] = this ;

    // Ajouter une classe de version majeure pour aider les css dans les plugins
    const majorVersion = version.split('.')[0] ;

    this.addClass(`vjs-v${majorVersion}`) ;

    // Lorsque le lecteur est initialisé pour la première fois, déclencher l'activité pour que les composants
    // comme la barre de contrôle s'affichent si nécessaire
    this.userActive(true) ;
    this.reportUserActivity() ;

    this.one('play', (e) => this.listenForUserActivity_(e)) ;
    this.on('stageclick', (e) => this.handleStageClick_(e)) ;
    this.on('keydown', (e) => this.handleKeyDown(e)) ;
    this.on('languagechange', (e) => this.handleLanguagechange(e)) ;

    this.breakpoints(this.options_.breakpoints) ;
    this.responsive(this.options_.responsive) ;

    // Appel des deux méthodes de mode audio après que le lecteur soit entièrement activé
    // configuration pour pouvoir écouter les événements déclenchés par eux
    this.on('ready', () => {
      // Appeler d'abord la méthode audioPosterMode de façon à ce que
      // l'option audioOnlyMode peut avoir la priorité lorsque les deux options sont réglées sur true
      this.audioPosterMode(this.options_.audioPosterMode) ;
      this.audioOnlyMode(this.options_.audioOnlyMode) ;
    }) ;
  }

  /**
   * Détruit le lecteur vidéo et effectue le nettoyage nécessaire.
   *
   * Ceci est particulièrement utile si vous ajoutez et supprimez des vidéos de manière dynamique
   * vers/depuis le DOM.
   *
   * @fires Player#dispose
   */
  dispose() {
    /**
     * Appelé lorsque le lecteur est éliminé.
     *
     * @event Player#dispose
     * @type {EventTarget~Event}
     */
    this.trigger('dispose') ;
    // éviter que la fonction "disposer" ne soit appelée deux fois
    this.off('dispose') ;

    // S'assurer que tous les auditeurs de documents spécifiques au lecteur sont déliés. Il s'agit de
    Events.off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_) ;
    Events.off(document, 'keydown', this.boundFullWindowOnEscKey_) ;

    if (this.styleEl_ && this.styleEl_.parentNode) {
      this.styleEl_.parentNode.removeChild(this.styleEl_) ;
      this.styleEl_ = null ;
    }

    // Tuer la référence à ce joueur
    Player.players[this.id_] = null ;

    if (this.tag && this.tag.player) {
      this.tag.player = null ;
    }

    if (this.el_ && this.el_.player) {
      this.el_.player = null ;
    }

    if (this.tech_) {
      this.tech_.dispose() ;
      this.isPosterFromTech_ = false ;
      this.poster_ = '' ;
    }

    if (this.playerElIngest_) {
      this.playerElIngest_ = null ;
    }

    if (this.tag) {
      this.tag = null ;
    }

    middleware.clearCacheForPlayer(this) ;

    // supprimer tous les gestionnaires d'événements pour les listes de pistes
    // toutes les pistes et tous les auditeurs de pistes sont supprimés sur
    // tech dispose
    TRACK_TYPES.names.forEach((name) => {
      const props = TRACK_TYPES[name] ;
      const list = this[props.getterName]() ;

      // s'il ne s'agit pas d'une liste native
      // nous devons supprimer manuellement les récepteurs d'événements
      if (list && list.off) {
        list.off() ;
      }
    }) ;

    // le .el_ actuel est supprimé ici, ou remplacé si
    super.dispose({restoreEl : this.options_.restoreEl}) ;
  }

  /**
   * Créer l'élément DOM du `Player`.
   *
   * @return {Element}
   *         L'élément DOM qui est créé.
   */
  createEl() {
    let tag = this.tag ;
    laisser el ;
    let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player') ;
    const divEmbed = this.tag.tagName.toLowerCase() === 'video-js' ;

    if (playerElIngest) {
      el = this.el_ = tag.parentNode ;
    } else if (!divEmbed) {
      el = this.el_ = super.createEl('div') ;
    }

    // Copie de tous les attributs de la balise, y compris l'ID et la classe
    // L'ID fera désormais référence à la boîte du lecteur et non à la balise vidéo
    const attrs = Dom.getAttributes(tag) ;

    if (divEmbed) {
      el = this.el_ = tag ;
      tag = this.tag = document.createElement('video') ;
      while (el.children.length) {
        tag.appendChild(el.firstChild) ;
      }

      if (!Dom.hasClass(el, 'video-js')) {
        Dom.addClass(el, 'video-js') ;
      }

      el.appendChild(tag) ;

      playerElIngest = this.playerElIngest_ = el ;
      // déplacer les propriétés de notre élément `video-js` personnalisé
      // à notre nouvel élément `video`. Cela permettra de déplacer des éléments tels que
      // `src` ou `controls` qui ont été définis par js avant le lecteur
      // a été initialisé.
      Object.keys(el).forEach((k) => {
        essayez {
          tag[k] = el[k] ;
        } catch (e) {
          // nous avons une propriété comme outerHTML que nous ne pouvons pas copier, ignorez-la
        }
      }) ;
    }

    // fixer l'indice de tabulation à -1 pour supprimer l'élément vidéo de l'ordre de mise au point
    tag.setAttribute('tabindex', '-1') ;
    attrs.tabindex = '-1' ;

    // Contournement du problème #4583 (JAWS+IE n'annonce pas le BPB ou le bouton de lecture), et
    // pour le même problème avec Chrome (sur Windows) avec JAWS.
    // Voir https://github.com/FreedomScientific/VFO-standards-support/issues/78
    // Notez que nous ne pouvons pas détecter si JAWS est utilisé, mais cet attribut ARIA
    // ne modifie pas le comportement d'IE11 ou de Chrome si JAWS n'est pas utilisé
    if (IE_VERSION || (IS_CHROME && IS_WINDOWS)) {
      tag.setAttribute('role', 'application') ;
      attrs.role = 'application' ;
    }

    // Supprimer les attrs width/height de la balise pour que CSS puisse la rendre 100% width/height
    tag.removeAttribute('width') ;
    tag.removeAttribute('height') ;

    if ("width" in attrs) {
      supprimer attrs.width ;
    }
    if ("height" in attrs) {
      supprimer attrs.height ;
    }

    Object.getOwnPropertyNames(attrs).forEach(function(attr) {
      // ne pas copier l'attribut class de l'élément player lorsque nous sommes dans un div embed
      // la classe est déjà configurée correctement dans le cas divEmbed
      // et nous voulons nous assurer que la classe `video-js` ne se perde pas
      if ( !(divEmbed && attr === 'class')) {
        el.setAttribute(attr, attrs[attr]) ;
      }

      if (divEmbed) {
        tag.setAttribute(attr, attrs[attr]) ;
      }
    }) ;

    // Mise à jour de l'id/classe de la balise pour une utilisation en tant que HTML5 playback tech
    // On pourrait penser qu'il faut le faire après l'intégration dans le conteneur, donc classe .vjs-tech
    // ne clignote pas à 100% en largeur/hauteur, mais la classe ne s'applique qu'avec le parent .video-js
    tag.playerId = tag.id ;
    tag.id += '_html5_api' ;
    tag.className = 'vjs-tech' ;

    // Rendre le joueur trouvable sur les éléments
    tag.player = el.player = this ;
    // L'état par défaut de la vidéo est la pause
    this.addClass('vjs-paused') ;

    // Ajouter un élément de style dans le lecteur que nous utiliserons pour définir la largeur/hauteur
    // du lecteur d'une manière qui peut être surchargée par le CSS, tout comme la balise
    // élément vidéo
    if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) {
      this.styleEl_ = stylesheet.createStyleElement('vjs-styles-dimensions') ;
      const defaultsStyleEl = Dom.$('.vjs-styles-defaults') ;
      const head = Dom.$('head') ;

      head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild) ;
    }

    this.fill_ = false ;
    this.fluid_ = false ;

    // Passez les options de largeur/hauteur/aspectRatio qui mettront à jour le style el
    this.width(this.options_.width) ;
    this.height(this.options_.height) ;
    this.fill(this.options_.fill) ;
    this.fluid(this.options_.fluid) ;
    this.aspectRatio(this.options_.aspectRatio) ;
    // supporte à la fois crossOrigin et crossorigin pour réduire la confusion et les problèmes liés au nom
    this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin) ;

    // Masquer tout lien dans la balise vidéo/audio,
    // car IE ne les cache pas complètement aux lecteurs d'écran.
    const links = tag.getElementsByTagName('a') ;

    for (let i = 0 ; i < links.length ; i++) {
      const linkEl = links.item(i) ;

      Dom.addClass(linkEl, 'vjs-hidden') ;
      linkEl.setAttribute('hidden', 'hidden') ;
    }

    // insertElFirst semble faire vaciller l'état du réseau de 3 à 2, donc
    // garder la trace de l'original pour plus tard afin de savoir si la source a échoué à l'origine
    tag.initNetworkState_ = tag.networkState ;

    // Envelopper la balise vidéo dans le conteneur div (el/box)
    if (tag.parentNode && !playerElIngest) {
      tag.parentNode.insertBefore(el, tag) ;
    }

    // insérer la balise comme premier enfant de l'élément player
    // puis l'ajoute manuellement au tableau des enfants de façon à ce que this.addChild
    // fonctionnera correctement pour les autres composants
    //
    // Casse l'iPhone, corrigé dans la configuration HTML5.
    Dom.prependTo(tag, el) ;
    this.children_.unshift(tag) ;

    // Définir l'attr lang sur le lecteur pour s'assurer que CSS :lang() est cohérent avec le lecteur
    // s'il a été réglé sur une valeur différente de celle du document
    this.el_.setAttribute('lang', this.language_) ;

    this.el_.setAttribute('translate', 'no') ;

    this.el_ = el ;

    return el ;
  }

  /**
   * Obtient ou définit l'option crossOrigin du `Player`. Pour le lecteur HTML5, il s'agit de
   * définit la propriété `crossOrigin` sur la balise `<video>` pour contrôler le CORS
   * comportement.
   *
   * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
   *
   * @param {string} [valeur]
   *        La valeur à donner au crossOrigin du `Player`. Si un argument est
   *        doit être `anonymous` ou `use-credentials`.
   *
   * @return {string|undefined}
   *         - La valeur crossOrigin actuelle du `Player` lors de l'obtention.
   *         - indéfinie lors de la mise en place
   */
  crossOrigin(value) {
    if (!value) {
      return this.techGet_('crossOrigin') ;
    }

    if (value !== 'anonymous' && value !== 'use-credentials') {
      log.warn(`crossOrigin doit être "anonymous" ou "use-credentials", donné "${valeur}"`) ;
      retour ;
    }

    this.techCall_('setCrossOrigin', value) ;

    retour ;
  }

  /**
   * Un getter/setter pour la largeur du `Player`. Renvoie la valeur configurée du joueur.
   * Pour obtenir la largeur actuelle, utilisez `currentWidth()`.
   *
   * @param {number} [value]
   *        La valeur de la largeur du `Player`.
   *
   * @return {number}
   *         La largeur actuelle du `Player` lors de l'obtention.
   */
  width(value) {
    return this.dimension('width', value) ;
  }

  /**
   * Un getter/setter pour la hauteur du `Player`. Renvoie la valeur configurée du joueur.
   * Pour obtenir la hauteur actuelle, utilisez `currentheight()`.
   *
   * @param {number} [value]
   *        La valeur de la taille du joueur.
   *
   * @return {number}
   *         La hauteur actuelle du `Player` lors de l'obtention.
   */
  height(value) {
    return this.dimension('height', value) ;
  }

  /**
   * Obtenir et définir la largeur du joueur & height.
   *
   * @param {string} dimension
   *        Cette chaîne peut être :
   *        - largeur
   *        - hauteur
   *
   * @param {number} [value]
   *        Valeur de la dimension spécifiée dans le premier argument.
   *
   * @return {number}
   *         La valeur des arguments de dimension lors de l'obtention (largeur/hauteur).
   */
  dimension(dimension, valeur) {
    const privDimension = dimension + '_' ;

    if (value === undefined) {
      return this[privDimension] || 0 ;
    }

    if (value === '' || value === 'auto') {
      // Si une chaîne vide est donnée, réinitialiser la dimension pour qu'elle soit automatique
      this[privDimension] = undefined ;
      this.updateStyleEl_() ;
      retour ;
    }

    const parsedVal = parseFloat(value) ;

    if (isNaN(parsedVal)) {
      log.error(`Valeur incorrecte "${valeur}" fournie pour ${dimension}`) ;
      retour ;
    }

    this[privDimension] = parsedVal ;
    this.updateStyleEl_() ;
  }

  /**
   * Un getter/setter/toggler pour le vjs-fluid `className` sur le `Player`.
   *
   * L'activation de ce paramètre désactive le mode de remplissage.
   *
   * @param {boolean} [bool]
   *        - Une valeur de true ajoute la classe.
   *        - La valeur false supprime la classe.
   *        - Aucune valeur ne sera un getter.
   *
   * @return {boolean|undefined}
   *         - La valeur du fluide lors de l'obtention.
   *         - `undefined` when setting.
   */
  fluid(bool) {
    if (bool === undefined) {
      return !!this.fluid_ ;
    }

    this.fluid_ = !!bool ;

    if (isEvented(this)) {
      this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_) ;
    }
    if (bool) {
      this.addClass('vjs-fluid') ;
      this.fill(false) ;
      addEventedCallback(this, () => {
        this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_) ;
      }) ;
    } else {
      this.removeClass('vjs-fluid') ;
    }

    this.updateStyleEl_() ;
  }

  /**
   * Un getter/setter/toggler pour le vjs-fill `className` sur le `Player`.
   *
   * En activant cette option, le mode fluide est désactivé.
   *
   * @param {boolean} [bool]
   *        - Une valeur de true ajoute la classe.
   *        - La valeur false supprime la classe.
   *        - Aucune valeur ne sera un getter.
   *
   * @return {boolean|undefined}
   *         - La valeur du fluide lors de l'obtention.
   *         - `undefined` when setting.
   */
  fill(bool) {
    if (bool === undefined) {
      return !!this.fill_ ;
    }

    this.fill_ = !!bool ;

    if (bool) {
      this.addClass('vjs-fill') ;
      this.fluid(false) ;
    } else {
      this.removeClass('vjs-fill') ;
    }
  }

  /**
   * Obtention/réglage du rapport d'aspect
   *
   * @param {string} [ratio]
   *        Rapport d'aspect du lecteur
   *
   * @return {string|undefined}
   *         renvoie le rapport d'aspect actuel lors de l'obtention de
   */

  /**
   * Un getter/setter pour le ratio d'aspect du `Player`.
   *
   * @param {string} [ratio]
   *        La valeur du ratio d'aspect du `Player`.
   *
   * @return {string|undefined}
   *         - Le rapport d'aspect actuel du `Player` lorsqu'il est obtenu.
   *         - indéfinie lors de la mise en place
   */
  aspectRatio(ratio) {
    if (ratio === undefined) {
      return this.aspectRatio_ ;
    }

    // Vérifier le format largeur:hauteur
    if ( !(/^\d+\:\d+$/).test(ratio)) {
      lancer une nouvelle erreur ('Improper value supplied for aspect ratio. Le format doit être largeur:hauteur, par exemple 16:9.') ;
    }
    this.aspectRatio_ = ratio ;

    // Nous supposons que si vous définissez un ratio d'aspect, vous souhaitez le mode fluide,
    // parce qu'en mode fixe, vous pouvez calculer vous-même la largeur et la hauteur.
    this.fluid(true) ;

    this.updateStyleEl_() ;
  }

  /**
   * Mise à jour des styles de l'élément `Player` (hauteur, largeur et rapport d'aspect).
   *
   * @private
   * @listens Tech#loadedmetadata
   */
  updateStyleEl_() {
    if (window.VIDEOJS_NO_DYNAMIC_STYLE === true) {
      const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width ;
      const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height ;
      const techEl = this.tech_ && this.tech_.el() ;

      if (techEl) {
        if (width >= 0) {
          techEl.width = width ;
        }
        if (height >= 0) {
          techEl.height = height ;
        }
      }

      retour ;
    }

    laisser la largeur ;
    let height ;
    let aspectRatio ;
    let idClass ;

    // Le rapport d'aspect est utilisé directement ou pour calculer la largeur et la hauteur.
    if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
      // Utiliser tout aspectRatio qui a été spécifiquement défini
      aspectRatio = this.aspectRatio_ ;
    } else if (this.videoWidth() > 0) {
      // Sinon, essayer d'obtenir le rapport hauteur/largeur à partir des métadonnées de la vidéo
      aspectRatio = this.videoWidth() + ':' + this.videoHeight() ;
    } else {
      // Ou utiliser une valeur par défaut. L'élément vidéo est 2:1, mais le 16:9 est plus courant.
      aspectRatio = '16:9' ;
    }

    // Obtenir le ratio sous forme de décimale que l'on peut utiliser pour calculer les dimensions
    const ratioParts = aspectRatio.split(':') ;
    const ratioMultiplier = ratioParts[1] / ratioParts[0] ;

    if (this.width_ !== undefined) {
      // Utiliser toute largeur qui a été spécifiquement définie
      width = this.width_ ;
    } else if (this.height_ !== undefined) {
      // Ou calculer la largeur à partir du rapport d'aspect si une hauteur a été définie
      width = this.height_ / ratioMultiplier ;
    } else {
      // Ou utiliser les métadonnées de la vidéo, ou utiliser la valeur par défaut de 300 de l'el vidéo
      width = this.videoWidth() || 300 ;
    }

    if (this.height_ !== undefined) {
      // Utiliser toute hauteur qui a été spécifiquement définie
      height = this.height_ ;
    } else {
      // Sinon, calculer la hauteur à partir du ratio et de la largeur
      hauteur = largeur * ratioMultiplier ;
    }

    // S'assurer que la classe CSS est valide en commençant par un caractère alpha
    if ((/^[^a-zA-Z]/).test(this.id())) {
      idClass = 'dimensions-' + this.id() ;
    } else {
      idClass = this.id() + '-dimensions' ;
    }

    // S'assurer que la bonne classe est toujours présente dans le lecteur pour l'élément de style
    this.addClass(idClass) ;

    stylesheet.setTextContent(this.styleEl_, `
      .${idClass} {
        largeur : ${width}px ;
        hauteur : ${height}px ;
      }

      .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) {
        padding-top : ${ratioMultiplier * 100}% ;
      }
    `) ;
  }

  /**
   * Chargement/création d'une instance de lecture {@link Tech} incluant un élément
   * et les méthodes de l'API. Puis ajoutez l'élément `Tech` dans `Player` en tant qu'enfant.
   *
   * @param {string} techName
   *        nom de la technologie de lecture
   *
   * @param {string} source
   *        source vidéo
   *
   * @private
   */
  loadTech_(techName, source) {

    // Mettre en pause et supprimer la technologie de lecture en cours
    if (this.tech_) {
      this.unloadTech_() ;
    }

    const titleTechName = toTitleCase(techName) ;
    const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1) ;

    // se débarrasser de la balise vidéo HTML5 dès que l'on utilise une autre technologie
    if (titleTechName !== 'Html5' && this.tag) {
      Tech.getTech('Html5').disposeMediaElement(this.tag) ;
      this.tag.player = null ;
      this.tag = null ;
    }

    this.techName_ = titleTechName ;

    // Désactiver l'accès à l'API car nous chargeons une nouvelle technologie qui pourrait se charger de manière asynchrone
    this.isReady_ = false ;

    let autoplay = this.autoplay() ;

    // si autoplay est une chaîne (ou `true` avec normalizeAutoplay : true) nous passons false à la tech
    // parce que le lecteur va gérer l'autoplay sur `loadstart`
    if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) {
      autoplay = false ;
    }

    // Récupère les options techniques spécifiques des options du lecteur et ajoute l'élément source et l'élément parent à utiliser.
    const techOptions = {
      source,
      autoplay,
      'nativeControlsForTouch' : this.options_.nativeControlsForTouch,
      'playerId' : this.id(),
      'techId' : `${this.id()}_${camelTechName}_api`,
      'playsinline' : this.options_.playsinline,
      'preload' : this.options_.preload,
      'loop' : this.options_.loop,
      'disablePictureInPicture' : this.options_.disablePictureInPicture,
      'muted' : this.options_.muted,
      'poster' : this.poster(),
      'language' : this.language(),
      'playerElIngest' : this.playerElIngest_ || false,
      'vtt.js' : this.options_['vtt.js'],
      'canOverridePoster' : !!this.options_.techCanOverridePoster,
      'enableSourceset' : this.options_.enableSourceset,
      'Promesse' : this.options_.Promise
    };

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

      techOptions[props.getterName] = this[props.privateName] ;
    }) ;

    assign(techOptions, this.options_[titleTechName]) ;
    assign(techOptions, this.options_[camelTechName]) ;
    assign(techOptions, this.options_[techName.toLowerCase()]) ;

    if (this.tag) {
      techOptions.tag = this.tag ;
    }

    if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) {
      techOptions.startTime = this.cache_.currentTime ;
    }

    // Initialisation de l'instance technique
    const TechClass = Tech.getTech(techName) ;

    if (!TechClass) {
      throw new Error(`Aucune technologie nommée '${titleTechName}' n'existe ! '${titleTechName}' doit être enregistré à l'aide de videojs.registerTech()'`) ;
    }

    this.tech_ = new TechClass(techOptions) ;

    // player.triggerReady est toujours asynchrone, il n'est donc pas nécessaire que ce soit asynchrone
    this.tech_.ready(Fn.bind(this, this.handleTechReady_), true) ;

    textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_) ;

    // Écouter tous les événements définis par HTML5 et les déclencher sur le lecteur
    TECH_EVENTS_RETRIGGER.forEach((event) => {
      this.on(this.tech_, event, (e) => this[`handleTech${toTitleCase(event)}_`](e)) ;
    }) ;

    Object.keys(TECH_EVENTS_QUEUE).forEach((event) => {
      this.on(this.tech_, event, (eventObj) => {
        if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
          this.queuedCallbacks_.push({
            callback : this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
            événement : eventObj
          }) ;
          retour ;
        }
        this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj) ;
      }) ;
    }) ;

    this.on(this.tech_, 'loadstart', (e) => this.handleTechLoadStart_(e)) ;
    this.on(this.tech_, 'sourceset', (e) => this.handleTechSourceset_(e)) ;
    this.on(this.tech_, 'waiting', (e) => this.handleTechWaiting_(e)) ;
    this.on(this.tech_, 'ended', (e) => this.handleTechEnded_(e)) ;
    this.on(this.tech_, 'seeking', (e) => this.handleTechSeeking_(e)) ;
    this.on(this.tech_, 'play', (e) => this.handleTechPlay_(e)) ;
    this.on(this.tech_, 'firstplay', (e) => this.handleTechFirstPlay_(e)) ;
    this.on(this.tech_, 'pause', (e) => this.handleTechPause_(e)) ;
    this.on(this.tech_, 'durationchange', (e) => this.handleTechDurationChange_(e)) ;
    this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data)) ;
    this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err)) ;
    this.on(this.tech_, 'enterpictureinpicture', (e) => this.handleTechEnterPictureInPicture_(e)) ;
    this.on(this.tech_, 'leavepictureinpicture', (e) => this.handleTechLeavePictureInPicture_(e)) ;
    this.on(this.tech_, 'error', (e) => this.handleTechError_(e)) ;
    this.on(this.tech_, 'posterchange', (e) => this.handleTechPosterChange_(e)) ;
    this.on(this.tech_, 'textdata', (e) => this.handleTechTextData_(e)) ;
    this.on(this.tech_, 'ratechange', (e) => this.handleTechRateChange_(e)) ;
    this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_) ;

    this.usingNativeControls(this.techGet_('controls')) ;

    if (this.controls() && !this.usingNativeControls()) {
      this.addTechControlsListeners_() ;
    }

    // Ajouter l'élément tech dans le DOM s'il n'y est pas déjà
    // Veillez à ne pas insérer l'élément vidéo original si vous utilisez Html5
    if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
      Dom.prependTo(this.tech_.el(), this.el()) ;
    }

    // Se débarrasser de la référence à la balise vidéo d'origine après le chargement de la première technologie
    if (this.tag) {
      this.tag.player = null ;
      this.tag = null ;
    }
  }

  /**
   * Décharge et élimine la lecture en cours {@link Tech}.
   *
   * @private
   */
  unloadTech_() {
    // Sauvegarde des pistes de texte actuelles afin de pouvoir réutiliser les mêmes pistes de texte avec la prochaine technologie
    TRACK_TYPES.names.forEach((name) => {
      const props = TRACK_TYPES[name] ;

      this[props.privateName] = this[props.getterName]() ;
    }) ;
    this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_) ;

    this.isReady_ = false ;

    this.tech_.dispose() ;

    this.tech_ = false ;

    if (this.isPosterFromTech_) {
      this.poster_ = '' ;
      this.trigger('posterchange') ;
    }

    this.isPosterFromTech_ = false ;
  }

  /**
   * Renvoie une référence au {@link Tech} actuel.
   * Il affichera par défaut un avertissement sur le danger d'utiliser la technologie directement
   * mais tout argument passé fera taire l'avertissement.
   *
   * @param {*} [safety]
   *        Tout ce qui est passé pour faire taire l'avertissement
   *
   * @return {Tech}
   *         La technologie
   */
  tech(safety) {
    if (safety === undefined) {
      log.warn('L'utilisation directe de la technologie peut être dangereuse. J'espère que vous savez ce que vous faites
        'Voir https://github.com/videojs/video.js/issues/2617 pour plus d'informations.\n') ;
    }

    return this.tech_ ;
  }

  /**
   * Configurer les récepteurs de clic et de toucher pour l'élément de lecture
   *
   * - Sur les ordinateurs de bureau : un clic sur la vidéo elle-même permet de basculer la lecture
   * - Sur les appareils mobiles : un clic sur la vidéo permet de basculer les contrôles
   *   ce qui se fait en faisant basculer l'état de l'utilisateur entre l'état actif et l'état actif
   *   inactif
   * - Une tape peut signaler qu'un utilisateur est devenu actif ou inactif
   *   par exemple, une pression rapide sur un film iPhone devrait faire apparaître les commandes. Autre
   *   un appui rapide devrait les masquer à nouveau (en signalant que l'utilisateur se trouve dans une zone inactive)
   *   état de visionnage)
   * - En outre, nous voulons toujours que l'utilisateur soit considéré comme inactif après
   *   quelques secondes d'inactivité.
   *
   * > Remarque : la seule partie de l'interaction avec iOS que nous ne pouvons pas imiter avec cette configuration
   * le fait de toucher et de maintenir l'élément vidéo compte-t-il comme une activité afin de
   * de garder les contrôles affichés, mais cela ne devrait pas poser de problème. Un toucher et un maintien
   * sur n'importe quel contrôle permettra à l'utilisateur de rester actif
   *
   * @private
   */
  addTechControlsListeners_() {
    // Veillez à supprimer tous les listeners précédents au cas où nous serions appelés plusieurs fois.
    this.removeTechControlsListeners_() ;

    this.on(this.tech_, 'click', this.boundHandleTechClick_) ;
    this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_) ;

    // Si les contrôles sont cachés, nous ne voulons pas que cela change sans qu'il y ait un événement "tap"
    // afin de vérifier si les contrôles étaient déjà affichés avant le rapport de l'utilisateur
    // activité
    this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_) ;
    this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_) ;
    this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_) ;

    // L'écouteur de tapotement doit être placé après l'écouteur de toucher, car l'écouteur de tapotement
    // l'auditeur annule toute activitéUserActivity signalée lors de la définition de userActive(false)
    this.on(this.tech_, 'tap', this.boundHandleTechTap_) ;
  }

  /**
   * Supprimez les écouteurs utilisés pour les commandes par clic et par tapotement. Cela est nécessaire pour
   * le basculement vers des contrôles désactivés, alors qu'un tapotement ne devrait rien faire.
   *
   * @private
   */
  removeTechControlsListeners_() {
    // Nous ne voulons pas simplement utiliser `this.off()` parce qu'il pourrait y avoir d'autres besoins
    // les auditeurs ajoutés par les techniciens qui étendent cette fonction.
    this.off(this.tech_, 'tap', this.boundHandleTechTap_) ;
    this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_) ;
    this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_) ;
    this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_) ;
    this.off(this.tech_, 'click', this.boundHandleTechClick_) ;
    this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_) ;
  }

  /**
   * Le joueur attend que la technologie soit prête
   *
   * @private
   */
  handleTechReady_() {
    this.triggerReady() ;

    // Conserver le même volume que précédemment
    if (this.cache_.volume) {
      this.techCall_('setVolume', this.cache_.volume) ;
    }

    // Vérifier si le technicien a trouvé une affiche de plus haute résolution pendant le chargement
    this.handleTechPosterChange_() ;

    // Mise à jour de la durée si elle est disponible
    this.handleTechDurationChange_() ;
  }

  /**
   * Redéclenche l'événement `loadstart` qui a été déclenché par le {@link Tech}. Cette
   * déclenchera également {@link Player#firstplay} s'il s'agit du premier loadstart
   * pour une vidéo.
   *
   * @fires Player#loadstart
   * @fires Player#firstplay
   * @listens Tech#loadstart
   * @private
   */
  handleTechLoadStart_() {
    // TODO : Mise à jour pour utiliser l'événement `emptied` à la place. Voir #1277.

    this.removeClass('vjs-ended') ;
    this.removeClass('vjs-seeking') ;

    // réinitialisation de l'état d'erreur
    this.error(null) ;

    // Mise à jour de la durée
    this.handleTechDurationChange_() ;

    // S'il est déjà en cours de lecture, nous voulons déclencher un événement de première lecture maintenant.
    // L'événement "firstplay" repose sur les événements "play" et "loadstart"
    // ce qui peut se produire dans n'importe quel ordre pour une nouvelle source
    if (!this.paused())) {
      /**
       * Déclenché lorsque l'agent utilisateur commence à rechercher des données multimédias
       *
       * @event Player#loadstart
       * @type {EventTarget~Event}
       */
      this.trigger('loadstart') ;
      this.trigger('firstplay') ;
    } else {
      // réinitialisation de l'état hasStarted
      this.hasStarted(false) ;
      this.trigger('loadstart') ;
    }

    // la lecture automatique se produit après le démarrage du navigateur,
    // nous imitons donc ce comportement
    this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay()) ;
  }

  /**
   * Traite les valeurs de chaîne de caractères autoplay, plutôt que les valeurs booléennes typiques
   * les valeurs qui doivent être gérées par le technicien. Notez qu'il ne s'agit pas d'une
   * de toute spécification. Les valeurs valides et ce qu'elles font peuvent être
   * trouvé sur le getter autoplay à Player#autoplay()
   */
  manualAutoplay_(type) {
    if (!this.tech_ || typeof type !== 'string') {
      retour ;
    }

    // Sauvegarder la valeur originale de muted(), mettre muted à true, et essayer de jouer().
    // En cas de rejet de la promesse, rétablir le silence à partir de la valeur sauvegardée
    const resolveMuted = () => {
      const previouslyMuted = this.muted() ;

      this.muted(true) ;

      const restoreMuted = () => {
        this.muted(previouslyMuted) ;
      };

      // rétablissement de la sourdine à la fin du jeu
      this.playTerminatedQueue_.push(restoreMuted) ;

      const mutedPromise = this.play() ;

      if (!isPromise(mutedPromise)) {
        retour ;
      }

      return mutedPromise.catch(err => {
        restoreMuted() ;
        lancer une nouvelle Erreur(`Rejet à manualAutoplay. Restauration de la valeur muette. ${err ? err : ''}`) ;
      }) ;
    };

    laisser promettre ;

    // en cas de sourdine, la valeur par défaut est true
    // la seule chose que nous puissions faire est d'appeler à jouer
    if (type === 'any' && !this.muted()) {
      promise = this.play() ;

      if (isPromise(promise)) {
        promise = promise.catch(resolveMuted) ;
      }
    } else if (type === 'muted' && !this.muted()) {
      promise = resolveMuted() ;
    } else {
      promise = this.play() ;
    }

    if (!isPromise(promise)) {
      retour ;
    }

    return promise.then(() => {
      this.trigger({type : 'autoplay-success', autoplay : type}) ;
    }).catch(() => {
      this.trigger({type : 'autoplay-failure', autoplay : type}) ;
    }) ;
  }

  /**
   * Mettre à jour les caches de sources internes afin de renvoyer la bonne source à partir de
   * `src()`, `currentSource()`, et `currentSources()`.
   *
   * > Note : `currentSources` ne sera pas mis à jour si la source fournie existe déjà
   *         dans le cache `currentSources` actuel.
   *
   *
   * @param {Tech~SourceObject} srcObj
   *        Une chaîne ou un objet source pour mettre à jour nos caches.
   */
  updateSourceCaches_(srcObj = '') {

    let src = srcObj ;
    let type = '' ;

    if (typeof src !== 'string') {
      src = srcObj.src ;
      type = srcObj.type ;
    }

    // s'assurer que tous les caches sont réglés sur les valeurs par défaut
    // pour éviter la vérification de la nullité
    this.cache_.source = this.cache_.source || {} ;
    this.cache_.sources = this.cache_.sources || [] ;

    // essaie d'obtenir le type du src qui lui a été transmis
    if (src && !type) {
      type = findMimetype(this, src) ;
    }

    // mettre à jour le cache `currentSource` toujours
    this.cache_.source = mergeOptions({}, srcObj, {src, type}) ;

    const matchingSources = this.cache_.sources.filter((s) => s.src && s.src === src) ;
    const sourceElSources = [] ;
    const sourceEls = this.$$('source') ;
    const matchingSourceEls = [] ;

    for (let i = 0 ; i < sourceEls.length ; i++) {
      const sourceObj = Dom.getAttributes(sourceEls[i]) ;

      sourceElSources.push(sourceObj) ;

      if (sourceObj.src && sourceObj.src === src) {
        matchingSourceEls.push(sourceObj.src) ;
      }
    }

    // si nous avons des listes de sources correspondantes mais pas de sources correspondantes
    // le cache source actuel n'est pas à jour
    if (matchingSourceEls.length && !matchingSources.length) {
      this.cache_.sources = sourceElSources ;
    // si nous n'avons pas de source ou d'autres sources correspondantes, fixer le
    // sources cache to the `currentSource` cache
    } else if (!matchingSources.length) {
      this.cache_.sources = [this.cache_.source] ;
    }

    // mettre à jour le cache tech `src`
    this.cache_.src = src ;
  }

  /**
   * *EXPERIMENTAL* Déclenché lorsque la source est définie ou modifiée sur le {@link Tech}
   * ce qui entraîne le rechargement de l'élément multimédia.
   *
   * Il se déclenchera pour la source initiale et pour chaque source suivante.
   * Cet événement est un événement personnalisé de Video.js et est déclenché par le {@link Tech}.
   *
   * L'objet de cet événement contient une propriété `src` qui contiendra la source
   * qui était disponible au moment où l'événement a été déclenché. Cela n'est généralement nécessaire que si Video.js
   * change de technicien pendant que la source est changée.
   *
   * Elle est également déclenchée lorsque la fonction `load` est appelée sur le lecteur (ou l'élément multimédia)
   * parce que la {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|spécification pour `load`}
   * indique que l'algorithme de sélection des ressources doit être interrompu et redémarré.
   * Dans ce cas, il est très probable que la propriété `src` soit fixée à la propriété
   * chaîne vide `""` pour indiquer que nous ne savons pas ce que sera la source, mais que nous ne savons pas ce que sera la source
   * qu'il est en train de changer.
   *
   * *Cet événement est actuellement encore expérimental et peut être modifié dans les versions mineures
   * __Pour l'utiliser, passez l'option `enableSourceset` au lecteur.__
   *
   * @event Player#sourceset
   * @type {EventTarget~Event}
   * @prop {string} src
   *                L'url de la source disponible lorsque le `sourceset` a été déclenché.
   *                Il s'agira d'une chaîne vide si nous ne pouvons pas savoir quelle est la source
   *                mais sachez que la source changera.
   */
  /**
   * Redéclenche l'événement `sourceset` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#sourceset
   * @listens Tech#sourceset
   * @private
   */
  handleTechSourceset_(event) {
    // ne met à jour le cache de la source que lorsque la source
    // n'a pas été mis à jour à l'aide de l'interface utilisateur du lecteur
    if (!this.changingSrc_) {
      let updateSourceCaches = (src) => this.updateSourceCaches_(src) ;
      const playerSrc = this.currentSource().src ;
      const eventSrc = event.src ;

      // si nous avons un playerSrc qui n'est pas un blob, et un tech src qui est un blob
      if (playerSrc && !(/^blob:/).test(playerSrc) && (/^blob:/).test(eventSrc)) {

        // si la source technique et la source joueur ont été mises à jour, nous supposons que
        // quelque chose comme @videojs/http-streaming a fait le jeu de sources et a ignoré la mise à jour du cache des sources.
        if (!this.lastSource_ || (this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc)) {
          updateSourceCaches = () => {} ;
        }
      }

      // mise à jour immédiate de la source vers la source initiale
      // dans certains cas, il s'agira d'une chaîne vide
      updateSourceCaches(eventSrc) ;

      // si le `sourceset` `src` est une chaîne vide
      // attendre un `loadstart` pour mettre à jour le cache vers `currentSrc`.
      // Si un sourceset se produit avant un `loadstart`, nous réinitialisons l'état
      if (!event.src) {
        this.tech_.any(['sourceset', 'loadstart'], (e) => {
          // si un sourceet se produit avant un `loadstart` à cet endroit
          // n'a rien à voir avec ce `handleTechSourceset_`
          // sera appelé à nouveau et la question sera traitée à ce moment-là.
          if (e.type === 'sourceset') {
            retour ;
          }

          const techSrc = this.techGet('currentSrc') ;

          this.lastSource_.tech = techSrc ;
          this.updateSourceCaches_(techSrc) ;
        }) ;
      }
    }
    this.lastSource_ = {player : this.currentSource().src, tech : event.src} ;

    this.trigger({
      src : event.src,
      type : "sourceset" (jeu de sources)
    }) ;
  }

  /**
   * Ajouter/supprimer la classe vjs-has-started
   *
   * @fires Player#firstplay
   *
   * @param {boolean} request
   *        - true : ajoute la classe
   *        - false : supprimer la classe
   *
   * @return {boolean}
   *         la valeur booléenne de hasStarted_
   */
  hasStarted(request) {
    if (request === undefined) {
      // agit comme getter, si nous n'avons pas de demande à modifier
      return this.hasStarted_ ;
    }

    if (request === this.hasStarted_) {
      retour ;
    }

    this.hasStarted_ = request ;

    if (this.hasStarted_) {
      this.addClass('vjs-has-started') ;
      this.trigger('firstplay') ;
    } else {
      this.removeClass('vjs-has-started') ;
    }
  }

  /**
   * Déclenchée chaque fois que le support commence ou reprend la lecture
   *
   * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
   * @fires Player#play
   * @listens Tech#play
   * @private
   */
  handleTechPlay_() {
    this.removeClass('vjs-ended') ;
    this.removeClass('vjs-paused') ;
    this.addClass('vjs-playing') ;

    // cacher l'affiche lorsque l'utilisateur appuie sur play
    this.hasStarted(true) ;
    /**
     * Déclenché chaque fois qu'un événement {@link Tech#play} se produit. Indique que
     * la lecture a commencé ou repris.
     *
     * @event Player#play
     * @type {EventTarget~Event}
     */
    this.trigger('play') ;
  }

  /**
   * Redéclenche l'événement `ratechange` qui a été déclenché par le {@link Tech}.
   *
   * Si des événements ont été mis en file d'attente alors que le taux de lecture était nul, déclencher
   * ces événements maintenant.
   *
   * @private
   * @method Player#handleTechRateChange_ (méthode du joueur)
   * @fires Player#ratechange
   * @listens Tech#ratechange
   */
  handleTechRateChange_() {
    if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
      this.queuedCallbacks_.forEach((queued) => queued.callback(queued.event)) ;
      this.queuedCallbacks_ = [] ;
    }
    this.cache_.lastPlaybackRate = this.tech_.playbackRate() ;
    /**
     * Se déclenche lorsque la vitesse de lecture de l'audio/vidéo est modifiée
     *
     * @event Player#ratechange
     * @type {event}
     */
    this.trigger('ratechange') ;
  }

  /**
   * Redéclenche l'événement `waiting` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#waiting
   * @listens Tech#attente
   * @private
   */
  handleTechWaiting_() {
    this.addClass('vjs-waiting') ;
    /**
     * Un changement de readyState sur l'élément DOM a provoqué l'arrêt de la lecture.
     *
     * @event Player#waiting
     * @type {EventTarget~Event}
     */
    this.trigger('waiting') ;

    // Les navigateurs peuvent émettre un événement timeupdate après un événement waiting. Afin d'éviter
    // retrait prématuré de la classe d'attente, attendre le changement d'heure.
    const timeWhenWaiting = this.currentTime() ;
    const timeUpdateListener = () => {
      if (timeWhenWaiting !== this.currentTime()) {
        this.removeClass('vjs-waiting') ;
        this.off('timeupdate', timeUpdateListener) ;
      }
    };

    this.on('timeupdate', timeUpdateListener) ;
  }

  /**
   * Redéclenche l'événement `canplay` qui a été déclenché par l'{@link Tech}.
   * > Remarque : Ce n'est pas cohérent entre les navigateurs. Voir #1351
   *
   * @fires Player#canplay
   * @listens Tech#canplay
   * @private
   */
  handleTechCanPlay_() {
    this.removeClass('vjs-waiting') ;
    /**
     * Le média a un readyState égal ou supérieur à HAVE_FUTURE_DATA.
     *
     * @event Player#canplay
     * @type {EventTarget~Event}
     */
    this.trigger('canplay') ;
  }

  /**
   * Redéclenche l'événement `canplaythrough` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#canplaythrough
   * @listens Tech#canplaythrough
   * @private
   */
  handleTechCanPlayThrough_() {
    this.removeClass('vjs-waiting') ;
    /**
     * Le média a un readyState égal ou supérieur à HAVE_ENOUGH_DATA. Cela signifie que le
     * l'intégralité du fichier multimédia peut être lue sans mise en mémoire tampon.
     *
     * @event Player#canplaythrough
     * @type {EventTarget~Event}
     */
    this.trigger('canplaythrough') ;
  }

  /**
   * Redéclenche l'événement `playing` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#playing
   * @listens Tech#playing
   * @private
   */
  handleTechPlaying_() {
    this.removeClass('vjs-waiting') ;
    /**
     * La lecture du média n'est plus bloquée et a commencé.
     *
     * @event Player#playing
     * @type {EventTarget~Event}
     */
    this.trigger('playing') ;
  }

  /**
   * Redéclenche l'événement `seeking` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#seeking
   * @listens Tech#seeking
   * @private
   */
  handleTechSeeking_() {
    this.addClass('vjs-seeking') ;
    /**
     * Déclenché chaque fois que le joueur passe à une nouvelle heure
     *
     * @event Player#seeking
     * @type {EventTarget~Event}
     */
    this.trigger('seeking') ;
  }

  /**
   * Redéclenche l'événement `seeked` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#seeked
   * @listens Tech#seeked
   * @private
   */
  handleTechSeeked_() {
    this.removeClass('vjs-seeking') ;
    this.removeClass('vjs-ended') ;
    /**
     * Déclenché lorsque le joueur a fini de sauter à un nouveau moment
     *
     * @event Player#seeked
     * @type {EventTarget~Event}
     */
    this.trigger('seeked') ;
  }

  /**
   * Redéclenche l'événement `firstplay` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#firstplay
   * @listens Tech#firstplay
   * @deprecated A partir de la version 6.0, l'événement firstplay est obsolète.
   *             A partir de la version 6.0, le passage de l'option `starttime` au joueur et l'événement firstplay sont dépréciés.
   * @private
   */
  handleTechFirstPlay_() {
    // Si le premier attribut "starttime" est spécifié
    // nous commencerons alors à l'offset donné en secondes
    if (this.options_.starttime) {
      log.warn('Passing the `starttime` option to the player will be deprecated in 6.0') ;
      this.currentTime(this.options_.starttime) ;
    }

    this.addClass('vjs-has-started') ;
    /**
     * Lancé lors de la première lecture d'une vidéo. Ne fait pas partie de la spécification HLS, et c'est
     * n'est probablement pas encore la meilleure implémentation, il faut donc l'utiliser avec parcimonie. Si vous n'avez pas de
     * pour empêcher la lecture, utilisez `myPlayer.one('play');` à la place.
     *
     * @event Player#firstplay
     * @deprecated A partir de la version 6.0, l'événement firstplay est obsolète.
     * @type {EventTarget~Event}
     */
    this.trigger('firstplay') ;
  }

  /**
   * Redéclenche l'événement `pause` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#pause
   * @listens Tech#pause
   * @private
   */
  handleTechPause_() {
    this.removeClass('vjs-playing') ;
    this.addClass('vjs-paused') ;
    /**
     * Déclenché lorsque le média a été mis en pause
     *
     * @event Player#pause
     * @type {EventTarget~Event}
     */
    this.trigger('pause') ;
  }

  /**
   * Redéclenche l'événement `ended` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#ended
   * @listens Tech#ended
   * @private
   */
  handleTechEnded_() {
    this.addClass('vjs-ended') ;
    this.removeClass('vjs-waiting') ;
    if (this.options_.loop) {
      this.currentTime(0) ;
      this.play() ;
    } else if (!this.paused()) {
      this.pause() ;
    }

    /**
     * Déclenché lorsque la fin de la ressource multimédia est atteinte (currentTime == duration)
     *
     * @event Player#ended
     * @type {EventTarget~Event}
     */
    this.trigger('ended') ;
  }

  /**
   * Déclenché lorsque la durée de la ressource multimédia est connue ou modifiée pour la première fois
   *
   * @listens Tech#durationchange
   * @private
   */
  handleTechDurationChange_() {
    this.duration(this.techGet_('duration')) ;
  }

  /**
   * Gestion d'un clic sur l'élément multimédia pour la lecture/pause
   *
   * @param {EventTarget~Event} event
   *        l'événement qui a provoqué le déclenchement de cette fonction
   *
   * @listens Tech#click
   * @private
   */
  handleTechClick_(event) {
    // Lorsque les contrôles sont désactivés, un clic ne doit pas faire basculer la lecture car
    // le clic est considéré comme un contrôle
    if (!this.controls_) {
      retour ;
    }

    si (
      this.options_ === undefined ||
      this.options_.userActions === undefined ||
      this.options_.userActions.click === undefined ||
      this.options_.userActions.click !== false
    ) {

      si (
        this.options_ !== undefined &&
        this.options_.userActions !== undefined &&
        typeof this.options_.userActions.click === 'function'
      ) {

        this.options_.userActions.click.call(this, event) ;

      } else if (this.paused()) {
        silencePromesse(this.play()) ;
      } else {
        this.pause() ;
      }
    }
  }

  /**
   * Gestion d'un double-clic sur l'élément multimédia pour entrer/sortir du plein écran
   *
   * @param {EventTarget~Event} event
   *        l'événement qui a provoqué le déclenchement de cette fonction
   *
   * @listens Tech#dblclick
   * @private
   */
  handleTechDoubleClick_(event) {
    if (!this.controls_) {
      retour ;
    }

    // nous ne voulons pas faire basculer l'état de plein écran
    // lors d'un double-clic à l'intérieur d'une barre de contrôle ou d'une fenêtre modale
    const inAllowedEls = Array.prototype.some.call(
      this.$$('.vjs-control-bar, .vjs-modal-dialog'),
      el => el.contains(event.target)
    ) ;

    if (!inAllowedEls) {
      /*
       * options.userActions.doubleClick
       *
       * Si `undefined` ou `true`, le double-clic fait basculer le plein écran si des contrôles sont présents
       * Mettre à `false` pour désactiver la gestion du double-clic
       * Définir une fonction pour remplacer un gestionnaire de double-clic externe
       */
      si (
        this.options_ === undefined ||
        this.options_.userActions === undefined ||
        this.options_.userActions.doubleClick === undefined ||
        this.options_.userActions.doubleClick !== false
      ) {

        si (
          this.options_ !== undefined &&
          this.options_.userActions !== undefined &&
          typeof this.options_.userActions.doubleClick === 'function'
        ) {

          this.options_.userActions.doubleClick.call(this, event) ;

        } else if (this.isFullscreen()) {
          this.exitFullscreen() ;
        } else {
          this.requestFullscreen() ;
        }
      }
    }
  }

  /**
   * Gérer un tapotement sur l'élément multimédia. Il fera basculer l'utilisateur
   * l'état de l'activité, qui masque et affiche les contrôles.
   *
   * @listens Tech#tap
   * @private
   */
  handleTechTap_() {
    this.userActive(!this.userActive()) ;
  }

  /**
   * Toucher la poignée pour démarrer
   *
   * @listens Tech#touchstart
   * @private
   */
  handleTechTouchStart_() {
    this.userWasActive = this.userActive() ;
  }

  /**
   * Toucher de la poignée pour se déplacer
   *
   * @listens Tech#touchmove
   * @private
   */
  handleTechTouchMove_() {
    if (this.userWasActive) {
      this.reportUserActivity() ;
    }
  }

  /**
   * Touche de la poignée à l'extrémité
   *
   * @param {EventTarget~Event} event
   *        l'événement tactile qui a déclenché
   *        cette fonction
   *
   * @listens Tech#touchend
   * @private
   */
  handleTechTouchEnd_(event) {
    // Empêche les événements de la souris de se produire également
    if (event.cancelable) {
      event.preventDefault() ;
    }
  }

  /**
   * les événements de clics natifs sur le SWF ne sont pas déclenchés sur IE11, Win8.1RT
   * utiliser les événements stageclick déclenchés à l'intérieur du SWF à la place
   *
   * @private
   * @listens stageclick
   */
  handleStageClick_() {
    this.reportUserActivity() ;
  }

  /**
   * @private
   */
  toggleFullscreenClass_() {
    if (this.isFullscreen()) {
      this.addClass('vjs-fullscreen') ;
    } else {
      this.removeClass('vjs-fullscreen') ;
    }
  }

  /**
   * lorsque l'événement fschange du document se déclenche, il appelle ceci
   */
  documentFullscreenChange_(e) {
    const targetPlayer = e.target.player ;

    // si un autre joueur est en plein écran
    // vérifie que le joueur cible est nul car les anciens firefox mettaient document comme e.target
    if (targetPlayer && targetPlayer !== this) {
      retour ;
    }

    const el = this.el() ;
    let isFs = document[this.fsApi_.fullscreenElement] === el ;

    if (!isFs && el.matches) {
      isFs = el.matches(':' + this.fsApi_.fullscreen) ;
    } else if (!isFs && el.msMatchesSelector) {
      isFs = el.msMatchesSelector(':' + this.fsApi_.fullscreen) ;
    }

    this.isFullscreen(isFs) ;
  }

  /**
   * Gérer le changement d'écran complet de la technologie
   *
   * @param {EventTarget~Event} event
   *        l'événement fullscreenchange qui a déclenché cette fonction
   *
   * @param {Object} data
   *        les données qui ont été envoyées avec l'événement
   *
   * @private
   * @listens Tech#fullscreenchange
   * @fires Player#fullscreenchange
   */
  handleTechFullscreenChange_(event, data) {
    if (data) {
      if (data.nativeIOSFullscreen) {
        this.addClass('vjs-ios-native-fs') ;
        this.tech_.one('webkitendfullscreen', () => {
          this.removeClass('vjs-ios-native-fs') ;
        }) ;
      }
      this.isFullscreen(data.isFullscreen) ;
    }
  }

  handleTechFullscreenError_(event, err) {
    this.trigger('fullscreenerror', err) ;
  }

  /**
   * @private
   */
  togglePictureInPictureClass_() {
    if (this.isInPictureInPicture()) {
      this.addClass('vjs-picture-in-picture') ;
    } else {
      this.removeClass('vjs-picture-in-picture') ;
    }
  }

  /**
   * Handle Tech Enter Picture-in-Picture.
   *
   * @param {EventTarget~Event} event
   *        l'événement enterpictureinpicture qui a déclenché cette fonction
   *
   * @private
   * @listens Tech#enterpictureinpicture
   */
  handleTechEnterPictureInPicture_(event) {
    this.isInPictureInPicture(true) ;
  }

  /**
   * Handle Tech Leave Picture-in-Picture.
   *
   * @param {EventTarget~Event} event
   *        l'événement leavepictureinpicture qui a déclenché cette fonction
   *
   * @private
   * @listens Tech#leavepictureinpicture
   */
  handleTechLeavePictureInPicture_(event) {
    this.isInPictureInPicture(false) ;
  }

  /**
   * Se déclenche lorsqu'une erreur s'est produite lors du chargement d'un fichier audio/vidéo.
   *
   * @private
   * @listens Tech#error
   */
  handleTechError_() {
    const error = this.tech_.error() ;

    this.error(error) ;
  }

  /**
   * Redéclenche l'événement `textdata` qui a été déclenché par le {@link Tech}.
   *
   * @fires Player#textdata
   * @listens Tech#textdata
   * @private
   */
  handleTechTextData_() {
    let data = null ;

    if (arguments.length > 1) {
      data = arguments[1] ;
    }

    /**
     * Se déclenche lorsque nous recevons un événement textdata de tech
     *
     * @event Player#textdata
     * @type {EventTarget~Event}
     */
    this.trigger('textdata', data) ;
  }

  /**
   * Obtention d'un objet pour les valeurs mises en cache.
   *
   * @return {Object}
   *         obtenir le cache d'objets actuel
   */
  getCache() {
    return this.cache_ ;
  }

  /**
   * Réinitialise l'objet de cache interne.
   *
   * L'utilisation de cette fonction en dehors du constructeur du lecteur ou de la méthode de réinitialisation peut
   * ont des effets secondaires inattendus.
   *
   * @private
   */
  resetCache_() {
    this.cache_ = {

      // Pour l'instant, l'heure actuelle n'est pas _vraiment_ mise en cache parce qu'elle est toujours
      // extrait de la technologie (voir : currentTime). Toutefois, par souci d'exhaustivité,
      // nous le mettons à zéro ici pour nous assurer que si nous commençons à mettre en cache
      // il, nous le réinitialisons avec tout le reste.
      l'heure actuelle : 0,
      initTime : 0,
      inactivityTimeout : this.options_.inactivityTimeout,
      durée: NaN,
      dernierVolume : 1,
      lastPlaybackRate : this.defaultPlaybackRate(),
      media : null,
      src : '',
      source : {},
      sources : [],
      playbackRates : [],
      volume : 1
    };
  }

  /**
   * Transmettre des valeurs à la technologie de lecture
   *
   * @param {string} [méthode]
   *        la méthode à appeler
   *
   * @param {Objet} arg
   *        l'argument à transmettre
   *
   * @private
   */
  techCall_(method, arg) {
    // S'il n'est pas encore prêt, appeler la méthode lorsqu'il l'est

    this.ready(function() {
      if (method in middleware.allowedSetters) {
        return middleware.set(this.middleware_, this.tech_, method, arg) ;

      else if (method in middleware.allowedMediators) {
        return middleware.mediate(this.middleware_, this.tech_, method, arg) ;
      }

      essayez {
        if (this.tech_) {
          this.tech_[method](arg) ;
        }
      } catch (e) {
        log(e) ;
        lancer e ;
      }
    }, true) ;
  }

  /**
   * Les appels ne peuvent pas attendre le technicien, et parfois n'ont pas besoin de l'être.
   *
   * @param {string} method
   *        Méthode technique
   *
   * @return {Fonction|undefined}
   *         la méthode ou non définie
   *
   * @private
   */
  techGet_(méthode) {
    if (!this.tech_ || !this.tech_.isReady_) {
      retour ;
    }

    if (method in middleware.allowedGetters) {
      return middleware.get(this.middleware_, this.tech_, method) ;

    else if (method in middleware.allowedMediators) {
      return middleware.mediate(this.middleware_, this.tech_, method) ;
    }

    // Flash aime mourir et se recharger lorsque vous le cachez ou le repositionnez.
    // Dans ce cas, les méthodes de l'objet disparaissent et nous obtenons des erreurs.
    // TODO : Est-ce nécessaire pour les techniciens autres que Flash ?
    // Lorsque cela se produit, nous détectons les erreurs et informons le technicien qu'il n'est plus prêt.
    essayez {
      return this.tech_[method]() ;
    } catch (e) {

      // Lors de la construction de librairies techniques supplémentaires, il se peut qu'une méthode attendue ne soit pas encore définie
      if (this.tech_[method] === undefined) {
        log(`Video.js : méthode ${method} non définie pour la technologie de lecture ${this.techName_}.`, e) ;
        lancer e ;
      }

      // Lorsqu'une méthode n'est pas disponible sur l'objet, une erreur de type (TypeError) est déclenchée
      if (e.name === 'TypeError') {
        log(`Video.js : ${method} unavailable on ${this.techName_} playback technology element.`, e) ;
        this.tech_.isReady_ = false ;
        lancer e ;
      }

      // Si l'erreur est inconnue, il suffit d'enregistrer et de lancer
      log(e) ;
      lancer e ;
    }
  }

  /**
   * Essayez de commencer la lecture à la première occasion.
   *
   * @return {Promesse|non défini}
   *         Renvoie une promesse si le navigateur prend en charge les promesses (ou l'une d'entre elles)
   *         a été passé en option). Cette promesse sera résolue le
   *         la valeur de retour du jeu. S'il n'est pas défini, il remplira la fonction
   *         sinon la chaîne de promesses sera remplie lorsque
   *         la promesse du jeu est tenue.
   */
  play() {
    const PromiseClass = this.options_.Promise || window.Promise ;

    if (PromiseClass) {
      return new PromiseClass((resolve) => {
        this.play_(resolve) ;
      }) ;
    }

    return this.play_() ;
  }

  /**
   * La logique de jeu proprement dite utilise une fonction de rappel qui sera résolue lors de l'exécution de la fonction
   * valeur de retour du jeu. Cela nous permet de résoudre la promesse de jeu s'il y a
   * est un sur les navigateurs modernes.
   *
   * @private
   * @param {Fonction} [callback]
   *        Le callback qui devrait être appelé lorsque le jeu des techniciens est effectivement appelé
   */
  play_(callback = silencePromise) {
    this.playCallbacks_.push(callback) ;

    const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc())) ;

    // traiter les appels à play_ un peu comme la fonction `one` event
    if (this.waitToPlay_) {
      this.off(['ready', 'loadstart'], this.waitToPlay_) ;
      this.waitToPlay_ = null ;
    }

    // si le joueur/technicien n'est pas prêt ou si le serveur lui-même n'est pas prêt
    // mettre en file d'attente un appel à jouer sur `ready` ou `loadstart`
    if (!this.isReady_ || !isSrcReady) {
      this.waitToPlay_ = (e) => {
        this.play_() ;
      };
      this.one(['ready', 'loadstart'], this.waitToPlay_) ;

      // si nous sommes dans Safari, il y a de fortes chances que loadstart se déclenche après la période de temps du geste
      // dans ce cas, nous devons amorcer l'élément vidéo en appelant load pour qu'il soit prêt à temps
      if (!isSrcReady && (browser.IS_ANY_SAFARI || browser.IS_IOS)) {
        this.load() ;
      }
      retour ;
    }

    // Si le lecteur/la technologie est prêt(e) et que nous disposons d'une source, nous pouvons tenter la lecture.
    const val = this.techGet_('play') ;

    // la lecture a été interrompue si la valeur renvoyée est nulle
    if (val === null) {
      this.runPlayTerminatedQueue_() ;
    } else {
      this.runPlayCallbacks_(val) ;
    }
  }

  /**
   * Ces fonctions seront exécutées lorsque le jeu sera terminé. Si le jeu
   * runPlayCallbacks_ est exécuté, ces fonctions ne seront pas exécutées. Cela nous permet
   * de faire la différence entre un jeu interrompu et un appel au jeu.
   */
  runPlayTerminatedQueue_() {
    const queue = this.playTerminatedQueue_.slice(0) ;

    this.playTerminatedQueue_ = [] ;

    queue.forEach(function(q) {
      q() ;
    }) ;
  }

  /**
   * Lorsqu'un appel à la lecture est retardé, nous devons exécuter les opérations suivantes
   * callbacks lorsque le jeu est effectivement appelé sur le technicien. Cette fonction
   * exécute les callbacks qui ont été retardés et accepte la valeur de retour
   * de la technologie.
   *
   * @param {undefined|Promesse} val
   *        La valeur de retour du tech.
   */
  runPlayCallbacks_(val) {
    const callbacks = this.playCallbacks_.slice(0) ;

    this.playCallbacks_ = [] ;
    // effacer le jeu terminé en file d'attente depuis que nous avons terminé un vrai jeu
    this.playTerminatedQueue_ = [] ;

    callbacks.forEach(function(cb) {
      cb(val) ;
    }) ;
  }

  /**
   * Pause de la lecture vidéo
   *
   * @return {Player}
   *         Une référence à l'objet joueur sur lequel cette fonction a été appelée
   */
  pause() {
    this.techCall_('pause') ;
  }

  /**
   * Vérifier si le lecteur est en pause ou s'il doit encore jouer
   *
   * @return {boolean}
   *         - false : si le média est en cours de lecture
   *         - true : si le média n'est pas en cours de lecture
   */
  paused() {
    // L'état initial de paused devrait être true (dans Safari, il est en fait false)
    return (this.techGet_('paused') === false) ? false : true ;
  }

  /**
   * Obtention d'un objet TimeRange représentant les plages de temps actuelles que l'utilisateur
   * a joué.
   *
   * @return {TimeRange}
   *         Un objet d'intervalle de temps qui représente tous les incréments de temps ayant
   *         ont été joués.
   */
  played() {
    return this.techGet_('played') || createTimeRange(0, 0) ;
  }

  /**
   * Indique si l'utilisateur est en train de "frotter" ou non. Le lavage est
   * lorsque l'utilisateur a cliqué sur la poignée de la barre de progression et qu'il est
   * en le faisant glisser le long de la barre de progression.
   *
   * @param {boolean} [isScrubbing]
   *        si l'utilisateur est ou n'est pas en train de se laver
   *
   * @return {boolean}
   *         L'intérêt de l'épuration lors de l'obtention
   */
  scrubbing(isScrubbing) {
    if (typeof isScrubbing === 'undefined') {
      return this.scrubbing_ ;
    }
    this.scrubbing_ = !!isScrubbing ;
    this.techCall_('setScrubbing', this.scrubbing_) ;

    if (isScrubbing) {
      this.addClass('vjs-scrubbing') ;
    } else {
      this.removeClass('vjs-scrubbing') ;
    }
  }

  /**
   * Obtenir ou régler l'heure actuelle (en secondes)
   *
   * @param {nombre|chaîne} [secondes]
   *        Temps de recherche en secondes
   *
   * @return {number}
   *         - l'heure actuelle en secondes lorsque l'on obtient
   */
  currentTime(seconds) {
    if (typeof seconds !== 'undefined') {
      if (seconds < 0) {
        secondes = 0 ;
      }
      if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) {
        this.cache_.initTime = seconds ;
        this.off('canplay', this.boundApplyInitTime_) ;
        this.one('canplay', this.boundApplyInitTime_) ;
        retour ;
      }
      this.techCall_('setCurrentTime', seconds) ;
      this.cache_.initTime = 0 ;
      retour ;
    }

    // met en cache la dernière heure courante et la renvoie. 0 secondes par défaut
    //
    // La mise en cache de l'heure actuelle a pour but d'éviter une quantité massive de lectures sur la page d'accueil de la technologie
    // currentTime lors de l'épuration, mais il se peut qu'il n'y ait pas beaucoup d'avantages en termes de performances après tout.
    // Doit être testé. Il faut aussi que quelque chose lise l'heure actuelle, sinon la mémoire cache
    // ne sont jamais mises à jour.
    this.cache_.currentTime = (this.techGet_('currentTime') || 0) ;
    return this.cache_.currentTime ;
  }

  /**
   * Appliquer la valeur de initTime stockée dans le cache en tant que currentTime.
   *
   * @private
   */
  applyInitTime_() {
    this.currentTime(this.cache_.initTime) ;
  }

  /**
   * Normalement, il s'agit de la durée de la vidéo en secondes ;
   * dans tous les cas d'utilisation, sauf les plus rares, un argument ne sera PAS transmis à la méthode
   *
   * > **NOTE** : Le chargement de la vidéo doit avoir commencé pour que la durée puisse être calculée
   * connu, et en fonction du comportement de préchargement, il peut ne pas être connu avant que la vidéo ne commence
   * jouer.
   *
   * @fires Player#durationchange
   *
   * @param {number} [seconds]
   *        Durée de la vidéo à définir en secondes
   *
   * @return {number}
   *         - La durée de la vidéo en secondes lors de l'obtention
   */
  durée(secondes) {
    if (seconds === undefined) {
      // renvoie NaN si la durée n'est pas connue
      return this.cache_.duration !== undefined ? this.cache_.duration : NaN ;
    }

    seconds = parseFloat(seconds) ;

    // Standardisation de l'infini pour signaler que la vidéo est en direct
    if (seconds < 0) {
      secondes = Infini ;
    }

    if (seconds !== this.cache_.duration) {
      // Mettre en cache la dernière valeur définie pour optimiser l'épuration (esp. Flash)
      // TODO : Requis pour les techniciens autres que Flash ?
      this.cache_.duration = seconds ;

      if (seconds === Infinity) {
        this.addClass('vjs-live') ;
      } else {
        this.removeClass('vjs-live') ;
      }
      if (!isNaN(seconds)) {
        // Ne déclenche pas le changement de durée si la valeur de la durée n'est pas connue.
        // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}

        /**
         * @event Player#durationchange
         * @type {EventTarget~Event}
         */
        this.trigger('durationchange') ;
      }
    }
  }

  /**
   * Calcule le temps restant dans la vidéo. Pas de partie
   * de l'API vidéo native.
   *
   * @return {number}
   *         Le temps restant en secondes
   */
  remainingTime() {
    return this.duration() - this.currentTime() ;
  }

  /**
   * Une fonction de temps restant qui est destinée à être utilisée lorsque
   * l'heure doit être affichée directement à l'utilisateur.
   *
   * @return {number}
   *         Le temps restant arrondi en secondes
   */
  remainingTimeDisplay() {
    return Math.floor(this.duration()) - Math.floor(this.currentTime()) ;
  }

  //
  // Une sorte de tableau des parties de la vidéo qui ont été téléchargées.

  /**
   * Obtention d'un objet TimeRange contenant un tableau des durées de la vidéo
   * qui ont été téléchargés. Si vous ne voulez que le pourcentage de la
   * vidéo qui a été téléchargée, utilisez bufferedPercent.
   *
   * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
   *
   * @return {TimeRange}
   *         Un objet TimeRange fictif (conformément à la spécification HTML)
   */
  buffered() {
    let buffered = this.techGet_('buffered') ;

    if (!buffered || !buffered.length) {
      buffered = createTimeRange(0, 0) ;
    }

    retourner buffered ;
  }

  /**
   * Obtenir le pourcentage (en décimal) de la vidéo qui a été téléchargée.
   * Cette méthode ne fait pas partie de l'API vidéo HTML native.
   *
   * @return {number}
   *         Une décimale entre 0 et 1 représentant le pourcentage
   *         qui est mis en mémoire tampon 0 étant 0 % et 1 étant 100 %
   */
  bufferedPercent() {
    return bufferedPercent(this.buffered(), this.duration()) ;
  }

  /**
   * Obtenir l'heure de fin de la dernière plage temporelle mise en mémoire tampon
   * Elle est utilisée dans la barre de progression pour encapsuler toutes les plages de temps.
   *
   * @return {number}
   *         Fin de la dernière plage de temps mise en mémoire tampon
   */
  bufferedEnd() {
    const buffered = this.buffered() ;
    const duration = this.duration() ;
    let end = buffered.end(buffered.length - 1) ;

    if (end > duration) {
      fin = durée ;
    }

    retour fin ;
  }

  /**
   * Obtenir ou définir le volume actuel du support
   *
   * @param {number} [percentAsDecimal]
   *         Le nouveau volume en pourcentage décimal :
   *         - 0 est en sourdine/0%/off
   *         - 1.0 est 100%/plein
   *         - 0,5 correspond à un demi-volume ou à 50 %
   *
   * @return {number}
   *         Le volume actuel en pourcentage lors de l'obtention
   */
  volume(percentAsDecimal) {
    laisser vol ;

    if (percentAsDecimal !== undefined) {
      // Forcer la valeur entre 0 et 1
      vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))) ;
      this.cache_.volume = vol ;
      this.techCall_('setVolume', vol) ;

      if (vol > 0) {
        this.lastVolume_(vol) ;
      }

      retour ;
    }

    // La valeur par défaut est 1 lorsque le volume actuel est renvoyé.
    vol = parseFloat(this.techGet_('volume')) ;
    return (isNaN(vol)) ? 1 : vol ;
  }

  /**
   * Obtenir l'état actuel de la sourdine, ou activer ou désactiver la sourdine
   *
   * @param {boolean} [muted]
   *        - true pour mettre en sourdine
   *        - faux pour rétablir le son
   *
   * @return {boolean}
   *         - vrai si la sourdine est activée et si l'on obtient
   *         - faux si la sourdine est désactivée et si l'on obtient
   */
  muted(muted) {
    if (muted !== undefined) {
      this.techCall_('setMuted', muted) ;
      retour ;
    }
    return this.techGet_('muted') || false ;
  }

  /**
   * Obtenir l'état actuel de defaultMuted, ou activer ou désactiver defaultMuted. defaultMuted
   * indique l'état de sourdine lors de la lecture initiale.
   *
   * ``js
   *   var myPlayer = videojs('some-player-id') ;
   *
   *   myPlayer.src("http://www.example.com/path/to/video.mp4") ;
   *
   *   // obtenir, devrait être faux
   *   console.log(myPlayer.defaultMuted()) ;
   *   // mis à true (vrai)
   *   myPlayer.defaultMuted(true) ;
   *   // get devrait être vrai
   *   console.log(myPlayer.defaultMuted()) ;
   * ```
   *
   * @param {boolean} [defaultMuted]
   *        - true pour mettre en sourdine
   *        - faux pour rétablir le son
   *
   * @return {boolean|Player}
   *         - true si defaultMuted est activé et obtient
   *         - false si defaultMuted est désactivé et obtient
   *         - Une référence au lecteur actuel lors du réglage
   */
  defaultMuted(defaultMuted) {
    if (defaultMuted !== undefined) {
      return this.techCall_('setDefaultMuted', defaultMuted) ;
    }
    return this.techGet_('defaultMuted') || false ;
  }

  /**
   * Obtenir le dernier volume ou le définir
   *
   * @param {number} [percentAsDecimal]
   *         Le nouveau dernier volume en pourcentage décimal :
   *         - 0 est en sourdine/0%/off
   *         - 1.0 est 100%/plein
   *         - 0,5 correspond à un demi-volume ou à 50 %
   *
   * @return {number}
   *         la valeur actuelle de lastVolume en pourcentage lors de l'obtention de
   *
   * @private
   */
  lastVolume_(percentAsDecimal) {
    if (percentAsDecimal !== undefined && percentAsDecimal !== 0) {
      this.cache_.lastVolume = percentAsDecimal ;
      retour ;
    }
    return this.cache_.lastVolume ;
  }

  /**
   * Vérifier si la technologie actuelle peut supporter le plein écran natif
   * (par exemple, avec des contrôles intégrés comme iOS)
   *
   * @return {boolean}
   *         si le plein écran natif est pris en charge
   */
  supportsFullScreen() {
    return this.techGet_('supportsFullScreen') || false ;
  }

  /**
   * Vérifier si le lecteur est en mode plein écran ou indiquer au lecteur qu'il est en mode plein écran
   * est ou n'est pas en mode plein écran.
   *
   * > NOTE : Depuis la dernière version de la spécification HTML5, isFullscreen n'est plus une norme officielle
   * et le document.fullscreenElement est utilisé à la place. Mais isFullscreen est
   * reste une propriété précieuse pour le fonctionnement interne des joueurs.
   *
   * @param {boolean} [isFS]
   *         Définir l'état actuel de plein écran du joueur
   *
   * @return {boolean}
   *         - true si le fullscreen est activé et obtient
   *         - false si le fullscreen est désactivé et obtient
   */
  isFullscreen(isFS) {
    if (isFS !== undefined) {
      const oldValue = this.isFullscreen_ ;

      this.isFullscreen_ = Boolean(isFS) ;

      // si nous avons changé l'état du fullscreen et que nous sommes en mode préfixé, déclencher le fullscreenchange
      // c'est le seul endroit où nous déclenchons des événements de changement d'écran pour les navigateurs plus anciens
      // le mode fullWindow est traité comme un événement préfixé et recevra également un événement fullscreenchange
      if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
        /**
           * @event Player#fullscreenchange
           * @type {EventTarget~Event}
           */
        this.trigger('fullscreenchange') ;
      }

      this.toggleFullscreenClass_() ;
      retour ;
    }
    return this.isFullscreen_ ;
  }

  /**
   * Augmenter la taille de la vidéo en plein écran
   * Dans certains navigateurs, le plein écran n'est pas pris en charge en mode natif
   * le mode "pleine fenêtre", dans lequel la vidéo remplit la fenêtre du navigateur.
   * Dans les navigateurs et les appareils qui prennent en charge le plein écran en mode natif, il arrive que l'option
   * les contrôles par défaut du navigateur seront affichés, et non l'habillage personnalisé Video.js.
   * Cela inclut la plupart des appareils mobiles (iOS, Android) et les anciennes versions de
   * Safari.
   *
   * @param {Object} [fullscreenOptions]
   *         Remplacer les options de plein écran du lecteur
   *
   * @fires Player#fullscreenchange
   */
  requestFullscreen(fullscreenOptions) {
    const PromiseClass = this.options_.Promise || window.Promise ;

    if (PromiseClass) {
      const self = this ;

      return new PromiseClass((resolve, reject) => {
        function offHandler() {
          self.off('fullscreenerror', errorHandler) ;
          self.off('fullscreenchange', changeHandler) ;
        }
        function changeHandler() {
          offHandler() ;
          resolve() ;
        }
        function errorHandler(e, err) {
          offHandler() ;
          reject(err) ;
        }

        self.one('fullscreenchange', changeHandler) ;
        self.one('fullscreenerror', errorHandler) ;

        const promise = self.requestFullscreenHelper_(fullscreenOptions) ;

        if (promise) {
          promise.then(offHandler, offHandler) ;
          promise.then(resolve, reject) ;
        }
      }) ;
    }

    return this.requestFullscreenHelper_() ;
  }

  requestFullscreenHelper_(fullscreenOptions) {
    let fsOptions ;

    // Ne transmettre les options de plein écran à requestFullscreen que dans les navigateurs conformes à la spécification.
    // Utiliser les valeurs par défaut ou l'option configurée par le lecteur, à moins qu'elle ne soit transmise directement à cette méthode.
    if (!this.fsApi_.prefixed) {
      fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {} ;
      if (fullscreenOptions !== undefined) {
        fsOptions = fullscreenOptions ;
      }
    }

    // Cette méthode fonctionne comme suit :
    // 1. si une interface utilisateur pour le plein écran est disponible, l'utiliser
    // 1. appeler requestFullscreen avec des options potentielles
    // 2. si nous avons obtenu une promesse, nous l'utilisons pour mettre à jour isFullscreen()
    // 2. sinon, si le tech supporte le fullscreen, appeler `enterFullScreen` sur lui.
    // Ceci est particulièrement utilisé pour l'iPhone, les anciens iPads, et les navigateurs non-Safari sur iOS.
    // 3. sinon, utiliser le mode "fullWindow
    if (this.fsApi_.requestFullscreen) {
      const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions) ;

      if (promise) {
        promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false)) ;
      }

      retourner la promesse ;
    } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
      // nous ne pouvons pas mettre les contrôles video.js en plein écran mais nous pouvons passer en plein écran
      // avec des contrôles natifs
      this.techCall_('enterFullScreen') ;
    } else {
      // le plein écran n'est pas pris en charge, nous allons donc étirer l'élément vidéo jusqu'à
      // remplir la fenêtre de visualisation
      this.enterFullWindow() ;
    }
  }

  /**
   * Ramener la vidéo à sa taille normale après avoir été en mode plein écran
   *
   * @fires Player#fullscreenchange
   */
  exitFullscreen() {
    const PromiseClass = this.options_.Promise || window.Promise ;

    if (PromiseClass) {
      const self = this ;

      return new PromiseClass((resolve, reject) => {
        function offHandler() {
          self.off('fullscreenerror', errorHandler) ;
          self.off('fullscreenchange', changeHandler) ;
        }
        function changeHandler() {
          offHandler() ;
          resolve() ;
        }
        function errorHandler(e, err) {
          offHandler() ;
          reject(err) ;
        }

        self.one('fullscreenchange', changeHandler) ;
        self.one('fullscreenerror', errorHandler) ;

        const promise = self.exitFullscreenHelper_() ;

        if (promise) {
          promise.then(offHandler, offHandler) ;
          // mappage de la promesse à nos méthodes de résolution/refus
          promise.then(resolve, reject) ;
        }
      }) ;
    }

    return this.exitFullscreenHelper_() ;
  }

  exitFullscreenHelper_() {
    if (this.fsApi_.requestFullscreen) {
      const promise = document[this.fsApi_.exitFullscreen]() ;

      if (promise) {
        // nous séparons la promesse ici, donc nous voulons attraper la promesse
        // erreur potentielle afin que cette chaîne n'ait pas d'erreurs non gérées
        silencePromise(promise.then(() => this.isFullscreen(false))) ;
      }

      retourner la promesse ;
    } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
      this.techCall_('exitFullScreen') ;
    } else {
      this.exitFullWindow() ;
    }
  }

  /**
   * Lorsque le plein écran n'est pas pris en charge, nous pouvons étirer l'image
   * le conteneur vidéo est aussi large que le navigateur le permet.
   *
   * @fires Player#enterFullWindow
   */
  enterFullWindow() {
    this.isFullscreen(true) ;
    this.isFullWindow = true ;

    // Stockage de la valeur de débordement de la documentation originale pour revenir à la valeur de débordement lorsque le plein écran est désactivé
    this.docOrigOverflow = document.documentElement.style.overflow ;

    // Ajout d'un récepteur pour la touche esc pour quitter le plein écran
    Events.on(document, 'keydown', this.boundFullWindowOnEscKey_) ;

    // Masquer les barres de défilement
    document.documentElement.style.overflow = 'hidden' ;

    // Appliquer les styles de plein écran
    Dom.addClass(document.body, 'vjs-full-window') ;

    /**
     * @event Player#enterFullWindow
     * @type {EventTarget~Event}
     */
    this.trigger('enterFullWindow') ;
  }

  /**
   * Vérifier s'il y a un appel à sortir de la fenêtre complète ou
   * plein écran sur la touche ESC
   *
   * @param {string} event
   *        Événement à vérifier en cas de pression sur une touche
   */
  fullWindowOnEscKey(event) {
    if (keycode.isEventKey(event, 'Esc')) {
      if (this.isFullscreen() === true) {
        if (!this.isFullWindow) {
          this.exitFullscreen() ;
        } else {
          this.exitFullWindow() ;
        }
      }
    }
  }

  /**
   * Quitter la fenêtre complète
   *
   * @fires Player#exitFullWindow
   */
  exitFullWindow() {
    this.isFullscreen(false) ;
    this.isFullWindow = false ;
    Events.off(document, 'keydown', this.boundFullWindowOnEscKey_) ;

    // Désactiver les barres de défilement.
    document.documentElement.style.overflow = this.docOrigOverflow ;

    // Supprimer les styles de plein écran
    Dom.removeClass(document.body, 'vjs-full-window') ;

    // Redimensionner la boîte, le contrôleur et l'affiche aux tailles d'origine
    // this.positionAll() ;
    /**
     * @event Player#exitFullWindow
     * @type {EventTarget~Event}
     */
    this.trigger('exitFullWindow') ;
  }

  /**
   * Désactiver le mode Image dans l'image.
   *
   * @param {boolean} value
   *                  - true désactive le mode Image dans l'image (Picture-in-Picture)
   *                  - false active le mode Image dans l'image
   */
  disablePictureInPicture(value) {
    if (value === undefined) {
      return this.techGet_('disablePictureInPicture') ;
    }
    this.techCall_('setDisablePictureInPicture', value) ;
    this.options_.disablePictureInPicture = value ;
    this.trigger('disablepictureinpicturechanged') ;
  }

  /**
   * Vérifiez si le lecteur est en mode Image dans l'image ou indiquez au lecteur qu'il est en mode Image dans l'image
   * est ou n'est pas en mode Image dans l'image.
   *
   * @param {boolean} [isPiP]
   *         Définir l'état actuel de l'image dans l'image du lecteur
   *
   * @return {boolean}
   *         - vrai si l'incrustation d'image est activée et en cours d'acquisition
   *         - faux si l'incrustation d'image est désactivée et en cours d'acquisition
   */
  isInPictureInPicture(isPiP) {
    if (isPiP !== undefined) {
      this.isInPictureInPicture_ = !!isPiP ;
      this.togglePictureInPictureClass_() ;
      retour ;
    }
    return !!this.isInPictureInPicture_ ;
  }

  /**
   * Créer une fenêtre vidéo flottante toujours au-dessus des autres fenêtres afin que les utilisateurs puissent
   * continuer à consommer des médias tout en interagissant avec d'autres sites de contenu, ou
   * sur leur appareil.
   *
   * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
   *
   * @fires Player#enterpictureinpicture
   *
   * @return {Promesse}
   *         Une promesse avec une fenêtre d'image dans l'image.
   */
  requestPictureInPicture() {
    if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) {
      /**
       * Cet événement se déclenche lorsque le lecteur passe en mode image par image
       *
       * @event Player#enterpictureinpicture
       * @type {EventTarget~Event}
       */
      return this.techGet_('requestPictureInPicture') ;
    }
  }

  /**
   * Quitter le mode Image dans l'image.
   *
   * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
   *
   * @fires Player#leavepictureinpicture
   *
   * @return {Promesse}
   *         Une promesse.
   */
  exitPictureInPicture() {
    if ("pictureInPictureEnabled" in document) {
      /**
       * Cet événement se déclenche lorsque le joueur quitte le mode image par image
       *
       * @event Player#leavepictureinpicture
       * @type {EventTarget~Event}
       */
      return document.exitPictureInPicture() ;
    }
  }

  /**
   * Appelé lorsque ce lecteur est au centre de l'attention et qu'une touche est enfoncée, ou lorsque
   * tout composant de ce lecteur reçoit une pression de touche qu'il ne gère pas.
   * Cela permet d'utiliser des raccourcis clavier pour l'ensemble du joueur (tels que définis ci-dessous, ou optionnellement
   * par une fonction externe).
   *
   * @param {EventTarget~Event} event
   *        L'événement `keydown` qui a provoqué l'appel de cette fonction.
   *
   * @listens keydown
   */
  handleKeyDown(event) {
    const {userActions} = this.options_ ;

    // S'en sortir si les raccourcis clavier ne sont pas configurés.
    if (!userActions || !userActions.hotkeys) {
      retour ;
    }

    // Fonction qui détermine s'il faut ou non exclure un élément de la liste des éléments à exclure de la liste des éléments à exclure de la liste des éléments à exclure
    // gestion des touches de raccourci.
    const excludeElement = (el) => {
      const tagName = el.tagName.toLowerCase() ;

      // Le premier test, le plus simple, concerne les éléments `contentable`.
      if (el.isContentEditable) {
        retourner vrai ;
      }

      // Les entrées correspondant à ces types déclencheront toujours la gestion des touches de raccourci en tant que
      // ce ne sont pas des entrées de texte.
      const allowedInputTypes = [
        'bouton',
        case à cocher",
        caché",
        'radio',
        'reset',
        soumettre
      ] ;

      if (tagName === 'input') {
        return allowedInputTypes.indexOf(el.type) === -1 ;
      }

      // Le test final se fait par nom de balise. Ces étiquettes seront entièrement exclues.
      const excludedTags = ['textarea'] ;

      return excludedTags.indexOf(tagName) !== -1 ;
    };

    // S'en sortir si l'utilisateur se concentre sur un élément de formulaire interactif.
    if (excludeElement(this.el_.ownerDocument.activeElement)) {
      retour ;
    }

    if (typeof userActions.hotkeys === 'function') {
      userActions.hotkeys.call(this, event) ;
    } else {
      this.handleHotkeys(event) ;
    }
  }

  /**
   * Appelé lorsque ce lecteur reçoit un événement de raccourci clavier.
   * Les raccourcis clavier pris en charge sont les suivants :
   *
   *   f - basculer en mode plein écran
   *   m - activer la mise en sourdine
   *   k ou Espace - basculer lecture/pause
   *
   * @param {EventTarget~Event} event
   *        L'événement `keydown` qui a provoqué l'appel de cette fonction.
   */
  handleHotkeys(event) {
    const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {} ;

    // définir fullscreenKey, muteKey, playPauseKey à partir de `hotkeys`, utiliser les valeurs par défaut si elles ne sont pas définies
    const {
      fullscreenKey = keydownEvent => keycode.isEventKey(keydownEvent, 'f'),
      muteKey = keydownEvent => keycode.isEventKey(keydownEvent, 'm'),
      playPauseKey = keydownEvent => (keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, 'Space'))
    } = hotkeys ;

    if (fullscreenKey.call(this, event)) {
      event.preventDefault() ;
      event.stopPropagation() ;

      const FSToggle = Component.getComponent('FullscreenToggle') ;

      if (document[this.fsApi_.fullscreenEnabled] !== false) {
        FSToggle.prototype.handleClick.call(this, event) ;
      }

    } else if (muteKey.call(this, event)) {
      event.preventDefault() ;
      event.stopPropagation() ;

      const MuteToggle = Component.getComponent('MuteToggle') ;

      MuteToggle.prototype.handleClick.call(this, event) ;

    } else if (playPauseKey.call(this, event)) {
      event.preventDefault() ;
      event.stopPropagation() ;

      const PlayToggle = Component.getComponent('PlayToggle') ;

      PlayToggle.prototype.handleClick.call(this, event) ;
    }
  }

  /**
   * Vérifier si le lecteur peut jouer un type d'image donné
   *
   * voir https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
   *
   * @param {string} type
   *        Le type d'image à vérifier
   *
   * @return {string}
   *         "probablement", "peut-être" ou '' (chaîne vide)
   */
  canPlayType(type) {
    peuvent le faire ;

    // Boucle sur chaque technologie de lecture dans l'ordre des options
    for (let i = 0, j = this.options_.techOrder ; i < j.length ; i++) {
      const techName = j[i] ;
      let tech = Tech.getTech(techName) ;

      // Prise en charge de l'ancien comportement des technologies enregistrées en tant que composants.
      // Supprimer une fois que ce comportement déprécié est supprimé.
      if (!tech) {
        tech = Component.getComponent(techName) ;
      }

      // Vérifier si la technologie actuelle est définie avant de continuer
      if (!tech) {
        log.error(`La technologie "${techName}" est indéfinie. La vérification de la prise en charge du navigateur a été omise pour cette technologie ;)
        continuer ;
      }

      // Vérifier si le navigateur supporte cette technologie
      if (tech.isSupported()) {
        can = tech.canPlayType(type) ;

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

    retourner '' ;
  }

  /**
   * Sélectionner la source en fonction de l'ordre technique ou de l'ordre de service
   * Utilise la sélection de l'ordre des sources si `options.sourceOrder` est vrai. Dans le cas contraire,
   * sélection par défaut de l'ordre technique
   *
   * @param {Array} sources
   *        Les sources d'un média
   *
   * @return {Object|boolean}
   *         Objet de l'ordre source et technique ou faux
   */
  selectSource(sources) {
    // Obtenir uniquement les technologies spécifiées dans `techOrder` qui existent et sont prises en charge par l'entreprise
    // plateforme actuelle
    const techs =
      this.options_.techOrder
        .map((techName) => {
          return [techName, Tech.getTech(techName)] ;
        })
        .filter(([techName, tech]) => {
          // Vérifier si la technologie actuelle est définie avant de continuer
          if (tech) {
            // Vérifier si le navigateur supporte cette technologie
            return tech.isSupported() ;
          }

          log.error(`La technologie "${techName}" est indéfinie. La vérification de la prise en charge du navigateur a été omise pour cette technologie ;)
          retourner faux ;
        }) ;

    // Itère sur chaque élément `innerArray` une fois par élément `outerArray` et exécute
    // `tester` avec les deux. Si `tester` renvoie une valeur non erronée, on quitte prématurément et on renvoie
    // cette valeur.
    const findFirstPassingTechSourcePair = function(outerArray, innerArray, tester) {
      ont été trouvés ;

      outerArray.some((outerChoice) => {
        return innerArray.some((innerChoice) => {
          found = tester(outerChoice, innerChoice) ;

          if (found) {
            retourner vrai ;
          }
        }) ;
      }) ;

      retour trouvé ;
    };

    let foundSourceAndTech ;
    const flip = (fn) => (a, b) => fn(b, a) ;
    const finder = ([techName, tech], source) => {
      if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
        return {source, tech : techName} ;
      }
    };

    // En fonction de la véracité de `options.sourceOrder`, nous intervertissons l'ordre des technologies et des sources
    // de les sélectionner en fonction de leur priorité.
    if (this.options_.sourceOrder) {
      // Ordonnancement à la source d'abord
      foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder)) ;
    } else {
      // La commande de technologie d'abord
      foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder) ;
    }

    return foundSourceAndTech || false ;
  }

  /**
   * Exécute la logique de réglage et d'obtention de la source
   *
   * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
   *        Un SourceObject, un tableau de SourceObjects, ou une chaîne de caractères faisant référence à
   *        une URL vers une source de média. Il est _hautement recommandé_ qu'un objet
   *        ou un tableau d'objets est utilisé ici, de sorte que la sélection de la source
   *        les algorithmes peuvent prendre en compte le `type`.
   *
   *        Si elle n'est pas fournie, cette méthode agit comme un getter.
   * @param {boolean} isRetry
   *        Indique si cette action est appelée en interne à la suite d'une nouvelle tentative
   *
   * @return {string|undefined}
   *         Si l'argument `source` est manquant, renvoie la source courante
   *         URL. Sinon, ne renvoie rien ou non défini.
   */
  handleSrc_(source, isRetry) {
    // utilisation du getter
    if (typeof source === 'undefined') {
      return this.cache_.src || '' ;
    }

    // Réinitialiser le comportement de réessai pour la nouvelle source
    if (this.resetRetryOnError_) {
      this.resetRetryOnError_() ;
    }

    // filtrer les sources non valides et transformer notre source en
    // un tableau d'objets sources
    const sources = filterSource(source) ;

    // si une source a été transmise, elle n'est pas valide car
    // il a été filtré en un tableau de longueur nulle. Nous devons donc
    // afficher une erreur
    if (!sources.length) {
      this.setTimeout(function() {
        this.error({ code : 4, message : this.options_.notSupportedMessage }) ;
      }, 0) ;
      retour ;
    }

    // sources initiales
    this.changingSrc_ = true ;

    // Ne mettre à jour la liste des sources en cache que si nous n'essayons pas de trouver une nouvelle source après une erreur,
    // puisque dans ce cas, nous voulons inclure la ou les source(s) défaillante(s) dans le cache
    if (!isRetry) {
      this.cache_.sources = sources ;
    }

    this.updateSourceCaches_(sources[0]) ;

    // middlewareSource est la source après qu'elle a été modifiée par le middleware
    middleware.setSource(this, sources[0], (middlewareSource, mws) => {
      this.middleware_ = mws ;

      // puisque sourceSet est asynchrone, nous devons à nouveau mettre à jour le cache après avoir sélectionné une source puisque
      // la source sélectionnée peut être désordonnée par rapport à la mise à jour du cache qui précède ce callback.
      if (!isRetry) {
        this.cache_.sources = sources ;
      }

      this.updateSourceCaches_(middlewareSource) ;

      const err = this.src_(middlewareSource) ;

      if (err) {
        if (sources.length > 1) {
          return this.handleSrc_(sources.slice(1)) ;
        }

        this.changingSrc_ = false ;

        // Nous devons envelopper ceci dans un délai d'attente pour donner aux gens une chance d'ajouter des gestionnaires d'événements d'erreur
        this.setTimeout(function() {
          this.error({ code : 4, message : this.options_.notSupportedMessage }) ;
        }, 0) ;

        // nous n'avons pas pu trouver de tech approprié, mais notifions tout de même le délégué que c'est ça
        // cela nécessite un meilleur commentaire sur la raison de cette nécessité
        this.triggerReady() ;

        retour ;
      }

      middleware.setTech(mws, this.tech_) ;
    }) ;

    // Essayer une autre source disponible si celle-ci échoue avant la lecture.
    if (this.options_.retryOnError && sources.length > 1) {
      const retry = () => {
        // Supprimer la fenêtre modale d'erreur
        this.error(null) ;
        this.handleSrc_(sources.slice(1), true) ;
      };

      const stopListeningForErrors = () => {
        this.off('error', retry) ;
      };

      this.one('error', retry) ;
      this.one('playing', stopListeningForErrors) ;

      this.resetRetryOnError_ = () => {
        this.off('error', retry) ;
        this.off('playing', stopListeningForErrors) ;
      };
    }
  }

  /**
   * Permet d'obtenir ou de définir la source vidéo.
   *
   * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
   *        Un SourceObject, un tableau de SourceObjects, ou une chaîne de caractères faisant référence à
   *        une URL vers une source de média. Il est _hautement recommandé_ qu'un objet
   *        ou un tableau d'objets est utilisé ici, de sorte que la sélection de la source
   *        les algorithmes peuvent prendre en compte le `type`.
   *
   *        Si elle n'est pas fournie, cette méthode agit comme un getter.
   *
   * @return {string|undefined}
   *         Si l'argument `source` est manquant, renvoie la source courante
   *         URL. Sinon, ne renvoie rien ou non défini.
   */
  src(source) {
    return this.handleSrc_(source, false) ;
  }

  /**
   * Définit l'objet source sur le tech, renvoie un booléen qui indique si
   * il y a un technicien qui peut jouer la source ou non
   *
   * @param {Tech~SourceObject} source
   *        L'objet source à placer sur le Tech
   *
   * @return {boolean}
   *         - Vrai s'il n'y a pas de technicien pour lire cette source
   *         - Faux sinon
   *
   * @private
   */
  src_(source) {
    const sourceTech = this.selectSource([source]) ;

    if (!sourceTech) {
      retourner vrai ;
    }

    if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
      this.changingSrc_ = true ;
      // charger cette technologie avec la source choisie
      this.loadTech_(sourceTech.tech, sourceTech.source) ;
      this.tech_.ready(() => {
        this.changingSrc_ = false ;
      }) ;
      retourner faux ;
    }

    // attendre que le technicien soit prêt à régler la source
    // et le définir de manière synchrone si possible (#2326)
    this.ready(function() {

      // La méthode setSource tech a été ajoutée avec les gestionnaires de sources
      // les techniciens plus âgés ne le prendront pas en charge
      // Nous devons vérifier le prototype direct pour le cas où les sous-classes
      // of the tech do not support source handlers (en anglais)
      if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
        this.techCall_('setSource', source) ;
      } else {
        this.techCall_('src', source.src) ;
      }

      this.changingSrc_ = false ;
    }, true) ;

    retourner faux ;
  }

  /**
   * Commencer à charger les données src.
   */
  load() {
    this.techCall_('load') ;
  }

  /**
   * Réinitialiser le lecteur. Charge la première technologie dans le techOrder,
   * supprime toutes les pistes de texte dans la `tech` existante,
   * et appelle le `reset` sur le `tech`.
   */
  reset() {
    const PromiseClass = this.options_.Promise || window.Promise ;

    if (this.paused() || !PromiseClass) {
      this.doReset_() ;
    } else {
      const playPromise = this.play() ;

      silencePromesse(playPromise.then(() => this.doReset_())) ;
    }
  }

  doReset_() {
    if (this.tech_) {
      this.tech_.clearTracks('text') ;
    }
    this.resetCache_() ;
    this.poster('') ;
    this.loadTech_(this.options_.techOrder[0], null) ;
    this.techCall_('reset') ;
    this.resetControlBarUI_() ;
    if (isEvented(this)) {
      this.trigger('playerreset') ;
    }
  }

  /**
   * Réinitialiser l'interface utilisateur de la barre de contrôle en appelant les sous-méthodes qui réinitialisent l'interface utilisateur de la barre de contrôle
   * tous les composants de la barre de contrôle
   */
  resetControlBarUI_() {
    this.resetProgressBar_() ;
    this.resetPlaybackRate_() ;
    this.resetVolumeBar_() ;
  }

  /**
   * Réinitialiser la progression de la technologie pour que la barre de progression soit réinitialisée dans l'interface utilisateur
   */
  resetProgressBar_() {
    this.currentTime(0) ;

    const { durationDisplay, remainingTimeDisplay } = this.controlBar || {} ;

    if (durationDisplay) {
      durationDisplay.updateContent() ;
    }

    if (remainingTimeDisplay) {
      remainingTimeDisplay.updateContent() ;
    }
  }

  /**
   * Réinitialiser le rapport de lecture
   */
  resetPlaybackRate_() {
    this.playbackRate(this.defaultPlaybackRate()) ;
    this.handleTechRateChange_() ;
  }

  /**
   * Réinitialiser la barre de volume
   */
  resetVolumeBar_() {
    ce.volume(1.0) ;
    this.trigger('volumechange') ;
  }

  /**
   * Renvoie tous les objets sources actuels.
   *
   * @return {Tech~SourceObject[]}
   *         Les objets sources actuels
   */
  currentSources() {
    const source = this.currentSource() ;
    const sources = [] ;

    // suppose `{}` ou `{ src }`
    if (Object.keys(source).length !== 0) {
      sources.push(source) ;
    }

    return this.cache_.sources || sources ;
  }

  /**
   * Renvoie l'objet source actuel.
   *
   * @return {Tech~SourceObject}
   *         L'objet source actuel
   */
  currentSource() {
    return this.cache_.source || {} ;
  }

  /**
   * Renvoie l'URL complète de la valeur source actuelle, par exemple http://mysite.com/video.mp4
   * Peut être utilisé en conjonction avec `currentType` pour aider à reconstruire l'objet source actuel.
   *
   * @return {string}
   *         La source de courant
   */
  currentSrc() {
    return this.currentSource() && this.currentSource().src || '' ;
  }

  /**
   * Obtenir le type de source actuel, par exemple vidéo/mp4
   * Cela peut vous permettre de reconstruire l'objet source actuel afin de pouvoir charger le même
   * source et technologie plus tard
   *
   * @return {string}
   *         Le type MIME source
   */
  currentType() {
    return this.currentSource() && this.currentSource().type || '' ;
  }

  /**
   * Obtenir ou définir l'attribut de précharge
   *
   * @param {boolean} [valeur]
   *        - true signifie que nous devons précharger
   *        - false signifie que nous ne devons pas précharger
   *
   * @return {string}
   *         La valeur de l'attribut de préchargement lors de l'obtention de
   */
  preload(value) {
    if (value !== undefined) {
      this.techCall_('setPreload', value) ;
      this.options_.preload = value ;
      retour ;
    }
    return this.techGet_('preload') ;
  }

  /**
   * Obtenir ou définir l'option de lecture automatique. Lorsqu'il s'agit d'un booléen, il s'agit de
   * modifier l'attribut sur le tech. Lorsqu'il s'agit d'une chaîne de caractères, l'attribut
   * la technologie sera supprimée et `Player` gérera l'autoplay lors des démarrages de chargement.
   *
   * @param {boolean|string} [valeur]
   *        - true : lecture automatique utilisant le comportement du navigateur
   *        - false : ne pas jouer automatiquement
   *        - 'play' : appelle play() à chaque démarrage de chargement
   *        - 'muted' : appelle muted() puis play() à chaque début de chargement
   *        - 'any' : appelle play() à chaque démarrage de chargement. si cela échoue, appelle muted() puis play().
   *        - * : les valeurs autres que celles listées ici seront fixées à true pour `autoplay`
   *
   * @return {boolean|string}
   *         La valeur actuelle de l'autoplay lors de l'obtention de
   */
  autoplay(value) {
    // utilisation du getter
    if (value === undefined) {
      return this.options_.autoplay || false ;
    }

    let techAutoplay ;

    // si la valeur est une chaîne de caractères valide, la mettre à cette valeur, ou normaliser `true` en 'play', si nécessaire
    if (typeof value === 'string' && (/(any|play|muted)/).test(value) || value === true && this.options_.normalizeAutoplay) {
      this.options_.autoplay = valeur ;
      this.manualAutoplay_(typeof value === 'string' ? value : 'play') ;
      techAutoplay = false ;

    // toute valeur falsy définit autoplay à false dans le navigateur,
    // faisons de même
    } else if (!value) {
      this.options_.autoplay = false ;

    // toute autre valeur (c'est à dire vraie) met autoplay à vrai
    } else {
      this.options_.autoplay = true ;
    }

    techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay ;

    // si nous n'avons pas de technicien, nous ne faisons pas la queue
    // un appel à setAutoplay sur tech ready. Nous le faisons parce que la
    // l'option autoplay sera passée dans le constructeur et nous
    // il n'est pas nécessaire de le définir deux fois
    if (this.tech_) {
      this.techCall_('setAutoplay', techAutoplay) ;
    }
  }

  /**
   * Active ou désactive l'attribut playsinline.
   * Playsinline indique au navigateur que la lecture en mode non-fullscreen est privilégiée.
   *
   * @param {boolean} [valeur]
   *        - true signifie que nous devrions essayer de jouer en ligne par défaut
   *        - faux signifie que nous devons utiliser le mode de lecture par défaut du navigateur,
   *          qui, dans la plupart des cas, est en ligne. iOS Safari est une exception notable
   *          et joue en plein écran par défaut.
   *
   * @return {string|Player}
   *         - la valeur actuelle de playsinline
   *         - le lecteur lors du réglage
   *
   * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
   */
  playsinline(value) {
    if (value !== undefined) {
      this.techCall_('setPlaysinline', value) ;
      this.options_.playsinline = value ;
      retournez ceci ;
    }
    return this.techGet_('playsinline') ;
  }

  /**
   * Permet d'obtenir ou de définir l'attribut loop de l'élément vidéo.
   *
   * @param {boolean} [valeur]
   *        - true signifie que la vidéo doit être mise en boucle
   *        - false signifie que la vidéo ne doit pas être mise en boucle
   *
   * @return {boolean}
   *         La valeur actuelle de la boucle lors de l'obtention
   */
  loop(value) {
    if (value !== undefined) {
      this.techCall_('setLoop', value) ;
      this.options_.loop = value ;
      retour ;
    }
    return this.techGet_('loop') ;
  }

  /**
   * Obtenir ou définir l'url de la source de l'image de l'affiche
   *
   * @fires Player#posterchange
   *
   * @param {string} [src]
   *        URL de la source de l'image du poster
   *
   * @return {string}
   *         La valeur actuelle de l'affiche lors de l'obtention
   */
  poster(src) {
    if (src === undefined) {
      return this.poster_ ;
    }

    // La bonne façon de supprimer une affiche est de la définir comme une chaîne vide
    // d'autres valeurs erronées provoqueront des erreurs
    if (!src) {
      src = '' ;
    }

    if (src === this.poster_) {
      retour ;
    }

    // mise à jour de la variable interne de l'affiche
    this.poster_ = src ;

    // mettre à jour l'affiche du technicien
    this.techCall_('setPoster', src) ;

    this.isPosterFromTech_ = false ;

    // avertit les composants que l'affiche a été placée
    /**
     * Cet événement se déclenche lorsque l'image de l'affiche est modifiée sur le lecteur.
     *
     * @event Player#posterchange
     * @type {EventTarget~Event}
     */
    this.trigger('posterchange') ;
  }

  /**
   * Certaines technologies (par exemple YouTube) peuvent fournir une source d'affichage dans un format
   * de manière asynchrone. Nous voulons que le composant "poster" utilise ceci
   * de manière à masquer les contrôles de la technologie.
   * (bouton de lecture de YouTube). Cependant, nous ne voulons utiliser que cette
   * si l'utilisateur du lecteur n'a pas défini de poster par le biais de
   * les API normales.
   *
   * @fires Player#posterchange
   * @listens Tech#posterchange
   * @private
   */
  handleTechPosterChange_() {
    if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
      const newPoster = this.tech_.poster() || '' ;

      if (newPoster !== this.poster_) {
        this.poster_ = newPoster ;
        this.isPosterFromTech_ = true ;

        // Indique aux composants que l'affiche a changé
        this.trigger('posterchange') ;
      }
    }
  }

  /**
   * Permet d'obtenir ou de définir si les contrôles sont affichés ou non.
   *
   * @fires Player#controlsenabled
   *
   * @param {boolean} [bool]
   *        - true pour activer les contrôles
   *        - false pour désactiver les contrôles
   *
   * @return {boolean}
   *         La valeur actuelle des contrôles lors de l'obtention de
   */
  controls(bool) {
    if (bool === undefined) {
      return !!this.controls_ ;
    }

    bool = !!bool ;

    // Ne déclenche pas d'événement de changement à moins qu'il n'ait réellement changé
    if (this.controls_ === bool) {
      retour ;
    }

    this.controls_ = bool ;

    if (this.usingNativeControls()) {
      this.techCall_('setControls', bool) ;
    }

    if (this.controls_) {
      this.removeClass('vjs-controls-disabled') ;
      this.addClass('vjs-controls-enabled') ;
      /**
       * @event Player#controlsenabled
       * @type {EventTarget~Event}
       */
      this.trigger('controlsenabled') ;
      if (!this.usingNativeControls()) {
        this.addTechControlsListeners_() ;
      }
    } else {
      this.removeClass('vjs-controls-enabled') ;
      this.addClass('vjs-controls-disabled') ;
      /**
       * @event Player#controlsdisabled
       * @type {EventTarget~Event}
       */
      this.trigger('controlsdisabled') ;
      if (!this.usingNativeControls()) {
        this.removeTechControlsListeners_() ;
      }
    }
  }

  /**
   * Activer/désactiver les contrôles natifs. Les contrôles natifs sont les contrôles intégrés dans les
   * (par exemple, les commandes par défaut de l'iPhone) ou d'autres techs
   * (par exemple, contrôles Vimeo)
   * **Ceci ne devrait être défini que par le technicien en place, car seul le technicien sait
   * s'il peut prendre en charge les contrôles natifs**
   *
   * @fires Player#usingnativecontrols
   * @fires Player#utiliser des commandes personnalisées
   *
   * @param {boolean} [bool]
   *        - true pour activer les contrôles natifs
   *        - false pour désactiver les contrôles natifs
   *
   * @return {boolean}
   *         La valeur actuelle des contrôles natifs lors de l'obtention de
   */
  usingNativeControls(bool) {
    if (bool === undefined) {
      return !!this.usingNativeControls_ ;
    }

    bool = !!bool ;

    // Ne déclenche pas d'événement de changement à moins qu'il n'ait réellement changé
    if (this.usingNativeControls_ === bool) {
      retour ;
    }

    this.usingNativeControls_ = bool ;

    if (this.usingNativeControls_) {
      this.addClass('vjs-using-native-controls') ;

      /**
       * le lecteur utilise les commandes natives de l'appareil
       *
       * @event Player#usingnativecontrols
       * @type {EventTarget~Event}
       */
      this.trigger('usingnativecontrols') ;
    } else {
      this.removeClass('vjs-using-native-controls') ;

      /**
       * le lecteur utilise les contrôles HTML personnalisés
       *
       * @event Player#utiliser des commandes personnalisées
       * @type {EventTarget~Event}
       */
      this.trigger('usingcustomcontrols') ;
    }
  }

  /**
   * Définir ou obtenir l'erreur MediaError actuelle
   *
   * @fires Player#error
   *
   * @param {MediaError|string|number} [err]
   *         Une MediaError ou une chaîne/un nombre à transformer
   *         en une MediaError
   *
   * @return {MediaError|null}
   *         L'erreur MediaError actuelle lors de l'obtention (ou null)
   */
  error(err) {
    if (err === undefined) {
      return this.error_ || null ;
    }

    // permet aux crochets de modifier l'objet d'erreur
    hooks('beforeerror').forEach((hookFunction) => {
      const newErr = hookFunction(this, err) ;

      si ( !(
        (isObject(newErr) && !Array.isArray(newErr)) ||
        typeof newErr === 'string' ||
        typeof newErr === 'number' ||
        newErr === null
      )) {
        this.log.error('please return a value that MediaError expects in beforeerror hooks') ;
        retour ;
      }

      err = newErr ;
    }) ;

    // Supprimer le premier message d'erreur en l'absence de source compatible jusqu'à ce que
    // interaction avec l'utilisateur
    si (this.options_.suppressNotSupportedError &&
        err && err.code === 4
    ) {
      const triggerSuppressedError = function() {
        this.error(err) ;
      };

      this.options_.suppressNotSupportedError = false ;
      this.any(['click', 'touchstart'], triggerSuppressedError) ;
      this.one('loadstart', function() {
        this.off(['click', 'touchstart'], triggerSuppressedError) ;
      }) ;
      retour ;
    }

    // rétablissement de la valeur par défaut
    if (err === null) {
      this.error_ = err ;
      this.removeClass('vjs-error') ;
      if (this.errorDisplay) {
        this.errorDisplay.close() ;
      }
      retour ;
    }

    this.error_ = new MediaError(err) ;

    // ajouter le nom de classe vjs-error au lecteur
    this.addClass('vjs-error') ;

    // enregistre le nom du type d'erreur et le message éventuel
    // IE11 enregistre "[objet objet]" et vous oblige à développer le message pour voir l'objet de l'erreur
    log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_) ;

    /**
     * @event Player#error
     * @type {EventTarget~Event}
     */
    this.trigger('error') ;

    // notifie les hooks de l'erreur par joueur
    hooks('error').forEach((hookFunction) => hookFunction(this, this.error_)) ;

    retour ;
  }

  /**
   * Signaler l'activité d'un utilisateur
   *
   * @param {Objet} événement
   *        Objet de l'événement
   */
  reportUserActivity(event) {
    this.userActivity_ = true ;
  }

  /**
   * Obtenir ou définir si l'utilisateur est actif
   *
   * @fires Player#useractive
   * @fires Player#userinactive
   *
   * @param {boolean} [bool]
   *        - true si l'utilisateur est actif
   *        - false si l'utilisateur est inactif
   *
   * @return {boolean}
   *         La valeur actuelle de userActive lors de l'obtention de
   */
  userActive(bool) {
    if (bool === undefined) {
      return this.userActive_ ;
    }

    bool = !!bool ;

    if (bool === this.userActive_) {
      retour ;
    }

    this.userActive_ = bool ;

    if (this.userActive_) {
      this.userActivity_ = true ;
      this.removeClass('vjs-user-inactive') ;
      this.addClass('vjs-user-active') ;
      /**
       * @event Player#useractive
       * @type {EventTarget~Event}
       */
      this.trigger('useractive') ;
      retour ;
    }

    // Chrome/Safari/IE ont des bugs où lorsque vous changez le curseur il peut
    // déclenche un événement de déplacement de la souris. Cela pose un problème lorsque vous cachez
    // le curseur lorsque l'utilisateur est inactif, et un déplacement de souris signale que l'utilisateur est inactif
    // activité. Ce qui rend impossible le passage en mode inactif. En particulier
    // cela se produit en plein écran lorsque nous avons vraiment besoin de cacher le curseur.
    //
    // Lorsque ce problème sera résolu dans TOUS les navigateurs, il pourra être supprimé
    // https://code.google.com/p/chromium/issues/detail?id=103041
    if (this.tech_) {
      this.tech_.one('mousemove', function(e) {
        e.stopPropagation() ;
        e.preventDefault() ;
      }) ;
    }

    this.userActivity_ = false ;
    this.removeClass('vjs-user-active') ;
    this.addClass('vjs-user-inactive') ;
    /**
     * @event Player#userinactive
     * @type {EventTarget~Event}
     */
    this.trigger('userinactive') ;
  }

  /**
   * Écoute de l'activité de l'utilisateur en fonction de la valeur du délai d'attente
   *
   * @private
   */
  listenForUserActivity_() {
    let mouseInProgress ;
    let lastMoveX ;
    let lastMoveY ;
    const handleActivity = Fn.bind(this, this.reportUserActivity) ;

    const handleMouseMove = function(e) {
      // #1068 - Empêcher le spam de mousemove
      // Bug Chrome : https://code.google.com/p/chromium/issues/detail?id=366970
      if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
        lastMoveX = e.screenX ;
        lastMoveY = e.screenY ;
        handleActivity() ;
      }
    };

    const handleMouseDown = function() {
      handleActivity() ;
      // Tant qu'ils touchent l'appareil ou qu'ils ont la souris enfoncée,
      // nous les considérons comme actifs même s'ils ne bougent pas le doigt ou la souris.
      // Nous voulons donc continuer à mettre à jour qu'ils sont actifs
      this.clearInterval(mouseInProgress) ;
      // Fixer userActivity=true maintenant et fixer l'intervalle au même moment
      // comme l'intervalle de contrôle de l'activité (250) devrait garantir que nous ne manquons jamais le contrôle de l'activité
      // Contrôle de l'activité suivante
      mouseInProgress = this.setInterval(handleActivity, 250) ;
    };

    const handleMouseUpAndMouseLeave = function(event) {
      handleActivity() ;
      // Arrêter l'intervalle qui maintient l'activité si la souris/touche est abaissée
      this.clearInterval(mouseInProgress) ;
    };

    // Tout mouvement de la souris sera considéré comme une activité de l'utilisateur
    this.on('mousedown', handleMouseDown) ;
    this.on('mousemove', handleMouseMove) ;
    this.on('mouseup', handleMouseUpAndMouseLeave) ;
    this.on('mouseleave', handleMouseUpAndMouseLeave) ;

    const controlBar = this.getChild('controlBar') ;

    // Corrige un bug sur Android & iOS où lorsque l'on tape sur la barre de progression (lorsque la barre de contrôle est affichée)
    // la barre de contrôle ne serait plus cachée par défaut timeout.
    if (controlBar && !browser.IS_IOS && !browser.IS_ANDROID) {

      controlBar.on('mouseenter', function(event) {
        if (this.player().options_.inactivityTimeout !== 0) {
          this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout ;
        }
        this.player().options_.inactivityTimeout = 0 ;
      }) ;

      controlBar.on('mouseleave', function(event) {
        this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout ;
      }) ;

    }

    // Écoute de la navigation au clavier
    // Il ne devrait pas être nécessaire d'utiliser l'intervalle inProgress en raison de la répétition des touches
    this.on('keydown', handleActivity) ;
    this.on('keyup', handleActivity) ;

    // Exécute un intervalle toutes les 250 millisecondes au lieu de tout mettre dans
    // la fonction mousemove/touchmove elle-même, afin d'éviter toute dégradation des performances.
    // `this.reportUserActivity` met simplement this.userActivity_ à true, qui
    // est ensuite repris par cette boucle
    // http://ejohn.org/blog/learning-from-twitter/
    let inactivityTimeout ;

    this.setInterval(function() {
      // Vérifie s'il y a eu une activité de la souris ou du toucher
      if (!this.userActivity_) {
        retour ;
      }

      // Réinitialiser le tracker d'activité
      this.userActivity_ = false ;

      // Si l'état de l'utilisateur était inactif, le mettre à l'état actif
      this.userActive(true) ;

      // Effacer tout délai d'inactivité existant pour redémarrer le minuteur
      this.clearTimeout(inactivityTimeout) ;

      const timeout = this.options_.inactivityTimeout ;

      if (timeout <= 0) {
        retour ;
      }

      // Dans le délai d'attente <> millisecondes, si aucune autre activité ne s'est produite, le
      // l'utilisateur sera considéré comme inactif
      inactivityTimeout = this.setTimeout(function() {
        // Se prémunir contre le cas où l'inactivityTimeout peut se déclencher juste après la fin de la période d'inactivité
        // avant que l'activité suivante de l'utilisateur ne soit prise en compte par la boucle de vérification de l'activité
        // provoquant un scintillement
        if (!this.userActivity_) {
          this.userActive(false) ;
        }
      }, timeout) ;

    }, 250) ;
  }

  /**
   * Permet d'obtenir ou de définir le taux de lecture actuel. Une vitesse de lecture de
   * 1,0 représente la vitesse normale et 0,5 la demi-vitesse
   * par exemple.
   *
   * voir https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
   *
   * @param {number} [rate]
   *       Nouveau taux de lecture à définir.
   *
   * @return {number}
   *         Le taux de lecture actuel lors de l'obtention ou de la 1.0
   */
  playbackRate(rate) {
    if (rate !== undefined) {
      // NOTE : this.cache_.lastPlaybackRate est défini par le tech handler
      // qui est enregistré ci-dessus
      this.techCall_('setPlaybackRate', rate) ;
      retour ;
    }

    if (this.tech_ && this.tech_.featuresPlaybackRate) {
      return this.cache_.lastPlaybackRate || this.techGet_('playbackRate') ;
    }
    retour 1.0 ;
  }

  /**
   * Obtient ou définit le taux de lecture par défaut actuel. La vitesse de lecture par défaut est de
   * 1,0 représente la vitesse normale et 0,5 indique une lecture à demi-vitesse, par exemple.
   * defaultPlaybackRate ne représentera que le taux de lecture initial d'une vidéo, et non le taux de lecture par défaut
   * et non le taux de lecture actuel.
   *
   * voir https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
   *
   * @param {number} [rate]
   *       Nouveau taux de lecture par défaut à définir.
   *
   * @return {number|Player}
   *         - Taux de lecture par défaut lorsque l'on obtient ou 1.0
   *         - le lecteur lors du réglage
   */
  defaultPlaybackRate(rate) {
    if (rate !== undefined) {
      return this.techCall_('setDefaultPlaybackRate', rate) ;
    }

    if (this.tech_ && this.tech_.featuresPlaybackRate) {
      return this.techGet_('defaultPlaybackRate') ;
    }
    retour 1.0 ;
  }

  /**
   * Obtient ou définit le drapeau audio
   *
   * @param {boolean} bool
   *        - true signale qu'il s'agit d'un lecteur audio
   *        - faux signaux indiquant qu'il ne s'agit pas d'un lecteur audio
   *
   * @return {boolean}
   *         La valeur actuelle de isAudio lors de l'obtention de
   */
  isAudio(bool) {
    if (bool !== undefined) {
      this.isAudio_ = !!bool ;
      retour ;
    }

    return !!this.isAudio_ ;
  }

  enableAudioOnlyUI_() {
    // Mise à jour immédiate du style pour afficher la barre de contrôle afin d'obtenir sa hauteur
    this.addClass('vjs-audio-only-mode') ;

    const playerChildren = this.children() ;
    const controlBar = this.getChild('ControlBar') ;
    const controlBarHeight = controlBar && controlBar.currentHeight() ;

    // Masquer tous les composants du lecteur à l'exception de la barre de contrôle. Composants de la barre de contrôle
    // nécessaires uniquement pour la vidéo sont cachés avec CSS
    playerChildren.forEach(child => {
      if (child === controlBar) {
        retour ;
      }

      if (child.el_ && !child.hasClass('vjs-hidden')) {
        child.hide() ;

        this.audioOnlyCache_.hiddenChildren.push(child) ;
      }
    }) ;

    this.audioOnlyCache_.playerHeight = this.currentHeight() ;

    // Fixer la hauteur du lecteur à la même hauteur que la barre de contrôle
    this.height(controlBarHeight) ;
    this.trigger('audioonlymodechange') ;
  }

  disableAudioOnlyUI_() {
    this.removeClass('vjs-audio-only-mode') ;

    // Afficher les composants du lecteur qui étaient auparavant cachés
    this.audioOnlyCache_.hiddenChildren.forEach(child => child.show()) ;

    // Réinitialisation de la hauteur du joueur
    this.height(this.audioOnlyCache_.playerHeight) ;
    this.trigger('audioonlymodechange') ;
  }

  /**
   * Obtenir l'état actuel de l'audioOnlyMode ou définir l'audioOnlyMode sur true ou false.
   *
   * La valeur `true` permet de cacher tous les composants du lecteur à l'exception de la barre de contrôle,
   * ainsi que les composants de la barre de contrôle nécessaires uniquement pour la vidéo.
   *
   * @param {boolean} [valeur]
   *         Valeur à attribuer à audioOnlyMode.
   *
   * @return {Promesse|boolean}
   *        Une promesse est renvoyée lors de la définition de l'état, et un booléen lors de la récupération
   *        l'état actuel
   */
  audioOnlyMode(value) {
    if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
      return this.audioOnlyMode_ ;
    }

    this.audioOnlyMode_ = valeur ;

    const PromiseClass = this.options_.Promise || window.Promise ;

    if (PromiseClass) {
      // Activer le mode audio uniquement
      if (value) {
        const exitPromises = [] ;

        // Le plein écran et le PiP ne sont pas pris en charge en mode audioOnly, il faut donc sortir si nécessaire.
        if (this.isInPictureInPicture()) {
          exitPromises.push(this.exitPictureInPicture()) ;
        }

        if (this.isFullscreen()) {
          exitPromises.push(this.exitFullscreen()) ;
        }

        if (this.audioPosterMode()) {
          exitPromises.push(this.audioPosterMode(false)) ;
        }

        return PromiseClass.all(exitPromises).then(() => this.enableAudioOnlyUI_()) ;
      }

      // Désactiver le mode audio uniquement
      return PromiseClass.resolve().then(() => this.disableAudioOnlyUI_()) ;
    }

    if (value) {
      if (this.isInPictureInPicture()) {
        this.exitPictureInPicture() ;
      }

      if (this.isFullscreen()) {
        this.exitFullscreen() ;
      }

      this.enableAudioOnlyUI_() ;
    } else {
      this.disableAudioOnlyUI_() ;
    }
  }

  enablePosterModeUI_() {
    // Masquer l'élément vidéo et afficher l'image du poster pour activer le posterModeUI
    const tech = this.tech_ && this.tech_ ;

    tech.hide() ;
    this.addClass('vjs-audio-poster-mode') ;
    this.trigger('audiopostermodechange') ;
  }

  disablePosterModeUI_() {
    // Afficher l'élément vidéo et cacher l'image du poster pour désactiver le posterModeUI
    const tech = this.tech_ && this.tech_ ;

    tech.show() ;
    this.removeClass('vjs-audio-poster-mode') ;
    this.trigger('audiopostermodechange') ;
  }

  /**
   * Obtenir l'état actuel de audioPosterMode ou définir audioPosterMode à true ou false
   *
   * @param {boolean} [valeur]
   *         La valeur à attribuer à audioPosterMode.
   *
   * @return {Promesse|boolean}
   *         Une promesse est renvoyée lors de la définition de l'état, et un booléen lors de la récupération
   *        l'état actuel
   */
  audioPosterMode(value) {

    if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
      return this.audioPosterMode_ ;
    }

    this.audioPosterMode_ = valeur ;

    const PromiseClass = this.options_.Promise || window.Promise ;

    if (PromiseClass) {

      if (value) {

        if (this.audioOnlyMode()) {
          const audioOnlyModePromise = this.audioOnlyMode(false) ;

          return audioOnlyModePromise.then(() => {
            // activer le mode poster audio après avoir désactivé le mode audio seul
            this.enablePosterModeUI_() ;
          }) ;
        }

        return PromiseClass.resolve().then(() => {
          // activer le mode poster audio
          this.enablePosterModeUI_() ;
        }) ;
      }

      return PromiseClass.resolve().then(() => {
        // désactive le mode poster audio
        this.disablePosterModeUI_() ;
      }) ;
    }

    if (value) {

      if (this.audioOnlyMode()) {
        this.audioOnlyMode(false) ;
      }

      this.enablePosterModeUI_() ;
      retour ;
    }

    this.disablePosterModeUI_() ;
  }

  /**
   * Une méthode d'aide pour ajouter un {@link TextTrack} à notre
   * {@link TextTrackList}.
   *
   * En plus des paramètres du W3C, nous permettons d'ajouter des informations supplémentaires par le biais d'options.
   *
   * voir http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
   *
   * @param {string} [kind]
   *        le type de TextTrack que vous ajoutez
   *
   * @param {string} [label]
   *        l'étiquette à donner à l'étiquette du TextTrack
   *
   * @param {string} [langue]
   *        la langue à définir sur le TextTrack
   *
   * @return {TextTrack|undefined}
   *         la piste de texte qui a été ajoutée ou non définie
   *         s'il n'y a pas de technologie
   */
  addTextTrack(kind, label, language) {
    if (this.tech_) {
      return this.tech_.addTextTrack(kind, label, language) ;
    }
  }

  /**
   * Créez un {@link TextTrack} et un {@link HTMLTrackElement} distants.
   * Lorsque la valeur de manualCleanup est fixée à false, la piste est automatiquement supprimée
   * sur les changements de source.
   *
   * @param {Objet} options
   *        Options à transmettre à {@link HTMLTrackElement} lors de la création. Voir
   *        {@link HTMLTrackElement} pour les propriétés de l'objet que vous devez utiliser.
   *
   * @param {boolean} [manualCleanup=true] si la valeur est false, le TextTrack sera nettoyé à la main
   *                                       supprimé lors d'un changement de source
   *
   * @return {HtmlTrackElement}
   *         l'élément HTMLTrackElement qui a été créé et ajouté
   *         à la liste HtmlTrackElementList et à l'élément distant
   *         TextTrackList
   *
   * @deprecated La valeur par défaut du paramètre "manualCleanup" sera par défaut
   *             à "false" dans les prochaines versions de Video.js
   */
  addRemoteTextTrack(options, manualCleanup) {
    if (this.tech_) {
      return this.tech_.addRemoteTextTrack(options, manualCleanup) ;
    }
  }

  /**
   * Retirer un {@link TextTrack} distant de l'élément correspondant
   * {@link TextTrackList} et {@link HtmlTrackElementList}.
   *
   * @param {Object} track
   *        {@link TextTrack} distant à supprimer
   *
   * @return {undefined}
   *         ne renvoie rien
   */
  removeRemoteTextTrack(obj = {}) {
    let {track} = obj ;

    if (!track) {
      piste = obj ;
    }

    // déstructure l'entrée en un objet avec un argument de piste, par défaut arguments[0]
    // l'argument entier est remplacé par défaut par un objet vide si rien n'a été transmis

    if (this.tech_) {
      return this.tech_.removeRemoteTextTrack(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 {Objet|non défini}
   *         Un objet avec les mesures de qualité de lecture des médias prises en charge ou non défini s'il n'y a pas de mesures de qualité de lecture des médias prises en charge
   *         il n'y a pas de technologie ou la technologie n'est pas compatible.
   */
  getVideoPlaybackQuality() {
    return this.techGet_('getVideoPlaybackQuality') ;
  }

  /**
   * Obtenir la largeur de la vidéo
   *
   * @return {number}
   *         largeur actuelle de la vidéo
   */
  videoWidth() {
    return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0 ;
  }

  /**
   * Obtenir la hauteur de la vidéo
   *
   * @return {number}
   *         hauteur actuelle de la vidéo
   */
  videoHeight() {
    return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0 ;
  }

  /**
   * Code de la langue du joueur.
   *
   * Le changement de langue déclenchera
   * [languagechange]{@link Player#event:languagechange}
   * que les composants peuvent utiliser pour mettre à jour le texte du contrôle.
   * ClickableComponent mettra à jour le texte de son contrôle par défaut sur
   * [changement de langue]{@link Player#event:changement de langue}.
   *
   * @fires Player#languagechange
   *
   * @param {string} [code]
   *        le code langue pour régler le lecteur sur
   *
   * @return {string}
   *         Le code de la langue actuelle lors de l'obtention
   */
  language(code) {
    if (code === undefined) {
      return this.language_ ;
    }

    if (this.language_ !== String(code).toLowerCase()) {
      this.language_ = String(code).toLowerCase() ;

      // lors du premier init, il est possible que certaines choses ne soient pas événementielles
      if (isEvented(this)) {
        /**
        * se déclenche lorsque la langue du joueur change
        *
        * @event Player#languagechange
        * @type {EventTarget~Event}
        */
        this.trigger('languagechange') ;
      }
    }
  }

  /**
   * Obtenir le dictionnaire de langue du joueur
   * Fusionner à chaque fois, car un plugin nouvellement ajouté peut appeler videojs.addLanguage() à tout moment
   * Les langues spécifiées directement dans les options du lecteur sont prioritaires
   *
   * @return {Array}
   *         Un tableau des langues prises en charge
   */
  languages() {
    return mergeOptions(Player.prototype.options_.languages, this.languages_) ;
  }

  /**
   * renvoie un objet JavaScript représentant la piste actuelle
   * informations. **ne les renvoie pas sous forme de JSON**
   *
   * @return {Object}
   *         Objet représentant le courant de l'information sur la piste
   */
  toJSON() {
    const options = mergeOptions(this.options_) ;
    const tracks = options.tracks ;

    options.tracks = [] ;

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

      // fusionner les pistes et annuler le lecteur afin d'éviter les références circulaires
      track = mergeOptions(track) ;
      track.player = undefined ;
      options.tracks[i] = track ;
    }

    les options de retour ;
  }

  /**
   * Crée un dialogue modal simple (une instance de {@link ModalDialog}
   * ) qui recouvre immédiatement le joueur d'une image arbitraire de
   * et se retire lorsqu'il est fermé.
   *
   * @param {string|Function|Element|Array|null} content
   *        Identique au paramètre du même nom de {@link ModalDialog#content}.
   *        L'utilisation la plus simple consiste à fournir une chaîne ou un DOM
   *        élément.
   *
   * @param {Objet} [options]
   *        Options supplémentaires qui seront transmises au {@link ModalDialog}.
   *
   * @return {ModalDialog}
   *         le {@link ModalDialog} qui a été créé
   */
  createModal(content, options) {
    options = options || {} ;
    options.content = content || '' ;

    const modal = new ModalDialog(this, options) ;

    this.addChild(modal) ;
    modal.on('dispose', () => {
      this.removeChild(modal) ;
    }) ;

    modal.open() ;
    retour modal ;
  }

  /**
   * Modifier les classes de points d'arrêt lorsque le lecteur se redimensionne.
   *
   * @private
   */
  updateCurrentBreakpoint_() {
    if (!this.responsive()) {
      retour ;
    }

    const currentBreakpoint = this.currentBreakpoint() ;
    const currentWidth = this.currentWidth() ;

    for (let i = 0 ; i < BREAKPOINT_ORDER.length ; i++) {
      const candidateBreakpoint = BREAKPOINT_ORDER[i] ;
      const maxWidth = this.breakpoints_[candidateBreakpoint] ;

      if (currentWidth <= maxWidth) {

        // Le point d'arrêt actuel n'a pas changé, il n'y a rien à faire.
        if (currentBreakpoint === candidateBreakpoint) {
          retour ;
        }

        // Ne supprimer une classe que s'il y a un point d'arrêt en cours.
        if (currentBreakpoint) {
          this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]) ;
        }

        this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]) ;
        this.breakpoint_ = candidateBreakpoint ;
        pause ;
      }
    }
  }

  /**
   * Supprime le point d'arrêt actuel.
   *
   * @private
   */
  removeCurrentBreakpoint_() {
    const className = this.currentBreakpointClass() ;

    this.breakpoint_ = '' ;

    if (className) {
      this.removeClass(className) ;
    }
  }

  /**
   * Obtenir ou définir des points d'arrêt sur le lecteur.
   *
   * L'appel de cette méthode avec un objet ou `true` supprimera tout objet précédent
   * les points d'arrêt personnalisés et recommencer à partir des valeurs par défaut.
   *
   * @param {Objet|booléen} [points d'arrêt]
   *         Si un objet est donné, il peut être utilisé pour fournir des informations personnalisées
   *         points d'arrêt. Si `true` est donné, des points d'arrêt par défaut seront mis en place.
   *         Si cet argument n'est pas fourni, le système renvoie simplement la valeur actuelle du
   *         points d'arrêt.
   *
   * @param {number} [breakpoints.tiny]
   *         La largeur maximale de la classe "vjs-layout-tiny".
   *
   * @param {number} [breakpoints.xsmall]
   *         Largeur maximale de la classe "vjs-layout-x-small".
   *
   * @param {number} [breakpoints.small]
   *         Largeur maximale de la classe "vjs-layout-small".
   *
   * @param {number} [breakpoints.medium]
   *         La largeur maximale de la classe "vjs-layout-medium".
   *
   * @param {number} [breakpoints.large]
   *         Largeur maximale de la classe "vjs-layout-large".
   *
   * @param {number} [breakpoints.xlarge]
   *         Largeur maximale de la classe "vjs-layout-x-large".
   *
   * @param {number} [breakpoints.huge]
   *         La largeur maximale de la classe "vjs-layout-huge".
   *
   * @return {Object}
   *         Objet associant les noms des points d'arrêt à des valeurs de largeur maximale.
   */
  breakpoints(breakpoints) {

    // Utilisé comme getter.
    if (breakpoints === undefined) {
      return assign(this.breakpoints_) ;
    }

    this.breakpoint_ = '' ;
    this.breakpoints_ = assign({}, DEFAULT_BREAKPOINTS, breakpoints) ;

    // Lorsque les définitions des points d'arrêt changent, nous devons mettre à jour la liste des points d'arrêt en cours
    // point d'arrêt sélectionné.
    this.updateCurrentBreakpoint_() ;

    // Clone les points d'arrêt avant de revenir.
    return assign(this.breakpoints_) ;
  }

  /**
   * Obtenir ou définir un drapeau indiquant si ce joueur doit ou non être ajusté
   * son interface utilisateur en fonction de ses dimensions.
   *
   * @param {boolean} value
   *         Doit être `true` si le joueur doit ajuster son UI en fonction de son
   *         sinon, elle doit être `false`.
   *
   * @return {boolean}
   *         Sera `vrai` si ce joueur doit ajuster son interface utilisateur en fonction de sa position dans la liste des joueurs
   *         sinon, ce sera `false`.
   */
  responsive(value) {

    // Utilisé comme getter.
    if (value === undefined) {
      return this.responsive_ ;
    }

    valeur = Booléen(valeur) ;
    const current = this.responsive_ ;

    // Rien n'a changé.
    if (value === current) {
      retour ;
    }

    // La valeur a changé, il faut la définir.
    this.responsive_ = value ;

    // Commencer à écouter les points d'arrêt et mettre en place le point d'arrêt initial si l'option
    // le lecteur est maintenant réactif.
    if (value) {
      this.on('playerresize', this.boundUpdateCurrentBreakpoint_) ;
      this.updateCurrentBreakpoint_() ;

    // Arrêtez d'écouter les points d'arrêt si le lecteur n'est plus réactif.
    } else {
      this.off('playerresize', this.boundUpdateCurrentBreakpoint_) ;
      this.removeCurrentBreakpoint_() ;
    }

    valeur de retour ;
  }

  /**
   * Obtenir le nom du point d'arrêt actuel, s'il y en a un.
   *
   * @return {string}
   *         Si un point d'arrêt est actuellement défini, renvoie une clé de l'élément
   *         l'objet des points d'arrêt correspondant. Sinon, renvoie une chaîne vide.
   */
  currentBreakpoint() {
    return this.breakpoint_ ;
  }

  /**
   * Obtenir le nom de la classe du point d'arrêt actuel.
   *
   * @return {string}
   *         Le nom de la classe correspondante (par exemple `"vjs-layout-tiny"` ou
   *         `"vjs-layout-large"`) pour le point d'arrêt actuel. Chaîne vide si
   *         il n'y a pas de point d'arrêt actuel.
   */
  currentBreakpointClass() {
    return BREAKPOINT_CLASSES[this.breakpoint_] || '' ;
  }

  /**
   * Objet décrivant un seul élément de média.
   *
   * Les propriétés qui ne font pas partie de cette description de type seront conservées,
   * il peut également être considéré comme un mécanisme générique de stockage des métadonnées.
   *
   * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
   * @typedef {Objet} Player~MediaObject
   *
   * @property {string} [album]
   *           Inutilisé, sauf si cet objet est passé à la `MediaSession`
   *           API.
   *
   * @property {string} [artiste]
   *           Inutilisé, sauf si cet objet est passé à la `MediaSession`
   *           API.
   *
   * @property {Objet[]} [artwork]
   *           Inutilisé, sauf si cet objet est passé à la `MediaSession`
   *           API. S'il n'est pas spécifié, il sera alimenté par le "poster", si
   *           disponible.
   *
   * @property {string} [poster]
   *           URL d'une image qui s'affichera avant la lecture.
   *
   * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
   *           Un seul objet source, un tableau d'objets sources ou une chaîne de caractères
   *           le référencement d'une URL vers une source de média. Il est _hautement recommandé_
   *           qu'un objet ou un tableau d'objets est utilisé ici, de sorte que la source
   *           les algorithmes de sélection peuvent prendre en compte le `type`.
   *
   * @property {string} [title]
   *           Inutilisé, sauf si cet objet est passé à la `MediaSession`
   *           API.
   *
   * @property {Object[]} [textTracks]
   *           Un tableau d'objets à utiliser pour créer des pistes de texte, comme suit
   *           le {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|format de l'élément de piste natif}.
   *           Pour faciliter le retrait, ces textes seront créés en tant que textes "distants"
   *           et configuré pour nettoyer automatiquement les changements de source.
   *
   *           Ces objets peuvent avoir des propriétés telles que `src`, `kind`, `label`,
   *           et `language`, voir {@link Tech#createRemoteTextTrack}.
   */

  /**
   * Remplir le lecteur à l'aide d'un {@link Player~MediaObject|MediaObject}.
   *
   * @param {Player~MediaObject} media
   *         Un objet média.
   *
   * @param {Fonction} ready
   *         Un rappel à appeler lorsque le lecteur est prêt.
   */
  loadMedia(media, ready) {
    if (!media || typeof media !== 'object') {
      retour ;
    }

    this.reset() ;

    // Clone l'objet média afin qu'il ne puisse pas être modifié de l'extérieur.
    this.cache_.media = mergeOptions(media) ;

    const {artwork, poster, src, textTracks} = this.cache_.media ;

    // Si `artwork` n'est pas donné, créez-le en utilisant `poster`.
    if (!artwork && poster) {
      this.cache_.media.artwork = [{
        src : poster,
        type : getMimetype(poster)
      }] ;
    }

    if (src) {
      this.src(src) ;
    }

    if (poster) {
      this.poster(poster) ;
    }

    if (Array.isArray(textTracks)) {
      textTracks.forEach(tt => this.addRemoteTextTrack(tt, false)) ;
    }

    this.ready(ready) ;
  }

  /**
   * Obtenir un clone du {@link Player~MediaObject} actuel pour ce lecteur.
   *
   * Si la méthode `loadMedia` n'a pas été utilisée, elle tentera de renvoyer un fichier
   * {@link Player~MediaObject} en fonction de l'état actuel du lecteur.
   *
   * @return {Player~MediaObject}
   */
  getMedia() {
    if (!this.cache_.media) {
      const poster = this.poster() ;
      const src = this.currentSources() ;
      const textTracks = Array.prototype.map.call(this.remoteTextTracks(), (tt) => ({
        type : tt.kind,
        label : tt.label,
        langue : tt.langue,
        src : tt.src
      })) ;

      const media = {src, textTracks} ;

      if (poster) {
        media.poster = poster ;
        media.artwork = [{
          src : media.poster,
          type : getMimetype(media.poster)
        }] ;
      }

      les médias de retour ;
    }

    return mergeOptions(this.cache_.media) ;
  }

  /**
   * Obtient les paramètres de l'étiquette
   *
   * @param {Element} tag
   *        L'étiquette du joueur
   *
   * @return {Object}
   *         Un objet contenant tous les paramètres
   *         pour une étiquette de joueur
   */
  static getTagSettings(tag) {
    const baseOptions = {
      sources : [],
      pistes : []
    };

    const tagOptions = Dom.getAttributes(tag) ;
    const dataSetup = tagOptions['data-setup'] ;

    if (Dom.hasClass(tag, 'vjs-fill')) {
      tagOptions.fill = true ;
    }
    if (Dom.hasClass(tag, 'vjs-fluid')) {
      tagOptions.fluid = true ;
    }

    // Vérifier si data-setup attr existe.
    if (dataSetup !== null) {
      // Analyse des options JSON
      // S'il s'agit d'une chaîne vide, il s'agit d'un objet json analysable.
      const [err, data] = safeParseTuple(dataSetup || '{}') ;

      if (err) {
        log.error(err) ;
      }
      assign(tagOptions, data) ;
    }

    assign(baseOptions, tagOptions) ;

    // Obtenir les paramètres des enfants de l'étiquette
    if (tag.hasChildNodes()) {
      const children = tag.childNodes ;

      for (let i = 0, j = children.length ; i < j ; i++) {
        const child = children[i] ;
        // Cas de changement nécessaire : http://ejohn.org/blog/nodename-case-sensitivity/
        const childName = child.nodeName.toLowerCase() ;

        if (childName === 'source') {
          baseOptions.sources.push(Dom.getAttributes(child)) ;
        else if (childName === 'track') {
          baseOptions.tracks.push(Dom.getAttributes(child)) ;
        }
      }
    }

    return baseOptions ;
  }

  /**
   * Déterminer si la boîte à outils est prise en charge ou non
   *
   * @return {boolean}
   *         - true si la boîte à outils est prise en charge
   *         - false si la boîte à outils n'est pas prise en charge
   */
  flexNotSupported_() {
    const elem = document.createElement('i') ;

    // Note : Nous n'utilisons pas réellement flexBasis (ou flexOrder), mais c'est l'un des éléments les plus importants de notre système
    // caractéristiques communes de flex sur lesquelles nous pouvons nous appuyer pour vérifier la prise en charge de flex.
    return !('flexBasis' in elem.style ||
            'webkitFlexBasis' dans elem.style ||
            'mozFlexBasis' dans elem.style ||
            'msFlexBasis' dans elem.style ||
            // Spécifique à IE10 (2012 flex spec), disponible pour l'exhaustivité
            'msFlexOrder' dans elem.style) ;
  }

  /**
   * Définir le mode de débogage pour activer/désactiver les journaux au niveau des informations.
   *
   * @param {boolean} enabled
   * @fires Player#debugon
   * @fires Player#debugoff
   */
  debug(enabled) {
    if (enabled === undefined) {
      return this.debugEnabled_ ;
    }
    if (enabled) {
      this.trigger('debugon') ;
      this.previousLogLevel_ = this.log.level ;
      this.log.level('debug') ;
      this.debugEnabled_ = true ;
    } else {
      this.trigger('debugoff') ;
      this.log.level(this.previousLogLevel_) ;
      this.previousLogLevel_ = undefined ;
      this.debugEnabled_ = false ;
    }

  }

  /**
   * Définir ou obtenir les taux de lecture actuels.
   * Prend un tableau et met à jour le menu des taux de lecture avec les nouveaux éléments.
   * Passez un tableau vide pour cacher le menu.
   * Les valeurs autres que les tableaux sont ignorées.
   *
   * @fires Player#playbackrateschange
   * @param {number[]} newRates
   *                   Les nouveaux taux que le menu des taux de lecture doit mettre à jour.
   *                   Un tableau vide masquera le menu
   * @return {number[]} Utilisé en tant que getter, il renvoie les taux de lecture actuels
   */
  playbackRates(newRates) {
    if (newRates === undefined) {
      return this.cache_.playbackRates ;
    }

    // ignore toute valeur qui n'est pas un tableau
    if (!Array.isArray(newRates)) {
      retour ;
    }

    // ignorer les tableaux qui ne contiennent pas que des nombres
    if (!newRates.every((rate) => typeof rate === 'number')) {
      retour ;
    }

    this.cache_.playbackRates = newRates ;

    /**
    * se déclenche lorsque les taux de lecture d'un lecteur sont modifiés
    *
    * @event Player#playbackrateschange
    * @type {EventTarget~Event}
    */
    this.trigger('playbackrateschange') ;
  }
}

/**
 * Obtenir la {@link VideoTrackList}
 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
 *
 * @return {VideoTrackList}
 *         la liste des pistes de la vidéo en cours
 *
 * @method Player.prototype.videoTracks
 */

/**
 * Obtenir la {@link AudioTrackList}
 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
 *
 * @return {AudioTrackList}
 *         la liste des pistes audio en cours
 *
 * @method Player.prototype.audioTracks
 */

/**
 * Obtenir la {@link TextTrackList}
 *
 * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
 *
 * @return {TextTrackList}
 *         la liste des pistes de texte en cours
 *
 * @method Player.prototype.textTracks
 */

/**
 * Obtenir la {@link TextTrackList} distante
 *
 * @return {TextTrackList}
 *         Liste des pistes du texte à distance en cours
 *
 * @method Player.prototype.remoteTextTracks
 */

/**
 * Obtenir les {@link HtmlTrackElementList} de pistes distantes.
 *
 * @return {HtmlTrackElementList}
 *         Liste des éléments de la piste de texte à distance
 *
 * @method Player.prototype.remoteTextTrackEls
 */

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

  Player.prototype[props.getterName] = function() {
    if (this.tech_) {
      return this.tech_[props.getterName]() ;
    }

    // si nous n'avons pas encore chargéTech_, nous créons des {vidéo, audio, texte}Pistes_
    // ces éléments seront transmis à la technologie lors du chargement
    this[props.privateName] = this[props.privateName] || new props.ListClass() ;
    return this[props.privateName] ;
  };
}) ;

/**
 * Obtient ou définit l'option crossorigin du `Player`. Pour le lecteur HTML5, il s'agit de
 * définit la propriété `crossOrigin` sur la balise `<video>` pour contrôler le CORS
 * comportement.
 *
 * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
 *
 * @param {string} [valeur]
 *        La valeur à donner au crossorigin du `Player`. Si un argument est
 *        doit être `anonymous` ou `use-credentials`.
 *
 * @return {string|undefined}
 *         - La valeur crossoriginale actuelle du `Player` lors de l'obtention.
 *         - indéfinie lors de la mise en place
 */
Player.prototype.crossorigin = Player.prototype.crossOrigin ;

/**
 * Énumération globale des acteurs.
 *
 * Les clés sont les ID des joueurs et les valeurs sont soit les {@link Player}
 * ou `null` pour les joueurs éliminés.
 *
 * @type {Objet}
 */
Player.players = {} ;

const navigator = window.navigator ;

/*
 * Options de l'instance du joueur, surfacées à l'aide d'options
 * options = Player.prototype.options_
 * Effectuez les changements dans les options, pas ici.
 *
 * @type {Objet}
 * @private
 */
Player.prototype.options_ = {
  // Ordre par défaut de la technologie de repli
  techOrder : Tech.defaultTechOrder_,

  html5 : {},

  // délai d'inactivité par défaut
  inactivityTimeout : 2000,

  // taux de lecture par défaut
  playbackRates : [],
  // Ajouter la sélection du taux de lecture en ajoutant des taux
  // 'playbackRates' : [0.5, 1, 1.5, 2],
  liveui : faux,

  // Ensembles de contrôle inclus
  enfants : [
    'mediaLoader',
    image de l'affiche",
    'textTrackDisplay',
    'loadingSpinner',
    'bigPlayButton',
    'liveTracker',
    'controlBar',
    'errorDisplay',
    'textTrackSettings',
    'resizeManager' (gestionnaire de redimensionnement)
  ],

  langue : navigateur && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',

  // les locales et leurs traductions linguistiques
  langues : {},

  // Message par défaut à afficher lorsqu'une vidéo ne peut pas être lue.
  notSupportedMessage : aucune source compatible n'a été trouvée pour ce média,

  normalizeAutoplay : false,

  fullscreen : {
    options : {
      navigationUI : "hide" (cacher)
    }
  },

  points d'arrêt : {},
  responsive : false,
  audioOnlyMode : false,
  audioPosterMode : false
};

[
  /**
   * Retourne si le joueur est dans l'état "terminé" ou non.
   *
   * @return {Boolean} True si le joueur est dans l'état terminé, false si ce n'est pas le cas.
   * @method Player#ended
   */
  terminé",
  /**
   * Indique si le joueur est dans l'état de "recherche" ou non.
   *
   * @return {Boolean} True si le joueur est en état de recherche, false s'il ne l'est pas.
   * @method Player#seeking
   */
  recherche",
  /**
   * Renvoie les intervalles de temps des médias qui sont actuellement disponibles
   * pour avoir cherché à.
   *
   * @return {TimeRanges} les intervalles recherchables de la chronologie des médias
   * @method Player#seekable
   */
  'cherchable',
  /**
   * Renvoie l'état actuel de l'activité du réseau pour l'élément, de
   * les codes dans la liste ci-dessous.
   * - NETWORK_EMPTY (valeur numérique 0)
   *   L'élément n'a pas encore été initialisé. Tous les attributs sont en
   *   leur état initial.
   * - NETWORK_IDLE (valeur numérique 1)
   *   L'algorithme de sélection des ressources de l'élément est actif et a
   *   a sélectionné une ressource, mais il n'utilise pas réellement le réseau à l'heure actuelle
   *   cette fois-ci.
   * - NETWORK_LOADING (valeur numérique 2)
   *   L'agent utilisateur essaie activement de télécharger des données.
   * - NETWORK_NO_SOURCE (valeur numérique 3)
   *   L'algorithme de sélection des ressources de l'élément est actif, mais il a
   *   n'a pas encore trouvé de ressource à utiliser.
   *
   * voir https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
   * @return {number} l'état actuel de l'activité du réseau
   * @méthode Joueur#état du réseau
   */
  'networkState',
  /**
   * Renvoie une valeur qui exprime l'état actuel de l'élément
   * en ce qui concerne le rendu de la position de lecture actuelle, à partir de l'élément
   * dans la liste ci-dessous.
   * - HAVE_NOTHING (valeur numérique 0)
   *   Aucune information concernant la ressource médiatique n'est disponible.
   * - HAVE_METADATA (valeur numérique 1)
   *   La ressource a été obtenue en quantité suffisante pour que la durée de la
   *   est disponible.
   * - HAVE_CURRENT_DATA (valeur numérique 2)
   *   Les données relatives à la position de lecture actuelle sont disponibles.
   * - HAVE_FUTURE_DATA (valeur numérique 3)
   *   Les données relatives à la position de lecture actuelle sont disponibles sous la forme suivante
   *   ainsi que suffisamment de données pour permettre à l'agent utilisateur de faire avancer le dossier en cours
   *   position de lecture dans le sens de la lecture.
   * - HAVE_ENOUGH_DATA (valeur numérique 4)
   *   L'agent utilisateur estime qu'il y a suffisamment de données disponibles pour
   *   la lecture se poursuit sans interruption.
   *
   * voir https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
   * @return {number} l'état actuel du rendu de la lecture
   * @method Player#readyState
   */
  état de préparation
].forEach(function(fn) {
  Player.prototype[fn] = function() {
    return this.techGet_(fn) ;
  };
}) ;

TECH_EVENTS_RETRIGGER.forEach(function(event) {
  Player.prototype[`handleTech${toTitleCase(event)}_`] = function() {
    return this.trigger(event) ;
  };
}) ;

/**
 * Déclenché lorsque le lecteur dispose des informations initiales sur la durée et les dimensions
 *
 * @event Player#loadedmetadata
 * @type {EventTarget~Event}
 */

/**
 * Déclenché lorsque le lecteur a téléchargé des données à la position de lecture actuelle
 *
 * @event Player#loadeddata
 * @type {EventTarget~Event}
 */

/**
 * Déclenché lorsque la position de lecture actuelle a changé *
 * Pendant la lecture, cette fonction est activée toutes les 15 à 250 millisecondes, en fonction de l'intensité du son
 * la technologie de lecture utilisée.
 *
 * @event Player#timeupdate
 * @type {EventTarget~Event}
 */

/**
 * Déclenché en cas de changement de volume
 *
 * @event Player#volumechange
 * @type {EventTarget~Event}
 */

/**
 * Indique si un joueur dispose ou non d'un plugin.
 *
 * Cela ne permet pas de savoir si le plugin a été initialisé ou non
 * sur ce lecteur. Pour cela, [usingPlugin]{@link Player#usingPlugin}.
 *
 * @method Player#hasPlugin
 * @param {string} name
 *         Le nom d'un plugin.
 *
 * @return {boolean}
 *         Le lecteur dispose ou non du plugin demandé.
 */

/**
 * Indique si un lecteur utilise ou non un plugin par son nom.
 *
 * Pour les plugins de base, il s'agit uniquement de savoir si le plugin a _jamais_ été utilisé
 * initialisé sur ce lecteur.
 *
 * @method Player#usingPlugin
 * @param {string} name
 *         Le nom d'un plugin.
 *
 * @return {boolean}
 *         Indique si le lecteur utilise ou non le plugin demandé.
 */

Component.registerComponent('Player', Player) ;
exporter le lecteur par défaut ;