/**
 * 플레이어 구성 요소 - 모든 UI 개체의 기본 클래스
 *
 * @파일 구성요소.js
 */
'글로벌/창'에서 창 가져오기;
'./mixins/evented'에서 이벤트 가져오기;
'./mixins/stateful'에서 상태 저장 가져오기;
import * as Dom from './utils/dom.js';
import * as Fn from './utils/fn.js';
import * as Guid from './utils/guid.js';
'./utils/string-cases.js'에서 {toTitleCase, toLowerCase} 가져오기;
'./utils/merge-options.js'에서 mergeOptions 가져오기;
'./utils/computed-style'에서 computedStyle 가져오기;
'./utils/map.js'에서 지도 가져오기;
'./utils/set.js'에서 세트 가져오기;
'키코드'에서 키코드 가져오기;

/**
 * 모든 UI 구성 요소의 기본 클래스입니다.
 * 구성요소는 자바스크립트 객체와 요소를 모두 나타내는 UI 객체입니다.
 * DOM에서. 다른 구성 요소의 자식일 수 있으며 다음을 가질 수 있습니다.
 * 아이들 스스로.
 *
 * 구성 요소는 {@link EventTarget}의 메서드도 사용할 수 있습니다.
 */
클래스 구성 요소 {

  /**
   * 구성 요소가 준비되면 호출되는 콜백입니다. 없음
   * 매개변수 및 모든 콜백 값은 무시됩니다.
   *
   * @callback 컴포넌트~ReadyCallback
   * @이 컴포넌트
   */

  /**
   * 이 클래스의 인스턴스를 만듭니다.
   *
   * @param {플레이어} 플레이어
   * 이 클래스가 연결되어야 하는 `Player`.
   *
   * @param {객체} [옵션]
   * 구성 요소 옵션의 키/값 저장소입니다.
   *
   * @param {객체[]} [옵션.어린이]
   * 이 구성 요소를 초기화할 자식 개체의 배열입니다. 하위 개체에는
   * 동일한 유형의 구성 요소가 두 개 이상 필요한 경우 사용할 이름 속성
   * 추가했습니다.
   *
   * @param {문자열} [옵션.클래스 이름]
   * 구성 요소를 추가할 클래스 또는 공백으로 구분된 클래스 목록
   *
   * @param {Component~ReadyCallback} [준비]
   * `Component`가 준비되면 호출되는 함수.
   */
  생성자(플레이어, 옵션, 준비) {

    // 구성 요소는 플레이어 자체일 수 있으며 `this`를 super에 전달할 수 없습니다.
    만약 (!플레이어 && 이.플레이) {
      this.player_ = 플레이어 = 이; // eslint 비활성화 라인
    } else {
      this.player_ = 플레이어;
    }

    this.isDisposed_ = 거짓;

    // `addChild` 메서드를 통해 부모 구성 요소에 대한 참조를 유지합니다.
    this.parentComponent_ = null;

    // 기본값을 덮어쓰지 않도록 보호하기 위해 prototype.options_의 복사본을 만듭니다.
    this.options_ = mergeOptions({}, this.options_);

    // 제공된 옵션으로 업데이트된 옵션
    options = this.options_ = mergeOptions(this.options_, options);

    // options 또는 options 요소가 제공된 경우 ID를 가져옵니다.
    this.id_ = options.id || (옵션.엘 && options.el.id);

    // 옵션에서 ID가 없으면 하나 생성
    if (!this.id_) {
      // 모의 플레이어의 경우 플레이어 ID 기능을 요구하지 않음
      const id = 플레이어 && player.id && player.id() || 'no_player';

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

    this.name_ = options.name || 없는;

    // 옵션에 요소가 제공되지 않은 경우 요소 생성
    if (옵션.엘) {
      this.el_ = options.el;
    } 그렇지 않으면 (options.createEl !== false) {
      this.el_ = this.createEl();
    }

    if (옵션.클래스이름 && 이.el_) {
      options.className.split(' ').forEach(c => this.addClass(c));
    }

    // evented가 거짓이 아닌 경우 evented에 혼합하려고 합니다.
    if (options.evented !== false) {
      // 이것을 이벤트 객체로 만들고 가능한 경우 `el_`을 이벤트 버스로 사용합니다.
      evented(이 {eventBusKey: this.el_ ? 'el_' : null});

      this.handleLanguagechange = this.handleLanguagechange.bind(this);
      this.on(this.player_, '언어변경', 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_ = 거짓;

    // 옵션에 하위 구성 요소를 추가합니다.
    if (options.initChildren !== false) {
      this.initChildren();
    }

    // 여기에서 준비를 트리거하지 않으려면 실제로 init가 시작되기 전에 실행됩니다.
    // 이 생성자를 실행하는 모든 자식에 대해 완료됨
    this.ready(준비);

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

  }

  /**
   * `Component` 및 모든 자식 구성 요소를 폐기하십시오.
   *
   * @fires 컴포넌트#dispose
   *
   * @param {객체} 옵션
   * @param {Element} options.originalEl 요소로 대체할 플레이어 요소
   */
  폐기(옵션 = {}) {

    // 구성 요소가 이미 삭제된 경우 구제 조치를 취합니다.
    if (this.isDisposed_) {
      반품;
    }

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

    /**
     * `Component`가 삭제될 때 트리거됩니다.
     *
     * @event 컴포넌트#dispose
     * @type {이벤트대상~이벤트}
     *
     * @property {부울} [버블=거짓]
     * dispose 이벤트가 발생하지 않도록 false로 설정
     * 버블 업
     */
    this.trigger({유형: '처리', 거품: 거짓});

    this.isDisposed_ = 참;

    // 모든 자식을 삭제합니다.
    if (이.어린이_) {
      for (let i = this.children_.length - 1; i > = 0; 나--) {
        if (this.children_[i].dispose) {
          this.children_[i].dispose();
        }
      }
    }

    // 하위 참조 삭제
    this.children_ = null;
    this.childIndex_ = null;
    this.childNameIndex_ = null;

    this.parentComponent_ = null;

    if (this.el_) {
      // DOM에서 요소 제거
      if (this.el_.parentNode) {
        if (옵션.restoreEl) {
          this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
        } else {
          this.el_.parentNode.removeChild(this.el_);
        }
      }

      this.el_ = null;
    }

    // 요소를 삭제한 후 플레이어에 대한 참조를 제거합니다.
    this.player_ = null;
  }

  /**
   * 이 구성 요소가 폐기되었는지 여부를 확인합니다.
   *
   * @return {부울}
   * 구성 요소가 삭제된 경우 'true'가 됩니다. 그렇지 않으면 '거짓'입니다.
   */
  isDisposed() {
    반환 부울(this.isDisposed_);
  }

  /**
   * `Component`가 연결된 {@link Player}를 반환합니다.
   *
   * @return {플레이어}
   * 이 `Component`가 연결된 플레이어입니다.
   */
  플레이어() {
    this.player_를 반환합니다.
  }

  /**
   * 옵션 개체와 새 옵션의 심층 병합.
   * > 메모: `obj`와 `options` 모두 값이 개체인 속성을 포함하는 경우.
   * 두 속성은 {@link module:mergeOptions}를 사용하여 병합됩니다.
   *
   * @param {객체} 객체
   * 새 옵션을 포함하는 개체입니다.
   *
   * @return {객체}
   * `this.options_`와 `obj`의 새 개체가 함께 병합되었습니다.
   */
  옵션(객체) {
    경우 (!obj) {
      this.options_를 반환합니다.
    }

    this.options_ = mergeOptions(this.options_, obj);
    this.options_를 반환합니다.
  }

  /**
   * `Component`의 DOM 요소 가져오기
   *
   * @return {요소}
   * 이 `Component`에 대한 DOM 요소입니다.
   */
  엘() {
    this.el_을 반환합니다.
  }

  /**
   * `Component`의 DOM 요소를 생성합니다.
   *
   * @param {문자열} [태그 이름]
   * 요소의 DOM 노드 유형. 예: 'div'
   *
   * @param {객체} [속성]
   * 설정해야 하는 속성의 개체입니다.
   *
   * @param {객체} [속성]
   * 설정되어야 하는 속성의 객체.
   *
   * @return {요소}
   * 생성되는 요소.
   */
  createEl(태그 이름, 속성, 속성) {
    return Dom.createEl(tagName, 속성, 속성);
  }

  /**
   * 주어진 문자열을 영어로 현지화합니다.
   *
   * 토큰이 제공되면 제공된 문자열에서 간단한 토큰 교체를 시도하고 실행합니다.
   * 찾는 토큰은 토큰 배열에 인덱스가 1인 `{1}`과 같습니다.
   *
   * `defaultValue`가 제공되면 `string`보다 이를 사용합니다.
   * 제공된 언어 파일에서 값을 찾을 수 없는 경우.
   * 토큰 교체를 위한 설명 키가 필요한 경우에 유용합니다.
   * 그러나 간결한 현지화 문자열이 있고 `en.json`을 포함할 필요가 없습니다.
   *
   * 현재 진행률 표시줄 타이밍에 사용됩니다.
   * ```js
   * {
   * "진행률 표시줄 타이밍: currentTime={1} duration={2}": "{1}/{2}"
   * }
   * ```
   * 다음과 같이 사용합니다.
   * ```js
   * this.localize('진행률 표시줄 타이밍: currentTime={1} duration{2}',
   * [this.player_.currentTime(), this.player_.duration()],
   * '{1}/{2}');
   * ```
   *
   * 다음과 같이 출력됩니다: `01:23 of 24:56`.
   *
   *
   * @param {문자열} 문자열
   * 현지화할 문자열과 언어 파일에서 조회할 키입니다.
   * @param {문자열[]} [토큰]
   * 현재 항목에 대체 토큰이 있는 경우 여기에 토큰을 제공하십시오.
   * @param {문자열} [기본값]
   * 기본값은 `문자열`입니다. 토큰 교체에 사용할 기본값이 될 수 있습니다.
   * 별도의 조회 키가 필요한 경우.
   *
   * @return {문자열}
   * 현지화된 문자열 또는 현지화가 없는 경우 영어 문자열.
   */
  localize(문자열, 토큰, defaultValue = 문자열) {

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

    let localizedString = defaultValue;

    만약 (언어 && 언어[문자열]) {
      localizedString = 언어[문자열];
    } 그렇지 않으면 (primaryLang && 기본 언어[문자열]) {
      localizedString = primaryLang[문자열];
    }

    if (토큰) {
      localizedString = localizedString.replace(/\{(\d+)\}/g, function(match, index) {
        const 값 = 토큰[인덱스 - 1];
        ret = 값;

        if (값 유형 === '정의되지 않음') {
          ret = 매치;
        }

        반환 ret;
      });
    }

    localizedString 반환;
  }

  /**
   * 구성 요소에서 플레이어의 언어 변경을 처리합니다. 하위 구성요소에 의해 재정의되어야 합니다.
   *
   * @추상적인
   */
  handleLanguagechange() {}

  /**
   * `Component`의 DOM 요소를 반환합니다. 이것은 아이들이 삽입되는 곳입니다.
   * 일반적으로 {@link Component#el}에서 반환되는 요소와 동일합니다.
   *
   * @return {요소}
   * 이 `Component`에 대한 콘텐츠 요소입니다.
   */
  내용엘() {
    return this.contentEl_ || this.el_;
  }

  /**
   * 이 `Component` ID 얻기
   *
   * @return {문자열}
   * 이 `Component`의 id
   */
  ID() {
    this.id_를 반환합니다.
  }

  /**
   * `컴포넌트` 이름을 얻습니다. 이름은 `Component`를 참조하는 데 사용됩니다.
   * 등록시 설정됩니다.
   *
   * @return {문자열}
   * 이 `컴포넌트`의 이름.
   */
  이름() {
    this.name_을 반환합니다.
  }

  /**
   * 모든 하위 구성 요소의 배열 가져오기
   *
   * @return {배열}
   * 아이들
   */
  어린이들() {
    this.children_을 반환합니다.
  }

  /**
   * 주어진 `id`를 가진 자식 `Component`를 반환합니다.
   *
   * @param {문자열} 아이디
   * 가져올 자식 `Component`의 ID입니다.
   *
   * @return {구성요소|정의되지 않음}
   * 주어진 `id` 또는 정의되지 않은 하위 `Component`.
   */
  getChildById(id) {
    return this.childIndex_[id];
  }

  /**
   * 주어진 `name`을 가진 자식 `Component`를 반환합니다.
   *
   * @param {문자열} 이름
   * 가져올 자식 `Component`의 이름입니다.
   *
   * @return {구성요소|정의되지 않음}
   * 주어진 `이름`이 있거나 정의되지 않은 자식 `구성 요소`.
   */
  getChild(이름) {
    if (!이름) {
      반품;
    }

    return this.childNameIndex_[이름];
  }

  /**
   * 주어진 다음의 자손 `Component`를 반환합니다.
   * 자손 `이름`. 예를 들어 ['foo', 'bar', 'baz']는
   * 현재 구성 요소에서 'foo'를 가져오고 'foo'에서 'bar'를 가져오려고 시도합니다.
   * 'bar' 컴포넌트의 component 및 'baz' 및 정의되지 않은 반환
   * 존재하지 않는 경우.
   *
   * @param {...string[]|...string} 이름
   * 가져올 자식 `Component`의 이름입니다.
   *
   * @return {구성요소|정의되지 않음}
   * 주어진 하위 항목을 따르는 하위 항목 `Component`
   * `이름` 또는 정의되지 않음.
   */
  getDescendant(...이름) {
    // 배열 인수를 기본 배열로 병합
    이름 = 이름.reduce((acc, n) => acc.concat(n), []);

    let currentChild = this;

    에 대한 (하자 i = 0; i < 이름.길이; i++) {
      currentChild = currentChild.getChild(이름[i]);

      if (!currentChild || !currentChild.getChild) {
        반품;
      }
    }

    반환 currentChild;
  }

  /**
   * 현재 `Component` 안에 자식 `Component`를 추가합니다.
   *
   *
   * @param {string|Component} 자식
   * 추가할 자식의 이름 또는 인스턴스입니다.
   *
   * @param {객체} [옵션={}]
   * 자식에게 전달될 옵션의 키/값 저장소
   * 아이.
   *
   * @param {번호} [인덱스=이.어린이_.길이]
   * 자식을 추가하려고 시도하는 인덱스입니다.
   *
   * @return {구성 요소}
   * 자식으로 추가되는 `Component`. 문자열을 사용할 때
   * 이 과정에서 `Component`가 생성됩니다.
   */
  addChild(자식, 옵션 = {}, 색인 = this.children_.length) {
    컴포넌트를 보자;
    let componentName;

    // 자식이 문자열이면 옵션으로 구성 요소를 만듭니다.
    if (자식 유형 === '문자열') {
      componentName = toTitleCase(자식);

      const componentClassName = options.componentClass || 구성 요소 이름;

      // 옵션을 통해 이름 설정
      options.name = 구성 요소 이름;

      // 새로운 객체 생성 & 이 컨트롤 세트의 요소
      // .player_가 없으면 플레이어입니다.
      const ComponentClass = Component.getComponent(componentClassName);

      if (!구성 요소 클래스) {
        throw new Error(`${componentClassName} 컴포넌트가 존재하지 않음`);
      }

      // videojs 객체에 직접 저장된 데이터는
      // 유지할 구성 요소로 잘못 식별됨
      // 4.x와의 하위 호환성. 확인하기 위해 확인
      // 구성 요소 클래스를 인스턴스화할 수 있습니다.
      if (구성 요소 클래스 유형 !== '함수') {
        null을 반환합니다.
      }

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

    // 자식은 구성 요소 인스턴스입니다.
    } else {
      구성 요소 = 자식;
    }

    if (component.parentComponent_) {
      component.parentComponent_.removeChild(컴포넌트);
    }
    this.children_.splice(index, 0, component);
    component.parentComponent_ = 이;

    if (typeof component.id === '함수') {
      this.childIndex_[component.id()] = 컴포넌트;
    }

    // 구성 요소를 만드는 데 이름을 사용하지 않은 경우 다음을 사용할 수 있는지 확인합니다.
    // 구성 요소의 이름 함수
    구성 요소 이름 = 구성 요소 이름 || (컴포넌트.이름 && toTitleCase(component.name()));

    if (구성 요소 이름) {
      this.childNameIndex_[componentName] = 컴포넌트;
      this.childNameIndex_[toLowerCase(componentName)] = 컴포넌트;
    }

    // UI 객체의 요소를 컨테이너에 추가 div(box)
    // 요소를 가질 필요가 없습니다.
    if (typeof component.el === '함수' && component.el()) {
      // 컴포넌트 앞에 삽입하는 경우 해당 컴포넌트의 요소 앞에 삽입
      let refNode = null;

      if (이.어린이_[인덱스 + 1]) {
        // 대부분의 자식은 구성 요소이지만 비디오 기술은 HTML 요소입니다.
        if (이.어린이_[인덱스 + 1].el_) {
          refNode = this.children_[인덱스 + 1].el_;
        } 그렇지 않으면 (Dom.isEl(this.children_[index + 1])) {
          refNode = this.children_[인덱스 + 1];
        }
      }

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

    // 원하는 경우 상위 개체에 저장할 수 있도록 반환합니다.
    반환 구성 요소;
  }

  /**
   * 이 `Component` 자식 목록에서 자식 `Component`를 제거합니다. 또한 제거
   * 이 `Component` 요소의 하위 `Component` 요소.
   *
   * @param {구성 요소} 구성 요소
   * 제거할 하위 `Component`.
   */
  removeChild(구성 요소) {
    if (구성 요소 유형 === '문자열') {
      구성 요소 = this.getChild(구성 요소);
    }

    if (!구성요소 || !this.children_) {
      반품;
    }

    let childFound = 거짓;

    for (let i = this.children_.length - 1; i > = 0; 나--) {
      if (this.children_[i] === 구성 요소) {
        childFound = 참;
        this.children_.splice(i, 1);
        부서지다;
      }
    }

    if (!childFound) {
      반품;
    }

    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.parentNode === this.contentEl()) {
      this.contentEl().removeChild(component.el());
    }
  }

  /**
   * 옵션에 따라 기본 자식 `Component`를 추가하고 초기화합니다.
   */
  초기화 어린이() {
    const children = this.options_.children;

    if (어린이) {
      // `this`는 `부모`입니다.
      const parentOptions = this.options_;

      const handleAdd = (자식) => {
        const 이름 = child.name;
        let opts = child.opts;

        // 자식 옵션을 부모 옵션에서 설정할 수 있도록 허용
        // 예를 들어 videojs(id, { controlBar: false });
        // 대신 videojs(id, { children: { controlBar: false });
        if (parentOptions[이름] !== 정의되지 않음) {
          opts = parentOptions[이름];
        }

        // 기본 구성 요소 비활성화 허용
        // 예: options['children']['posterImage'] = false
        경우 (옵션 === 거짓) {
          반품;
        }

        // 구성이 없는 경우 옵션이 단순 부울로 전달되도록 허용
        // 필수적이다.
        if (옵션 === 참) {
          옵션 = {};
        }

        // 원래 플레이어 옵션도 전달하고 싶습니다.
        // 각 구성 요소에도 적용되므로 필요하지 않습니다.
        // 나중에 옵션을 위해 플레이어에 다시 도달합니다.
        opts.playerOptions = this.options_.playerOptions;

        // 자식 구성 요소를 만들고 추가합니다.
        // 부모 인스턴스의 이름으로 자식에 대한 직접 참조를 추가합니다.
        // 같은 컴포넌트를 2개 사용하는 경우 다른 이름을 지정해야 함
        // 각각
        const newChild = this.addChild(이름, 옵션);

        경우 (newChild) {
          this[이름] = newChild;
        }
      };

      // 자식 세부 정보의 배열이 옵션에 전달되도록 허용
      일하는 아이들을 보자;
      const Tech = Component.getComponent('기술');

      if (Array.isArray(어린이)) {
        workingChildren = 어린이;
      } else {
        workingChildren = Object.keys(어린이);
      }

      일하는 아이들
      // this.options_에 있지만 workingChildren에도 있는 자식은
      // 원하지 않는 추가 자식을 제공합니다. 그래서 우리는 그것들을 걸러내고 싶습니다.
        .concat(Object.keys(this.options_)
          .filter(함수(하위) {
            반환 !workingChildren.some(function(wchild) {
              if (wchild 유형 === '문자열') {
                반환 자식 === wchild;
              }
              자식 반환 === wchild.name;
            });
          }))
        .map((하위) => {
          이름을 짓다;
          선택하자;

          if (자식 유형 === '문자열') {
            이름 = 아이;
            선택 = 어린이[이름] || this.options_[이름] || {};
          } else {
            이름 = 아이.이름;
            옵션 = 자식;
          }

          {이름, 옵션}을 반환합니다.
        })
        .filter((하위) => {
        // child.name이 techOrder에 없는지 확인해야 합니다.
        // 기술자는 구성 요소로 등록되지만 호환되지 않습니다.
        // https://github.com/videojs/video.js/issues/2772 참조
          const c = Component.getComponent(child.opts.componentClass ||
                                       toTitleCase(child.name));

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

  /**
   * 기본 DOM 클래스 이름을 빌드합니다. 하위 구성요소에 의해 재정의되어야 합니다.
   *
   * @return {문자열}
   * 이 개체의 DOM 클래스 이름입니다.
   *
   * @추상적인
   */
  buildCSSClass() {
    // 자식 클래스는 다음을 수행하는 함수를 포함할 수 있습니다.
    // return 'CLASS NAME' + this._super();
    반품 '';
  }

  /**
   * 수신기를 구성 요소의 준비 상태에 바인딩합니다.
   * ready 이벤트가 이미 발생한 경우라는 점에서 이벤트 리스너와 다릅니다.
   * 즉시 기능을 트리거합니다.
   *
   * @return {구성 요소}
   * 자신을 반환합니다. 메서드를 연결할 수 있습니다.
   */
  준비(fn, 동기화 = 거짓) {
    경우 (!fn) {
      반품;
    }

    if (!this.isReady_) {
      this.readyQueue_ = this.readyQueue_ || [];
      this.readyQueue_.push(fn);
      반품;
    }

    경우 (동기화) {
      fn.call(이);
    } else {
      // 일관성을 위해 기본적으로 함수를 비동기적으로 호출합니다.
      this.setTimeout(fn, 1);
    }
  }

  /**
   * 이 `Component`에 대해 준비된 모든 리스너를 트리거합니다.
   *
   * @fires 컴포넌트#ready
   */
  트리거준비() {
    this.isReady_ = 참;

    // 준비가 비동기적으로 트리거되도록 합니다.
    this.setTimeout(함수() {
      const readyQueue = this.readyQueue_;

      // 준비 대기열 재설정
      this.readyQueue_ = [];

      if (readyQueue && readyQueue.length > 0) {
        readyQueue.forEach(기능(fn) {
          fn.call(이);
        }, 이것);
      }

      // 이벤트 리스너 사용도 허용
      /**
       * `Component`가 준비되면 트리거됩니다.
       *
       * @event 컴포넌트#ready
       * @type {이벤트대상~이벤트}
       */
      this.trigger('준비');
    }, 1);
  }

  /**
   * `selector`와 일치하는 단일 DOM 요소를 찾습니다. 이것은 `Component`s 내에 있을 수 있습니다.
   * `contentEl()` 또는 다른 사용자 지정 컨텍스트.
   *
   * @param {문자열} 선택자
   * `querySelector`에 전달되는 유효한 CSS 선택기.
   *
   * @param {요소|문자열} [context=this.contentEl()]
   * 조회할 DOM 요소. 다음에서 선택자 문자열이 될 수도 있습니다.
   * 이 경우 첫 번째로 일치하는 요소가 컨텍스트로 사용됩니다. 만약에
   * 누락된 `this.contentEl()`이 사용됨. `this.contentEl()`이 반환하는 경우
   * `문서`로 돌아가는 것은 없습니다.
   *
   * @return {요소|null}
   * 찾은 dom 요소 또는 null
   *
   * @see [CSS 선택자 정보](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   */
  $(선택자, 컨텍스트) {
    return Dom.$(selector, context || this.contentEl());
  }

  /**
   * `selector`와 일치하는 모든 DOM 요소를 찾습니다. 이것은 `Component`s 내에 있을 수 있습니다.
   * `contentEl()` 또는 다른 사용자 지정 컨텍스트.
   *
   * @param {문자열} 선택자
   * `querySelectorAll`에 전달되는 유효한 CSS 선택기.
   *
   * @param {요소|문자열} [context=this.contentEl()]
   * 조회할 DOM 요소. 다음에서 선택자 문자열이 될 수도 있습니다.
   * 이 경우 첫 번째로 일치하는 요소가 컨텍스트로 사용됩니다. 만약에
   * 누락된 `this.contentEl()`이 사용됨. `this.contentEl()`이 반환하는 경우
   * `문서`로 돌아가는 것은 없습니다.
   *
   * @return {노드리스트}
   * 발견된 dom 요소 목록
   *
   * @see [CSS 선택자 정보](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
   */
  $$(선택자, 컨텍스트) {
    return Dom.$$(selector, context || this.contentEl());
  }

  /**
   * 구성 요소의 요소에 CSS 클래스 이름이 있는지 확인하십시오.
   *
   * @param {string} classToCheck
   * 확인할 CSS 클래스 이름.
   *
   * @return {부울}
   * - `Component`에 클래스가 있으면 True입니다.
   * - `Component`에 class`가 없으면 False
   */
  hasClass(classToCheck) {
    return Dom.hasClass(this.el_, classToCheck);
  }

  /**
   * `Component` 요소에 CSS 클래스 이름을 추가합니다.
   *
   * @param {문자열} classToAdd
   * 추가할 CSS 클래스 이름
   */
  addClass(classToAdd) {
    Dom.addClass(this.el_, classToAdd);
  }

  /**
   * `Component` 요소에서 CSS 클래스 이름을 제거합니다.
   *
   * @param {string} classToRemove
   * 제거할 CSS 클래스 이름
   */
  removeClass(classToRemove) {
    Dom.removeClass(this.el_, classToRemove);
  }

  /**
   * 구성 요소의 요소에서 CSS 클래스 이름을 추가하거나 제거합니다.
   * - {@link Component#hasClass}가 false를 반환하면 `classToToggle`이 추가됩니다.
   * - {@link Component#hasClass}가 true를 반환하면 `classToToggle`이 제거됩니다.
   *
   * @param {문자열} 클래스투토글
   * (@link Component#hasClass} 기반으로 추가하거나 제거할 클래스
   *
   * @param {boolean|Dom~predicate} [술어]
   * {@link Dom~predicate} 함수 또는 부울
   */
  toggleClass(classToToggle, 조건자) {
    Dom.toggleClass(this.el_, classToToggle, predicate);
  }

  /**
   * 숨겨진 경우 `Component` 요소를 제거하여 표시
   * 'vjs-hidden' 클래스 이름.
   */
  보여주다() {
    this.removeClass('vjs-hidden');
  }

  /**
   * `Component` 요소를 추가하여 현재 표시 중인 경우 숨기기
   * 'vjs-hidden` 클래스 이름.
   */
  숨다() {
    this.addClass('vjs-hidden');
  }

  /**
   * 'vjs-lock-showing'을 추가하여 `Component` 요소를 보이는 상태로 잠급니다.
   * 클래스 이름. fadeIn/fadeOut 중에 사용됩니다.
   *
   * @사적인
   */
  잠금 표시() {
    this.addClass('vjs-lock-showing');
  }

  /**
   * 'vjs-lock-showing'을 제거하여 `Component` 요소를 보이는 상태에서 잠금 해제
   * 그것의 클래스 이름. fadeIn/fadeOut 중에 사용됩니다.
   *
   * @사적인
   */
  잠금해제표시() {
    this.removeClass('vjs-lock-showing');
  }

  /**
   * `Component` 요소의 속성 값을 가져옵니다.
   *
   * @param {문자열} 속성
   * 값을 가져올 속성의 이름입니다.
   *
   * @return {문자열|널}
   * - 요청된 속성의 값입니다.
   * - 속성이 존재하지 않는 경우 일부 브라우저에서 빈 문자열일 수 있습니다.
   * 또는 값이 없음
   * - 속성이 존재하지 않거나 속성이 있는 경우 대부분의 브라우저는 null을 반환합니다.
   * 값이 없습니다.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
   */
  getAttribute(속성) {
    return Dom.getAttribute(this.el_, 속성);
  }

  /**
   * `Component` 요소의 속성 값 설정
   *
   * @param {문자열} 속성
   * 설정할 속성의 이름.
   *
   * @param {문자열} 값
   * 속성을 설정할 값.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
   */
  setAttribute(속성, 값) {
    Dom.setAttribute(this.el_, 속성, 값);
  }

  /**
   * `Component` 요소에서 속성을 제거합니다.
   *
   * @param {문자열} 속성
   * 제거할 속성의 이름.
   *
   * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
   */
  제거속성(속성) {
    Dom.removeAttribute(this.el_, 속성);
  }

  /**
   * CSS 스타일에 따라 구성 요소의 너비를 가져오거나 설정합니다.
   * 자세한 내용은 {@link Component#dimension}을 참조하세요.
   *
   * @param {숫자|문자열} [숫자]
   * 설정하려는 폭은 '%', 'px' 또는 아무것도 뒤에 붙지 않습니다.
   *
   * @param {부울} [skipListeners]
   * componentresize 이벤트 트리거 건너뛰기
   *
   * @return {숫자|문자열}
   * 가져올 때 너비, 너비가 없으면 0입니다. 문자열이 될 수 있습니다.
   * 뒤에 '%' 또는 'px'가 붙습니다.
   */
  너비(숫자, skipListeners) {
    return this.dimension('width', num, skipListeners);
  }

  /**
   * CSS 스타일을 기반으로 구성 요소의 높이를 가져오거나 설정합니다.
   * 자세한 내용은 {@link Component#dimension}을 참조하세요.
   *
   * @param {숫자|문자열} [숫자]
   * 설정하려는 높이는 '%', 'px' 또는 아무것도 뒤에 붙지 않습니다.
   *
   * @param {부울} [skipListeners]
   * componentresize 이벤트 트리거 건너뛰기
   *
   * @return {숫자|문자열}
   * 가져올 때 너비, 너비가 없으면 0입니다. 문자열이 될 수 있습니다.
   * 뒤에 '%' 또는 'px'가 붙습니다.
   */
  높이(숫자, skipListeners) {
    return this.dimension('높이', num, skipListeners);
  }

  /**
   * `Component` 요소의 너비와 높이를 동시에 설정합니다.
   *
   * @param {숫자|문자열} 너비
   * `Component` 요소를 설정할 너비.
   *
   * @param {숫자|문자열} 높이
   * `Component` 요소를 설정할 높이.
   */
  치수(너비, 높이) {
    // 최적화를 위해 width에서 componentresize 리스너 건너뛰기
    this.width(너비, 참);
    this.높이(높이);
  }

  /**
   * `Component` 요소의 너비 또는 높이를 가져오거나 설정합니다. 이것은 공유 코드입니다
   * {@link Component#width} 및 {@link Component#height}의 경우.
   *
   * 알아야 할 사항:
   * - 숫자의 너비 또는 높이인 경우 'px'가 뒤에 붙은 숫자를 반환합니다.
   * - 너비/높이가 퍼센트인 경우 '%'가 뒤에 붙은 퍼센트를 반환합니다.
   * - 숨겨진 요소의 너비는 `window.getComputedStyle`과 함께 0입니다. 이 기능
   * 기본값은 `Component`의 `style.width`이며 `window.getComputedStyle`으로 돌아갑니다.
   * [이]{@링크 http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}를 참조하세요.
   * 자세한 내용은
   * - 구성 요소의 계산된 스타일을 원하면 {@link Component#currentWidth}를 사용하십시오.
   * 및 {@link {Component#currentHeight}
   *
   * @fires 컴포넌트#componentresize
   *
   * @param {문자열} 너비 또는 높이
   8 '너비' 또는 '높이'
   *
   * @param {숫자|문자열} [숫자]
   8 새로운 차원
   *
   * @param {부울} [skipListeners]
   * componentresize 이벤트 트리거 건너뛰기
   *
   * @return {숫자}
   * 가져올 때 차원 또는 설정하지 않으면 0
   */
  차원(너비 또는 높이, 숫자, skipListeners) {
    if (num !== 정의되지 않음) {
      // null이거나 문자 그대로 NaN인 경우 0으로 설정합니다(NaN !== NaN).
      if (숫자 === null || 숫자 !== 숫자) {
        숫자 = 0;
      }

      // css 너비/높이(% 또는 px)를 사용하는지 확인하고 조정
      if (('' + 숫자).indexOf('%') !== -1 || ('' + 숫자).indexOf('px') !== -1) {
        this.el_.style[너비 또는 높이] = 숫자;
      } 그렇지 않으면 (숫자 === '자동') {
        this.el_.style[너비 또는 높이] = '';
      } else {
        this.el_.style[너비 또는 높이] = 숫자 + 'px';
      }

      // skipListeners를 사용하면 너비와 높이를 모두 설정할 때 크기 조정 이벤트가 발생하지 않도록 할 수 있습니다.
      if (!skipListeners) {
        /**
         * 구성 요소의 크기가 조정될 때 트리거됩니다.
         *
         * @event 컴포넌트#componentresize
         * @type {이벤트대상~이벤트}
         */
        this.trigger('componentresize');
      }

      반품;
    }

    // 값을 설정하지 않았으므로 가져옴
    // 요소가 존재하는지 확인
    if (!this.el_) {
      0을 반환합니다.
    }

    // 스타일에서 차원 값 가져오기
    const val = this.el_.style[너비 또는 높이];
    const pxIndex = val.indexOf('px');

    if (pxIndex !== -1) {
      // 'px'가 없는 픽셀 값 반환
      return parseInt(val.slice(0, pxIndex), 10);
    }

    // px가 없으므로 %를 사용하거나 스타일이 설정되지 않았으므로 offsetWidth/height로 되돌아갑니다.
    // 구성 요소에 display:none이 있는 경우 오프셋은 0을 반환합니다.
    // TODO: px를 사용하여 display:none 및 no dimension 스타일 처리
    return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
  }

  /**
   * 구성요소 요소의 계산된 너비 또는 높이를 가져옵니다.
   *
   * `window.getComputedStyle`을 사용합니다.
   *
   * @param {문자열} 너비 또는 높이
   * '너비' 또는 '높이'를 포함하는 문자열. 당신이 얻고 싶은 것.
   *
   * @return {숫자}
   * 요청되는 차원 또는 아무것도 설정되지 않은 경우 0
   * 해당 차원에 대해.
   */
  현재 치수(너비 또는 높이) {
    let computedWidthOrHeight = 0;

    if (너비또는높이 !== '너비' && 너비 또는 높이 !== '높이') {
      throw new Error('currentDimension은 너비 또는 높이 값만 허용합니다.');
    }

    computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);

    // 변수에서 'px'를 제거하고 정수로 구문 분석
    computedWidthOrHeight = parseFloat(computedWidthOrHeight);

    // 계산된 값이 여전히 0이면 브라우저가 거짓말을 하고 있을 가능성이 있습니다.
    // 오프셋 값을 확인하고 싶습니다.
    // 이 코드는 getComputedStyle이 존재하지 않는 곳에서도 실행됩니다.
    if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
      const 규칙 = `offset${toTitleCase(widthOrHeight)}`;

      computedWidthOrHeight = this.el_[rule];
    }

    computedWidthOrHeight 반환;
  }

  /**
   * `Component`의 너비와 높이 값을 포함하는 객체
   * 계산 스타일. `window.getComputedStyle`을 사용합니다.
   *
   * @typedef {객체} Component~DimensionObject
   *
   * @property {숫자} 너비
   * `컴포넌트` 계산 스타일의 너비.
   *
   * @property {숫자} 높이
   * `컴포넌트` 계산 스타일의 높이.
   */

  /**
   * 계산된 너비와 높이 값을 포함하는 개체를 가져옵니다.
   * 컴포넌트의 요소.
   *
   * `window.getComputedStyle`을 사용합니다.
   *
   * @return {Component~DimensionObject}
   * 구성요소 요소의 계산된 치수.
   */
  현재 치수() {
    반환 {
      너비: this.currentDimension('너비'),
      높이: this.currentDimension('높이')
    };
  }

  /**
   * 구성 요소 요소의 계산된 너비를 가져옵니다.
   *
   * `window.getComputedStyle`을 사용합니다.
   *
   * @return {숫자}
   * 구성 요소 요소의 계산된 너비입니다.
   */
  전류폭() {
    return this.currentDimension('너비');
  }

  /**
   * 구성요소 요소의 계산된 높이를 가져옵니다.
   *
   * `window.getComputedStyle`을 사용합니다.
   *
   * @return {숫자}
   * 구성요소 요소의 계산된 높이입니다.
   */
  현재 높이() {
    return this.currentDimension('높이');
  }

  /**
   * 이 구성 요소에 초점을 맞춥니다.
   */
  집중하다() {
    this.el_.focus();
  }

  /**
   * 이 구성 요소에서 포커스 제거
   */
  블러() {
    this.el_.blur();
  }

  /**
   * 이 컴포넌트가 처리하지 않는 `keydown` 이벤트를 수신하면,
   * 처리를 위해 플레이어에게 이벤트를 전달합니다.
   *
   * @param {EventTarget~Event} 이벤트
   * 이 함수를 호출한 `keydown` 이벤트.
   */
  handleKeyDown(이벤트) {
    if (이.플레이어_) {

      // 처리되지 않은 이벤트가 떨어지기를 원하기 때문에 여기서 전파를 중지합니다.
      // 브라우저로 돌아갑니다. 포커스 트래핑을 위해 탭을 제외합니다.
      if (!keycode.isEventKey(event, 'Tab')) {
        event.stopPropagation();
      }
      this.player_.handleKeyDown(event);
    }
  }

  /**
   * 많은 구성요소에 'handleKeyPress' 메서드가 있었는데, 이는 형편없었습니다.
   * `keydown` 이벤트를 수신했기 때문에 이름이 지정되었습니다. 이 방법 이름은 지금
   * `handleKeyDown`에 위임합니다. 이것은 `handleKeyPress`를 호출하는 모든 사용자를 의미합니다.
   * 메소드 호출이 작동을 멈추는 것을 볼 수 없습니다.
   *
   * @param {EventTarget~Event} 이벤트
   * 이 함수를 호출한 이벤트.
   */
  handleKeyPress(이벤트) {
    this.handleKeyDown(이벤트);
  }

  /**
   * 터치 이벤트 지원이 감지되면 '탭' 이벤트를 내보냅니다. 이게 익숙해진다
   * 비디오 탭을 통해 컨트롤 전환을 지원합니다. 활성화됩니다.
   * 그렇지 않으면 모든 하위 구성 요소에 추가 오버헤드가 있기 때문입니다.
   *
   * @사적인
   * @fires 컴포넌트#tap
   * @listens 컴포넌트#touchstart
   * @listens 컴포넌트#touchmove
   * @listens 컴포넌트#touchleave
   * @listens 컴포넌트#touchcancel
   * @listens 컴포넌트#touchend

   */
  emitTapEvents() {
    // 터치가 지속된 시간을 확인할 수 있도록 시작 시간을 추적합니다.
    let touchStart = 0;
    let firstTouch = null;

    // 여전히 탭으로 간주하기 위해 터치 이벤트 동안 허용되는 최대 이동
    // 다른 대중적인 라이브러리는 2(hammer.js)에서 15까지 사용합니다.
    // 따라서 10은 근사하고 근사한 숫자처럼 보입니다.
    const tapMovementThreshold = 10;

    // 여전히 탭으로 간주되는 동안 터치할 수 있는 최대 길이
    const touchTimeThreshold = 200;

    let couldBeTap;

    this.on('터치스타트', function(event) {
      // 손가락이 두 개 이상인 경우 클릭으로 간주하지 마세요.
      if (event.touches.length === 1) {
        // 객체에서 pageX/pageY 복사
        퍼스트터치 = {
          pageX: event.touches[0].pageX,
          pageY: event.touches[0].pageY
        };
        // 탭과 "길게 누르기"를 감지할 수 있도록 시작 시간을 기록합니다.
        touchStart = window.performance.now();
        // couldBeTap 추적 재설정
        couldBeTap = 참;
      }
    });

    this.on('터치무브', 함수(이벤트) {
      // 손가락이 두 개 이상인 경우 클릭으로 간주하지 마세요.
      if (이벤트.터치.길이 > 1) {
        couldBeTap = 거짓;
      } 그렇지 않으면 (firstTouch) {
        // 일부 장치는 약간의 탭을 제외하고 모두 터치 무브를 발생시킵니다.
        // 따라서 우리가 약간의 거리만 움직였다면 여전히 탭이 될 수 있습니다.
        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 (터치거리 > tapMovementThreshold) {
          couldBeTap = 거짓;
        }
      }
    });

    const noTap = 함수() {
      couldBeTap = 거짓;
    };

    // 할 것: 원래 대상을 듣습니다. http://youtu.be/DujfpXOKUp8?t=13m8s
    this.on('touchleave', noTap);
    this.on('touchcancel', noTap);

    // 터치가 끝나면 걸린 시간을 측정하고 적절한
    // 이벤트
    this.on('터치엔드', function(event) {
      퍼스트터치 = null;
      // touchmove/leave/cancel 이벤트가 발생하지 않은 경우에만 진행
      if (couldBeTap === 참) {
        // 터치가 지속된 시간 측정
        const touchTime = window.performance.now() - touchStart;

        // 터치가 탭으로 간주되는 임계값 미만인지 확인합니다.
        if (터치타임 < touchTimeThreshold) {
          // 브라우저가 이것을 클릭으로 바꾸지 못하게 합니다.
          event.preventDefault();
          /**
           * `Component`를 탭하면 트리거됩니다.
           *
           * @event 컴포넌트#tap
           * @type {이벤트대상~이벤트}
           */
          this.trigger('탭');
          // 터치엔드 이벤트 객체를 복사하여 변경하는 것이 좋을 수 있습니다.
          // 입력 후 다른 이벤트 속성이 정확하지 않은 경우 탭합니다.
          // Events.fixEvent 실행(예: event.target)
        }
      }
    });
  }

  /**
   * 이 기능은 터치 이벤트가 발생할 때마다 사용자 활동을 보고합니다. 이것은 얻을 수 있습니다
   * 터치 이벤트가 다른 방식으로 작동하기를 원하는 하위 구성 요소에 의해 꺼집니다.
   *
   * 터치 이벤트가 발생할 때 사용자 터치 활동을 보고합니다. 사용자 활동에 익숙해짐
   * 컨트롤을 표시하거나 숨길 시기를 결정합니다. 마우스에 관해서는 간단합니다
   * 이벤트, 모든 마우스 이벤트가 컨트롤을 표시해야 하기 때문입니다. 그래서 우리는 마우스를 캡처
   * 플레이어에게 버블링되는 이벤트와 발생 시 활동을 보고합니다.
   * 터치 이벤트를 사용하면 `touchstart` 및 `touchend` 토글 플레이어만큼 쉽지 않습니다.
   * 컨트롤. 따라서 터치 이벤트는 플레이어 수준에서도 도움이 되지 않습니다.
   *
   * 사용자 활동이 비동기적으로 확인됩니다. 따라서 발생할 수 있는 것은 탭 이벤트입니다.
   * 비디오에서 컨트롤을 끕니다. 그런 다음 `touchend` 이벤트가
   * 플레이어. 사용자 활동을 보고하면 컨트롤을 오른쪽으로 돌립니다.
   * 다시. 또한 터치 이벤트가 버블링되는 것을 완전히 차단하고 싶지 않습니다.
   * 또한 `touchmove` 이벤트 및 탭 이외의 모든 것은 회전하지 않아야 합니다.
   * 컨트롤을 다시 켭니다.
   *
   * @listens 컴포넌트#touchstart
   * @listens 컴포넌트#touchmove
   * @listens 컴포넌트#touchend
   * @listens 컴포넌트#touchcancel
   */
  enableTouchActivity() {
    // 루트 플레이어가 사용자 활동 보고를 지원하지 않는 경우 계속하지 마십시오.
    if (!this.player() || !this.player().reportUserActivity) {
      반품;
    }

    // 사용자가 활성 상태임을 보고하기 위한 리스너
    const report = Fn.bind(this.player(), this.player().reportUserActivity);

    let touchHolding;

    this.on('터치스타트', function() {
      보고서();
      // 그들이 장치를 만지거나 마우스를 누르고 있는 동안,
      // 손가락이나 마우스를 움직이지 않아도 활성 상태로 간주합니다.
      // 그래서 우리는 그들이 활성 상태임을 계속 업데이트하고 싶습니다.
      this.clearInterval(touchHolding);
      // activityCheck와 같은 간격으로 보고
      touchHolding = this.setInterval(보고서, 250);
    });

    const touchEnd = 함수(이벤트) {
      보고서();
      // 터치가 유지되는 경우 활동을 유지하는 간격을 중지합니다.
      this.clearInterval(touchHolding);
    };

    this.on('touchmove', 보고서);
    this.on('터치엔드', 터치엔드);
    this.on('touchcancel', touchEnd);
  }

  /**
   * 매개변수가 없고 `Component` 컨텍스트에 바인딩되는 콜백.
   *
   * @callback 컴포넌트~GenericCallback
   * @이 컴포넌트
   */

  /**
   * `x`밀리초 시간 초과 후 실행되는 함수를 생성합니다. 이 기능은
   * `window.setTimeout` 주변의 래퍼. 이것을 사용하는 몇 가지 이유가 있습니다.
   * 대신:
   * 1. 다음과 같은 경우 {@link Component#clearTimeout}을 통해 지워집니다.
   * {@link Component#dispose}가 호출됩니다.
   * 2. 함수 콜백은 {@link Component~GenericCallback}
   *
   * > 메모: 이 함수에서 반환된 ID에는 `window.clearTimeout`을 사용할 수 없습니다. 이
   * dispose 리스너가 정리되지 않도록 합니다! 사용 해주세요
   * 대신 {@link Component#clearTimeout} 또는 {@link Component#dispose}.
   *
   * @param {Component~GenericCallback} fn
   * `timeout` 이후에 실행될 함수입니다.
   *
   * @param {숫자} 타임아웃
   * 지정된 기능을 실행하기 전에 지연되는 시간 초과(밀리초).
   *
   * @return {숫자}
   * 시간 초과를 식별하는 데 사용되는 시간 초과 ID를 반환합니다. 그것은 또한 할 수 있습니다
   * {@link Component#clearTimeout}에서 사용하여 제한 시간을 지웁니다.
   * 설정했습니다.
   *
   * @listens 컴포넌트#dispose
   * @see [유사]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
   */
  setTimeout(fn, 타임아웃) {
    // 타임아웃 함수에서 적절하게 사용할 수 있도록 변수로 선언
    // eslint-비활성화-다음 줄
    var timeoutId, disposeFn;

    fn = Fn.bind(이, fn);

    this.clearTimersOnDispose_();

    timeoutId = window.setTimeout(() => {
      if (this.setTimeoutIds_.has(timeoutId)) {
        this.setTimeoutIds_.delete(timeoutId);
      }
      fn();
    }, 타임아웃);

    this.setTimeoutIds_.add(timeoutId);

    timeoutId 반환;
  }

  /**
   * `window.setTimeout` 또는
   * {@link Component#setTimeout}. {@link Component#setTimeout}을 통해 시간 초과를 설정한 경우
   * `window.clearTimout` 대신 이 함수를 사용하십시오. 처분하지 않으면
   * 리스너는 {@link Component#dispose}까지 정리되지 않습니다!
   *
   * @param {숫자} timeoutId
   * 지울 제한 시간의 ID입니다. 반환 값
   * {@link Component#setTimeout} 또는 `window.setTimeout`.
   *
   * @return {숫자}
   * 지워진 타임아웃 ID를 반환합니다.
   *
   * @see [유사]{@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);
    }

    timeoutId 반환;
  }

  /**
   * `x`밀리초마다 실행되는 함수를 만듭니다. 이 함수는 래퍼입니다.
   * `window.setInterval` 주변. 대신 이것을 사용해야 하는 몇 가지 이유가 있습니다.
   * 1. 다음과 같은 경우 {@link Component#clearInterval}을 통해 지워집니다.
   * {@link Component#dispose}가 호출됩니다.
   * 2. 함수 콜백은 {@link Component~GenericCallback}
   *
   * @param {Component~GenericCallback} fn
   * `x`초마다 실행되는 기능.
   *
   * @param {숫자} 간격
   * `x`밀리초마다 지정된 기능을 실행합니다.
   *
   * @return {숫자}
   * 간격을 식별하는 데 사용할 수 있는 ID를 반환합니다. 그것은 또한에서 사용될 수 있습니다
   * {@link Component#clearInterval} 간격을 지우십시오.
   *
   * @listens 컴포넌트#dispose
   * @see [유사]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
   */
  setInterval(fn, 간격) {
    fn = Fn.bind(이, fn);

    this.clearTimersOnDispose_();

    const intervalId = window.setInterval(fn, 간격);

    this.setIntervalIds_.add(intervalId);

    반환 간격 ID;
  }

  /**
   * `window.setInterval`을 통해 생성되는 간격을 지우거나
   * {@link Component#setInterval}. {@link Component#setInterval}을 통해 간격을 설정하면
   * `window.clearInterval` 대신 이 함수를 사용하십시오. 처분하지 않으면
   * 리스너는 {@link Component#dispose}까지 정리되지 않습니다!
   *
   * @param {숫자} 간격 ID
   * 지울 간격의 ID입니다. 반환 값
   * {@link Component#setInterval} 또는 `window.setInterval`.
   *
   * @return {숫자}
   * 지워진 간격 ID를 반환합니다.
   *
   * @see [유사]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
   */
  clearInterval(간격 ID) {
    if (this.setIntervalIds_.has(intervalId)) {
      this.setIntervalIds_.delete(intervalId);
      window.clearInterval(intervalId);
    }

    반환 간격 ID;
  }

  /**
   * requestAnimationFrame(rAF)에 전달할 콜백을 대기하지만
   * 몇 가지 추가 보너스 포함:
   *
   * - rAF를 지원하지 않는 브라우저를 지원합니다.
   * {@link Component#setTimeout}.
   *
   * - 콜백은 {@link Component~GenericCallback}(예:
   * 구성 요소에 바인딩됨).
   *
   * - 구성 요소가 있는 경우 rAF 콜백의 자동 취소가 처리됩니다.
   * 호출되기 전에 폐기됩니다.
   *
   * @param {Component~GenericCallback} fn
   * 이 구성 요소에 바인딩되어 바로 실행되는 함수
   * 브라우저의 다음 다시 그리기 전에.
   *
   * @return {숫자}
   * 시간 초과를 식별하는 데 사용되는 rAF ID를 반환합니다. 할 수 있습니다
   * 취소를 위해 {@link Component#cancelAnimationFrame}에서도 사용
   * 애니메이션 프레임 콜백.
   *
   * @listens 컴포넌트#dispose
   * @see [유사]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
   */
  requestAnimationFrame(fn) {
    // 타이머를 사용하여 폴백합니다.
    if (!this.supportsRaf_) {
      this.setTimeout(fn, 1000 / 60)을 반환합니다.
    }

    this.clearTimersOnDispose_();

    // rAF 함수에서 적절하게 사용할 수 있도록 변수로 선언
    // eslint-비활성화-다음 줄
    변수 ID;
    fn = Fn.bind(이, fn);

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

    반환 ID;
  }

  /**
   * 애니메이션 프레임을 요청하지만 명명된 애니메이션은 하나만
   * 프레임이 대기됩니다. 다른 사람은 때까지 추가되지 않습니다
   * 이전 것이 끝납니다.
   *
   * @param {문자열} 이름
   * 이 requestAnimationFrame에 제공할 이름
   *
   * @param {Component~GenericCallback} fn
   * 이 구성 요소에 바인딩되어 바로 실행되는 함수
   * 브라우저의 다음 다시 그리기 전에.
   */
  requestNamedAnimationFrame(이름, fn) {
    if (this.namedRafs_.has(이름)) {
      반품;
    }
    this.clearTimersOnDispose_();

    fn = Fn.bind(이, fn);

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

    this.namedRafs_.set(이름, ID);

    반환 이름;
  }

  /**
   * 현재 명명된 애니메이션 프레임이 있는 경우 취소합니다.
   *
   * @param {문자열} 이름
   * 취소할 requestAnimationFrame의 이름입니다.
   */
  cancelNamedAnimationFrame(이름) {
    if (!this.namedRafs_.has(이름)) {
      반품;
    }

    this.cancelAnimationFrame(this.namedRafs_.get(이름));
    this.namedRafs_.delete(이름);
  }

  /**
   * {@link Component#requestAnimationFrame}에 전달된 대기 중인 콜백을 취소합니다.
   * (RF).
   *
   * {@link Component#requestAnimationFrame}을 통해 rAF 콜백을 대기시키는 경우,
   * `window.cancelAnimationFrame` 대신 이 함수를 사용하십시오. 그렇지 않으면
   * 폐기 리스너는 {@link Component#dispose}까지 정리되지 않습니다!
   *
   * @param {숫자} id
   * 삭제할 rAF ID입니다. {@link Component#requestAnimationFrame}의 반환 값입니다.
   *
   * @return {숫자}
   * 지워진 rAF ID를 반환합니다.
   *
   * @see [유사]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
   */
  cancelAnimationFrame(id) {
    // 타이머를 사용하여 폴백합니다.
    if (!this.supportsRaf_) {
      return this.clearTimeout(id);
    }

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

    반환 ID;

  }

  /**
   * `requestAnimationFrame`, `setTimeout`,
   * 및 `setInterval`, 폐기 시 삭제.
   *
   * > 이전에는 각 타이머가 자체적으로 처리 리스너를 추가 및 제거했습니다.
   * 더 나은 성능을 위해 모두 일괄 처리하고 `Set`을 사용하기로 결정했습니다.
   * 뛰어난 타이머 ID를 추적합니다.
   *
   * @사적인
   */
  clearTimersOnDispose_() {
    if (this.clearingTimersOnDispose_) {
      반품;
    }

    this.clearingTimersOnDispose_ = 참;
    this.one('처분', () => {
      [
        ['namedRafs_', 'cancelNamedAnimationFrame'],
        ['rafIds_', 'cancelAnimationFrame'],
        ['setTimeoutIds_', 'clearTimeout'],
        ['setIntervalIds_', 'clearInterval']
      ].forEach(([idName, 취소이름]) => {
        // `Set` 키의 경우 실제로 다시 값이 됩니다.
        // 따라서 forEach((값, 값) => ` 하지만 우리가 사용하고 싶은 지도에는
        // 열쇠.
        this[idName].forEach((값, 키) => this[취소이름](키));
      });

      this.clearingTimersOnDispose_ = 거짓;
    });
  }

  /**
   * 이름과 컴포넌트가 주어진 `videojs`로 `Component`를 등록합니다.
   *
   * > 참고: {@link Tech}는 `Component`로 등록하면 안 됩니다. {@link Tech}
   * {@link Tech.registerTech}를 사용하여 등록해야 합니다. 또는
   * {@link videojs:videojs.registerTech}.
   *
   * > 메모: 이 기능은 videojs에서도 다음과 같이 볼 수 있습니다.
   * {@link videojs:videojs.registerComponent}.
   *
   * @param {문자열} 이름
   * 등록할 `Component`의 이름입니다.
   *
   * @param {구성요소} ComponentToRegister
   * 등록할 `Component` 클래스.
   *
   * @return {구성 요소}
   * 등록된 `Component`.
   */
  static registerComponent(이름, ComponentToRegister) {
    if (이름 유형 !== '문자열' || !이름) {
      throw new Error(`잘못된 구성 요소 이름, "${name}"; 비어 있지 않은 문자열이어야 합니다.`);
    }

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

    // Tech가 등록된 경우에만 이 검사가 수행되는지 확인해야 합니다.
    const isTech = 기술 && Tech.isTech(ComponentToRegister);
    const isComp = 컴포넌트 === ComponentToRegister ||
      Component.prototype.isPrototypeOf(ComponentToRegister.prototype);

    if (isTech || !isComp) {
      이유를 보자;

      경우 (isTech) {
        이유 = '기술자는 Tech.registerTech()를 사용하여 등록해야 합니다.';
      } else {
        이유 = '구성 요소 하위 클래스여야 함';
      }

      throw new Error(`잘못된 구성 요소, "${name}"; ${reason}.`);
    }

    name = toTitleCase(이름);

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

    const Player = Component.getComponent('플레이어');

    if (이름 === '플레이어' && 플레이어 && 플레이어.플레이어) {
      const 플레이어 = Player.players;
      const playerNames = Object.keys(플레이어);

      // 폐기된 플레이어가 있는 경우 이름은 그대로 유지됩니다.
      // Players.players에서. 따라서 루프를 통해 값을 확인해야 합니다.
      // 각 항목은 null이 아닙니다. 이렇게 하면 플레이어 구성 요소를 등록할 수 있습니다.
      // 모든 플레이어가 삭제된 후 또는 생성되기 전.
      만약 (플레이어 &&
          플레이어 이름.길이 > 0 &&
          playerNames.map((pname) => players[pname]).every(Boolean)) {
        throw new Error('플레이어 생성 후 플레이어 컴포넌트를 등록할 수 없습니다.');
      }
    }

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

    ComponentToRegister 반환;
  }

  /**
   * 등록된 이름을 기반으로 `Component`를 가져옵니다.
   *
   * @param {문자열} 이름
   * 가져올 구성 요소의 이름입니다.
   *
   * @return {구성 요소}
   * 주어진 이름으로 등록된 `Component`.
   */
  정적 getComponent(이름) {
    if (!name || !Component.components_) {
      반품;
    }

    return Component.components_[이름];
  }
}

/**
 * 이 컴포넌트가 `requestAnimationFrame`을 지원하는지 여부.
 *
 * 주로 테스트 목적으로 노출됩니다.
 *
 * @사적인
 * @type {부울}
 */
Component.prototype.supportsRaf_ = typeof window.requestAnimationFrame === '함수' &&
  typeof window.cancelAnimationFrame === '함수';

Component.registerComponent('컴포넌트', 컴포넌트);

기본 구성 요소 내보내기;