/**
 * Player Component - Basisklasse für alle UI-Objekte
 *
 * @Datei component.js
 * /
import window from 'global/window';
importiere evented von './mixins/evented';
import stateful from './mixins/stateful';
importiere * as Dom aus './utils/dom.js';
import * as Fn from './utils/fn.js';
importiere * als Guid aus './utils/guid.js';
importiere {toTitleCase, toLowerCase} aus './utils/string-cases.js';
importiere mergeOptions aus './utils/merge-options.js';
import computedStyle from './utils/computed-style';
importiere Map aus './utils/map.js';
importieren Sie Set aus './utils/set.js';
import keycode from 'keycode';

/**
 * Basisklasse für alle UI-Komponenten.
 * Komponenten sind UI-Objekte, die sowohl ein Javascript-Objekt als auch ein Element darstellen
 * im DOM. Sie können Kinder von anderen Komponenten sein und können
 * kinder selbst.
 *
 * Komponenten können auch Methoden von {@link EventTarget} verwenden
 * /
klasse Component {

  /**
   * Ein Callback, der aufgerufen wird, wenn eine Komponente bereit ist. Enthält keine
   * parameter und alle Rückrufwerte werden ignoriert.
   *
   * @callback Komponente~ReadyCallback
   * @diese Komponente
   * /

  /**
   * Erzeugt eine Instanz dieser Klasse.
   *
   * @param {Player} Spieler
   *        Der `Player`, dem diese Klasse zugeordnet werden soll.
   *
   * @param {Object} [Optionen]
   *        Der Schlüssel/Wertspeicher der Komponentenoptionen.
   *
   * @param {Object[]} [options.children]
   *        Ein Array von Child-Objekten, mit denen diese Komponente intialisiert werden soll. Kinder haben Objekte
   *        eine Namenseigenschaft, die verwendet wird, wenn mehr als eine Komponente des gleichen Typs verwendet werden soll
   *        hinzugefügt.
   *
   * @param {string} [options.className]
   *         Eine Klasse oder eine durch Leerzeichen getrennte Liste von Klassen zum Hinzufügen der Komponente
   *
   * @param {Component~ReadyCallback} [ready]
   *        Funktion, die aufgerufen wird, wenn die "Komponente" fertig ist.
   * /
  constructor(player, options, ready) {

    // Die Komponente könnte der Player selbst sein und wir können `this` nicht an super übergeben
    if (!player && this.play) {
      this.player_ = player = this; // eslint-disable-line
    } else {
      this.player_ = player;
    }

    this.isDisposed_ = false;

    // Den Verweis auf die übergeordnete Komponente über die Methode `addChild` halten
    this.parentComponent_ = null;

    // Erstellen Sie eine Kopie von prototype.options_, um ein Überschreiben der Standardeinstellungen zu verhindern
    this.options_ = mergeOptions({}, this.options_);

    // Aktualisierte Optionen mit gelieferten Optionen
    options = this.options_ = mergeOptions(this.options_, options);

    // Holt die ID aus den Optionen oder dem Optionselement, falls eines angegeben ist
    this.id_ = options.id || (options.el && options.el.id);

    // Wenn keine ID in den Optionen angegeben wurde, wird eine erzeugt
    if (!this.id_) {
      // Im Falle von Scheinspielern ist die Funktion Spieler-ID nicht erforderlich
      const id = player && player.id && player.id() || 'no_player';

      this.id_ = `${id}_component_${Guid.newGUID()}`;
    }

    this.name_ = options.name || null;

    // Element erstellen, wenn in den Optionen kein Element angegeben wurde
    if (options.el) {
      this.el_ = options.el;
    } else if (options.createEl !== false) {
      this.el_ = this.createEl();
    }

    if (options.className && this.el_) {
      options.className.split(' ').forEach(c => this.addClass(c));
    }

    // wenn evented irgendetwas anderes als false ist, wollen wir in evented mixin
    if (options.evented !== false) {
      // Machen Sie dies zu einem ereignisgesteuerten Objekt und verwenden Sie `el_`, falls vorhanden, als seinen Ereignisbus
      evented(this, {eventBusKey: this.el_ ? 'el_' : null});

      this.handleLanguagechange = this.handleLanguagechange.bind(this);
      this.on(this.player_, 'languagechange', this.handleLanguagechange);
    }
    stateful(this, this.constructor.defaultState);

    this.children_ = [];
    this.childIndex_ = {};
    this.childNameIndex_ = {};

    this.setTimeoutIds_ = new Set();
    this.setIntervalIds_ = new Set();
    this.rafIds_ = new Set();
    this.namedRafs_ = new Map();
    this.clearingTimersOnDispose_ = false;

    // Hinzufügen von untergeordneten Komponenten in Optionen
    if (options.initChildren !== false) {
      this.initChildren();
    }

    // Wir wollen hier nicht "ready" auslösen, sonst geht es los, bevor "init" tatsächlich ist
    // fertig für alle Kinder, die diesen Konstruktor ausführen
    this.ready(ready);

    if (options.reportTouchActivity !== false) {
      this.enableTouchActivity();
    }

  }

  /**
   * Entsorgt die Komponente und alle untergeordneten Komponenten.
   *
   * @feuert Komponente#entsorgen
   *
   * @param {Object} options
   * @param {Element} options.originalEl Element, mit dem das Player-Element ersetzt werden soll
   * /
  dispose(options = {}) {

    // Bailout, wenn die Komponente bereits entsorgt wurde.
    if (this.isDisposed_) {
      rückkehr;
    }

    if (this.readyQueue_) {
      this.readyQueue_.length = 0;
    }

    /**
     * Wird ausgelöst, wenn eine "Komponente" entsorgt wird.
     *
     * @event Komponente#dispose
     * @Typ {EventTarget~Event}
     *
     * @property {boolean} [bubbles=false]
     *           auf false gesetzt, damit das Dispose-Ereignis nicht
     *           aufsprudeln
     * /
    this.trigger({type: 'dispose', bubbles: false});

    this.isDisposed_ = true;

    // Alle Kinder entsorgen.
    if (this.children_) {
      for (let i = this.children_.length - 1; i >= 0; i--) {
        if (this.children_[i].dispose) {
          this.children_[i].dispose();
        }
      }
    }

    // Untergeordnete Referenzen löschen
    this.children_ = null;
    this.childIndex_ = null;
    this.childNameIndex_ = null;

    this.parentComponent_ = null;

    if (this.el_) {
      // Element aus dem DOM entfernen
      if (this.el_.parentNode) {
        if (options.restoreEl) {
          this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
        } else {
          this.el_.parentNode.removeChild(this.el_);
        }
      }

      this.el_ = null;
    }

    // Entfernen Sie den Verweis auf den Player, nachdem Sie das Element entsorgt haben
    this.player_ = null;
  }

  /**
   * Ermitteln Sie, ob diese Komponente entsorgt wurde oder nicht.
   *
   * @return {boolean}
   *         Wenn die Komponente entsorgt wurde, wird `true` angezeigt. Andernfalls `false`.
   * /
  isDisposed() {
    return Boolean(this.isDisposed_);
  }

  /**
   * Gibt den {@link Player} zurück, an den die "Komponente" angehängt ist.
   *
   * @return {Player}
   *         Der Spieler, an den diese Komponente angeschlossen ist.
   * /
  player() {
    return this.player_;
  }

  /**
   * Tiefes Zusammenführen von Optionsobjekten mit neuen Optionen.
   * > Anmerkung: Wenn sowohl `obj` als auch `options` Eigenschaften enthalten, deren Werte Objekte sind.
   *         Die beiden Eigenschaften werden mit {@link module:mergeOptions} zusammengeführt
   *
   * @param {Object} obj
   *        Das Objekt, das neue Optionen enthält.
   *
   * @return {Object}
   *         Ein neues Objekt aus `this.options_` und `obj` zusammengefügt.
   * /
  optionen(obj) {
    if (!obj) {
      return this.options_;
    }

    this.options_ = mergeOptions(this.options_, obj);
    return this.options_;
  }

  /**
   * Das DOM-Element "Komponente" abrufen
   *
   * @return {Element}
   *         Das DOM-Element für diese "Komponente".
   * /
  el() {
    return this.el_;
  }

  /**
   * Erstellen Sie das DOM-Element "Komponente".
   *
   * @param {string} [tagName]
   *        Der DOM-Knotentyp des Elements. z.B. 'div'
   *
   * @param {Object} [Eigenschaften]
   *        Ein Objekt mit Eigenschaften, die festgelegt werden sollen.
   *
   * @param {Object} [Attribute]
   *        Ein Objekt mit Attributen, die gesetzt werden sollen.
   *
   * @return {Element}
   *         Das Element, das erstellt wird.
   * /
  createEl(tagName, properties, attributes) {
    return Dom.createEl(tagName, properties, attributes);
  }

  /**
   * Lokalisieren Sie eine Zeichenkette, die auf Englisch vorliegt.
   *
   * Wenn Token angegeben werden, wird versucht, die angegebene Zeichenkette durch ein einfaches Token zu ersetzen.
   * Die Token, nach denen gesucht wird, sehen aus wie `{1}`, wobei der Index im Token-Array mit 1 indiziert ist.
   *
   * Wenn ein `defaultValue` angegeben wird, wird dieser über `string` verwendet,
   * wenn ein Wert in den bereitgestellten Sprachdateien nicht gefunden wird.
   * Dies ist nützlich, wenn Sie einen beschreibenden Schlüssel für die Token-Ersetzung haben möchten
   * aber eine knappe lokalisierte Zeichenkette haben und nicht verlangen, dass "de.json" enthalten ist.
   *
   * Derzeit wird sie für die Zeitmessung des Fortschrittsbalkens verwendet.
   * ``js
   * {
   *   "Fortschrittsbalken-Timing: currentTime={1} duration={2}": "{1} von {2}"
   * }
   * ```
   * Sie wird dann wie folgt verwendet:
   * ``js
   * this.localize('progress bar timing: currentTime={1} duration{2}',
   *               [this.player_.currentTime(), this.player_.duration()],
   *               '{1} von {2}');
   * ```
   *
   * Das Ergebnis ist etwa so: `01:23 von 24:56`.
   *
   *
   * @param {string} string
   *        Die zu lokalisierende Zeichenkette und der Schlüssel, nach dem in den Sprachdateien gesucht werden soll.
   * @param {string[]} [tokens]
   *        Wenn das aktuelle Element über Token-Ersetzungen verfügt, geben Sie die Token hier an.
   * @param {string} [defaultValue]
   *        Der Standardwert ist `string`. Kann ein Standardwert sein, der für die Ersetzung von Token verwendet wird
   *        wenn der Nachschlageschlüssel getrennt sein muss.
   *
   * @return {string}
   *         Die lokalisierte Zeichenkette oder, falls keine Lokalisierung existiert, die englische Zeichenkette.
   * /
  localize(string, tokens, defaultValue = string) {

    const code = this.player_.language && this.player_.language();
    const languages = this.player_.languages && this.player_.languages();
    const language = languages && languages[code];
    const primaryCode = code && code.split('-')[0];
    const primaryLang = languages && languages[primaryCode];

    let localizedString = defaultValue;

    if (language && language[string]) {
      localizedString = language[string];
    } else if (primaryLang && primaryLang[string]) {
      localizedString = primaryLang[string];
    }

    wenn (Token) {
      localizedString = localizedString.replace(/\{(\d+)\}/g, function(match, index) {
        const value = tokens[index - 1];
        let ret = Wert;

        if (typeof wert === 'undefined') {
          ret = Treffer;
        }

        return ret;
      });
    }

    return localizedString;
  }

  /**
   * Handhabt den Sprachwechsel für den Player in Komponenten. Sollte durch Unterkomponenten außer Kraft gesetzt werden.
   *
   * @Abstrakt
   * /
  handleLanguagechange() {}

  /**
   * Rückgabe des DOM-Elements `Komponente`. Hier werden Kinder eingefügt.
   * Dies ist normalerweise dasselbe wie das Element, das in {@link Component#el} zurückgegeben wird.
   *
   * @return {Element}
   *         Das Inhaltselement für diese "Komponente".
   * /
  contentEl() {
    return this.contentEl_ || this.el_;
  }

  /**
   * Die ID dieser Komponente abrufen
   *
   * @return {string}
   *         Die Kennung dieser `Komponente`
   * /
  id() {
    return this.id_;
  }

  /**
   * Ermittelt den Namen der Komponente. Der Name wird verwendet, um auf die "Komponente" zu verweisen
   * und wird bei der Registrierung festgelegt.
   *
   * @return {string}
   *         Der Name dieser "Komponente".
   * /
  name() {
    return this.name_;
  }

  /**
   * Abrufen eines Arrays aller untergeordneten Komponenten
   *
   * @return {Array}
   *         Die Kinder
   * /
  kinder() {
    return this.children_;
  }

  /**
   * Gibt die untergeordnete "Komponente" mit der angegebenen "ID" zurück.
   *
   * @param {string} id
   *        Die ID der zu erhaltenden untergeordneten Komponente.
   *
   * @return {Komponente|undefined}
   *         Die untergeordnete `Komponente` mit der angegebenen `ID` oder undefiniert.
   * /
  getChildById(id) {
    return this.childIndex_[id];
  }

  /**
   * Gibt die untergeordnete `Komponente` mit dem angegebenen `Name` zurück.
   *
   * @param {string} name
   *        Der Name der zu erhaltenden untergeordneten Komponente.
   *
   * @return {Komponente|undefined}
   *         Die untergeordnete `Komponente` mit dem angegebenen `Name` oder undefiniert.
   * /
  getChild(name) {
    if (!name) {
      rückkehr;
    }

    return this.childNameIndex_[name];
  }

  /**
   * Liefert die auf die angegebene Komponente folgende abhängige Komponente
   * nachkomme `Namen`. Zum Beispiel würde ['foo', 'bar', 'baz']
   * versuchen, 'foo' auf der aktuellen Komponente zu erhalten, 'bar' auf der 'foo'
   * komponente und 'baz' auf die 'bar'-Komponente und geben undefiniert zurück
   * wenn eine davon nicht vorhanden ist.
   *
   * @param {...string[]|...string} names
   *        Der Name der zu erhaltenden untergeordneten Komponente.
   *
   * @return {Komponente|undefined}
   *         Der auf den angegebenen Nachkommen folgende Nachkomme `Component`
   *         `Namen` oder undefiniert.
   * /
  getDescendant(...names) {
    // Array-Argumente in das Hauptarray umwandeln
    names = names.reduce((acc, n) => acc.concat(n), []);

    let currentChild = this;

    for (let i = 0; i < names.length; i++) {
      currentChild = currentChild.getChild(names[i]);

      if (!currentChild || !currentChild.getChild) {
        rückkehr;
      }
    }

    return currentChild;
  }

  /**
   * Hinzufügen einer untergeordneten `Komponente` innerhalb der aktuellen `Komponente`.
   *
   *
   * @param {String|Komponente} Kind
   *        Der Name oder die Instanz eines hinzuzufügenden Kindes.
   *
   * @param {Object} [options={}]
   *        Der Schlüssel/Wertspeicher der Optionen, die an die Kinder von
   *        das Kind.
   *
   * @param {Anzahl} [index=this.children_.length]
   *        Der Index, in dem versucht wird, ein Kind hinzuzufügen.
   *
   * @return {Komponente}
   *         Die Komponente, die als untergeordnetes Element hinzugefügt wird. Bei Verwendung einer Zeichenkette wird die
   *         komponente" wird durch diesen Prozess erstellt.
   * /
  addChild(child, options = {}, index = this.children_.length) {
    lassen Sie Komponente;
    let componentName;

    // Wenn Kind ein String ist, Komponente mit Optionen erstellen
    if (typeof kind === 'string') {
      componentName = toTitleCase(child);

      const componentClassName = options.componentClass || componentName;

      // Name über Optionen festlegen
      options.name = componentName;

      // Erstellen eines neuen Objekts & Element für dieses Kontrollset
      // Wenn kein .player_ vorhanden ist, ist dies ein Spieler
      const ComponentClass = Component.getComponent(componentClassName);

      if (!ComponentClass) {
        throw new Error(`Komponente ${componentClassName} existiert nicht`);
      }

      // Die direkt im videojs-Objekt gespeicherten Daten können
      // fälschlicherweise als zu behaltende Komponente identifiziert
      // Abwärtskompatibilität mit 4.x. Prüfen Sie, ob die
      // Komponentenklasse kann instanziiert werden.
      if (typeof ComponentClass !== 'function') {
        null zurückgeben;
      }

      component = new ComponentClass(this.player_ || this, options);

    // Kind ist eine Komponenteninstanz
    } else {
      komponente = Kind;
    }

    if (component.parentComponent_) {
      component.parentComponent_.removeChild(component);
    }
    this.children_.splice(index, 0, component);
    component.parentComponent_ = this;

    if (typeof component.id === 'function') {
      this.childIndex_[component.id()] = component;
    }

    // Wenn kein Name zum Erstellen der Komponente verwendet wurde, prüfen Sie, ob wir die
    // Name der Funktion der Komponente
    componentName = componentName || (component.name && toTitleCase(component.name()));

    if (Komponentenname) {
      this.childNameIndex_[Komponentenname] = Komponente;
      this.childNameIndex_[toLowerCase(componentName)] = component;
    }

    // Hinzufügen des Elements des UI-Objekts zum Container-Div (Box)
    // Das Vorhandensein eines Elements ist nicht erforderlich
    if (typeof component.el === 'function' && component.el()) {
      // Wenn vor einer Komponente eingefügt wird, vor dem Element dieser Komponente einfügen
      let refNode = null;

      if (this.children_[index + 1]) {
        // Die meisten untergeordneten Elemente sind Komponenten, aber die Videotechnik ist ein HTML-Element
        if (this.children_[index + 1].el_) {
          refNode = this.children_[index + 1].el_;
        } else if (Dom.isEl(this.children_[index + 1])) {
          refNode = this.children_[index + 1];
        }
      }

      this.contentEl().insertBefore(component.el(), refNode);
    }

    // Zurückgeben, damit es auf dem übergeordneten Objekt gespeichert werden kann, falls gewünscht.
    komponente zurück;
  }

  /**
   * Entfernen einer untergeordneten Komponente aus der Liste der untergeordneten Komponenten dieser Komponente. Entfernt auch
   * das untergeordnete "Komponenten"-Element von diesem "Komponenten"-Element.
   *
   * @param {Komponente} Komponente
   *        Die zu entfernende untergeordnete `Komponente`.
   * /
  removeChild(component) {
    if (typeof component === 'string') {
      component = this.getChild(component);
    }

    if (!component || !this.children_) {
      rückkehr;
    }

    let childFound = false;

    for (let i = this.children_.length - 1; i >= 0; i--) {
      if (this.children_[i] === component) {
        childFound = true;
        this.children_.splice(i, 1);
        pause;
      }
    }

    if (!childFound) {
      rückkehr;
    }

    component.parentComponent_ = null;

    this.childIndex_[component.id()] = null;
    this.childNameIndex_[toTitleCase(component.name())] = null;
    this.childNameIndex_[toLowerCase(component.name())] = null;

    const compEl = component.el();

    if (compEl && compEl.parentNode === this.contentEl()) {
      this.contentEl().removeChild(component.el());
    }
  }

  /**
   * Hinzufügen und Initialisieren von untergeordneten Standard-Komponenten auf der Grundlage von Optionen.
   * /
  initChildren() {
    const children = this.options_.children;

    wenn (Kinder) {
      // `das` ist `Elternteil`
      const parentOptions = this.options_;

      const handleAdd = (Kind) => {
        const name = kind.name;
        let opts = child.opts;

        // Zulassen, dass Optionen für Kinder an den Elternoptionen eingestellt werden können
        // z.B. videojs(id, { controlBar: false });
        // anstelle von videojs(id, { children: { controlBar: false });
        if (parentOptions[name] !== undefined) {
          opts = parentOptions[name];
        }

        // Deaktivieren von Standardkomponenten ermöglichen
        // z.B. options['children']['posterImage'] = false
        if (opts === false) {
          rückkehr;
        }

        // Erlaubt die Übergabe von Optionen als einfachen Booleschen Wert, wenn keine Konfiguration vorliegt
        // notwendig ist.
        if (opts === true) {
          opts = {};
        }

        // Wir wollen auch die ursprünglichen Spieleroptionen übergeben
        // zu jeder Komponente, so dass sie nicht in der Lage sind
        // für spätere Optionen wieder in den Player greifen.
        opts.playerOptions = this.options_.playerOptions;

        // Erstellen und Hinzufügen der untergeordneten Komponente.
        // Fügen Sie der übergeordneten Instanz einen direkten Verweis auf das Kind über den Namen hinzu.
        // Wenn zwei gleiche Komponenten verwendet werden, sollten unterschiedliche Namen angegeben werden
        // für jede
        const newChild = this.addChild(name, opts);

        if (newChild) {
          this[name] = newChild;
        }
      };

      // Erlaubt die Übergabe eines Arrays von Kinderdetails in den Optionen
      let workingChildren;
      const Tech = Component.getComponent('Tech');

      if (Array.isArray(Kinder)) {
        workingChildren = Kinder;
      } else {
        workingChildren = Object.keys(children);
      }

      workingChildren
      // Kinder, die in this.options_, aber auch in workingChildren enthalten sind, würden
      // geben uns zusätzliche Kinder, die wir nicht wollen. Wir wollen sie also herausfiltern.
        .concat(Object.keys(this.options_)
          .filter(function(child) {
            return !workingChildren.some(function(wchild) {
              if (typeof wchild === 'string') {
                return child === wchild;
              }
              return kind === wchild.name;
            });
          }))
        .map((Kind) => {
          name lassen;
          let opts;

          if (typeof kind === 'string') {
            name = Kind;
            opts = children[name] || this.options_[name] || {};
          } else {
            name = kind.name;
            opts = Kind;
          }

          return {Name, opts};
        })
        .filter((Kind) => {
        // Wir müssen sicherstellen, dass child.name nicht in der techOrder enthalten ist, da
        // Techs sind als Komponenten registriert, können aber nicht kompatibel sein
        // Siehe https://github.com/videojs/video.js/issues/2772
          const c = Component.getComponent(child.opts.componentClass ||
                                       toTitleCase(child.name));

          return c && !Tech.isTech(c);
        })
        .forEach(handleAdd);
    }
  }

  /**
   * Erzeugt den Standardnamen der DOM-Klasse. Sollte durch Unterkomponenten außer Kraft gesetzt werden.
   *
   * @return {string}
   *         Der DOM-Klassenname für dieses Objekt.
   *
   * @Abstrakt
   * /
  buildCSSClass() {
    // Untergeordnete Klassen können eine Funktion enthalten, die dies tut:
    // return 'CLASS NAME' + this._super();
    zurückgeben '';
  }

  /**
   * Binden Sie einen Listener an den Bereitschaftsstatus der Komponente.
   * Anders als bei Ereignis-Listenern, wenn das Ereignis ready bereits eingetreten ist
   * wird die Funktion sofort ausgelöst.
   *
   * @return {Komponente}
   *         Gibt sich selbst zurück; die Methode kann verkettet werden.
   * /
  ready(fn, sync = false) {
    if (!fn) {
      rückkehr;
    }

    if (!this.isReady_) {
      this.readyQueue_ = this.readyQueue_ || [];
      this.readyQueue_.push(fn);
      rückkehr;
    }

    wenn (sync) {
      fn.call(this);
    } else {
      // Rufen Sie die Funktion aus Konsistenzgründen standardmäßig asynchron auf
      this.setTimeout(fn, 1);
    }
  }

  /**
   * Löst alle bereitstehenden Zuhörer für diese Komponente aus.
   *
   * @feuer komponente#bereit
   * /
  triggerReady() {
    this.isReady_ = true;

    // Sicherstellen, dass die Bereitschaft asynchron ausgelöst wird
    this.setTimeout(function() {
      const readyQueue = this.readyQueue_;

      // Bereitschaftswarteschlange zurücksetzen
      this.readyQueue_ = [];

      if (readyQueue && readyQueue.length > 0) {
        readyQueue.forEach(function(fn) {
          fn.call(this);
        }, this);
      }

      // Erlaubt auch die Verwendung von Ereignis-Listenern
      /**
       * Wird ausgelöst, wenn eine "Komponente" bereit ist.
       *
       * @Ereignis Komponente#ready
       * @Typ {EventTarget~Event}
       * /
      this.trigger('ready');
    }, 1);
  }

  /**
   * Ein einzelnes DOM-Element finden, das einem "Selektor" entspricht. Dies kann innerhalb der `Component`s
   * `contentEl()` oder einen anderen benutzerdefinierten Kontext.
   *
   * @param {string} selector
   *        Ein gültiger CSS-Selektor, der an `querySelector` übergeben wird.
   *
   * @param {Element|String} [context=this.contentEl()]
   *        Ein DOM-Element, innerhalb dessen die Abfrage erfolgen soll. Kann auch ein Selektorstring sein in
   *        in diesem Fall wird das erste übereinstimmende Element als Kontext verwendet. Wenn
   *        fehlende `this.contentEl()` wird verwendet. Wenn `this.contentEl()` zurückkommt
   *        nichts fällt es auf `Dokument` zurück.
   *
   * @return {Element|null}
   *         das gefundene Dom-Element oder null
   *
   * @siehe [Informationen zu CSS-Selektoren](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   * /
  $(Selektor, Kontext) {
    return Dom.$(selector, context || this.contentEl());
  }

  /**
   * Findet alle DOM-Elemente, die einem "Selektor" entsprechen. Dies kann innerhalb der `Component`s
   * `contentEl()` oder einen anderen benutzerdefinierten Kontext.
   *
   * @param {string} selector
   *        Ein gültiger CSS-Selektor, der an `querySelectorAll` übergeben wird.
   *
   * @param {Element|String} [context=this.contentEl()]
   *        Ein DOM-Element, innerhalb dessen die Abfrage erfolgen soll. Kann auch ein Selektorstring sein in
   *        in diesem Fall wird das erste übereinstimmende Element als Kontext verwendet. Wenn
   *        fehlende `this.contentEl()` wird verwendet. Wenn `this.contentEl()` zurückkommt
   *        nichts fällt es auf `Dokument` zurück.
   *
   * @return {NodeList}
   *         eine Liste der gefundenen Dom-Elemente
   *
   * @siehe [Informationen zu CSS-Selektoren](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   * /
  $$(Selektor, Kontext) {
    return Dom.$$(selector, context || this.contentEl());
  }

  /**
   * Prüfen Sie, ob das Element einer Komponente einen CSS-Klassennamen hat.
   *
   * @param {string} classToCheck
   *        Name der zu prüfenden CSS-Klasse.
   *
   * @return {boolean}
   *         - Wahr, wenn die "Komponente" die Klasse hat.
   *         - False, wenn die "Komponente" nicht die "Klasse" hat
   * /
  hasClass(classToCheck) {
    return Dom.hasClass(this.el_, classToCheck);
  }

  /**
   * Fügen Sie dem Element "Komponente" einen CSS-Klassennamen hinzu.
   *
   * @param {string} classToAdd
   *        CSS-Klassenname zum Hinzufügen
   * /
  addClass(classToAdd) {
    Dom.addClass(this.el_, classToAdd);
  }

  /**
   * Entfernen eines CSS-Klassennamens aus dem Element "Component".
   *
   * @param {string} classToRemove
   *        Name der zu entfernenden CSS-Klasse
   * /
  removeClass(classToRemove) {
    Dom.removeClass(this.el_, classToRemove);
  }

  /**
   * Hinzufügen oder Entfernen eines CSS-Klassennamens aus dem Element der Komponente.
   * - ClassToToggle" wird hinzugefügt, wenn {@link Component#hasClass} false zurückgeben würde.
   * - ClassToToggle" wird entfernt, wenn {@link Component#hasClass} true zurückgeben würde.
   *
   * @param {string} classToToggle
   *         Die hinzuzufügende oder zu entfernende Klasse, basierend auf (@link Component#hasClass}
   *
   * @param {boolean|Dom~predicate} [predicate]
   *         Eine {@link Dom~predicate}-Funktion oder ein boolescher
   * /
  toggleClass(classToToggle, predicate) {
    Dom.toggleClass(this.el_, classToToggle, predicate);
  }

  /**
   * Zeigen Sie das Element "Komponente" an, wenn es durch Entfernen des Feldes "Komponente" verborgen ist
   * 'vjs-hidden' Klassenname aus.
   * /
  show() {
    this.removeClass('vjs-hidden');
  }

  /**
   * Verstecken Sie das Element `Component`, wenn es gerade angezeigt wird, indem Sie die
   * 'vjs-hidden'-Klassenname hinzugefügt werden.
   * /
  hide() {
    this.addClass('vjs-hidden');
  }

  /**
   * Sperren Sie ein "Komponenten"-Element in seinem sichtbaren Zustand, indem Sie die Option "vjs-lock-showing" hinzufügen
   * klassennamen zu. Wird während des Ein- und Ausblendens verwendet.
   *
   * @privat
   * /
  lockShowing() {
    this.addClass('vjs-lock-showing');
  }

  /**
   * Entsperren eines "Komponenten"-Elements aus seinem sichtbaren Zustand durch Entfernen der "vjs-lock-showing"-Einstellung
   * klassennamen aus. Wird während des Ein- und Ausblendens verwendet.
   *
   * @privat
   * /
  unlockShowing() {
    this.removeClass('vjs-lock-showing');
  }

  /**
   * Ermittelt den Wert eines Attributs des Elements "Komponente".
   *
   * @param {string} attribut
   *        Name des Attributs, von dem der Wert abgeleitet werden soll.
   *
   * @return {string|null}
   *         - Der Wert des Attributs, nach dem gefragt wurde.
   *         - Kann bei einigen Browsern ein leerer String sein, wenn das Attribut nicht existiert
   *           oder keinen Wert hat
   *         - Die meisten Browser geben null zurück, wenn das Attribut nicht existiert oder eine
   *           keinen Wert.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
   * /
  getAttribute(attribute) {
    return Dom.getAttribute(this.el_, attribute);
  }

  /**
   * Den Wert eines Attributs des Elements "Komponente" festlegen
   *
   * @param {string} attribut
   *        Name des zu setzenden Attributs.
   *
   * @param {string} Wert
   *        Wert, auf den das Attribut gesetzt werden soll.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
   * /
  setAttribute(attribut, wert) {
    Dom.setAttribute(this.el_, attribute, value);
  }

  /**
   * Entfernen Sie ein Attribut aus dem Element "Komponente".
   *
   * @param {string} attribut
   *        Name des zu entfernenden Attributs.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
   * /
  removeAttribute(attribute) {
    Dom.removeAttribute(this.el_, attribute);
  }

  /**
   * Holt oder setzt die Breite der Komponente basierend auf den CSS-Styles.
   * Siehe {@link Component#dimension} für weitere Informationen.
   *
   * @param {Zahl|String} [num]
   *        Die Breite, die Sie mit '%', 'px' oder gar nicht setzen wollen.
   *
   * @param {boolean} [skipListeners]
   *        Überspringen Sie den Ereignisauslöser componentresize
   *
   * @return {Zahl|String}
   *         Die Breite beim Erhalten, Null, wenn es keine Breite gibt. Kann eine Zeichenfolge sein
   *           nachgestellt mit '%' oder 'px'.
   * /
  width(num, skipListeners) {
    return this.dimension('width', num, skipListeners);
  }

  /**
   * Holt oder setzt die Höhe der Komponente basierend auf den CSS-Styles.
   * Siehe {@link Component#dimension} für weitere Informationen.
   *
   * @param {Zahl|String} [num]
   *        Die Höhe, die Sie mit '%', 'px' oder gar nicht setzen wollen.
   *
   * @param {boolean} [skipListeners]
   *        Überspringen Sie den Ereignisauslöser componentresize
   *
   * @return {Zahl|String}
   *         Die Breite beim Erhalten, Null, wenn es keine Breite gibt. Kann eine Zeichenfolge sein
   *         nachgestellt mit '%' oder 'px'.
   * /
  height(num, skipListeners) {
    return this.dimension('Höhe', num, skipListeners);
  }

  /**
   * Setzen Sie gleichzeitig die Breite und Höhe des Elements "Komponente".
   *
   * @param {Zahl|String} Breite
   *         Breite, auf die das Element `Component` gesetzt werden soll.
   *
   * @param {Zahl|String} Höhe
   *         Höhe, auf die das Element `Komponente` gesetzt werden soll.
   * /
  dimensions(width, height) {
    // Komponententresize-Listener auf Breite zur Optimierung überspringen
    this.width(width, true);
    this.height(height);
  }

  /**
   * Holt oder setzt die Breite oder Höhe des Elements "Component". Dies ist der gemeinsame Code
   * für die {@link Component#width} und {@link Component#height}.
   *
   * Wissenswertes:
   * - Wenn die Breite oder Höhe in einer Zahl angegeben ist, wird die Zahl mit dem Postfix 'px' zurückgegeben.
   * - Wenn die Breite/Höhe in Prozent angegeben ist, wird der Prozentsatz mit dem Postfix '%' zurückgegeben
   * - Ausgeblendete Elemente haben mit `window.getComputedStyle` eine Breite von 0. Diese Funktion
   *   standardmäßig auf die `style.width` der Komponente und greift auf `window.getComputedStyle` zurück.
   *   Siehe [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
   *   für weitere Informationen
   * - Wenn Sie den berechneten Stil der Komponente wünschen, verwenden Sie {@link Component#currentWidth}
   *   und {@link {Component#currentHeight}
   *
   * @feuert Komponente#Komponententresize
   *
   * @param {string} widthOrHeight
   8 "Breite" oder "Höhe
   *
   * @param {Zahl|String} [num]
   8 Neue Dimension
   *
   * @param {boolean} [skipListeners]
   *         Ereignisauslöser componentresize überspringen
   *
   * @return {number}
   *         Die Dimension, wenn sie erhalten wird oder 0, wenn sie nicht gesetzt ist
   * /
  dimension(widthOrHeight, num, skipListeners) {
    if (num !== undefiniert) {
      // Auf Null setzen, wenn null oder buchstäblich NaN (NaN !== NaN)
      if (num === null || num !== num) {
        num = 0;
      }

      // Prüfen Sie, ob Sie css width/height (% oder px) verwenden und passen Sie es an
      if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
        this.el_.style[widthOrHeight] = num;
      } else if (num === 'auto') {
        this.el_.style[widthOrHeight] = '';
      } else {
        this.el_.style[widthOrHeight] = num + 'px';
      }

      // skipListeners ermöglicht es uns, das Auslösen des Größenänderungsereignisses zu vermeiden, wenn sowohl Breite als auch Höhe eingestellt werden
      if (!skipListeners) {
        /**
         * Wird ausgelöst, wenn die Größe einer Komponente geändert wird.
         *
         * @event Komponente#componentresize
         * @Typ {EventTarget~Event}
         * /
        this.trigger('componentresize');
      }

      rückkehr;
    }

    // Es wird kein Wert gesetzt, also wird er ermittelt
    // Sicherstellen, dass das Element existiert
    if (!this.el_) {
      0 zurückgeben;
    }

    // Abfrage des Dimensionswerts aus dem Stil
    const val = this.el_.style[widthOrHeight];
    const pxIndex = val.indexOf('px');

    if (pxIndex !== -1) {
      // Rückgabe des Pixelwerts ohne 'px'
      return parseInt(val.slice(0, pxIndex), 10);
    }

    // Kein px, also % verwenden oder kein Stil wurde gesetzt, also Rückgriff auf offsetWidth/height
    // Wenn die Komponente display:none hat, wird 0 zurückgegeben
    // TODO: Handhabung von display:none und no dimension style mit px
    return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
  }

  /**
   * Ermittelt die berechnete Breite oder Höhe des Elements der Komponente.
   *
   * Verwendet `window.getComputedStyle`.
   *
   * @param {string} widthOrHeight
   *        Eine Zeichenkette, die 'Breite' oder 'Höhe' enthält. Welchen auch immer du kriegen willst.
   *
   * @return {number}
   *         Die Dimension, nach der gefragt wird, oder 0, wenn nichts festgelegt wurde
   *         für diese Dimension.
   * /
  currentDimension(widthOrHeight) {
    let computedWidthOrHeight = 0;

    if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
      throw new Error('currentDimension akzeptiert nur den Wert für Breite oder Höhe');
    }

    computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);

    // px" aus der Variable entfernen und als Ganzzahl auswerten
    computedWidthOrHeight = parseFloat(computedWidthOrHeight);

    // Wenn der berechnete Wert immer noch 0 ist, lügt der Browser möglicherweise
    // und wir wollen die Offset-Werte überprüfen.
    // Dieser Code wird auch ausgeführt, wenn getComputedStyle nicht vorhanden ist.
    if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
      const rule = `offset${toTitleCase(widthOrHeight)}`;

      computedWidthOrHeight = this.el_[rule];
    }

    return computedWidthOrHeight;
  }

  /**
   * Ein Objekt, das die Werte für Breite und Höhe der Komponente enthält
   * berechneter Stil. Verwendet `window.getComputedStyle`.
   *
   * @typedef {Object} Komponente~DimensionObject
   *
   * @Eigenschaft {Nummer} Breite
   *           Die Breite des berechneten Stils der "Komponente".
   *
   * @Eigenschaft {Zahl} Höhe
   *           Die Höhe des berechneten Stils der "Komponente".
   * /

  /**
   * Holt ein Objekt, das die berechneten Werte für Breite und Höhe des
   * element der Komponente.
   *
   * Verwendet `window.getComputedStyle`.
   *
   * @return {Component~DimensionObject}
   *         Die berechneten Abmessungen des Elements der Komponente.
   * /
  currentDimensions() {
    Rückkehr {
      breite: this.currentDimension('Breite'),
      höhe: this.currentDimension('Höhe')
    };
  }

  /**
   * Ermittelt die berechnete Breite des Elements der Komponente.
   *
   * Verwendet `window.getComputedStyle`.
   *
   * @return {number}
   *         Die berechnete Breite des Elements der Komponente.
   * /
  currentWidth() {
    return this.currentDimension('width');
  }

  /**
   * Ermittelt die berechnete Höhe des Elements der Komponente.
   *
   * Verwendet `window.getComputedStyle`.
   *
   * @return {number}
   *         Die berechnete Höhe des Elements der Komponente.
   * /
  currentHeight() {
    return this.currentDimension('Höhe');
  }

  /**
   * Den Fokus auf diese Komponente setzen
   * /
  focus() {
    this.el_.focus();
  }

  /**
   * Den Fokus von dieser Komponente entfernen
   * /
  blur() {
    this.el_.blur();
  }

  /**
   * Wenn diese Komponente ein "Keydown"-Ereignis empfängt, das sie nicht verarbeitet,
   *  wird das Ereignis zur Bearbeitung an den Player weitergegeben.
   *
   * @param {EventTarget~Event} event
   *        Das "Keydown"-Ereignis, das zum Aufruf dieser Funktion geführt hat.
   * /
  handleKeyDown(event) {
    if (this.player_) {

      // Wir stoppen die Ausbreitung hier nur, weil wir wollen, dass unbehandelte Ereignisse fallen
      // zurück zum Browser. Tabulator für Fokus-Trapping ausschließen.
      if (!keycode.isEventKey(event, 'Tab')) {
        event.stopPropagation();
      }
      this.player_.handleKeyDown(event);
    }
  }

  /**
   * Viele Komponenten hatten früher eine Methode `handleKeyPress`, die schlecht
   * benannt, weil es auf ein "Keydown"-Ereignis reagiert hat. Dieser Methodenname lautet jetzt
   * delegiert an `handleKeyDown`. Das bedeutet, dass jeder Aufruf von `handleKeyPress`
   * nicht erleben, dass ihre Methodenaufrufe nicht mehr funktionieren.
   *
   * @param {EventTarget~Event} event
   *        Das Ereignis, das zum Aufruf dieser Funktion geführt hat.
   * /
  handleKeyPress(event) {
    this.handleKeyDown(event);
  }

  /**
   * Ein "Tap"-Ereignis ausgeben, wenn die Unterstützung für Berührungsereignisse erkannt wird. Dies wird genutzt, um
   * unterstützung des Umschaltens der Steuerung durch Antippen des Videos. Sie werden aktiviert
   * weil jede Unterkomponente sonst zusätzlichen Aufwand verursachen würde.
   *
   * @privat
   * @Feuer Komponente#tap
   * @listens Komponente#touchstart
   * @listens Komponente#touchmove
   * @listens Komponente#touchleave
   * @listens Komponente#touchcancel
   * @listens Komponente#touchend

   * /
  emitTapEvents() {
    // Erfassen Sie die Startzeit, damit wir feststellen können, wie lange die Berührung gedauert hat
    let touchStart = 0;
    let firstTouch = null;

    // Maximale Bewegung, die während eines Berührungsereignisses erlaubt ist, um noch als Tippen zu gelten
    // Andere populäre Bibliotheken verwenden zwischen 2 (hammer.js) und 15,
    // 10 scheint also eine schöne, runde Zahl zu sein.
    const tapMovementThreshold = 10;

    // Die maximale Länge einer Berührung, die noch als Antippen gewertet werden kann
    const touchTimeThreshold = 200;

    let couldBeTap;

    this.on('touchstart', function(event) {
      // Bei mehr als einem Finger ist dies nicht als Klick zu werten
      if (event.touches.length === 1) {
        // Kopieren von pageX/pageY aus dem Objekt
        firstTouch = {
          pageX: event.touches[0].pageX,
          pageY: event.touches[0].pageY
        };
        // Aufzeichnung der Startzeit, damit wir ein Antippen im Gegensatz zu "Berühren und Halten" erkennen können
        touchStart = window.performance.now();
        // Zurücksetzen der couldBeTap-Verfolgung
        couldBeTap = true;
      }
    });

    this.on('touchmove', function(event) {
      // Bei mehr als einem Finger ist dies nicht als Klick zu werten
      if (event.touches.length > 1) {
        couldBeTap = false;
      } else if (firstTouch) {
        // Einige Geräte lösen bei jeder noch so kleinen Berührung Touchmoves aus.
        // Wenn wir uns also nur ein kleines Stück bewegen, könnte dies immer noch ein Wasserhahn sein
        const xdiff = event.touches[0].pageX - firstTouch.pageX;
        const ydiff = event.touches[0].pageY - firstTouch.pageY;
        const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);

        if (touchDistance > tapMovementThreshold) {
          couldBeTap = false;
        }
      }
    });

    const noTap = function() {
      couldBeTap = false;
    };

    // TODO: Hören Sie sich das ursprüngliche Ziel an. http://youtu.be/DujfpXOKUp8?t=13m8s
    this.on('touchleave', noTap);
    this.on('touchcancel', noTap);

    // Wenn die Berührung endet, messen Sie, wie lange sie gedauert hat, und lösen Sie die entsprechende
    // Ereignis
    this.on('touchend', function(event) {
      firstTouch = null;
      // Nur fortfahren, wenn das Ereignis touchmove/leave/cancel nicht stattgefunden hat
      if (couldBeTap === true) {
        // Messen, wie lange die Berührung gedauert hat
        const touchTime = window.performance.now() - touchStart;

        // Vergewissern Sie sich, dass die Berührung unter dem Schwellenwert lag, um als Antippen zu gelten
        wenn (touchTime < touchTimeThreshold) {
          // Lassen Sie nicht zu, dass der Browser dies in einen Klick verwandelt
          event.preventDefault();
          /**
           * Wird ausgelöst, wenn eine "Komponente" angetippt wird.
           *
           * @event Komponente#tap
           * @Typ {EventTarget~Event}
           * /
          this.trigger('tap');
          // Es kann sinnvoll sein, das berührende Ereignisobjekt zu kopieren und die
          // Typ zu tippen, wenn die anderen Ereigniseigenschaften nicht exakt sind nach
          // Events.fixEvent läuft (z.B. event.target)
        }
      }
    });
  }

  /**
   * Diese Funktion meldet Benutzeraktivitäten, wenn Berührungsereignisse auftreten. Dies kann zu
   * von allen Unterkomponenten ausgeschaltet werden, die Berührungsereignisse auf eine andere Weise wirken lassen wollen.
   *
   * Berichten Sie die Berührungsaktivitäten des Benutzers, wenn Berührungsereignisse auftreten. Benutzeraktivität wird genutzt, um
   * bestimmen, wann Steuerelemente ein- und ausgeblendet werden sollen. Es ist ganz einfach, wenn es um die Maus geht
   * ereignisse, denn jedes Mausereignis sollte die Steuerelemente anzeigen. So erfassen wir die Maus
   * ereignisse, die auf den Spieler zukommen, und melden Aktivitäten, wenn dies geschieht.
   * Bei Berührungsereignissen ist es nicht so einfach, den Spieler zwischen "Touchstart" und "Touchende" umzuschalten
   * kontrollen. Touch-Events können uns also auch auf Spielerebene nicht helfen.
   *
   * Die Benutzeraktivität wird asynchron überprüft. Was also passieren könnte, ist ein Tap-Ereignis
   * auf dem Video schaltet die Steuerung aus. Dann sprudelt das "berührende" Ereignis bis zu
   * den Spieler. Wenn es Benutzeraktivitäten melden würde, würde es die Kontrollen nach rechts drehen
   * wieder auf. Wir möchten auch nicht vollständig verhindern, dass Touch-Events aufblasen.
   * Außerdem sollte ein "touchmove"-Ereignis und alles andere als ein Antippen nicht zu
   * kontrollen wieder ein.
   *
   * @listens Komponente#touchstart
   * @listens Komponente#touchmove
   * @listens Komponente#touchend
   * @listens Komponente#touchcancel
   * /
  enableTouchActivity() {
    // Nicht fortfahren, wenn der Stammplayer die Meldung von Benutzeraktivitäten nicht unterstützt
    if (!this.player() || !this.player().reportUserActivity) {
      rückkehr;
    }

    // Listener für die Meldung, dass der Benutzer aktiv ist
    const report = Fn.bind(this.player(), this.player().reportUserActivity);

    let touchHolding;

    this.on('touchstart', function() {
      report();
      // Solange sie das Gerät berühren oder die Maus gedrückt halten,
      // Wir betrachten sie als aktiv, auch wenn sie ihren Finger oder ihre Maus nicht bewegen.
      // Wir wollen also weiterhin aktualisieren, dass sie aktiv sind
      this.clearInterval(touchHolding);
      // Bericht in demselben Intervall wie activityCheck
      touchHolding = this.setInterval(report, 250);
    });

    const touchEnd = function(event) {
      report();
      // Anhalten des Intervalls, das die Aktivität aufrechterhält, wenn die Berührung gehalten wird
      this.clearInterval(touchHolding);
    };

    this.on('touchmove', report);
    this.on('touchend', touchEnd);
    this.on('touchcancel', touchEnd);
  }

  /**
   * Ein Callback, der keine Parameter hat und in den `Component`s Kontext gebunden ist.
   *
   * @callback Component~GenericCallback
   * @diese Komponente
   * /

  /**
   * Erzeugt eine Funktion, die nach einer Zeitüberschreitung von `x` Millisekunden ausgeführt wird. Diese Funktion ist eine
   * umhüllung von `window.setTimeout`. Es gibt einige Gründe, die für diese Variante sprechen
   * stattdessen aber:
   * 1. Es wird über {@link Component#clearTimeout} gelöscht, wenn
   *    {@link Component#dispose} wird aufgerufen.
   * 2. Der Funktions-Callback wird in einen {@link Component~GenericCallback} umgewandelt
   *
   * > Anmerkung: Sie können `window.cleArtimeOut` für die von dieser Funktion zurückgegebene ID nicht verwenden. Diese
   *         wird dazu führen, dass sein Dispose-Listener nicht bereinigt wird! Bitte verwenden Sie
   *         {@link Component#clearTimeout} oder {@link Component#dispose} stattdessen.
   *
   * @param {Component~GenericCallback} fn
   *        Die Funktion, die nach `timeout` ausgeführt wird.
   *
   * @param {number} timeout
   *        Zeitüberschreitung in Millisekunden, die vor der Ausführung der angegebenen Funktion verstreichen soll.
   *
   * @return {number}
   *         Gibt eine Timeout-ID zurück, die zur Identifizierung des Timeouts verwendet wird. Sie kann auch
   *         wird in {@link Component#clearTimeout} verwendet, um die Zeitüberschreitung zu löschen, die
   *         festgelegt wurde.
   *
   * @listens Komponente#dispose
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
   * /
  setTimeout(fn, timeout) {
    // als Variablen deklarieren, damit sie in der Timeout-Funktion ordnungsgemäß verfügbar sind
    // eslint-disable-next-line
    var timeoutId, disposeFn;

    fn = Fn.bind(this, fn);

    this.clearTimersOnDispose_();

    timeoutId = window.setTimeout(() => {
      if (this.setTimeoutIds_.has(timeoutId)) {
        this.setTimeoutIds_.delete(timeoutId);
      }
      fn();
    }, timeout);

    this.setTimeoutIds_.add(timeoutId);

    return timeoutId;
  }

  /**
   * Löscht eine Zeitüberschreitung, die durch `window.setTimeout` oder
   * {@link Component#setTimeout}. Wenn Sie über {@link Component#setTimeout} eine Zeitüberschreitung festlegen
   * diese Funktion anstelle von `window.clearTimout` verwenden. Wenn nicht, entsorgen Sie
   * listener wird erst nach {@link Component#dispose} aufgeräumt!
   *
   * @param {Nummer} timeoutId
   *        Die ID der zu löschenden Zeitüberschreitung. Der Rückgabewert von
   *        {@link Component#setTimeout} oder `window.setTimeout`.
   *
   * @return {number}
   *         Gibt die Timeout-ID zurück, die gelöscht wurde.
   *
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
   * /
  clearTimeout(timeoutId) {
    if (this.setTimeoutIds_.has(timeoutId)) {
      this.setTimeoutIds_.delete(timeoutId);
      window.clearTimeout(timeoutId);
    }

    return timeoutId;
  }

  /**
   * Erzeugt eine Funktion, die alle `x` Millisekunden ausgeführt wird. Diese Funktion ist ein Wrapper
   * um `window.setInterval`. Es gibt jedoch ein paar Gründe, diesen stattdessen zu verwenden.
   * 1. Es wird über {@link Component#clearInterval} gelöscht, wenn
   *    {@link Component#dispose} wird aufgerufen.
   * 2. Der Funktions-Callback ist ein {@link Component~GenericCallback}
   *
   * @param {Component~GenericCallback} fn
   *        Die Funktion, die alle `x` Sekunden ausgeführt werden soll.
   *
   * @param {Anzahl} Intervall
   *        Führt die angegebene Funktion alle `x` Millisekunden aus.
   *
   * @return {number}
   *         Gibt eine ID zurück, die zur Identifizierung des Intervalls verwendet werden kann. Es kann auch verwendet werden in
   *         {@link Component#clearInterval}, um das Intervall zu löschen.
   *
   * @listens Komponente#dispose
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
   * /
  setInterval(fn, interval) {
    fn = Fn.bind(this, fn);

    this.clearTimersOnDispose_();

    const intervalId = window.setInterval(fn, interval);

    this.setIntervalIds_.add(intervalId);

    return intervalId;
  }

  /**
   * Löscht ein Intervall, das mit `window.setInterval` erstellt wurde oder
   * {@link Component#setInterval}. Wenn Sie einen Inteval über {@link Component#setInterval} setzen
   * diese Funktion anstelle von `window.clearInterval` verwenden. Wenn nicht, entsorgen Sie
   * listener wird erst nach {@link Component#dispose} aufgeräumt!
   *
   * @param {Nummer} intervalId
   *        Die ID des zu löschenden Intervalls. Der Rückgabewert von
   *        {@link Component#setInterval} oder `window.setInterval`.
   *
   * @return {number}
   *         Gibt die Intervall-ID zurück, die gelöscht wurde.
   *
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
   * /
  clearInterval(intervalId) {
    if (this.setIntervalIds_.has(intervalId)) {
      this.setIntervalIds_.delete(intervalId);
      window.clearInterval(intervalId);
    }

    return intervalId;
  }

  /**
   * Stellt einen Callback in die Warteschlange, der an requestAnimationFrame (rAF) übergeben wird, aber
   * mit ein paar zusätzlichen Boni:
   *
   * - Unterstützt Browser, die rAF nicht unterstützen, indem sie auf
   *   {@link Component#setTimeout}.
   *
   * - Der Callback wird in einen {@link Component~GenericCallback} (d.h..
   *   an die Komponente gebunden).
   *
   * - Der automatische Abbruch des RAF-Callbacks erfolgt, wenn die Komponente
   *   entsorgt wird, bevor es aufgerufen wird.
   *
   * @param {Component~GenericCallback} fn
   *         Eine Funktion, die an diese Komponente gebunden ist und gerade ausgeführt wird
   *         vor dem nächsten Repaint des Browsers.
   *
   * @return {number}
   *         Gibt eine RAF-ID zurück, die zur Identifizierung der Zeitüberschreitung verwendet wird. Sie kann
   *         kann auch in {@link Component#cancelAnimationFrame} zum Abbrechen verwendet werden
   *         der Animationsrahmen-Rückruf.
   *
   * @listens Komponente#dispose
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
   * /
  requestAnimationFrame(fn) {
    // Zurück zur Verwendung eines Timers.
    if (!this.supportsRaf_) {
      return this.setTimeout(fn, 1000 / 60);
    }

    this.clearTimersOnDispose_();

    // als Variablen deklarieren, damit sie in der RAF-Funktion ordnungsgemäß verfügbar sind
    // eslint-disable-next-line
    var id;
    fn = Fn.bind(this, fn);

    id = window.requestAnimationFrame(() => {
      if (this.rafIds_.has(id)) {
        this.rafIds_.delete(id);
      }
      fn();
    });
    this.rafIds_.add(id);

    return id;
  }

  /**
   * Einen Animationsrahmen anfordern, aber nur eine benannte Animation
   * rahmen wird in die Warteschlange gestellt. Eine weitere wird erst dann hinzugefügt, wenn
   * die vorhergehende beendet ist.
   *
   * @param {string} name
   *        Der Name, der diesem requestAnimationFrame gegeben werden soll
   *
   * @param {Component~GenericCallback} fn
   *         Eine Funktion, die an diese Komponente gebunden ist und gerade ausgeführt wird
   *         vor dem nächsten Repaint des Browsers.
   * /
  requestNamedAnimationFrame(name, fn) {
    if (this.namedRafs_.has(name)) {
      rückkehr;
    }
    this.clearTimersOnDispose_();

    fn = Fn.bind(this, fn);

    const id = this.requestAnimationFrame(() => {
      fn();
      if (this.namedRafs_.has(name)) {
        this.namedRafs_.delete(name);
      }
    });

    this.namedRafs_.set(name, id);

    name zurückgeben;
  }

  /**
   * Bricht ein aktuelles benanntes Animationsbild ab, wenn es existiert.
   *
   * @param {string} name
   *        Der Name des abzubrechenden requestAnimationFrame.
   * /
  cancelNamedAnimationFrame(name) {
    if (!this.namedRafs_.has(name)) {
      rückkehr;
    }

    this.cancelAnimationFrame(this.namedRafs_.get(name));
    this.namedRafs_.delete(name);
  }

  /**
   * Bricht einen an {@link Component#requestAnimationFrame} übergebenen Warteschlangen-Callback ab
   * (rAF).
   *
   * Wenn Sie einen rAF-Callback über {@link Component#requestAnimationFrame} in die Warteschlange stellen,
   * diese Funktion anstelle von `window.cancelAnimationFrame` verwenden. Wenn Sie das nicht tun,
   * ihr Dispose-Listener wird nicht aufgeräumt, bis {@link Component#dispose}!
   *
   * @param {Nummer} id
   *        Die RAF-ID ist zu löschen. Der Rückgabewert von {@link Component#requestAnimationFrame}.
   *
   * @return {number}
   *         Gibt die RAF-ID zurück, die gelöscht wurde.
   *
   * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
   * /
  cancelAnimationFrame(id) {
    // Zurück zur Verwendung eines Timers.
    if (!this.supportsRaf_) {
      return this.clearTimeout(id);
    }

    if (this.rafIds_.has(id)) {
      this.rafIds_.delete(id);
      window.cancelAnimationFrame(id);
    }

    return id;

  }

  /**
   * Eine Funktion zur Einrichtung von `requestAnimationFrame`, `setTimeout`,
   * und `setInterval`, Löschung bei Dispose.
   *
   * > Zuvor fügte jeder Timer selbständig Dispose-Listener hinzu und entfernte sie wieder.
   * Um die Leistung zu verbessern, wurde beschlossen, sie alle zu stapeln und "Set" zu verwenden
   * um ausstehende Timer-IDs zu verfolgen.
   *
   * @privat
   * /
  clearTimersOnDispose_() {
    if (this.clearingTimersOnDispose_) {
      rückkehr;
    }

    this.clearingTimersOnDispose_ = true;
    this.one('dispose', () => {
      [
        ['namedRafs_', 'cancelNamedAnimationFrame'],
        ['rafIds_', 'cancelAnimationFrame'],
        ['setTimeoutIds_', 'clearTimeout'],
        ['setIntervalIds_', 'clearInterval']
      ].forEach(([idName, cancelName]) => {
        // für einen "Set"-Schlüssel wird tatsächlich wieder der Wert sein
        // also forEach((val, val) =>` aber für Maps wollen wir verwenden
        // den Schlüssel.
        this[idName].forEach((val, key) => this[cancelName](key));
      });

      this.clearingTimersOnDispose_ = false;
    });
  }

  /**
   * Registrieren Sie eine "Komponente" mit "videojs" unter Angabe des Namens und der Komponente.
   *
   * > HINWEIS: {@link Tech}s sollten nicht als `Komponente` registriert werden. {@link Tech}s
   *         sollte mit {@link Tech.registerTech} registriert werden oder
   *         {@link videojs:videojs.registerTech}.
   *
   * > HINWEIS: Diese Funktion ist auch bei videojs zu sehen als
   *         {@link videojs:videojs.registerComponent}.
   *
   * @param {string} name
   *        Der Name der zu registrierenden `Komponente`.
   *
   * @param {Component} ComponentToRegister
   *        Die zu registrierende `Komponenten`-Klasse.
   *
   * @return {Komponente}
   *         Die "Komponente", die registriert wurde.
   * /
  static registerComponent(name, ComponentToRegister) {
    if (typeof name !== 'string' || !name) {
      throw new Error(`Ungültiger Komponentenname, "${name}"; muss eine nicht leere Zeichenkette sein.`);
    }

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

    // Wir müssen sicherstellen, dass diese Prüfung nur durchgeführt wird, wenn Tech registriert wurde.
    const isTech = Tech && Tech.isTech(ComponentToRegister);
    const isComp = Component === ComponentToRegister ||
      Component.prototype.isPrototypeOf(ComponentToRegister.prototype);

    if (isTech || !isComp) {
      grund lassen;

      wenn (isTech) {
        reason = 'Techs müssen mit Tech.registerTech() registriert werden';
      } else {
        grund = 'muss eine Komponenten-Unterklasse sein';
      }

      throw new Error(`Unzulässige Komponente, "${Name}"; ${Grund}.`);
    }

    name = toTitleCase(name);

    if (!Component.components_) {
      Component.components_ = {};
    }

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

    if (name === 'Player' && Player && Player.players) {
      const players = Player.players;
      const playerNames = Object.keys(players);

      // Wenn wir Spieler haben, die entsorgt wurden, dann wird ihr Name immer noch
      // in Players.players. Wir müssen also eine Schleife durchlaufen und überprüfen, ob der Wert
      // für jedes Element ist nicht null. Dies ermöglicht die Registrierung der Player-Komponente
      // nachdem alle Spieler entsorgt wurden oder bevor welche erstellt wurden.
      wenn (Spieler &&
          playerNames.length > 0 &&
          playerNames.map((pname) => players[pname]).every(Boolean)) {
        throw new Error('Kann die Komponente Player nicht registrieren, nachdem der Player erstellt wurde.');
      }
    }

    Component.components_[name] = ComponentToRegister;
    Component.components_[toLowerCase(name)] = ComponentToRegister;

    return ComponentToRegister;
  }

  /**
   * Ermittelt eine "Komponente" anhand des Namens, unter dem sie registriert wurde.
   *
   * @param {string} name
   *        Der Name der zu ermittelnden Komponente.
   *
   * @return {Komponente}
   *         Die "Komponente", die unter dem angegebenen Namen registriert wurde.
   * /
  static getComponent(name) {
    if (!name || !Component.components_) {
      rückkehr;
    }

    return Component.components_[name];
  }
}

/**
 * Ob diese Komponente `requestAnimationFrame` unterstützt oder nicht.
 *
 * Dies wird in erster Linie zu Testzwecken ausgesetzt.
 *
 * @privat
 * @Typ {Boolean}
 * /
Component.prototype.supportsRaf_ = typeof window.requestAnimationFrame === 'function' &&
  typeof window.cancelAnimationFrame === 'function';

Component.registerComponent('Component', Component);

standardkomponente exportieren;