/**
 * @file player.js
 */
// 하위 클래스 컴포넌트
'./component.js'에서 컴포넌트 가져오기;

'../../package.json'에서 {버전} 가져오기;
'글로벌/문서'에서 문서 가져오기;
'글로벌/창'에서 창 가져오기;
'./mixins/evented'에서 이벤트 가져오기;
'./mixins/evented'에서 {isEvented, addEventedCallback} 가져오기;
import * as Events from './utils/events.js';
import * as Dom from './utils/dom.js';
import * as Fn from './utils/fn.js';
import * as Guid from './utils/guid.js';
* './utils/browser.js'에서 브라우저로 가져오기;
'./utils/browser.js'에서 {IE_VERSION, IS_CHROME, IS_WINDOWS} 가져오기;
로그 가져오기, './utils/log.js'에서 { createLogger };
import {toTitleCase, titleCaseEquals} from './utils/string-cases.js';
import { createTimeRange } from './utils/time-ranges.js';
import { bufferedPercent } from './utils/buffer.js';
import * as stylesheet from './utils/stylesheet.js';
'./fullscreen-api.js'에서 FullscreenApi 가져오기;
'./media-error.js'에서 MediaError 가져오기;
'safe-json-parse/tuple'에서 safeParseTuple 가져오기;
'./utils/obj'에서 {assign} 가져오기;
'./utils/merge-options.js'에서 mergeOptions 가져오기;
'./utils/promise'에서 {silencePromise, isPromise} 가져오기;
'./tracks/text-track-list-converter.js'에서 textTrackConverter 가져오기;
'./modal-dialog'에서 ModalDialog 가져오기;
'./tech/tech.js'에서 기술 가져오기;
'./tech/middleware.js'에서 *를 미들웨어로 가져오기;
'./tracks/track-types'에서 {ALL을 TRACK_TYPES}로 가져오기;
import filterSource from './utils/filter-source';
import {getMimetype, findMimetype} from './utils/mimetypes';
'./utils/hooks'에서 {hooks} 가져오기;
'./utils/obj'에서 {isObject} 가져오기;
'키코드'에서 키코드 가져오기;

// 다음 가져오기는 해당 모듈이
// 항상 video.js 패키지에 포함되어 있습니다. 모듈을 가져오면
// 실행하면 video.js에 등록됩니다.
import './tech/loader.js';
import './poster-image.js';
import './tracks/text-track-display.js';
import './loading-spinner.js';
import './big-play-button.js';
import './close-button.js';
import './control-bar/control-bar.js';
import './error-display.js';
import './tracks/text-track-settings.js';
import './resize-manager.js';
import './live-tracker.js';

// 적어도 원본 비디오 태그를 처리하기 위해 Html5 기술을 가져옵니다.
import './tech/html5.js';

// 다음 기술 이벤트는 단순히 다시 트리거됩니다.
// 발생 시 플레이어에서
const TECH_EVENTS_RETRIGGER = [
  /**
   * 사용자 에이전트가 미디어 데이터를 다운로드하는 동안 발생합니다.
   *
   * @event 플레이어#progress
   * @type {이벤트대상~이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `progress` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechProgress_
   * @fires Player#progress
   * @listens Tech#progress
   */
  '진전',

  /**
   * 오디오/비디오 로드가 중단되면 발생합니다.
   *
   * @event 플레이어#abort
   * @type {이벤트대상~이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `abort` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechAbort_
   * @fires Player#abort
   * @listens Tech#abort
   */
  '중단',

  /**
   * 브라우저가 의도적으로 미디어 데이터를 가져오지 않을 때 발생합니다.
   *
   * @event 플레이어#suspend
   * @type {이벤트대상~이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `suspend` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechSuspend_
   * @fires Player#suspend
   * @listens Tech#suspend
   */
  '유예하다',

  /**
   * 현재 재생 목록이 비어 있을 때 발생합니다.
   *
   * @event 플레이어#emptied
   * @type {이벤트대상~이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `emptied` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechEmptied_
   * @fires Player#emptied
   * @listens Tech#emptied
   */
  '비워지다',
  /**
   * 브라우저가 미디어 데이터를 얻으려고 시도하지만 데이터를 사용할 수 없을 때 발생합니다.
   *
   * @event Player#stalled
   * @type {이벤트대상~이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `중단` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechStalled_
   * @fires Player#stalled
   * @listens Tech#stalled
   */
  '정지',

  /**
   * 브라우저가 오디오/비디오에 대한 메타 데이터를 로드했을 때 발생합니다.
   *
   * @event Player#loadedmetadata
   * @type {이벤트대상~이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `loadedmetadata` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechLoadedmetadata_
   * @fires Player#loadedmetadata
   * @listens Tech#loadedmetadata
   */
  '로드된 메타데이터',

  /**
   * 브라우저가 오디오/비디오의 현재 프레임을 로드했을 때 발생합니다.
   *
   * @event 플레이어#loadeddata
   * @type {이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `loadeddata` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechLoad addeddata_
   * @fires Player#loadeddata
   * @listens Tech#loadeddata
   */
  '로드데이터',

  /**
   * 현재 재생 위치가 변경되면 발생합니다.
   *
   * @event 플레이어#timeupdate
   * @type {이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `timeupdate` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechTimeUpdate_
   * @fires Player#timeupdate
   * @listens Tech#timeupdate
   */
  '시간 업데이트',

  /**
   * 동영상의 고유 크기가 변경되면 실행됩니다.
   *
   * @event 플레이어#resize
   * @type {이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `resize` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechResize_
   * @fires Player#resize
   * @listens Tech#resize
   */
  '크기 조정',

  /**
   * 볼륨이 변경되면 발생
   *
   * @event 플레이어#volumechange
   * @type {이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `volumechange` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechVolumechange_
   * @fires Player#volumechange
   * @listens Tech#volumechange
   */
  '볼륨 변경',

  /**
   * 텍스트 트랙이 변경되면 발생
   *
   * @event Player#texttrackchange
   * @type {이벤트}
   */
  /**
   * {@link Tech}에 의해 트리거된 `texttrackchange` 이벤트를 다시 트리거합니다.
   *
   * @사적인
   * @method Player#handleTechTexttrackchange_
   * @fires Player#texttrackchange
   * @listens Tech#texttrackchange
   */
  '텍스트트랙체인지'
];

// 재생 속도가 0일 때 대기할 이벤트
// 이것은 카멜 케이스가 아닌 이벤트 이름을 매핑하기 위한 유일한 목적을 위한 해시입니다.
// 카멜 케이스 함수 이름으로
const TECH_EVENTS_QUEUE = {
  놀 수있다: '놀 수있다',
  canplaythrough: 'CanPlayThrough',
  놀이: '놀이',
  추구: '찾았다'
};

const BREAKPOINT_ORDER = [
  '매우 작은',
  '작은',
  '작은',
  '중간',
  '크기가 큰',
  '대형',
  '거대한'
];

const BREAKPOINT_CLASSES = {};

// 그렙: vjs-layout-tiny
// 그렙: vjs-layout-x-small
// grep: vjs-레이아웃-소형
// grep: vjs-레이아웃-매체
// grep: vjs-레이아웃-라지
// grep: vjs-레이아웃-x-대형
// grep: vjs-레이아웃-거대한
BREAKPOINT_ORDER.forEach(k => {
  const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;

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

const DEFAULT_BREAKPOINTS = {
  매우 작은: 210,
  작은: 320,
  작은: 425,
  중간: 768,
  크기가 큰: 1440,
  특대형: 2560,
  거대한: 무한대
};

/**
 * Video.js 설정 방법 중 하나가 실행될 때 `Player` 클래스의 인스턴스가 생성됩니다.
 * 비디오를 초기화하는 데 사용됩니다.
 *
 * 인스턴스가 생성된 후에는 두 가지 방법으로 전역적으로 액세스할 수 있습니다.
 * 1. `videojs('example_video_1');`를 호출하여
 * 2. `videojs.players.example_video_1;`을 통해 직접 사용
 *
 * @extends 컴포넌트
 */
클래스 플레이어 확장 구성 요소 {

  /**
   * 이 클래스의 인스턴스를 만듭니다.
   *
   * @param {요소} 태그
   * 옵션 구성에 사용되는 원본 비디오 DOM 요소입니다.
   *
   * @param {객체} [옵션]
   * 옵션 이름 및 값의 개체.
   *
   * @param {Component~ReadyCallback} [준비]
   * 준비 콜백 기능.
   */
  생성자(태그, 옵션, 준비) {
    // 태그 ID가 존재하는지 확인
    tag.id = tag.id || options.id || `vjs_video_${Guid.newGUID()}`;

    // 옵션 설정
    // 옵션 인수는 비디오 태그에 설정된 옵션을 재정의합니다.
    // 전역적으로 설정된 옵션을 무시합니다.
    // 이 후반부는 로드 순서와 일치합니다.
    // (플레이어 앞에 태그가 있어야 함)
    options = assign(Player.getTagSettings(tag), 옵션);

    // 설정이 필요하므로 자식 초기화를 지연합니다.
    // 플레이어 속성이 먼저고 `super()` 전에 `this`를 사용할 수 없습니다.
    options.initChildren = 거짓;

    // 요소 생성과 동일
    options.createEl = 거짓;

    // 이벤트 믹스인을 자동 믹스인하지 않음
    options.evented = 거짓;

    // 플레이어가 자체적으로 터치 활동을 보고하는 것을 원하지 않습니다.
    // 구성 요소의 enableTouchActivity 참조
    options.reportTouchActivity = 거짓;

    // 언어가 설정되지 않은 경우 가장 가까운 lang 속성을 가져옵니다.
    if (!옵션.언어) {
      if (typeof tag.closest === '함수') {
        가장 가까운 const = tag.closest('[lang]');

        만약 (가장 가까운 && 가장 가까운.getAttribute) {
          options.language =closed.getAttribute('lang');
        }
      } else {
        let 요소 = 태그;

        동안 (요소 && element.nodeType === 1) {
          if (Dom.getAttributes(요소).hasOwnProperty('lang')) {
            options.language = element.getAttribute('lang');
            부서지다;
          }
          요소 = element.parentNode;
        }
      }
    }

    // 새 옵션으로 초기화하는 기본 구성 요소 실행
    슈퍼(널, 옵션, 준비);

    // 문서 수신기에 대한 바인딩된 메서드를 만듭니다.
    this.boundDocumentFullscreenChange_ = (e) => this.documentFullscreenChange_(e);
    this.boundFullWindowOnEscKey_ = (e) => this.fullWindowOnEscKey(e);

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

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

    // 기본값은 Fullscreen_을 false로
    this.isFullscreen_ = 거짓;

    // 로거 생성
    this.log = createLogger(this.id_);

    // 전체 화면 API에 대한 자체 참조를 유지하여 테스트에서 조롱할 수 있도록 합니다.
    this.fsApi_ = FullscreenApi;

    // 기술이 포스터를 변경할 때 추적
    this.isPosterFromTech_ = 거짓;

    // 재생 속도가 0일 때 대기 중인 콜백 정보를 보유합니다.
    // 검색이 발생합니다.
    this.queuedCallbacks_ = [];

    // 비동기적으로 로드될 수 있는 새로운 기술을 로드하고 있으므로 API 액세스를 끕니다.
    this.isReady_ = 거짓;

    // 초기화 상태 hasStarted_
    this.hasStarted_ = 거짓;

    // 초기화 상태 userActive_
    this.userActive_ = 거짓;

    // debugEnabled_ 초기화
    this.debugEnabled_ = 거짓;

    // 초기화 상태 audioOnlyMode_
    this.audioOnlyMode_ = 거짓;

    // 초기화 상태 audioPosterMode_
    this.audioPosterMode_ = 거짓;

    // 초기화 상태 audioOnlyCache_
    this.audioOnlyCache_ = {
      플레이어 높이: null,
      숨은 아이들: []
    };

    // 글로벌 옵션 객체가 실수로 날아간 경우
    // 누군가, 유익한 오류로 일찍 보석금을 내십시오.
    if (!this.options_ ||
        !this.options_.techOrder ||
        !this.options_.techOrder.length) {
      throw new Error('techOrder가 지정되지 않았습니다. 덮어쓰셨나요' +
                      ' 그냥 변경하는 대신 'videojs.options' +
                      '무시할 속성?');
    }

    // 옵션을 설정하는 데 사용된 원래 태그를 저장합니다.
    this.태그 = 태그;

    // html5 요소를 복원하는 데 사용되는 태그 속성을 저장합니다.
    this.tagAttributes = 태그 && Dom.getAttributes(태그);

    // 현재 언어 업데이트
    this.language(this.options_.language);

    // 지원되는 언어 업데이트
    if (옵션.언어) {
      // 플레이어 옵션 언어를 소문자로 정규화
      const languagesToLower = {};

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

    this.resetCache_();

    // 포스터 설정
    this.poster_ = options.poster || '';

    // 컨트롤 설정
    this.controls_ = !!options.controls;

    // 옵션에 저장된 원래 태그 설정
    // 이제 네이티브 컨트롤이 깜박이지 않도록 즉시 제거합니다.
    // nativeControlsForTouch가 true인 경우 HTML5 기술에 의해 다시 켜질 수 있음
    tag.controls = 거짓;
    tag.removeAttribute('컨트롤');

    this.changesrc_ = 거짓;
    this.playCallbacks_ = [];
    this.playTerminatedQueue_ = [];

    // 속성이 옵션을 재정의합니다.
    if (tag.hasAttribute('autoplay')) {
      this.autoplay(true);
    } else {
      // 그렇지 않으면 setter를 사용하여 유효성을 검사하고
      // 올바른 값을 설정합니다.
      this.autoplay(this.options_.autoplay);
    }

    // 플러그인 확인
    if (옵션.플러그인) {
      Object.keys(options.plugins).forEach((이름) => {
        if (typeof this[이름] !== '함수') {
          throw new Error(`플러그인 "${name}"이 존재하지 않습니다`);
        }
      });
    }

    /*
     * 스크러빙 내부 상태 저장
     *
     * @사적인
     * @return {Boolean} 사용자가 스크러빙하는 경우 참
     */
    this.scrubbing_ = 거짓;

    this.el_ = this.createEl();

    // 이것을 이벤트 객체로 만들고 `el_`을 이벤트 버스로 사용합니다.
    evented(이것, {eventBusKey: 'el_'});

    // 문서 및 플레이어 fullscreenchange 핸들러를 수신하여 해당 이벤트를 수신합니다.
    // isFullscreen을 적절하게 업데이트할 수 있도록 사용자가 수신하기 전에.
    // 다른 모든 것보다 먼저 fullscreenchange 이벤트를 수신하여
    // 우리의 isFullscreen 메서드는 내부 구성 요소와 외부 구성 요소에 대해 적절하게 업데이트됩니다.
    if (this.fsApi_.requestFullscreen) {
      Events.on(문서, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
      this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
    }

    if (this.fluid_) {
      this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
    }
    // 또한 원래 플레이어 옵션을 각 구성 요소 및 플러그인에 전달하려고 합니다.
    // 또한 나중에 옵션을 위해 플레이어에 다시 도달할 필요가 없습니다.
    // 또한 this.options_의 또 다른 복사본을 만들어야 합니다.
    // 무한 루프.
    const playerOptionsCopy = mergeOptions(this.options_);

    // 플러그인 로드
    if (옵션.플러그인) {
      Object.keys(options.plugins).forEach((이름) => {
        this[이름](options.plugins[이름]);
      });
    }

    // 디버그 모드를 활성화하여 모든 플러그인에 대해 debugon 이벤트를 발생시킵니다.
    경우 (옵션. 디버그) {
      this.debug(참);
    }

    this.options_.playerOptions = playerOptionsCopy;

    this.middleware_ = [];

    this.playbackRates(options.playbackRates);

    this.initChildren();

    // 오디오 태그 사용 여부에 따라 isAudio 설정
    this.isAudio(tag.nodeName.toLowerCase() === '오디오');

    // 컨트롤 className을 업데이트합니다. 컨트롤이 처음에 있는 경우 이 작업을 수행할 수 없습니다.
    // 요소가 아직 존재하지 않기 때문에 설정합니다.
    if (이.컨트롤()) {
      this.addClass('vjs-controls-enabled');
    } else {
      this.addClass('vjs-controls-disabled');
    }

    // 플레이어 유형에 따라 ARIA 레이블 및 지역 역할 설정
    this.el_.setAttribute('역할', '지역');
    if (this.isAudio()) {
      this.el_.setAttribute('aria-label', this.localize('오디오 플레이어'));
    } else {
      this.el_.setAttribute('aria-label', this.localize('비디오 플레이어'));
    }

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

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

    // 할 것: 이것을 더 똑똑하게 만드십시오. 터치/마우스 사용 간 사용자 상태 전환
    // 장치가 터치 및 마우스 이벤트를 모두 가질 수 있으므로 이벤트를 사용합니다.
    // 할 것: 창이 모니터 간에 전환될 때 이 검사를 다시 수행하도록 합니다.
    // (https://github.com/videojs/video.js/issues/5683 참조)
    if (browser.TOUCH_ENABLED) {
      this.addClass('vjs-touch-enabled');
    }

    // iOS Safari에서 호버 처리가 중단되었습니다.
    if (!브라우저.IS_IOS) {
      this.addClass('vjs-workinghover');
    }

    // 플레이어를 ID로 쉽게 찾을 수 있도록 합니다.
    Player.players[this.id_] = this;

    // 플러그인에서 css를 지원하기 위해 주 버전 클래스 추가
    const majorVersion = version.split('.')[0];

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

    // 플레이어가 처음 초기화될 때 활동을 트리거하므로 구성 요소
    // 필요한 경우 컨트롤 막대가 표시되는 것처럼
    this.userActive(참);
    this.reportUserActivity();

    this.one('재생', (e) => this.listenForUserActivity_(e));
    this.on('stageclick', (e) => this.handleStageClick_(e));
    this.on('키다운', (e) => this.handleKeyDown(e));
    this.on('언어변경', (e) => this.handleLanguagechange(e));

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

    // 플레이어가 완전히 끝난 후 오디오 모드 메서드를 모두 호출합니다.
    // 그들에 의해 트리거된 이벤트를 들을 수 있도록 설정
    this.on('준비', () => {
      // 먼저 audioPosterMode 메서드를 호출하여
      // 두 옵션이 모두 true로 설정된 경우 audioOnlyMode가 우선할 수 있습니다.
      this.audioPosterMode(this.options_.audioPosterMode);
      this.audioOnlyMode(this.options_.audioOnlyMode);
    });
  }

  /**
   * 비디오 플레이어를 파괴하고 필요한 정리를 수행합니다.
   *
   * 동영상을 동적으로 추가하고 제거하는 경우 특히 유용합니다.
   * DOM으로/로부터.
   *
   * @fires Player#dispose
   */
  폐기() {
    /**
     * 플레이어가 폐기될 때 호출됩니다.
     *
     * @event 플레이어#dispose
     * @type {이벤트대상~이벤트}
     */
    this.trigger('처리');
    // dispose가 두 번 호출되는 것을 방지합니다.
    this.off('처리');

    // 모든 플레이어별 문서 리스너가 언바운드인지 확인합니다. 이것은
    Events.off(문서, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
    Events.off(문서, '키다운', this.boundFullWindowOnEscKey_);

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

    // 이 플레이어에 대한 참조를 죽입니다.
    Player.players[this.id_] = null;

    만약 (이.태그 && this.태그.플레이어) {
      this.tag.player = null;
    }

    만약 (이것.el_ && this.el_.player) {
      this.el_.player = null;
    }

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

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

    if(이.태그) {
      this.태그 = null;
    }

    middleware.clearCacheForPlayer(이);

    // 트랙 목록에 대한 모든 이벤트 핸들러 제거
    // 모든 트랙과 트랙 리스너는 다음에 제거됩니다.
    // 기술 폐기
    TRACK_TYPES.names.forEach((이름) => {
      const 소품 = TRACK_TYPES[이름];
      const list = this[props.getterName]();

      // 네이티브 리스트가 아닌 경우
      // 이벤트 리스너를 수동으로 제거해야 합니다.
      만약 (목록 && 목록.꺼짐) {
        list.off();
      }
    });

    // 실제 .el_은 여기에서 제거되거나 다음과 같은 경우 대체됩니다.
    super.dispose({restoreEl: this.options_.restoreEl});
  }

  /**
   * `Player`의 DOM 요소를 만듭니다.
   *
   * @return {요소}
   * 생성되는 DOM 요소.
   */
  createEl() {
    let 태그 = this.tag;
    엘하자;
    let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
    const divEmbed = this.tag.tagName.toLowerCase() === '비디오 js';

    if (playerElIngest) {
      el = this.el_ = tag.parentNode;
    } 그렇지 않으면 (!divEmbed) {
      el = this.el_ = super.createEl('div');
    }

    // ID 및 클래스를 포함하여 태그의 모든 속성을 복사합니다.
    // ID는 이제 비디오 태그가 아닌 플레이어 상자를 참조합니다.
    const attrs = Dom.getAttributes(tag);

    if(divEmbed) {
      el = this.el_ = 태그;
      tag = this.tag = document.createElement('동영상');
      동안 (el.children.length) {
        tag.appendChild(el.firstChild);
      }

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

      el.appendChild(태그);

      playerElIngest = this.playerElIngest_ = 엘;
      // 커스텀 `video-js` 요소에서 속성을 이동합니다.
      // 새로운 `video` 요소로. 이것은 다음과 같은 것을 움직일 것입니다
      // 플레이어 이전에 js를 통해 설정된 `src` 또는 `controls`
      // 초기화되었습니다.
      Object.keys(el).forEach((k) => {
        {
          태그[k] = el[k];
        } 잡기 (e) {
          // 실제로 복사할 수 없는 outerHTML과 같은 속성이 있습니다. 무시하세요.
        }
      });
    }

    // 포커스 순서에서 비디오 요소를 제거하려면 tabindex를 -1로 설정합니다.
    tag.setAttribute('tabindex', '-1');
    attrs.tabindex = '-1';

    // #4583에 대한 해결 방법(JAWS+IE가 BPB 또는 재생 버튼을 알리지 않음) 및
    // JAWS가 있는 Chrome(Windows)의 동일한 문제.
    // https://github.com/FreedomScientific/VFO-standards-support/issues/78 참조
    // JAWS가 사용되고 있는지 감지할 수 없지만 이 ARIA 속성은
    // JAWS를 사용하지 않는 경우 IE11 또는 Chrome의 동작을 변경하지 않습니다.
    if (IE_VERSION || (IS_CHROME && IS_WINDOWS)) {
      tag.setAttribute('역할', '응용 프로그램');
      attrs.role = '응용 프로그램';
    }

    // CSS가 너비/높이를 100%로 만들 수 있도록 태그에서 너비/높이 속성을 제거합니다.
    tag.removeAttribute('너비');
    tag.removeAttribute('높이');

    if (attrs의 '너비') {
      삭제 attrs.width;
    }
    if (attrs의 '높이') {
      삭제 attrs.height;
    }

    Object.getOwnPropertyNames(attrs).forEach(function(attr) {
      // div 임베드에 있을 때 플레이어 요소에 클래스 속성을 복사하지 마십시오.
      // 클래스는 이미 divEmbed 케이스에 적절하게 설정되어 있습니다.
      // 그리고 `video-js` 클래스가 손실되지 않도록 하고 싶습니다.
      if (!(divEmbed && 속성 === '클래스')) {
        el.setAttribute(attr, attrs[attr]);
      }

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

    // HTML5 재생 기술로 사용하기 위해 태그 ID/클래스 업데이트
    // 컨테이너에 삽입한 후에 이 작업을 수행해야 한다고 생각할 수 있으므로 .vjs-tech 클래스
    // 100% 너비/높이로 깜박이지 않지만 클래스는 .video-js 부모에만 적용됩니다.
    tag.playerId = tag.id;
    tag.id += '_html5_api';
    tag.className = 'vjs-tech';

    // 요소에서 플레이어를 찾을 수 있도록 합니다.
    tag.player = el.player = 이;
    // 비디오의 기본 상태는 일시 정지입니다.
    this.addClass('vjs-paused');

    // 너비/높이를 설정하는 데 사용할 플레이어에 스타일 요소를 추가합니다.
    // 여전히 CSS로 재정의할 수 있는 방식으로 플레이어의
    // 비디오 요소
    if (window.VIDEOJS_NO_DYNAMIC_STYLE !== 참) {
      this.styleEl_ = stylesheet.createStyleElement('vjs-styles-dimensions');
      const defaultsStyleEl = Dom.$('.vjs-styles-defaults');
      const 머리 = Dom.$('머리');

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

    this.fill_ = 거짓;
    this.fluid_ = 거짓;

    // 스타일 el을 업데이트할 width/height/aspectRatio 옵션을 전달합니다.
    this.width(this.options_.width);
    this.height(this.options_.height);
    this.fill(this.options_.fill);
    this.fluid(this.options_.fluid);
    this.aspectRatio(this.options_.aspectRatio);
    // 이름에 대한 혼동과 문제를 줄이기 위해 crossOrigin과 crossorigin을 모두 지원합니다.
    this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);

    // 비디오/오디오 태그 내의 모든 링크를 숨깁니다.
    // IE는 화면 판독기에서 완전히 숨기지 않기 때문입니다.
    const 링크 = tag.getElementsByTagName('a');

    에 대한 (하자 i = 0; i < 링크 길이; i++) {
      const linkEl = 링크.항목(i);

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

    // insertElFirst는 networkState가 3에서 2로 깜박이는 것 같습니다.
    // 소스가 원래 실패했는지 알 수 있도록 나중을 위해 원본을 추적합니다.
    tag.initNetworkState_ = 태그.네트워크상태;

    // 비디오 태그를 div(el/box) 컨테이너에 래핑합니다.
    if (태그.부모노드 && !playerElIngest) {
      tag.parentNode.insertBefore(엘, 태그);
    }

    // 플레이어 요소의 첫 번째 자식으로 태그를 삽입합니다.
    // 그런 다음 수동으로 자식 배열에 추가하여 this.addChild
    // 다른 구성 요소에 대해 제대로 작동합니다.
    //
    // HTML5 설정에서 수정된 iPhone을 깨뜨립니다.
    Dom.prependTo(태그, 엘);
    this.children_.unshift(태그);

    // CSS :lang()이 플레이어와 일치하도록 플레이어에 lang 속성을 설정합니다.
    // 문서와 다른 것으로 설정된 경우
    this.el_.setAttribute('lang', this.language_);

    this.el_.setAttribute('번역', '아니오');

    this.el_ = 엘;

    반환 엘;
  }

  /**
   * `Player`의 crossOrigin 옵션을 가져오거나 설정합니다. HTML5 플레이어의 경우 이
   * `에 `crossOrigin` 속성을 설정합니다.< 동영상> ` CORS를 제어하는 태그
   * 행동.
   *
   * @see [동영상 요소 속성]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
   *
   * @param {문자열} [값]
   * `Player`의 crossOrigin을 설정할 값. 인수가
   * 주어진 'anonymous' 또는 'use-credentials' 중 하나여야 합니다.
   *
   * @return {문자열|정의되지 않음}
   * - 가져올 때 `Player`의 현재 crossOrigin 값입니다.
   * - 설정 시 정의되지 않음
   */
  crossOrigin(값) {
    if(!값) {
      return this.techGet_('crossOrigin');
    }

    if (값 !== '익명' && 값 !== '사용 자격 증명') {
      log.warn(`crossOrigin은 "${value}"가 지정된 "익명" 또는 "사용 자격 증명"이어야 합니다.`);
      반품;
    }

    this.techCall_('setCrossOrigin', 값);

    반품;
  }

  /**
   * `Player`의 너비에 대한 getter/setter. 플레이어의 구성된 값을 반환합니다.
   * 현재 너비를 얻으려면 `currentWidth()`를 사용하십시오.
   *
   * @param {숫자} [값]
   * `Player`의 너비를 설정할 값.
   *
   * @return {숫자}
   * 가져올 때 `Player`의 현재 너비입니다.
   */
  너비(값) {
    return this.dimension('너비', 값);
  }

  /**
   * `Player`의 높이에 대한 getter/setter. 플레이어의 구성된 값을 반환합니다.
   * 현재 높이를 얻으려면 `currentheight()`를 사용하십시오.
   *
   * @param {숫자} [값]
   * `Player`의 높이를 설정할 값.
   *
   * @return {숫자}
   * 가져올 때 `Player`의 현재 높이입니다.
   */
  높이(값) {
    return this.dimension('높이', 값);
  }

  /**
   * `Player`의 너비에 대한 getter/setter & 키.
   *
   * @param {문자열} 차원
   * 이 문자열은 다음과 같을 수 있습니다.
   * - '너비'
   * - '키'
   *
   * @param {숫자} [값]
   * 첫 번째 인수에 지정된 차원 값.
   *
   * @return {숫자}
   * (너비/높이)를 가져올 때 차원 인수 값입니다.
   */
  치수(치수, 값) {
    const privDimension = 치수 + '_';

    if (값 === 정의되지 않음) {
      return this[privDimension] || 0;
    }

    if (값 === '' || 값 === '자동') {
      // 빈 문자열이 주어지면 차원을 자동으로 재설정
      this[privDimension] = 정의되지 않음;
      this.updateStyleEl_();
      반품;
    }

    const parsedVal = parseFloat(값);

    if (isNaN(parsedVal)) {
      log.error(`${dimension}에 대해 잘못된 값 "${value}"이(가) 제공됨`);
      반품;
    }

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

  /**
   * `Player`의 vjs-fluid `className`에 대한 getter/setter/toggler.
   *
   * 이 기능을 켜면 채우기 모드가 꺼집니다.
   *
   * @param {부울} [부울]
   * - true 값은 클래스를 추가합니다.
   * - false 값은 클래스를 제거합니다.
   * - 어떤 값도 게터가 되지 않습니다.
   *
   * @return {부울|정의되지 않음}
   * - 얻을 때 유체의 가치.
   * - 설정시 `정의되지 않음`.
   */
  유체(부울) {
    if (부울 === 정의되지 않음) {
      반환 !!this.fluid_;
    }

    this.fluid_ = !!부울;

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

    this.updateStyleEl_();
  }

  /**
   * `Player`의 vjs-fill `className`에 대한 getter/setter/toggler.
   *
   * 이 기능을 켜면 유체 모드가 꺼집니다.
   *
   * @param {부울} [부울]
   * - true 값은 클래스를 추가합니다.
   * - false 값은 클래스를 제거합니다.
   * - 어떤 값도 게터가 되지 않습니다.
   *
   * @return {부울|정의되지 않음}
   * - 얻을 때 유체의 가치.
   * - 설정시 `정의되지 않음`.
   */
  채우기(부울) {
    if (부울 === 정의되지 않음) {
      반환 !!this.fill_;
    }

    this.fill_ = !!부울;

    경우 (부울) {
      this.addClass('vjs-fill');
      this.fluid(false);
    } else {
      this.removeClass('vjs-fill');
    }
  }

  /**
   * 종횡비 가져오기/설정
   *
   * @param {문자열} [비율]
   * 플레이어의 종횡비
   *
   * @return {문자열|정의되지 않음}
   * 가져올 때 현재 종횡비를 반환합니다.
   */

  /**
   * `Player`의 종횡비에 대한 getter/setter입니다.
   *
   * @param {문자열} [비율]
   * `Player`의 종횡비를 설정할 값입니다.
   *
   * @return {문자열|정의되지 않음}
   * - 가져올 때 `Player`의 현재 종횡비.
   * - 설정 시 정의되지 않음
   */
  aspectRatio(비율) {
    if (비율 === 정의되지 않음) {
      this.aspectRatio_를 반환합니다.
    }

    // 너비:높이 형식 확인
    if (!(/^\d+\:\d+$/).test(비율)) {
      throw new Error('가로 세로 비율에 대해 잘못된 값이 제공되었습니다. 형식은 너비:높이여야 합니다(예: 16:9).');
    }
    this.aspectRatio_ = 비율;

    // 유동 모드를 원하는 종횡비를 설정하면
    // 고정 모드에서는 너비와 높이를 직접 계산할 수 있기 때문입니다.
    this.fluid(참);

    this.updateStyleEl_();
  }

  /**
   * `Player` 요소의 스타일 업데이트(높이, 너비 및 종횡비).
   *
   * @사적인
   * @listens Tech#loadedmetadata
   */
  updateStyleEl_() {
    if (window.VIDEOJS_NO_DYNAMIC_STYLE === 참) {
      const 너비 = typeof this.width_ === '숫자' ? this.width_ : this.options_.width;
      const height = typeof this.height_ === '숫자' ? this.height_ : this.options_.height;
      const techEl = this.tech_ && this.tech_.el();

      경우 (techEl) {
        if (너비 > = 0) {
          techEl.width = 폭;
        }
        만약 (높이 > = 0) {
          techEl.height = 높이;
        }
      }

      반품;
    }

    너비를 보자;
    높이를 보자;
    let aspectRatio;
    let idClass;

    // 종횡비는 직접 사용되거나 너비와 높이를 계산하는 데 사용됩니다.
    if (this.aspectRatio_ !== 정의되지 않음 && this.aspectRatio_ !== '자동') {
      // 특별히 설정된 모든 aspectRatio 사용
      aspectRatio = this.aspectRatio_;
    } 그렇지 않으면 (this.videoWidth() > 0) {
      // 그렇지 않으면 비디오 메타데이터에서 종횡비를 가져오려고 시도합니다.
      aspectRatio = this.videoWidth() + ':' + this.videoHeight();
    } else {
      // 또는 기본값을 사용합니다. 비디오 요소는 2:1이지만 16:9가 더 일반적입니다.
      종횡비 = '16:9';
    }

    // 크기를 계산하는 데 사용할 수 있는 비율을 십진수로 가져옵니다.
    const ratioParts = aspectRatio.split(':');
    const ratioMultiplier = ratioParts[1] / ratioParts[0];

    if (this.width_ !== 정의되지 않음) {
      // 특별히 설정된 너비를 사용합니다.
      너비 = this.width_;
    } 그렇지 않으면 (this.height_ !== 정의되지 않음) {
      // 또는 높이가 설정된 경우 종횡비에서 너비를 계산합니다.
      너비 = this.height_ / ratioMultiplier;
    } else {
      // 또는 비디오의 메타데이터를 사용하거나 비디오 엘의 기본값인 300을 사용합니다.
      너비 = this.videoWidth() || 300;
    }

    if (this.height_ !== 정의되지 않음) {
      // 특별히 설정된 높이를 사용합니다.
      높이 = this.height_;
    } else {
      // 그렇지 않으면 비율과 너비에서 높이를 계산합니다.
      높이 = 너비 * ratioMultiplier;
    }

    // 알파 문자로 시작하여 CSS 클래스가 유효한지 확인
    if ((/^[^a-zA-Z]/).test(this.id())) {
      idClass = '치수-' + this.id();
    } else {
      idClass = this.id() + '-dimensions';
    }

    // 스타일 요소에 대해 올바른 클래스가 여전히 플레이어에 있는지 확인합니다.
    this.addClass(idClass);

    stylesheet.setTextContent(this.styleEl_, `
      .${idClass} {
        폭: ${폭}px;
        높이: ${높이}픽셀;
      }

      .${idClass}.vjs-fluid:not(.vjs-오디오 전용 모드) {
        패딩 상단: ${ratioMultiplier * 100}%;
      }
    `);
  }

  /**
   * 요소를 포함한 재생 {@link Tech} 인스턴스 로드/생성
   * 및 API 메서드. 그런 다음 `Player`에 `Tech` 요소를 자식으로 추가합니다.
   *
   * @param {문자열} 기술 이름
   * 재생 기술의 이름
   *
   * @param {문자열} 소스
   * 영상 출처
   *
   * @사적인
   */
  loadTech_(기술자 이름, 소스) {

    // 현재 재생 기술을 일시 중지하고 제거합니다.
    if (this.tech_) {
      this.unloadTech_();
    }

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

    // 다른 기술을 사용하는 즉시 HTML5 비디오 태그를 제거합니다.
    if (titleTechName !== 'Html5' && 이 태그) {
      Tech.getTech('Html5').disposeMediaElement(this.tag);
      this.tag.player = null;
      this.태그 = null;
    }

    this.techName_ = titleTechName;

    // 비동기적으로 로드될 수 있는 새로운 기술을 로드하고 있으므로 API 액세스를 끕니다.
    this.isReady_ = 거짓;

    let autoplay = this.autoplay();

    // autoplay가 문자열(또는 normalizeAutoplay: true인 `true`)인 경우 기술팀에 false를 전달합니다.
    // 플레이어가 `loadstart`에서 자동 재생을 처리하기 때문입니다.
    if (typeof this.autoplay() === '문자열' || this.autoplay() === 참 && this.options_.normalizeAutoplay) {
      자동재생 = 거짓;
    }

    // 플레이어 옵션에서 기술별 옵션을 가져오고 사용할 소스 및 상위 요소를 추가합니다.
    const 기술 옵션 = {
      원천,
      자동 재생,
      'nativeControlsForTouch': this.options_.nativeControlsForTouch,
      'playerId': this.id(),
      'techId': `${this.id()}_${camelTechName}_api`,
      'playsinline': this.options_.playsinline,
      '사전 로드': this.options_.preload,
      '루프': this.options_.loop,
      'disablePictureInPicture': this.options_.disablePictureInPicture,
      '음소거됨': this.options_.muted,
      '포스터': this.poster(),
      '언어': this.언어(),
      'playerElIngest': this.playerElIngest_ || 거짓,
      'vtt.js': this.options_['vtt.js'],
      'canOverridePoster': !!this.options_.techCanOverridePoster,
      'enableSourceset': this.options_.enableSourceset,
      '약속': this.options_.Promise
    };

    TRACK_TYPES.names.forEach((이름) => {
      const 소품 = TRACK_TYPES[이름];

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

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

    if(이.태그) {
      techOptions.tag = this.tag;
    }

    만약 (출처 && source.src === this.cache_.src && this.cache_.currentTime > 0) {
      techOptions.startTime = this.cache_.currentTime;
    }

    // 기술 인스턴스 초기화
    const TechClass = Tech.getTech(techName);

    if (!TechClass) {
      throw new Error(`'${titleTechName}'이라는 이름의 기술이 없습니다! '${titleTechName}'은 videojs.registerTech()를 사용하여 등록해야 합니다'`);
    }

    this.tech_ = new TechClass(techOptions);

    // player.triggerReady는 항상 비동기이므로 이것이 비동기일 필요는 없습니다.
    this.tech_.ready(Fn.bind(this, this.handleTechReady_), true);

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

    // 모든 HTML5 정의 이벤트를 수신하고 플레이어에서 트리거합니다.
    TECH_EVENTS_RETRIGGER.forEach((이벤트) => {
      this.on(this.tech_, 이벤트, (e) => this[`handleTech${toTitleCase(event)}_`](e));
    });

    Object.keys(TECH_EVENTS_QUEUE).forEach((이벤트) => {
      this.on(this.tech_, 이벤트, (eventObj) => {
        if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
          this.queuedCallbacks_.push({
            콜백: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
            이벤트: eventObj
          });
          반품;
        }
        this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
      });
    });

    this.on(this.tech_, 'loadstart', (e) => this.handleTechLoadStart_(e));
    this.on(this.tech_, 'sourceset', (e) => this.handleTechSourceset_(e));
    this.on(this.tech_, '대기 중', (e) => this.handleTechWaiting_(e));
    this.on(this.tech_, '종료', (e) => this.handleTechEnded_(e));
    this.on(this.tech_, '탐구', (e) => this.handleTechSeeking_(e));
    this.on(this.tech_, '재생', (e) => this.handleTechPlay_(e));
    this.on(this.tech_, 'firstplay', (e) => this.handleTechFirstPlay_(e));
    this.on(this.tech_, '일시중지', (e) => this.handleTechPause_(e));
    this.on(this.tech_, 'durationchange', (e) => this.handleTechDurationChange_(e));
    this.on(this.tech_, 'fullscreenchange', (e, 데이터) => this.handleTechFullscreenChange_(e, 데이터));
    this.on(this.tech_, '전체 화면 오류', (e, 오류) => this.handleTechFullscreenError_(e, err));
    this.on(this.tech_, 'enterpictureinpicture', (e) => this.handleTechEnterPictureInPicture_(e));
    this.on(this.tech_, 'leavepictureinpicture', (e) => this.handleTechLeavePictureInPicture_(e));
    this.on(this.tech_, '오류', (e) => this.handleTechError_(e));
    this.on(this.tech_, 'posterchange', (e) => this.handleTechPosterChange_(e));
    this.on(this.tech_, 'textdata', (e) => this.handleTechTextData_(e));
    this.on(this.tech_, 'ratechange', (e) => this.handleTechRateChange_(e));
    this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_);

    this.usingNativeControls(this.techGet_('컨트롤'));

    if (이.컨트롤() && !this.usingNativeControls()) {
      this.addTechControlsListeners_();
    }

    // 기술 요소가 이미 없는 경우 DOM에 기술 요소를 추가합니다.
    // Html5를 사용하는 경우 원본 비디오 요소를 삽입하지 않도록 합니다.
    if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
      Dom.prependTo(this.tech_.el(), this.el());
    }

    // 첫 번째 기술이 로드된 후 원본 비디오 태그 참조를 제거합니다.
    if(이.태그) {
      this.tag.player = null;
      this.태그 = null;
    }
  }

  /**
   * 현재 재생 {@link Tech}를 언로드하고 폐기합니다.
   *
   * @사적인
   */
  unloadTech_() {
    // 다음 기술에서 동일한 텍스트 트랙을 재사용할 수 있도록 현재 텍스트 트랙을 저장합니다.
    TRACK_TYPES.names.forEach((이름) => {
      const 소품 = TRACK_TYPES[이름];

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

    this.isReady_ = 거짓;

    this.tech_.dispose();

    this.tech_ = 거짓;

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

    this.isPosterFromTech_ = 거짓;
  }

  /**
   * 현재 {@link Tech}에 대한 참조를 반환합니다.
   * 기본적으로 기술을 직접 사용하는 위험에 대한 경고를 인쇄합니다.
   * 그러나 전달된 모든 인수는 경고를 음소거합니다.
   *
   * @param {*} [안전]
   * 경고를 침묵시키기 위해 전달된 모든 것
   *
   * @return {기술}
   * 기술
   */
  기술(안전) {
    if (안전성 === 정의되지 않음) {
      log.warn('기술을 직접 사용하는 것은 위험할 수 있습니다. 당신이 무엇을 하고 있는지 알고 있기를 바랍니다.\n' +
        '자세한 내용은 https://github.com/videojs/video.js/issues/2617을 참조하세요.\n');
    }

    this.tech_를 반환합니다.
  }

  /**
   * 재생 요소에 대한 클릭 및 터치 리스너 설정
   *
   * - 데스크톱: 동영상 자체를 클릭하면 재생이 전환됩니다.
   * - 모바일 기기: 동영상을 클릭하면 컨트롤이 전환됩니다.
   * 활성 상태와 활성 상태 사이에서 사용자 상태를 전환하여 수행됩니다.
   * 비활성
   * - 탭은 사용자가 활성화되었거나 비활성화되었음을 알릴 수 있습니다.
   * 예를 들어 iPhone 동영상을 빠르게 탭하면 컨트롤이 표시됩니다. 또 다른
   * 빠르게 탭하면 다시 숨겨야 함(사용자가 비활성 상태임을 알림)
   * 보기 상태)
   * - 이 외에도 다음 이후에도 사용자가 비활성 상태로 간주되기를 원합니다.
   * 몇 초 동안 활동이 없습니다.
   *
   * > 참고: 이 설정으로 모방할 수 없는 iOS 상호 작용의 유일한 부분
   * 활동으로 계산되는 동영상 요소를 길게 터치하여
   * 컨트롤을 계속 표시하지만 문제가 되지 않습니다. 터치 앤 홀드
   * 모든 컨트롤에서 여전히 사용자를 활성 상태로 유지합니다.
   *
   * @사적인
   */
  addTechControlsListeners_() {
    // 여러 번 호출되는 경우를 대비하여 이전 리스너를 모두 제거해야 합니다.
    this.removeTechControlsListeners_();

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

    // 컨트롤이 숨겨진 경우 탭 이벤트 없이 변경되지 않도록 합니다.
    // 따라서 사용자를 보고하기 전에 컨트롤이 이미 표시되었는지 확인합니다.
    // 활동
    this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
    this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
    this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);

    // 탭 리스너는 터치엔드 리스너 뒤에 와야 합니다.
    // 수신기는 userActive(false)를 설정할 때 보고된 사용자 활동을 취소합니다.
    this.on(this.tech_, '탭', this.boundHandleTechTap_);
  }

  /**
   * 클릭 및 탭 컨트롤에 사용되는 리스너를 제거합니다. 다음을 위해 필요합니다.
   * 컨트롤이 비활성화되어 탭/터치가 아무 작업도 수행하지 않아야 합니다.
   *
   * @사적인
   */
  removeTechControlsListeners_() {
    // 단순히 `this.off()`를 사용하고 싶지는 않습니다.
    // 이것을 확장하는 기술자가 추가한 리스너.
    this.off(this.tech_, '탭', this.boundHandleTechTap_);
    this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
    this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
    this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
    this.off(this.tech_, '클릭', this.boundHandleTechClick_);
    this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
  }

  /**
   * 플레이어는 기술이 준비될 때까지 기다립니다.
   *
   * @사적인
   */
  handleTechReady_() {
    this.triggerReady();

    // 이전과 동일한 볼륨 유지
    if (this.cache_.volume) {
      this.techCall_('setVolume', this.cache_.volume);
    }

    // 로드하는 동안 기술자가 더 높은 해상도의 포스터를 찾았는지 확인합니다.
    this.handleTechPosterChange_();

    // 가능한 경우 기간을 업데이트합니다.
    this.handleTechDurationChange_();
  }

  /**
   * {@link Tech}에 의해 트리거된 `loadstart` 이벤트를 다시 트리거합니다. 이
   * 함수는 첫 번째 로드 시작인 경우 {@link Player#firstplay}도 트리거합니다.
   * 동영상용.
   *
   * @fires Player#loadstart
   * @fires 플레이어#firstplay
   * @listens Tech#loadstart
   * @사적인
   */
  handleTechLoadStart_() {
    // 할 것: 대신 `emptied` 이벤트를 사용하도록 업데이트했습니다. #1277을 참조하십시오.

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

    // 오류 상태 재설정
    this.오류(null);

    // 기간 업데이트
    this.handleTechDurationChange_();

    // 이미 재생 중인 경우 지금 firstplay 이벤트를 트리거하려고 합니다.
    // firstplay 이벤트는 play 및 loadstart 이벤트 모두에 의존합니다.
    // 새로운 소스에 대해 임의의 순서로 발생할 수 있습니다.
    if (!this.paused()) {
      /**
       * 사용자 에이전트가 미디어 데이터를 찾기 시작할 때 발생
       *
       * @event 플레이어#loadstart
       * @type {이벤트대상~이벤트}
       */
      this.trigger('loadstart');
      this.trigger('첫 플레이');
    } else {
      // hasStarted 상태 재설정
      this.hasStarted(거짓);
      this.trigger('loadstart');
    }

    // 브라우저 로드가 시작된 후 자동 재생이 발생합니다.
    // 그래서 우리는 그 행동을 모방합니다
    this.manualAutoplay_(this.autoplay() === 참 && this.options_.normalizeAutoplay ? '재생' : this.autoplay());
  }

  /**
   * 일반적인 부울이 아닌 자동 재생 문자열 값 처리
   * 기술자가 처리해야 하는 값입니다. 아니니 참고하세요
   * 사양의 일부. 유효한 값과 그 기능은 다음과 같습니다.
   * Player#autoplay()의 autoplay getter에서 찾을 수 있습니다.
   */
  manualAutoplay_(유형) {
    if (!this.tech_ || typeof type !== '문자열') {
      반품;
    }

    // 원래 muted() 값을 저장하고 muted를 true로 설정하고 play()를 시도합니다.
    // Promise 거부 시 저장된 값에서 음소거 복원
    const resolveMuted = () => {
      const 이전에 음소거됨 = this.muted();

      this.muted(참);

      const restoreMuted = () => {
        this.muted(이전에 음소거됨);
      };

      // 재생 종료 시 음소거 복원
      this.playTerminatedQueue_.push(restoreMuted);

      const mutedPromise = this.play();

      if (!isPromise(mutedPromise)) {
        반품;
      }

      mutedPromise.catch(오류 => {
        복원음소거();
        throw new Error(`manualAutoplay에서 거부되었습니다. 음소거된 값을 복원합니다. ${오류 ? 오류 : ''}`);
      });
    };

    약속하자;

    // 음소거된 경우 기본값은 true입니다.
    // 우리가 할 수 있는 유일한 일은 play를 호출하는 것입니다.
    if (유형 === '임의' && !this.muted()) {
      약속 = this.play();

      if (isPromise(약속)) {
        약속 = 약속.catch(resolveMuted);
      }
    } 그렇지 않으면 (유형 === '음소거됨' && !this.muted()) {
      약속 = resolveMuted();
    } else {
      약속 = this.play();
    }

    if (!isPromise(약속)) {
      반품;
    }

    반환 약속.then(() => {
      this.trigger({type: 'autoplay-success', autoplay: type});
    }).캐치(() => {
      this.trigger({type: 'autoplay-failure', autoplay: type});
    });
  }

  /**
   * 올바른 소스를 반환하도록 내부 소스 캐시를 업데이트합니다.
   * `src()`, `currentSource()` 및 `currentSources()`.
   *
   * > 참고: 전달된 소스가 존재하는 경우 `currentSources`는 업데이트되지 않습니다.
   * 현재 `currentSources` 캐시에 있습니다.
   *
   *
   * @param {Tech~SourceObject} srcObj
   * 캐시를 업데이트할 문자열 또는 객체 소스.
   */
  updateSourceCaches_(srcObj = '') {

    let src = srcObj;
    let type = '';

    if (src 유형 !== '문자열') {
      src = srcObj.src;
      유형 = srcObj.유형;
    }

    // 모든 캐시가 기본값으로 설정되어 있는지 확인
    // null 검사를 방지하기 위해
    this.cache_.source = this.cache_.source || {};
    this.cache_.sources = this.cache_.sources || [];

    // 전달된 src의 유형을 가져오려고 시도합니다.
    만약 (src && !유형) {
      type = findMimetype(this, src);
    }

    // `currentSource` 캐시를 항상 업데이트합니다.
    this.cache_.source = mergeOptions({}, srcObj, {src, type});

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

    에 대한 (하자 i = 0; i < sourceEls.length; i++) {
      const sourceObj = Dom.getAttributes(sourceEls[i]);

      sourceElSources.push(sourceObj);

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

    // 일치하는 소스 엘이 있지만 일치하는 소스가 없는 경우
    // 현재 소스 캐시가 최신이 아닙니다.
    if (matchingSourceEls.length && !matchingSources.length) {
      this.cache_.sources = sourceElSources;
    // 일치하는 소스 또는 소스 els가 없는 경우
    // `currentSource` 캐시에 대한 소스 캐시
    } 그렇지 않으면 (!matchingSources.length) {
      this.cache_.sources = [이.cache_.소스];
    }

    // 기술 `src` 캐시 업데이트
    this.cache_.src = src;
  }

  /**
   * *실험적* 소스가 {@link Tech}에서 설정되거나 변경될 때 실행됩니다.
   * 미디어 요소가 다시 로드되도록 합니다.
   *
   * 초기 소스와 각 후속 소스에 대해 실행됩니다.
   * 이 이벤트는 Video.js의 맞춤 이벤트이며 {@link Tech}에 의해 트리거됩니다.
   *
   * 이 이벤트의 이벤트 개체에는 소스를 포함할 `src` 속성이 포함되어 있습니다.
   * 이벤트가 발동되었을 때 사용 가능했던 것입니다. 일반적으로 Video.js인 경우에만 필요합니다.
   * 소스가 변경되는 동안 기술을 전환하고 있습니다.
   *
   * 플레이어(또는 미디어 요소)에서 `load`가 호출될 때도 실행됩니다.
   * {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|사양 `로드`} 때문입니다.
   * 자원 선택 알고리즘을 중단하고 다시 시작해야 한다고 말합니다.
   * 이 경우 `src` 속성이
   * 소스가 무엇인지 알 수 없지만 빈 문자열 `""`
   * 변하고 있다는 것.
   *
   * *이 이벤트는 현재 아직 실험 중이며 마이너 릴리스에서 변경될 수 있습니다.*
   * __사용하려면 `enableSourceset` 옵션을 플레이어에게 전달하세요.__
   *
   * @event 플레이어#sourceset
   * @type {이벤트대상~이벤트}
   * @prop {문자열} src
   * `sourceset`이 트리거되었을 때 사용할 수 있는 소스 URL입니다.
   * 소스가 무엇인지 알 수 없으면 빈 문자열이 됩니다.
   * 하지만 출처는 변경될 것임을 알아두세요.
   */
  /**
   * {@link Tech}에 의해 트리거된 `sourceset` 이벤트를 다시 트리거합니다.
   *
   * @fires Player#sourceset
   * @listens Tech#sourceset
   * @사적인
   */
  handleTechSourceset_(이벤트) {
    // 소스 캐시를 업데이트할 때만 소스 캐시를 업데이트합니다.
    // 플레이어 API를 사용하여 업데이트되지 않았습니다.
    if (!this.changesrc_) {
      let updateSourceCaches = (src) => this.updateSourceCaches_(src);
      const playerSrc = this.currentSource().src;
      const eventSrc = event.src;

      // blob이 아닌 playerSrc와 blob인 tech src가 있는 경우
      if (playerSrc && !(/^blob:/).test(playerSrc) && (/^blob:/).test(eventSrc)) {

        // 기술 소스와 플레이어 소스가 모두 업데이트된 경우 가정합니다.
        // @videojs/http-streaming과 같은 것이 소스를 설정하고 소스 캐시 업데이트를 건너뜁니다.
        if (!this.lastSource_ || (this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc)) {
          updateSourceCaches = () => {};
        }
      }

      // 소스를 초기 소스로 바로 업데이트
      // 경우에 따라 빈 문자열이 됩니다.
      updateSourceCaches(eventSrc);

      // `sourceset` `src`가 빈 문자열인 경우
      // `loadstart`가 캐시를 `currentSrc`로 업데이트하기를 기다립니다.
      // `loadstart` 이전에 sourceset이 발생하면 상태를 재설정합니다.
      if (!event.src) {
        this.tech_.any(['sourceset', 'loadstart'], (e) => {
          // 소스셋이 `loadstart` 이전에 발생한 경우
          // 이 `handleTechSourceset_`으로 할 일이 없습니다.
          // 다시 호출될 것이고 이것은 그곳에서 처리될 것입니다.
          if (e.type === '소스셋') {
            반품;
          }

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

          this.lastSource_.tech = techSrc;
          this.updateSourceCaches_(techSrc);
        });
      }
    }
    this.lastSource_ = {플레이어: this.currentSource().src, 기술: event.src};

    this.trigger({
      src: 이벤트.src,
      유형: '소스셋'
    });
  }

  /**
   * vjs-has-started 클래스 추가/제거
   *
   * @fires 플레이어#firstplay
   *
   * @param {부울} 요청
   * - true: 클래스를 추가합니다.
   * - false: 클래스 제거
   *
   * @return {부울}
   * hasStarted_의 부울 값
   */
  hasStarted(요청) {
    if (요청 === 정의되지 않음) {
      // 변경 요청이 없으면 getter로 작동
      this.hasStarted_를 반환합니다.
    }

    if (요청 === this.hasStarted_) {
      반품;
    }

    this.hasStarted_ = 요청;

    if (this.hasStarted_) {
      this.addClass('vjs-has-started');
      this.trigger('첫 플레이');
    } else {
      this.removeClass('vjs-has-started');
    }
  }

  /**
   * 미디어가 재생을 시작하거나 재개할 때마다 발생
   *
   * @see [사양]{@링크 https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
   * @fires 플레이어#플레이
   * @listens Tech#play
   * @사적인
   */
  handleTechPlay_() {
    this.removeClass('vjs-ended');
    this.removeClass('vjs-paused');
    this.addClass('vjs-playing');

    // 사용자가 재생을 누르면 포스터를 숨깁니다.
    this.hasStarted(참);
    /**
     * {@link Tech#play} 이벤트가 발생할 때마다 트리거됩니다. 나타냅니다
     * 재생이 시작되었거나 재개되었습니다.
     *
     * @event 플레이어#play
     * @type {이벤트대상~이벤트}
     */
    this.trigger('재생');
  }

  /**
   * {@link Tech}에 의해 트리거된 `ratechange` 이벤트를 다시 트리거합니다.
   *
   * 재생 속도가 0일 때 대기 중인 이벤트가 있으면 실행
   * 지금 그 사건들.
   *
   * @사적인
   * @method Player#handleTechRateChange_
   * @fires Player#ratechange
   * @listens Tech#ratechange
   */
  handleTechRateChange_() {
    if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
      this.queuedCallbacks_.forEach((대기) => queued.callback(queued.event));
      this.queuedCallbacks_ = [];
    }
    this.cache_.lastPlaybackRate = this.tech_.playbackRate();
    /**
     * 오디오/비디오 재생 속도 변경 시 발생
     *
     * @event 플레이어#ratechange
     * @type {이벤트}
     */
    this.trigger('ratechange');
  }

  /**
   * {@link Tech}에 의해 트리거된 `waiting` 이벤트를 다시 트리거합니다.
   *
   * @fires Player#waiting
   * @listens Tech#waiting
   * @사적인
   */
  handleTechWaiting_() {
    this.addClass('vjs-waiting');
    /**
     * DOM 요소의 readyState 변경으로 인해 재생이 중지되었습니다.
     *
     * @event 플레이어#waiting
     * @type {이벤트대상~이벤트}
     */
    this.trigger('기다림');

    // 브라우저는 대기 이벤트 후에 timeupdate 이벤트를 내보낼 수 있습니다. 방지하기 위해
    // 대기 중인 클래스를 조기에 제거하고 시간이 변경될 때까지 기다립니다.
    const timeWhenWaiting = this.currentTime();
    const timeUpdateListener = () => {
      if (timeWhenWaiting !== this.currentTime()) {
        this.removeClass('vjs-waiting');
        this.off('timeupdate', timeUpdateListener);
      }
    };

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

  /**
   * {@link Tech}에 의해 트리거된 `canplay` 이벤트를 다시 트리거합니다.
   * > 메모: 이는 브라우저 간에 일관성이 없습니다. #1351 참조
   *
   * @fires 플레이어#canplay
   * @listens Tech#canplay
   * @사적인
   */
  handleTechCanPlay_() {
    this.removeClass('vjs-waiting');
    /**
     * 미디어에 HAVE_FUTURE_DATA 이상의 readyState가 있습니다.
     *
     * @event 플레이어#canplay
     * @type {이벤트대상~이벤트}
     */
    this.trigger('canplay');
  }

  /**
   * {@link Tech}에 의해 트리거된 `canplaythrough` 이벤트를 다시 트리거합니다.
   *
   * @fires Player#canplaythrough
   * @listens Tech#canplaythrough
   * @사적인
   */
  handleTechCanPlayThrough_() {
    this.removeClass('vjs-waiting');
    /**
     * 미디어에 HAVE_ENOUGH_DATA 이상의 readyState가 있습니다. 이것은
     * 전체 미디어 파일을 버퍼링 없이 재생할 수 있습니다.
     *
     * @event Player#canplaythrough
     * @type {이벤트대상~이벤트}
     */
    this.trigger('canplaythrough');
  }

  /**
   * {@link Tech}에 의해 트리거된 `playing` 이벤트를 다시 트리거합니다.
   *
   * @fires 플레이어#playing
   * @listens Tech#playing
   * @사적인
   */
  handleTechPlaying_() {
    this.removeClass('vjs-waiting');
    /**
     * 미디어 재생이 더 이상 차단되지 않고 재생이 시작되었습니다.
     *
     * @event 플레이어#playing
     * @type {이벤트대상~이벤트}
     */
    this.trigger('재생 중');
  }

  /**
   * {@link Tech}에 의해 트리거된 `seeking` 이벤트를 다시 트리거합니다.
   *
   * @fires Player#seeking
   * @listens Tech#seeking
   * @사적인
   */
  handleTechSeeking_() {
    this.addClass('vjs-seeking');
    /**
     * 플레이어가 새로운 시간으로 점프할 때마다 발생
     *
     * @event 플레이어#seeking
     * @type {이벤트대상~이벤트}
     */
    this.trigger('검색');
  }

  /**
   * {@link Tech}에 의해 트리거된 `seeked` 이벤트를 다시 트리거합니다.
   *
   * @fires Player#seeked
   * @listens Tech#seeked
   * @사적인
   */
  handleTechSeeked_() {
    this.removeClass('vjs-seeking');
    this.removeClass('vjs-ended');
    /**
     * 플레이어가 새로운 시간으로 점프를 마쳤을 때 발생
     *
     * @event Player#seeked
     * @type {이벤트대상~이벤트}
     */
    this.trigger('검색');
  }

  /**
   * {@link Tech}에 의해 트리거된 `firstplay` 이벤트를 다시 트리거합니다.
   *
   * @fires 플레이어#firstplay
   * @listens Tech#firstplay
   * @deprecated 6.0부터 firstplay 이벤트는 더 이상 사용되지 않습니다.
   * 6.0부터 플레이어에게 `starttime` 옵션을 전달하고 firstplay 이벤트는 더 이상 사용되지 않습니다.
   * @사적인
   */
  handleTechFirstPlay_() {
    // 첫 번째 starttime 속성이 지정된 경우
    // 그런 다음 주어진 오프셋에서 초 단위로 시작합니다.
    if (this.options_.starttime) {
      log.warn('플레이어에게 `starttime` 옵션을 전달하는 것은 6.0에서 더 이상 사용되지 않습니다.');
      this.currentTime(this.options_.starttime);
    }

    this.addClass('vjs-has-started');
    /**
     * 동영상이 처음 재생될 때 실행됩니다. HLS 사양의 일부가 아니며 이는
     * 아마도 아직 최고의 구현은 아니므로 아껴서 사용하십시오. 당신이 가지고 있지 않은 경우
     * 재생을 금지하는 이유는 `myPlayer.one('play');`을 대신 사용하십시오.
     *
     * @event 플레이어#firstplay
     * @deprecated 6.0부터 firstplay 이벤트는 더 이상 사용되지 않습니다.
     * @type {이벤트대상~이벤트}
     */
    this.trigger('첫 플레이');
  }

  /**
   * {@link Tech}에 의해 트리거된 `pause` 이벤트를 다시 트리거합니다.
   *
   * @fires Player#pause
   * @listens Tech#pause
   * @사적인
   */
  handleTechPause_() {
    this.removeClass('vjs-playing');
    this.addClass('vjs-paused');
    /**
     * 미디어가 일시 중지될 때마다 실행됨
     *
     * @event 플레이어#pause
     * @type {이벤트대상~이벤트}
     */
    this.trigger('일시정지');
  }

  /**
   * {@link Tech}에 의해 트리거된 `ended` 이벤트를 다시 트리거합니다.
   *
   * @fires Player#ended
   * @listens Tech#ended
   * @사적인
   */
  handleTechEnded_() {
    this.addClass('vjs-ended');
    this.removeClass('vjs-waiting');
    if (this.options_.loop) {
      this.currentTime(0);
      this.play();
    } 그렇지 않으면 (!this.paused()) {
      this.pause();
    }

    /**
     * 미디어 리소스의 끝에 도달하면 시작됨(currentTime == duration)
     *
     * @event 플레이어#ended
     * @type {이벤트대상~이벤트}
     */
    this.trigger('종료');
  }

  /**
   * 미디어 리소스의 재생 시간이 처음 알려지거나 변경될 때 발생
   *
   * @listens Tech#durationchange
   * @사적인
   */
  handleTechDurationChange_() {
    this.duration(this.techGet_('기간'));
  }

  /**
   * 미디어 요소를 클릭하여 재생/일시 정지 처리
   *
   * @param {EventTarget~Event} 이벤트
   * 이 기능을 트리거한 이벤트
   *
   * @listens Tech#click
   * @사적인
   */
  handleTechClick_(이벤트) {
    // 컨트롤이 비활성화되면 클릭으로 재생이 전환되지 않아야 합니다.
    // 클릭은 컨트롤로 간주됩니다.
    if (!this.controls_) {
      반품;
    }

    만약에 (
      this.options_ === 정의되지 않음 ||
      this.options_.userActions === 정의되지 않음 ||
      this.options_.userActions.click === 정의되지 않음 ||
      this.options_.userActions.click !== 거짓
    ) {

      만약에 (
        this.options_ !== 정의되지 않음 &&
        this.options_.userActions !== 정의되지 않음 &&
        typeof this.options_.userActions.click === '함수'
      ) {

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

      } 그렇지 않으면 (this.paused()) {
        침묵 약속(this.play());
      } else {
        this.pause();
      }
    }
  }

  /**
   * 미디어 요소를 두 번 클릭하여 전체 화면을 시작/종료합니다.
   *
   * @param {EventTarget~Event} 이벤트
   * 이 기능을 트리거한 이벤트
   *
   * @listens Tech#dblclick
   * @사적인
   */
  handleTechDoubleClick_(이벤트) {
    if (!this.controls_) {
      반품;
    }

    // 전체 화면 상태를 전환하고 싶지 않습니다.
    // 컨트롤 바 또는 모달 내부를 더블 클릭했을 때
    const inAllowedEls = Array.prototype.some.call(
      this.$$('.vjs-control-bar, .vjs-modal-dialog'),
      엘 => el.contains(이벤트.대상)
    );

    if (!inAllowedEls) {
      /*
       * options.userActions.doubleClick
       *
       * `undefined` 또는 `true`인 경우 컨트롤이 있는 경우 두 번 클릭하면 전체 화면이 전환됩니다.
       * 더블 클릭 처리를 비활성화하려면 `false`로 설정하십시오.
       * 외부 더블 클릭 핸들러를 대체하는 기능으로 설정
       */
      만약에 (
        this.options_ === 정의되지 않음 ||
        this.options_.userActions === 정의되지 않음 ||
        this.options_.userActions.doubleClick === 정의되지 않음 ||
        this.options_.userActions.doubleClick !== 거짓
      ) {

        만약에 (
          this.options_ !== 정의되지 않음 &&
          this.options_.userActions !== 정의되지 않음 &&
          typeof this.options_.userActions.doubleClick === '함수'
        ) {

          this.options_.userActions.doubleClick.call(이, 이벤트);

        } 그렇지 않으면 (this.isFullscreen()) {
          this.exitFullscreen();
        } else {
          this.requestFullscreen();
        }
      }
    }
  }

  /**
   * 미디어 요소의 탭을 처리합니다. 그것은 사용자를 토글합니다
   * 컨트롤을 숨기고 표시하는 활동 상태.
   *
   * @listens Tech#tap
   * @사적인
   */
  handleTechTap_() {
    this.userActive(!this.userActive());
  }

  /**
   * 시작하려면 터치를 처리하십시오.
   *
   * @listens Tech#touchstart
   * @사적인
   */
  handleTechTouchStart_() {
    this.userWasActive = this.userActive();
  }

  /**
   * 핸들 터치로 이동
   *
   * @listens Tech#touchmove
   * @사적인
   */
  handleTechTouchMove_() {
    if (이.userWasActive) {
      this.reportUserActivity();
    }
  }

  /**
   * 터치를 끝까지 처리
   *
   * @param {EventTarget~Event} 이벤트
   * 트리거된 touchend 이벤트
   * 이 기능
   *
   * @listens Tech#touchend
   * @사적인
   */
  handleTechTouchEnd_(이벤트) {
    // 마우스 이벤트도 발생하지 않도록 중지
    경우 (event.cancelable) {
      event.preventDefault();
    }
  }

  /**
   * SWF의 기본 클릭 이벤트는 IE11, Win8.1RT에서 트리거되지 않음
   * 대신 SWF 내부에서 트리거된 stageclick 이벤트 사용
   *
   * @사적인
   * @listens stageclick
   */
  handleStageClick_() {
    this.reportUserActivity();
  }

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

  /**
   * 문서 fschange 이벤트가 트리거되면 이를 호출합니다.
   */
  documentFullscreenChange_(e) {
    const targetPlayer = e.target.player;

    // 다른 플레이어가 전체 화면인 경우
    // 구형 firefox는 문서를 e.target으로 지정하므로 targetPlayer에 대해 null 검사를 수행합니다.
    if (targetPlayer && targetPlayer !== 이) {
      반품;
    }

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

    if (!isFs && el.matches) {
      isFs = el.matches(':' + this.fsApi_.fullscreen);
    } 그렇지 않으면 (!isFs && el.msMatchesSelector) {
      isFs = el.msMatchesSelector(':' + this.fsApi_.fullscreen);
    }

    this.isFullscreen(isFs);
  }

  /**
   * 기술 전체 화면 변경 처리
   *
   * @param {EventTarget~Event} 이벤트
   * 이 함수를 트리거한 fullscreenchange 이벤트
   *
   * @param {객체} 데이터
   * 이벤트와 함께 전송된 데이터
   *
   * @사적인
   * @listens Tech#fullscreenchange
   * @fires Player#fullscreenchange
   */
  handleTechFullscreenChange_(이벤트, 데이터) {
    만약 (데이터) {
      if (data.nativeIOSFullscreen) {
        this.addClass('vjs-ios-native-fs');
        this.tech_.one('webkitendfullscreen', () => {
          this.removeClass('vjs-ios-native-fs');
        });
      }
      this.isFullscreen(data.isFullscreen);
    }
  }

  handleTechFullscreenError_(이벤트, 오류) {
    this.trigger('전체 화면 오류', 오류);
  }

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

  /**
   * 핸들 기술 Picture-in-Picture를 입력하십시오.
   *
   * @param {EventTarget~Event} 이벤트
   * 이 함수를 트리거한 enterpictureinpicture 이벤트
   *
   * @사적인
   * @listens Tech#enterpictureinpicture
   */
  handleTechEnterPictureInPicture_(이벤트) {
    this.isInPictureInPicture(true);
  }

  /**
   * 처리 기술은 Picture-in-Picture를 남깁니다.
   *
   * @param {EventTarget~Event} 이벤트
   * 이 기능을 트리거한 leavepictureinpicture 이벤트
   *
   * @사적인
   * @listens Tech#leavepictureinpicture
   */
  handleTechLeavePictureInPicture_(이벤트) {
    this.isInPictureInPicture(거짓);
  }

  /**
   * 오디오/비디오를 로드하는 동안 오류가 발생하면 발생합니다.
   *
   * @사적인
   * @listens Tech#error
   */
  handleTechError_() {
    const error = this.tech_.error();

    this.오류(오류);
  }

  /**
   * {@link Tech}에 의해 트리거된 `textdata` 이벤트를 다시 트리거합니다.
   *
   * @fires Player#textdata
   * @listens Tech#textdata
   * @사적인
   */
  handleTechTextData_() {
    let 데이터 = null;

    if (인수.길이 > 1) {
      데이터 = 인수[1];
    }

    /**
     * 기술팀에서 textdata 이벤트를 받으면 발생합니다.
     *
     * @event 플레이어#textdata
     * @type {이벤트대상~이벤트}
     */
    this.trigger('텍스트데이터', 데이터);
  }

  /**
   * 캐시된 값에 대한 개체를 가져옵니다.
   *
   * @return {객체}
   * 현재 객체 캐시 가져오기
   */
  get캐시() {
    this.cache_를 반환합니다.
  }

  /**
   * 내부 캐시 개체를 재설정합니다.
   *
   * 플레이어 생성자 또는 재설정 메서드 외부에서 이 함수를 사용하면
   * 의도하지 않은 부작용이 있습니다.
   *
   * @사적인
   */
  resetCache_() {
    this.cache_ = {

      // 현재 currentTime은 항상 _really_ 캐시되지 않습니다.
      // 기술에서 검색됨(참조: currentTime). 그러나 완전성을 위해,
      // 여기서는 0으로 설정하여 실제로 캐싱을 시작하는지 확인합니다.
      // 다른 모든 것과 함께 재설정합니다.
      현재 시간: 0,
      초기화 시간: 0,
      inactivityTimeout: this.options_.inactivityTimeout,
      지속: 난,
      마지막 볼륨: 1,
      lastPlaybackRate: this.defaultPlaybackRate(),
      미디어: null,
      소스: '',
      원천: {},
      출처: [],
      재생 속도: [],
      용량: 1
    };
  }

  /**
   * 재생 기술에 값 전달
   *
   * @param {문자열} [방법]
   * 호출 방법
   *
   * @param {객체} 인수
   * 전달 인수
   *
   * @사적인
   */
  techCall_(방법, 인수) {
    // 아직 준비되지 않았다면 준비되었을 때 메소드를 호출합니다.

    this.ready(함수() {
      if (middleware.allowedSetters의 메소드) {
        return middleware.set(this.middleware_, this.tech_, 메소드, arg);

      } else if (middleware.allowedMediators의 메서드) {
        return middleware.mediate(this.middleware_, this.tech_, 메소드, arg);
      }

      {
        if (this.tech_) {
          this.tech_[방법](인수);
        }
      } 잡기 (e) {
        log(e);
        전자를 던져;
      }
    }, 진실);
  }

  /**
   * 전화 받기는 기술을 기다릴 수 없으며 때로는 필요하지 않습니다.
   *
   * @param {문자열} 메소드
   * 기술 방법
   *
   * @return {함수|정의되지 않음}
   * 메서드 또는 정의되지 않음
   *
   * @사적인
   */
  techGet_(방법) {
    if (!this.tech_ || !this.tech_.isReady_) {
      반품;
    }

    if (middleware.allowedGetters의 메서드) {
      return middleware.get(this.middleware_, this.tech_, 메소드);

    } else if (middleware.allowedMediators의 메서드) {
      return middleware.mediate(this.middleware_, this.tech_, 메소드);
    }

    // Flash는 숨기거나 위치를 변경할 때 죽고 다시 로드되는 것을 좋아합니다.
    // 이 경우 개체 메서드가 사라지고 오류가 발생합니다.
    // 할 것: Flash 이외의 기술에도 필요합니까?
    // 그런 일이 발생하면 오류를 포착하고 기술 담당자에게 더 이상 준비가 되지 않았음을 알립니다.
    {
      return this.tech_[방법]();
    } 잡기 (e) {

      // 추가 기술 라이브러리를 빌드할 때 예상되는 메서드가 아직 정의되지 않았을 수 있습니다.
      if (this.tech_[method] === 정의되지 않음) {
        log(`Video.js: ${this.techName_} 재생 기술에 대해 ${method} 메서드가 정의되지 않았습니다.`, e);
        전자를 던져;
      }

      // 객체에서 메서드를 사용할 수 없으면 TypeError가 발생합니다.
      if (e.name === '유형 오류') {
        log(`Video.js: ${this.techName_} 재생 기술 요소에서 ${method}를 사용할 수 없습니다.`, e);
        this.tech_.isReady_ = 거짓;
        전자를 던져;
      }

      // 알 수 없는 오류인 경우 그냥 기록하고 던집니다.
      log(e);
      전자를 던져;
    }
  }

  /**
   * 첫 번째 기회에 재생을 시작하려고 시도합니다.
   *
   * @return {약속|정의되지 않음}
   * 브라우저가 약속(또는 하나의
   * 옵션으로 전달되었습니다). 이 약속은
   * play의 반환값. 이것이 정의되지 않은 경우 다음을 충족합니다.
   * Promise Chain 그렇지 않으면 다음과 같은 경우 Promise Chain이 이행됩니다.
   * 놀이의 약속이 이루어집니다.
   */
  놀다() {
    const PromiseClass = this.options_.Promise || 창. 약속;

    경우 (PromiseClass) {
      새로운 PromiseClass를 반환((해결) => {
        this.play_(해결);
      });
    }

    return this.play_();
  }

  /**
   * 재생을 위한 실제 로직은
   * 플레이의 반환 값. 이를 통해 플레이 약속을 확인할 수 있습니다.
   *는 최신 브라우저에 있는 것입니다.
   *
   * @사적인
   * @param {함수} [콜백]
   * 테크니션 플레이 시 호출해야 하는 콜백은 실제로 호출했을 때
   */
  play_(콜백 = 침묵 약속) {
    this.playCallbacks_.push(콜백);

    const isSrcReady = 부울(!이. && (this.src() || this.currentSrc()));

    // play_에 대한 호출을 `one` 이벤트 함수처럼 처리합니다.
    if (this.waitToPlay_) {
      this.off(['ready', 'loadstart'], this.waitToPlay_);
      this.waitToPlay_ = null;
    }

    // 플레이어/기술자가 준비되지 않았거나 src 자체가 준비되지 않은 경우
    // `ready` 또는 `loadstart`에서 재생할 호출을 대기열에 넣습니다.
    if (!this.isReady_ || !isSrcReady) {
      this.waitToPlay_ = (e) => {
        this.play_();
      };
      this.one(['ready', 'loadstart'], this.waitToPlay_);

      // Safari에 있는 경우 제스처 기간 후에 loadstart가 트리거될 가능성이 높습니다.
      // 이 경우 로드를 호출하여 비디오 요소를 프라이밍해야 제 시간에 준비됩니다.
      if (!isSrcReady && (브라우저.IS_ANY_SAFARI || 브라우저.IS_IOS)) {
        this.load();
      }
      반품;
    }

    // 플레이어/기술이 준비되고 소스가 있으면 재생을 시도할 수 있습니다.
    const val = this.techGet_('플레이');

    // 리턴 값이 null이면 재생 종료
    경우 (값 === null) {
      this.runPlayTerminatedQueue_();
    } else {
      this.runPlayCallbacks_(val);
    }
  }

  /**
   * 플레이가 종료되면 이 기능들이 실행됩니다. 놀면
   * runPlayCallbacks_가 실행되면 이 함수는 실행되지 않습니다. 이를 통해 우리는
   * 종료된 플레이와 실제 콜 투 플레이를 구별하기 위해.
   */
  runPlayTerminatedQueue_() {
    const queue = this.playTerminatedQueue_.slice(0);

    this.playTerminatedQueue_ = [];

    queue.forEach(함수(q) {
      큐();
    });
  }

  /**
   * 재생 콜백이 지연되면 다음을 실행해야 합니다.
   * 기술에서 플레이가 실제로 호출될 때 콜백. 이 기능
   * 지연된 콜백을 실행하고 반환 값을 수락합니다.
   * 기술에서.
   *
   * @param {정의되지 않은|약속} 값
   * 기술에서 반환 값입니다.
   */
  runPlayCallbacks_(val) {
    const 콜백 = this.playCallbacks_.slice(0);

    this.playCallbacks_ = [];
    // 실제 플레이가 끝났기 때문에 플레이 종료 대기열을 지웁니다.
    this.playTerminatedQueue_ = [];

    callbacks.forEach(함수(cb) {
      cb(값);
    });
  }

  /**
   * 비디오 재생 일시 중지
   *
   * @return {플레이어}
   * 이 함수가 호출된 플레이어 개체에 대한 참조
   */
  정지시키다() {
    this.techCall_('일시정지');
  }

  /**
   * 플레이어가 일시 중지되었거나 아직 재생되지 않았는지 확인
   *
   * @return {부울}
   * - false: 미디어가 현재 재생 중인 경우
   * - true: 미디어가 현재 재생되지 않는 경우
   */
  일시 중지() {
    // paused의 초기 상태는 true여야 합니다(Safari에서는 실제로 false임).
    return (this.techGet_('paused') === false) ? 거짓 : 참;
  }

  /**
   * 사용자가 현재 시간 범위를 나타내는 TimeRange 개체를 가져옵니다.
   * 연주했습니다.
   *
   * @return {시간 범위}
   * 모든 시간 증분을 나타내는 시간 범위 객체
   * 연주되었습니다.
   */
  플레이() {
    return this.techGet_('재생됨') || createTimeRange(0, 0);
  }

  /**
   * 사용자가 "스크러빙"하는지 여부를 반환합니다. 스크럽은
   * 사용자가 진행률 표시줄 핸들을 클릭하고
   * 진행률 표시줄을 따라 드래그합니다.
   *
   * @param {부울} [isScrubbing]
   * 사용자가 스크러빙을 하는지 여부
   *
   * @return {부울}
   * 얻을 때 스크럽 값
   */
  스크러빙(isScrubbing) {
    if (isScrubbing 유형 === '정의되지 않음') {
      return this.scrubbing_;
    }
    this.scrubbing_ = !!isScrubbing;
    this.techCall_('setScrubbing', this.scrubbing_);

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

  /**
   * 현재 시간을 가져오거나 설정합니다(초).
   *
   * @param {숫자|문자열} [초]
   * 찾는 시간(초)
   *
   * @return {숫자}
   * - 가져올 때의 현재 시간(초)
   */
  현재 시간(초) {
    if (초 유형 !== '정의되지 않음') {
      만약 (초 < 0) {
        초 = 0;
      }
      if (!this.isReady_ || this.changesrc_ || !this.tech_ || !this.tech_.isReady_) {
        this.cache_.initTime = 초;
        this.off('canplay', this.boundApplyInitTime_);
        this.one('canplay', this.boundApplyInitTime_);
        반품;
      }
      this.techCall_('setCurrentTime', 초);
      this.cache_.initTime = 0;
      반품;
    }

    // 마지막 currentTime을 캐시하고 반환합니다. 기본값은 0초
    //
    // currentTime 캐싱은 기술자의 엄청난 양의 읽기를 방지하기 위한 것입니다.
    // 스크러빙할 때 currentTime이지만 결과적으로 많은 성능 이점을 제공하지 않을 수 있습니다.
    // 테스트해야 합니다. 또한 무언가는 실제 현재 시간을 읽어야 합니다. 그렇지 않으면 캐시가
    // 절대 업데이트되지 않습니다.
    this.cache_.currentTime = (this.techGet_('currentTime') || 0);
    this.cache_.currentTime을 반환합니다.
  }

  /**
   * 캐시에 저장된 initTime의 값을 currentTime으로 적용합니다.
   *
   * @사적인
   */
  applyInitTime_() {
    this.currentTime(this.cache_.initTime);
  }

  /**
   * 일반적으로 비디오의 시간 길이를 초 단위로 가져옵니다.
   * 가장 드문 사용 사례를 제외한 모든 경우에서 인수가 메서드에 전달되지 않습니다.
   *
   * > **메모**: 재생 시간이 끝나기 전에 동영상이 로드되기 시작했어야 합니다.
   * 알려져 있으며 사전 로드 동작에 따라 비디오가 시작될 때까지 알려지지 않을 수 있습니다.
   * 놀이.
   *
   * @fires Player#durationchange
   *
   * @param {숫자} [초]
   * 동영상 재생 시간을 초 단위로 설정
   *
   * @return {숫자}
   * - 얻을 때 비디오의 길이는 초 단위입니다.
   */
  기간(초) {
    if (초 === 정의되지 않음) {
      // 기간을 알 수 없는 경우 NaN을 반환합니다.
      return this.cache_.duration !== 정의되지 않음 ? this.cache_.duration : NaN;
    }

    초 = parseFloat(초);

    // 라이브 신호 비디오를 위해 Infinity로 표준화
    만약 (초 < 0) {
      초 = 무한대;
    }

    if (초 !== this.cache_.duration) {
      // 최적화된 스크러빙을 위해 마지막으로 설정한 값을 캐시합니다(특히. 플래시)
      // 할 것: Flash 이외의 기술자에게 필요합니까?
      this.cache_.duration = 초;

      if (초 === 무한대) {
        this.addClass('vjs-live');
      } else {
        this.removeClass('vjs-live');
      }
      if (!isNaN(초)) {
        // 기간 값을 알 수 없으면 기간 변경을 실행하지 않습니다.
        // @[사양] 참조{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}

        /**
         * @event 플레이어#durationchange
         * @type {이벤트대상~이벤트}
         */
        this.trigger('durationchange');
      }
    }
  }

  /**
   * 비디오에 남은 시간을 계산합니다. 일부가 아님
   * 네이티브 비디오 API의.
   *
   * @return {숫자}
   * 남은 시간(초)
   */
  남은 시간() {
    return this.duration() - this.currentTime();
  }

  /**
   * 다음과 같은 경우에 사용하도록 의도된 남은 시간 기능
   * 시간은 사용자에게 직접 표시됩니다.
   *
   * @return {숫자}
   * 남은 반올림 시간(초)
   */
  남은시간표시() {
    return Math.floor(this.duration()) - Math.floor(this.currentTime());
  }

  //
  // 다운로드된 비디오 부분의 배열과 비슷합니다.

  /**
   * 비디오 시간의 배열로 TimeRange 객체를 가져옵니다.
   * 다운받은 것. 퍼센트만 알고 싶다면
   * 다운로드된 비디오는 bufferedPercent를 사용합니다.
   *
   * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
   *
   * @return {시간 범위}
   * 모의 TimeRange 객체(HTML 사양을 따름)
   */
  버퍼링() {
    let buffered = this.techGet_('버퍼링됨');

    if (!버퍼 || !버퍼.길이) {
      buffered = createTimeRange(0, 0);
    }

    반환 버퍼;
  }

  /**
   * 다운로드된 비디오의 백분율(소수점)을 가져옵니다.
   * 이 메소드는 네이티브 HTML 비디오 API의 일부가 아닙니다.
   *
   * @return {숫자}
   * 백분율을 나타내는 0과 1 사이의 소수점
   * 버퍼링됨 0은 0%이고 1은 100%임
   */
  bufferedPercent() {
    return bufferedPercent(this.buffered(), this.duration());
  }

  /**
   * 마지막 버퍼링된 시간 범위의 종료 시간 가져오기
   * 진행률 표시줄에서 모든 시간 범위를 캡슐화하는 데 사용됩니다.
   *
   * @return {숫자}
   * 마지막 버퍼링된 시간 범위의 끝
   */
  bufferedEnd() {
    const buffered = this.buffered();
    지속 시간 = this.duration();
    let end = buffered.end(buffered.length - 1);

    만약 (종료 > 지속) {
      끝 = 기간;
    }

    반환 종료;
  }

  /**
   * 미디어의 현재 볼륨 가져오기 또는 설정
   *
   * @param {숫자} [percentAsDecimal]
   * 소수점 퍼센트로 표시된 새 볼륨:
   * - 0은 음소거됨/0%/꺼짐
   * - 1.0은 100%/전체
   * - 0.5는 부피의 절반 또는 50%입니다.
   *
   * @return {숫자}
   * 얻을 때 백분율로 현재 볼륨
   */
  볼륨(percentAsDecimal) {
    권하자;

    if (percentAsDecimal !== 정의되지 않음) {
      // 값을 0과 1 사이로 강제 설정
      vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal)));
      this.cache_.volume = 볼륨;
      this.techCall_('setVolume', vol);

      만약 (권 > 0) {
        this.lastVolume_(vol);
      }

      반품;
    }

    // 현재 볼륨을 반환할 때 기본값은 1입니다.
    vol = parseFloat(this.techGet_('볼륨'));
    반환 (isNaN(vol)) ? 1: 권;
  }

  /**
   * 현재 음소거 상태를 가져오거나 음소거를 켜거나 끕니다.
   *
   * @param {부울} [음소거됨]
   * - 음소거
   * - 음소거 해제하려면 false
   *
   * @return {부울}
   * - 음소거가 켜져 있는 경우 true
   * - 음소거가 해제되어 있으면 false
   */
  음소거(음소거) {
    if (음소거!== 정의되지 않음) {
      this.techCall_('setMute', 음소거);
      반품;
    }
    return this.techGet_('음소거됨') || 거짓;
  }

  /**
   * 현재 defaultMute 상태를 가져오거나 defaultMuted를 켜거나 끕니다. 기본음소거됨
   *는 초기 재생 시 음소거 상태를 나타냅니다.
   *
   * ```js
   * var myPlayer = videojs('some-player-id');
   *
   * myPlayer.src("http://www.example.com/path/to/video.mp4");
   *
   * // get, false여야 합니다.
   * console.log(myPlayer.defaultMute());
   * // 참으로 설정
   * myPlayer.defaultMute(true);
   * // get이 참이어야 합니다.
   * console.log(myPlayer.defaultMute());
   * ```
   *
   * @param {부울} [defaultMuted]
   * - 음소거
   * - 음소거 해제하려면 false
   *
   * @return {부울|플레이어}
   * - defaultMute가 켜져 있고 가져오는 경우 true
   * - defaultMute가 꺼져 있고 가져오는 경우 false
   * - 설정할 때 현재 플레이어에 대한 참조
   */
  defaultMute(기본 음소거) {
    if (defaultMute!== 정의되지 않음) {
      return this.techCall_('setDefaultMute', defaultMuted);
    }
    return this.techGet_('defaultMuted') || 거짓;
  }

  /**
   * 마지막 볼륨을 가져오거나 설정
   *
   * @param {숫자} [percentAsDecimal]
   * 새로운 마지막 볼륨(소수점 퍼센트):
   * - 0은 음소거됨/0%/꺼짐
   * - 1.0은 100%/전체
   * - 0.5는 부피의 절반 또는 50%입니다.
   *
   * @return {숫자}
   * 가져올 때 lastVolume의 현재 값(퍼센트)
   *
   * @사적인
   */
  lastVolume_(percentAsDecimal) {
    if (percentAsDecimal !== 정의되지 않음 && 퍼센트 AsDecimal !== 0) {
      this.cache_.lastVolume = percentAsDecimal;
      반품;
    }
    this.cache_.lastVolume을 반환합니다.
  }

  /**
   * 현재 기술이 기본 전체 화면을 지원할 수 있는지 확인
   * (예: iOS와 같은 내장 컨트롤 포함)
   *
   * @return {부울}
   * 네이티브 전체화면이 지원되는 경우
   */
  supportFullScreen() {
    return this.techGet_('supportsFullScreen') || 거짓;
  }

  /**
   * 플레이어가 전체 화면 모드인지 확인하거나 플레이어에게 알립니다.
   *가 전체 화면 모드인지 아닌지.
   *
   * > 메모: 최신 HTML5 사양부터 isFullscreen은 더 이상 공식이 아닙니다.
   * 속성 대신 document.fullscreenElement가 사용됩니다. 그러나 isFullscreen은
   * 여전히 내부 플레이어 작업을 위한 귀중한 자산입니다.
   *
   * @param {부울} [isFS]
   * 플레이어의 현재 전체 화면 상태 설정
   *
   * @return {부울}
   * - 전체 화면이 켜져 있는 경우 true
   * - 전체 화면이 꺼져 있으면 false
   */
  isFullscreen(isFS) {
    if (isFS !== 정의되지 않음) {
      const oldValue = this.isFullscreen_;

      this.isFullscreen_ = 부울(isFS);

      // 전체 화면 상태를 변경하고 접두사 모드에 있는 경우 전체 화면 변경을 트리거합니다.
      // 이전 브라우저에 대해 fullscreenchange 이벤트를 트리거하는 유일한 위치입니다.
      // fullWindow 모드는 접두사 이벤트로 처리되며 fullscreenchange 이벤트도 받습니다.
      if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
        /**
           * @event Player#fullscreenchange
           * @type {이벤트대상~이벤트}
           */
        this.trigger('전체 화면 변경');
      }

      this.toggleFullscreenClass_();
      반품;
    }
    this.isFullscreen_을 반환합니다.
  }

  /**
   * 동영상 크기를 전체 화면으로 확대
   * 일부 브라우저에서는 기본적으로 전체화면이 지원되지 않아 진입합니다.
   * 비디오가 브라우저 창을 채우는 "전체 창 모드".
   * 네이티브 전체 화면을 지원하는 브라우저 및 기기에서는 간혹
   * Video.js 사용자 정의 스킨이 아닌 브라우저의 기본 컨트롤이 표시됩니다.
   * 여기에는 대부분의 모바일 장치(iOS, Android) 및 이전 버전의
   * 사파리.
   *
   * @param {객체} [fullscreenOptions]
   * 플레이어 전체 화면 옵션 재정의
   *
   * @fires Player#fullscreenchange
   */
  요청전체화면(전체화면옵션) {
    const PromiseClass = this.options_.Promise || 창. 약속;

    경우 (PromiseClass) {
      const 자기 = this;

      새 PromiseClass((해결, 거부) 반환 => {
        함수 오프핸들러() {
          self.off('전체 화면 오류', errorHandler);
          self.off('전체 화면 변경', changeHandler);
        }
        함수 changeHandler() {
          오프핸들러();
          해결하다();
        }
        함수 errorHandler(e, 오류) {
          오프핸들러();
          거부(오류);
        }

        self.one('전체화면변경', changeHandler);
        self.one('전체 화면 오류', errorHandler);

        const 약속 = self.requestFullscreenHelper_(fullscreenOptions);

        if (약속) {
          promise.then(offHandler, offHandler);
          promise.then(해결, 거부);
        }
      });
    }

    return this.requestFullscreenHelper_();
  }

  requestFullscreenHelper_(fullscreenOptions) {
    let fsOptions;

    // 사양을 준수하는 브라우저에서는 전체 화면 옵션만 requestFullscreen에 전달합니다.
    // 이 메서드에 직접 전달되지 않는 한 기본값 또는 플레이어 구성 옵션을 사용합니다.
    if (!this.fsApi_.prefixed) {
      fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
      if (fullscreenOptions !== 정의되지 않음) {
        fsOptions = 전체화면옵션;
      }
    }

    // 이 메서드는 다음과 같이 작동합니다.
    // 1. 전체 화면 api를 사용할 수 있는 경우 사용
    // 1. 가능한 옵션을 사용하여 requestFullscreen을 호출합니다.
    // 2. 위에서 약속을 받은 경우 이를 사용하여 isFullscreen()을 업데이트합니다.
    // 2. 그렇지 않으면 기술이 전체 화면을 지원하는 경우 `enterFullScreen`을 호출합니다.
    // 이것은 특히 iPhone, 구형 iPad 및 iOS의 사파리가 아닌 브라우저에 사용됩니다.
    // 3. 그렇지 않으면 "fullWindow" 모드를 사용합니다.
    if (this.fsApi_.requestFullscreen) {
      const 약속 = this.el_[this.fsApi_.requestFullscreen](fsOptions);

      if (약속) {
        약속.다음(() => this.isFullscreen(참), () => this.isFullscreen(false));
      }

      반환 약속;
    } 그렇지 않으면 (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === 참) {
      // video.js 컨트롤을 전체 화면으로 표시할 수는 없지만 전체 화면으로 표시할 수는 있습니다.
      // 기본 컨트롤 사용
      this.techCall_('enterFullScreen');
    } else {
      // 전체 화면은 지원되지 않으므로 비디오 요소를
      // 뷰포트 채우기
      this.enterFullWindow();
    }
  }

  /**
   * 전체 화면 모드로 전환한 후 동영상을 원래 크기로 되돌립니다.
   *
   * @fires Player#fullscreenchange
   */
  exitFullscreen() {
    const PromiseClass = this.options_.Promise || 창. 약속;

    경우 (PromiseClass) {
      const 자기 = this;

      새 PromiseClass((해결, 거부) 반환 => {
        함수 오프핸들러() {
          self.off('전체 화면 오류', errorHandler);
          self.off('전체 화면 변경', changeHandler);
        }
        함수 changeHandler() {
          오프핸들러();
          해결하다();
        }
        함수 errorHandler(e, 오류) {
          오프핸들러();
          거부(오류);
        }

        self.one('전체화면변경', changeHandler);
        self.one('전체 화면 오류', errorHandler);

        const 약속 = self.exitFullscreenHelper_();

        if (약속) {
          promise.then(offHandler, offHandler);
          // resolve/reject 메서드에 약속을 매핑합니다.
          promise.then(해결, 거부);
        }
      });
    }

    return this.exitFullscreenHelper_();
  }

  exitFullscreenHelper_() {
    if (this.fsApi_.requestFullscreen) {
      const 약속 = 문서[this.fsApi_.exitFullscreen]();

      if (약속) {
        // 여기에서 Promise를 분할하므로
        // 이 체인에 처리되지 않은 오류가 없도록 잠재적인 오류
        침묵 약속(promise.then(() => this.isFullscreen(false)));
      }

      반환 약속;
    } 그렇지 않으면 (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === 참) {
      this.techCall_('exitFullScreen');
    } else {
      this.exitFullWindow();
    }
  }

  /**
   * 전체 화면이 지원되지 않는 경우 확장할 수 있습니다.
   * 비디오 컨테이너는 브라우저가 허용하는 만큼 넓습니다.
   *
   * @fires Player#enterFullWindow
   */
  enterFullWindow() {
    this.isFullscreen(참);
    this.isFullWindow = 참;

    // 전체 화면이 꺼져 있을 때 반환할 원본 문서 오버플로 값 저장
    this.docOrigOverflow = document.documentElement.style.overflow;

    // Esc 키에 대한 리스너를 추가하여 전체 화면을 종료합니다.
    Events.on(document, 'keydown', this.boundFullWindowOnEscKey_);

    // 스크롤 막대 숨기기
    document.documentElement.style.overflow = '숨김';

    // 전체화면 스타일 적용
    Dom.addClass(document.body, 'vjs-full-window');

    /**
     * @event Player#enterFullWindow
     * @type {이벤트대상~이벤트}
     */
    this.trigger('enterFullWindow');
  }

  /**
   * 전체 창을 종료하거나
   * ESC 키의 전체 화면
   *
   * @param {문자열} 이벤트
   * 키 누름을 확인하는 이벤트
   */
  fullWindowOnEscKey(이벤트) {
    if (keycode.isEventKey(event, 'Esc')) {
      if (this.isFullscreen() === 참) {
        if (!this.isFullWindow) {
          this.exitFullscreen();
        } else {
          this.exitFullWindow();
        }
      }
    }
  }

  /**
   * 전체 창 종료
   *
   * @fires Player#exitFullWindow
   */
  exitFullWindow() {
    this.isFullscreen(거짓);
    this.isFullWindow = 거짓;
    Events.off(문서, '키다운', this.boundFullWindowOnEscKey_);

    // 스크롤 막대를 숨김 해제합니다.
    document.documentElement.style.overflow = this.docOrigOverflow;

    // 전체화면 스타일 제거
    Dom.removeClass(document.body, 'vjs-full-window');

    // 상자, 컨트롤러, 포스터를 원래 크기로 조정
    // this.positionAll();
    /**
     * @event Player#exitFullWindow
     * @type {이벤트대상~이벤트}
     */
    this.trigger('exitFullWindow');
  }

  /**
   * Picture-in-Picture 모드를 비활성화합니다.
   *
   * @param {부울} 값
   * - true는 Picture-in-Picture 모드를 비활성화합니다.
   * - false는 Picture-in-Picture 모드를 활성화합니다.
   */
  disablePictureInPicture(값) {
    if (값 === 정의되지 않음) {
      return this.techGet_('disablePictureInPicture');
    }
    this.techCall_('setDisablePictureInPicture', 값);
    this.options_.disablePictureInPicture = 값;
    this.trigger('disablepictureinpicturechanged');
  }

  /**
   * 플레이어가 Picture-in-Picture 모드인지 확인하거나 플레이어에게 알립니다.
   * Picture-in-Picture 모드에 있거나 그렇지 않습니다.
   *
   * @param {부울} [isPiP]
   * 플레이어의 현재 Picture-in-Picture 상태 설정
   *
   * @return {부울}
   * - Picture-in-Picture가 켜져 있고 가져오는 경우 true
   * - Picture-in-Picture가 꺼져 있으면 false
   */
  isInPictureInPicture(isPiP) {
    if (isPiP !== 정의되지 않음) {
      this.isInPictureInPicture_ = !!isPiP;
      this.togglePictureInPictureClass_();
      반품;
    }
    반환 !!this.isInPictureInPicture_;
  }

  /**
   * 플로팅 비디오 창을 항상 다른 창 위에 생성하여 사용자가
   * 다른 콘텐츠 사이트와 상호 작용하는 동안 미디어를 계속 소비하거나
   * 기기의 애플리케이션.
   *
   * @see [사양]{@link https://wicg.github.io/picture-in-picture}
   *
   * @fires Player#enterpictureinpicture
   *
   * @return {약속}
   * Picture-in-Picture 창과의 약속.
   */
  requestPictureInPicture() {
    if (문서의 'pictureInPictureEnabled' && this.disablePictureInPicture() === false) {
      /**
       * 이 이벤트는 플레이어가 PIP 모드로 들어갈 때 발생합니다.
       *
       * @event 플레이어#enterpictureinpicture
       * @type {이벤트대상~이벤트}
       */
      return this.techGet_('requestPictureInPicture');
    }
  }

  /**
   * Picture-in-Picture 모드를 종료합니다.
   *
   * @see [사양]{@link https://wicg.github.io/picture-in-picture}
   *
   * @fires Player#leavepictureinpicture
   *
   * @return {약속}
   * 약속.
   */
  exitPictureInPicture() {
    if (문서의 'pictureInPictureEnabled') {
      /**
       * 이 이벤트는 플레이어가 Picture in Picture 모드에서 나갈 때 발생합니다.
       *
       * @event Player#leavepictureinpicture
       * @type {이벤트대상~이벤트}
       */
      return document.exitPictureInPicture();
    }
  }

  /**
   * 이 플레이어에 포커스가 있고 키가 눌리거나
   * 이 플레이어의 모든 구성 요소는 처리하지 않는 키 누름을 받습니다.
   * 이렇게 하면 플레이어 전체 핫키(아래에 정의된 대로 또는 선택적으로
   * 외부 함수에 의해).
   *
   * @param {EventTarget~Event} 이벤트
   * 이 함수를 호출한 `keydown` 이벤트.
   *
   * @listens 키다운
   */
  handleKeyDown(이벤트) {
    const {userActions} = this.options_;

    // 핫키가 구성되지 않은 경우 구제합니다.
    if (!userActions || !userActions.hotkeys) {
      반품;
    }

    // 요소를 제외할지 여부를 결정하는 함수
    // 핫키 처리.
    const excludeElement = (엘) => {
      const tagName = el.tagName.toLowerCase();

      // 첫 번째이자 가장 쉬운 테스트는 `contenteditable` 요소에 대한 것입니다.
      if (el.isContentEditable) {
        true를 반환합니다.
      }

      // 이 유형과 일치하는 입력은 여전히 다음과 같이 핫키 처리를 트리거합니다.
      // 텍스트 입력이 아닙니다.
      const allowedInputTypes = [
        '단추',
        '체크박스',
        '숨겨진',
        '라디오',
        '초기화',
        '제출하다'
      ];

      if (태그 이름 === '입력') {
        반환 allowedInputTypes.indexOf(el.type) === -1;
      }

      // 최종 테스트는 태그 이름으로 합니다. 이러한 태그는 완전히 제외됩니다.
      const excludeTags = ['텍스트 영역'];

      제외 태그를 반환합니다.indexOf(태그 이름) !== -1;
    };

    // 사용자가 대화형 양식 요소에 포커스가 있는 경우 구제합니다.
    if (excludeElement(this.el_.ownerDocument.activeElement)) {
      반품;
    }

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

  /**
   * 이 플레이어가 핫키 키다운 이벤트를 수신할 때 호출됩니다.
   * 지원되는 플레이어 전체 핫키는 다음과 같습니다.
   *
   * f - 전체 화면 전환
   * m - 음소거 전환
   * k 또는 Space - 재생/일시정지 전환
   *
   * @param {EventTarget~Event} 이벤트
   * 이 함수를 호출한 `keydown` 이벤트.
   */
  handleHotkeys(이벤트) {
    const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {};

    // `hotkeys`에서 fullscreenKey, muteKey, playPauseKey 설정, 설정되지 않은 경우 기본값 사용
    {
      fullscreenKey = keydownEvent => keycode.isEventKey(keydownEvent, 'f'),
      음소거키 = keydownEvent => keycode.isEventKey(keydownEvent, 'm'),
      playPauseKey = keydownEvent => (keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, '공간'))
    } = 핫키;

    if (fullscreenKey.call(이, 이벤트)) {
      event.preventDefault();
      event.stopPropagation();

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

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

    } 그렇지 않으면 (muteKey.call(this, event)) {
      event.preventDefault();
      event.stopPropagation();

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

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

    } 그렇지 않으면 (playPauseKey.call(this, event)) {
      event.preventDefault();
      event.stopPropagation();

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

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

  /**
   * 플레이어가 주어진 MIME 유형을 재생할 수 있는지 확인
   *
   * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
   *
   * @param {문자열} 유형
   * 확인할 mimetype
   *
   * @return {문자열}
   * 'probably', 'maybe' 또는 '' (빈 문자열)
   */
  canPlayType(유형) {
    할 수 있습니다;

    // 옵션 순서로 각 재생 기술을 반복합니다.
    for (let i = 0, j = this.options_.techOrder; i < j.길이; i++) {
      const techName = j[i];
      let tech = Tech.getTech(techName);

      // 구성 요소로 등록되는 기술자의 이전 동작을 지원합니다.
      // 더 이상 사용되지 않는 동작이 제거되면 제거합니다.
      if (!기술) {
        기술 = Component.getComponent(techName);
      }

      // 계속하기 전에 현재 기술이 정의되어 있는지 확인
      if (!기술) {
        log.error(`"${techName}" 기술이 정의되지 않았습니다. 해당 기술에 대한 브라우저 지원 확인을 건너뛰었습니다.`);
        계속하다;
      }

      // 브라우저가 이 기술을 지원하는지 확인
      if (tech.isSupported()) {
        수 = tech.canPlayType(유형);

        만약 (할 수) {
          반환할 수 있습니다.
        }
      }
    }

    반품 '';
  }

  /**
   * 기술 주문 또는 소스 주문을 기반으로 소스 선택
   * `options.sourceOrder`가 참이면 소스 순서 선택을 사용합니다. 그렇지 않으면,
   * 기본적으로 기술 주문 선택
   *
   * @param {배열} 소스
   * 미디어 자산의 소스
   *
   * @return {객체|부울}
   * 소스 및 기술 주문의 대상 또는 허위
   */
  selectSource(소스) {
    // 존재하고 지원되는 `techOrder`에 지정된 기술만 가져옵니다.
    // 현재 플랫폼
    const 기술자 =
      this.options_.techOrder
        .map((기술 이름) => {
          return [기술명, Tech.getTech(기술명)];
        })
        .filter(([기술 이름, 기술]) => {
          // 계속하기 전에 현재 기술이 정의되어 있는지 확인
          경우 (기술) {
            // 브라우저가 이 기술을 지원하는지 확인
            return tech.isSupported();
          }

          log.error(`"${techName}" 기술이 정의되지 않았습니다. 해당 기술에 대한 브라우저 지원 확인을 건너뛰었습니다.`);
          거짓을 반환합니다.
        });

    // `outerArray` 요소마다 한 번씩 각 `innerArray` 요소를 반복하고 실행합니다.
    // 둘 다 사용하는 `tester`. `tester`가 거짓이 아닌 값을 반환하면 일찍 종료하고 반환
    // 그 값.
    const findFirstPassingTechSourcePair = function(outerArray, innerArray, tester) {
      찾자;

      outerArray.some((outerChoice) => {
        return innerArray.some((innerChoice) => {
          발견 = 테스터(outerChoice, innerChoice);

          경우 (발견) {
            true를 반환합니다.
          }
        });
      });

      반품을 찾았습니다.
    };

    let foundSourceAndTech;
    const 뒤집기 = (fn) => (a, b) => fn(b,a);
    const finder = ([techName, tech], 소스) => {
      if (tech.canPlaySource(소스, this.options_[techName.toLowerCase()])) {
        return {출처, 기술: techName};
      }
    };

    // `options.sourceOrder`의 진실성에 따라 기술자와 출처의 순서를 바꿉니다.
    // 우선 순위에 따라 선택합니다.
    if (this.options_.sourceOrder) {
      // 소스 우선 정렬
      foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder));
    } else {
      // 기술 우선 주문
      foundSourceAndTech = findFirstPassingTechSourcePair(기술자, 소스, 파인더);
    }

    foundSourceAndTech 반환 || 거짓;
  }

  /**
   * 소스 설정 및 로직 가져오기 실행
   *
   * @param {Tech~SourceObject|Tech~SourceObject[]|string} [소스]
   * SourceObject, SourceObject 배열 또는 문자열 참조
   * 미디어 소스에 대한 URL. _적극 권장_합니다.
   * 또는 개체 배열이 여기에서 사용되므로 소스 선택
   * 알고리즘은 `유형`을 고려할 수 있습니다.
   *
   * 제공되지 않으면 이 메서드는 getter 역할을 합니다.
   * @param {부울} isRetry
   * 재시도 결과 내부적으로 호출되는지 여부를 나타냅니다.
   *
   * @return {문자열|정의되지 않음}
   * `source` 인수가 없으면 현재 소스를 반환합니다.
   * URL. 그렇지 않으면 아무것도 반환하지 않거나 정의되지 않습니다.
   */
  handleSrc_(소스, isRetry) {
    // getter 사용법
    if (소스 유형 === '정의되지 않음') {
      this.cache_.src를 반환 || '';
    }

    // 새 소스에 대한 재시도 동작 재설정
    if (this.resetRetryOnError_) {
      this.resetRetryOnError_();
    }

    // 유효하지 않은 소스를 필터링하고 소스를
    // 소스 객체의 배열
    const sources = filterSource(source);

    // 소스가 전달된 경우 유효하지 않습니다.
    // 길이가 0인 배열로 필터링되었습니다. 그래서 우리는
    // 오류 표시
    if (!소스.길이) {
      this.setTimeout(함수() {
        this.error({ 코드: 4, 메시지: this.options_.notSupportedMessage });
      }, 0);
      반품;
    }

    // 초기 소스
    this.changesrc_ = 참;

    // 오류 발생 후 새 소스를 재시도하지 않는 경우에만 캐시된 소스 목록을 업데이트합니다.
    // 이 경우 실패한 소스를 캐시에 포함하고 싶기 때문입니다.
    if (!isRetry) {
      this.cache_.sources = 소스;
    }

    this.updateSourceCaches_(소스[0]);

    // middlewareSource는 미들웨어에 의해 변경된 후의 소스입니다.
    미들웨어.setSource(이, 소스[0], (middlewareSource, mws) => {
      this.middleware_ = mws;

      // sourceSet이 비동기이기 때문에 소스를 선택한 후 캐시를 다시 업데이트해야 합니다.
      // 선택된 소스는 이 콜백 위의 캐시 업데이트에서 순서가 맞지 않을 수 있습니다.
      if (!isRetry) {
        this.cache_.sources = 소스;
      }

      this.updateSourceCaches_(middlewareSource);

      const err = this.src_(middlewareSource);

      경우 (오류) {
        if (소스.길이 > 1) {
          return this.handleSrc_(sources.slice(1));
        }

        this.changesrc_ = 거짓;

        // 사람들에게 오류 이벤트 처리기를 추가할 기회를 주기 위해 이것을 타임아웃으로 래핑해야 합니다.
        this.setTimeout(함수() {
          this.error({ 코드: 4, 메시지: this.options_.notSupportedMessage });
        }, 0);

        // 적절한 기술을 찾을 수 없지만 대리인에게 이 기술을 알립니다.
        // 이것이 필요한 이유에 대한 더 나은 설명이 필요합니다.
        this.triggerReady();

        반품;
      }

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

    // 재생 전에 이 소스가 실패하면 다른 사용 가능한 소스를 시도합니다.
    if (this.options_.retryOnError && 출처.길이 > 1) {
      const 재시도 = () => {
        // 오류 모달 제거
        this.오류(null);
        this.handleSrc_(sources.slice(1), true);
      };

      const stopListeningForErrors = () => {
        this.off('오류', 재시도);
      };

      this.one('오류', 재시도);
      this.one('재생 중', stopListeningForErrors);

      this.resetRetryOnError_ = () => {
        this.off('오류', 재시도);
        this.off('재생 중', stopListeningForErrors);
      };
    }
  }

  /**
   * 비디오 소스를 가져오거나 설정합니다.
   *
   * @param {Tech~SourceObject|Tech~SourceObject[]|string} [소스]
   * SourceObject, SourceObject 배열 또는 문자열 참조
   * 미디어 소스에 대한 URL. _적극 권장_합니다.
   * 또는 개체 배열이 여기에서 사용되므로 소스 선택
   * 알고리즘은 `유형`을 고려할 수 있습니다.
   *
   * 제공되지 않으면 이 메서드는 getter 역할을 합니다.
   *
   * @return {문자열|정의되지 않음}
   * `source` 인수가 없으면 현재 소스를 반환합니다.
   * URL. 그렇지 않으면 아무것도 반환하지 않거나 정의되지 않습니다.
   */
  src(소스) {
    return this.handleSrc_(소스, 거짓);
  }

  /**
   * 기술에 소스 개체를 설정하고 여부를 나타내는 부울을 반환합니다.
   * 음원 재생이 가능한 테크가 있습니다.
   *
   * @param {Tech~SourceObject} 소스
   * Tech에 설정할 소스 객체
   *
   * @return {부울}
   * - 이 소스를 재생할 기술이 없는 경우 참
   * - 그렇지 않으면 거짓
   *
   * @사적인
   */
  src_(소스) {
    const sourceTech = this.selectSource([소스]);

    if (!sourceTech) {
      true를 반환합니다.
    }

    if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
      this.changesrc_ = 참;
      // 선택한 소스로 이 기술을 로드합니다.
      this.loadTech_(sourceTech.tech, sourceTech.source);
      this.tech_.ready(() => {
        this.changesrc_ = 거짓;
      });
      거짓을 반환합니다.
    }

    // 기술자가 소스를 설정할 준비가 될 때까지 기다립니다.
    // 가능한 경우 동기적으로 설정합니다(#2326).
    this.ready(함수() {

      // 소스 핸들러와 함께 setSource 기술 메서드가 추가되었습니다.
      // 따라서 이전 기술은 지원하지 않습니다.
      // 하위 클래스가 있는 경우 직접 프로토타입을 확인해야 합니다.
      // 기술은 소스 핸들러를 지원하지 않습니다.
      if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
        this.techCall_('setSource', 소스);
      } else {
        this.techCall_('src', source.src);
      }

      this.changesrc_ = 거짓;
    }, 진실);

    거짓을 반환합니다.
  }

  /**
   * src 데이터 로드를 시작합니다.
   */
  짐() {
    this.techCall_('로드');
  }

  /**
   * 플레이어를 재설정합니다. techOrder에 첫 번째 기술을 로드합니다.
   * 기존 `tech`의 모든 텍스트 트랙을 제거합니다.
   * 그리고 `tech`에서 `reset`을 호출합니다.
   */
  초기화() {
    const PromiseClass = this.options_.Promise || 창. 약속;

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

      침묵 약속(playPromise.then(() => this.doReset_()));
    }
  }

  doReset_() {
    if (this.tech_) {
      this.tech_.clearTracks('텍스트');
    }
    this.resetCache_();
    this.poster('');
    this.loadTech_(this.options_.techOrder[0], null);
    this.techCall_('초기화');
    this.resetControlBarUI_();
    if (isEvented(이)) {
      this.trigger('playerreset');
    }
  }

  /**
   * 재설정하는 하위 메서드를 호출하여 Control Bar의 UI 재설정
   * Control Bar의 모든 구성 요소
   */
  resetControlBarUI_() {
    this.resetProgressBar_();
    this.resetPlaybackRate_();
    this.resetVolumeBar_();
  }

  /**
   * UI에서 진행률 표시줄이 재설정되도록 기술의 진행률을 재설정합니다.
   */
  resetProgressBar_() {
    this.currentTime(0);

    const { 기간 표시, 남은 시간 표시 } = this.controlBar || {};

    if (기간 표시) {
      durationDisplay.updateContent();
    }

    if (remainingTimeDisplay) {
      남은시간디스플레이.업데이트컨텐트();
    }
  }

  /**
   * 재생 비율 초기화
   */
  resetPlaybackRate_() {
    this.playbackRate(this.defaultPlaybackRate());
    this.handleTechRateChange_();
  }

  /**
   * 볼륨 바 재설정
   */
  resetVolumeBar_() {
    this.볼륨(1.0);
    this.trigger('볼륨 변경');
  }

  /**
   * 현재 소스 개체를 모두 반환합니다.
   *
   * @return {Tech~SourceObject[]}
   * 현재 소스 개체
   */
  전류 소스() {
    const 소스 = this.currentSource();
    const 소스 = [];

    // `{}` 또는 `{ src }`로 가정
    if (Object.keys(source).length !== 0) {
      sources.push(소스);
    }

    this.cache_.sources를 반환 || 출처;
  }

  /**
   * 현재 소스 개체를 반환합니다.
   *
   * @return {기술~소스 객체}
   * 현재 소스 객체
   */
  전류 소스() {
    this.cache_.source를 반환 || {};
  }

  /**
   * 현재 소스 값의 정규화된 URL(예: http://mysite.com/video.mp4)을 반환합니다.
   * 현재 소스 개체를 다시 빌드하는 데 도움이 되도록 `currentType`과 함께 사용할 수 있습니다.
   *
   * @return {문자열}
   * 현재 소스
   */
  currentSrc() {
    this.currentSource()를 반환합니다. && this.currentSource().src || '';
  }

  /**
   * 현재 소스 유형 가져오기(예: video/mp4)
   * 이렇게 하면 현재 소스 개체를 다시 빌드하여 동일한 소스 개체를 로드할 수 있습니다.
   * 나중에 소스 및 기술
   *
   * @return {문자열}
   * 소스 MIME 유형
   */
  전류 유형() {
    this.currentSource()를 반환합니다. && this.currentSource().type || '';
  }

  /**
   * 사전 로드 속성을 가져오거나 설정합니다.
   *
   * @param {부울} [값]
   * - true는 미리 로드해야 함을 의미합니다.
   * - false는 미리 로드하지 않아야 함을 의미합니다.
   *
   * @return {문자열}
   * 가져올 때 preload 속성 값
   */
  예압(값) {
    if (값 !== 정의되지 않음) {
      this.techCall_('setPreload', 값);
      this.options_.preload = 값;
      반품;
    }
    return this.techGet_('프리로드');
  }

  /**
   * 자동 재생 옵션을 가져오거나 설정합니다. 이것이 부울이면
   * 기술의 속성을 수정합니다. 문자열일 때 속성
   * 기술이 제거되고 `Player`가 로드 시작 시 자동 재생을 처리합니다.
   *
   * @param {부울|문자열} [값]
   * - true: 브라우저 동작을 사용하여 자동 재생
   * - false: 자동 재생하지 않음
   * - '재생': 로드 시작 시마다 play() 호출
   * - 'muted': loadstart마다 muted()를 호출한 다음 play()를 호출합니다.
   * - 'any': 로드가 시작될 때마다 play()를 호출합니다. 그것이 실패하면 muted()를 호출한 다음 play()를 호출합니다.
   * - *: 여기에 나열된 값 이외의 값은 `autoplay`를 true로 설정합니다.
   *
   * @return {부울|문자열}
   * 얻을 때 자동 재생의 현재 값
   */
  자동재생(값) {
    // getter 사용법
    if (값 === 정의되지 않음) {
      this.options_.autoplay를 반환 || 거짓;
    }

    let techAutoplay;

    // 값이 유효한 문자열인 경우 이를 설정하거나 필요에 따라 'true'를 'play'로 정규화합니다.
    if (값 유형 === '문자열' && (/(any|play|muted)/).test(value) || 값 === 참 && this.options_.normalizeAutoplay) {
      this.options_.autoplay = 값;
      this.manualAutoplay_(typeof value === 'string' ? value : 'play');
      techAutoplay = 거짓;

    // 잘못된 값은 브라우저에서 autoplay를 false로 설정합니다.
    // 똑같이 해보자
    } 그렇지 않으면 (!값) {
      this.options_.autoplay = 거짓;

    // 다른 값(예: truthy)은 자동 재생을 true로 설정합니다.
    } else {
      this.options_.autoplay = 참;
    }

    techAutoplay = typeof techAutoplay === '정의되지 않음' ? this.options_.autoplay : techAutoplay;

    // 기술이 없으면 대기하지 않습니다.
    // 기술 준비에 대한 setAutoplay 호출. 우리는 이것을 하기 때문에
    // 자동 재생 옵션이 생성자에 전달되고 우리는
    // 두 번 설정할 필요가 없습니다.
    if (this.tech_) {
      this.techCall_('setAutoplay', techAutoplay);
    }
  }

  /**
   * playsinline 속성을 설정하거나 해제합니다.
   * Playsinline은 전체 화면이 아닌 재생이 선호됨을 브라우저에 알립니다.
   *
   * @param {부울} [값]
   * - true는 기본적으로 인라인 재생을 시도해야 함을 의미합니다.
   * - false는 브라우저의 기본 재생 모드를 사용해야 함을 의미합니다.
   * 대부분의 경우 인라인입니다. iOS Safari는 주목할만한 예외입니다.
   * 기본적으로 전체 화면으로 재생됩니다.
   *
   * @return {문자열|플레이어}
   * - playinline의 현재 값
   * - 설정시 플레이어
   *
   * @see [사양]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
   */
  재생인라인(값) {
    if (값 !== 정의되지 않음) {
      this.techCall_('setPlaysinline', value);
      this.options_.playsinline = 값;
      이것을 반환하십시오.
    }
    return this.techGet_('playsinline');
  }

  /**
   * 동영상 요소의 루프 속성을 가져오거나 설정합니다.
   *
   * @param {부울} [값]
   * - true는 비디오를 반복해야 함을 의미합니다.
   * - false는 동영상을 반복하지 않아야 함을 의미합니다.
   *
   * @return {부울}
   * 가져올 때 루프의 현재 값
   */
  루프(값) {
    if (값 !== 정의되지 않음) {
      this.techCall_('setLoop', 값);
      this.options_.loop = 값;
      반품;
    }
    return this.techGet_('루프');
  }

  /**
   * 포스터 이미지 소스 URL 가져오기 또는 설정
   *
   * @fires Player#posterchange
   *
   * @param {문자열} [소스]
   * 포스터 이미지 출처 URL
   *
   * @return {문자열}
   * 획득 시 포스터의 현재 가치
   */
  포스터(src) {
    if (src === 정의되지 않음) {
      this.poster_를 반환합니다.
    }

    // 포스터를 제거하는 올바른 방법은 빈 문자열로 설정하는 것입니다.
    // 다른 falsey 값은 오류를 발생시킵니다.
    if (!src) {
      소스 = '';
    }

    if (src === this.poster_) {
      반품;
    }

    // 내부 포스터 변수 업데이트
    this.poster_ = src;

    // 기술자의 포스터를 업데이트합니다.
    this.techCall_('setPoster', src);

    this.isPosterFromTech_ = 거짓;

    // 포스터가 설정되었음을 알리는 구성 요소
    /**
     * 이 이벤트는 플레이어에서 포스터 이미지가 변경될 때 발생합니다.
     *
     * @event Player#posterchange
     * @type {이벤트대상~이벤트}
     */
    this.trigger('posterchange');
  }

  /**
   * 일부 기술자(예: YouTube)는 포스터 소스를 제공할 수 있습니다.
   * 비동기 방식. 우리는 포스터 구성 요소가 이것을 사용하기를 원합니다.
   * 기술의 제어를 가릴 수 있도록 포스터 소스.
   * (YouTube의 재생 버튼). 그러나 우리는 이것을 사용하고 싶습니다
   * 플레이어 사용자가 포스터를 설정하지 않은 경우 출처
   * 일반 API.
   *
   * @fires Player#posterchange
   * @listens Tech#posterchange
   * @사적인
   */
  handleTechPosterChange_() {
    if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
      const newPoster = this.tech_.poster() || '';

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

        // 구성 요소에 포스터가 변경되었음을 알립니다.
        this.trigger('posterchange');
      }
    }
  }

  /**
   * 컨트롤이 표시되는지 여부를 가져오거나 설정합니다.
   *
   * @fires Player#controlsenabled
   *
   * @param {부울} [부울]
   * - 컨트롤을 켜려면 true
   * - 컨트롤을 끄려면 false
   *
   * @return {부울}
   * 가져올 때 컨트롤의 현재 값
   */
  컨트롤(부울) {
    if (부울 === 정의되지 않음) {
      반환 !!this.controls_;
    }

    부울 = !!부울;

    // 실제로 변경되지 않는 한 변경 이벤트를 트리거하지 않습니다.
    if (this.controls_ === 부울) {
      반품;
    }

    this.controls_ = 부울;

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

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

  /**
   * 네이티브 컨트롤을 켜거나 끕니다. 네이티브 컨트롤은 내장된 컨트롤입니다.
   * 장치(예: 기본 iPhone 컨트롤) 또는 기타 기술
   * (예: Vimeo 컨트롤)
   * **기술자만 알고 있기 때문에 현재 기술자만 설정해야 합니다.
   * 네이티브 컨트롤을 지원할 수 있는 경우**
   *
   * @fires Player#usingnativecontrols
   * @fires Player#usingcustomcontrols
   *
   * @param {부울} [부울]
   * - 네이티브 컨트롤을 켜려면 true
   * - 네이티브 컨트롤을 끄려면 false
   *
   * @return {부울}
   * 얻을 때 네이티브 컨트롤의 현재 값
   */
  usingNativeControls(부울) {
    if (부울 === 정의되지 않음) {
      반환 !!this.usingNativeControls_;
    }

    부울 = !!부울;

    // 실제로 변경되지 않는 한 변경 이벤트를 트리거하지 않습니다.
    if (this.usingNativeControls_ === 부울) {
      반품;
    }

    this.usingNativeControls_ = 부울;

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

      /**
       * 플레이어가 기본 장치 컨트롤을 사용하고 있습니다.
       *
       * @event Player#usingnativecontrols
       * @type {이벤트대상~이벤트}
       */
      this.trigger('usingnativecontrols');
    } else {
      this.removeClass('vjs-using-native-controls');

      /**
       * 플레이어가 맞춤 HTML 컨트롤을 사용하고 있습니다.
       *
       * @event Player#usingcustomcontrols
       * @type {이벤트대상~이벤트}
       */
      this.trigger('usingcustomcontrols');
    }
  }

  /**
   * 현재 MediaError 설정 또는 가져오기
   *
   * @fires 플레이어#오류
   *
   * @param {MediaError|문자열|숫자} [오류]
   * MediaError 또는 전환할 문자열/숫자
   * MediaError로
   *
   * @return {MediaError|null}
   * 가져올 때의 현재 MediaError(또는 null)
   */
  오류(오류) {
    if (오류 === 정의되지 않음) {
      반환 this.error_ || 없는;
    }

    // 후크가 오류 객체를 수정하도록 허용
    hooks('beforeerror').forEach((hookFunction) => {
      const newErr = hookFunction(this, err);

      만약에 (!(
        (isObject(newErr) && !Array.isArray(newErr)) ||
        typeof newErr === '문자열' ||
        typeof newErr === '숫자' ||
        newErr === null
      )) {
        this.log.error('MediaError가 beforeerror 후크에서 예상하는 값을 반환하세요.');
        반품;
      }

      오류 = newErr;
    });

    // 호환 가능한 소스가 없는 경우 첫 번째 오류 메시지를 억제합니다.
    // 사용자 상호 작용
    if (this.options_.suppressNotSupportedError &&
        잘못 && 오류 코드 === 4
    ) {
      const triggerSuppressedError = 함수() {
        this.오류(err);
      };

      this.options_.suppressNotSupportedError = 거짓;
      this.any(['클릭', '터치스타트'], triggerSuppressedError);
      this.one('loadstart', function() {
        this.off(['클릭', '터치스타트'], triggerSuppressedError);
      });
      반품;
    }

    // 기본값으로 복원
    경우 (오류 === null) {
      this.error_ = 오류;
      this.removeClass('vjs-오류');
      if (이.오류 표시) {
        this.errorDisplay.close();
      }
      반품;
    }

    this.error_ = new MediaError(err);

    // 플레이어에 vjs-error 클래스 이름을 추가합니다.
    this.addClass('vjs-오류');

    // 오류 유형의 이름과 메시지를 기록합니다.
    // IE11은 "[개체 개체]"를 기록하고 오류 개체를 보려면 메시지를 확장해야 합니다.
    log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);

    /**
     * @event 플레이어#오류
     * @type {이벤트대상~이벤트}
     */
    this.trigger('오류');

    // 플레이어별 오류를 후크에 알립니다.
    hooks('오류').forEach((hookFunction) => hookFunction(this, this.error_));

    반품;
  }

  /**
   * 사용자 활동 보고
   *
   * @param {객체} 이벤트
   * 이벤트 객체
   */
  reportUserActivity(이벤트) {
    this.userActivity_ = 참;
  }

  /**
   * 사용자가 활성 상태인지 가져오기/설정
   *
   * @fires 플레이어#useractive
   * @fires 플레이어#userinactive
   *
   * @param {부울} [부울]
   * - 사용자가 활성화된 경우 true
   * - 사용자가 비활성 상태이면 false
   *
   * @return {부울}
   * 가져올 때 userActive의 현재 값
   */
  userActive(부울) {
    if (부울 === 정의되지 않음) {
      this.userActive_를 반환합니다.
    }

    부울 = !!부울;

    if (부울 === this.userActive_) {
      반품;
    }

    this.userActive_ = 부울;

    if (이.userActive_) {
      this.userActivity_ = 참;
      this.removeClass('vjs-user-inactive');
      this.addClass('vjs-user-active');
      /**
       * @event 플레이어#useractive
       * @type {이벤트대상~이벤트}
       */
      this.trigger('useractive');
      반품;
    }

    // Chrome/Safari/IE에는 커서를 변경할 때
    // mousemove 이벤트를 트리거합니다. 숨길 때 문제가 발생합니다.
    // 사용자가 비활성 상태이고 mousemove가 사용자에게 신호를 보낼 때 커서
    // 활동. 비활성 모드로 전환하는 것을 불가능하게 만듭니다. 구체적으로
    // 이것은 커서를 정말로 숨길 필요가 있을 때 전체 화면에서 발생합니다.
    //
    // 모든 브라우저에서 해결되면 제거할 수 있습니다.
    // https://code.google.com/p/chromium/issues/detail?id=103041
    if (this.tech_) {
      this.tech_.one('마우스무브', function(e) {
        e.stopPropagation();
        e.preventDefault();
      });
    }

    this.userActivity_ = 거짓;
    this.removeClass('vjs-user-active');
    this.addClass('vjs-user-inactive');
    /**
     * @event 플레이어#userinactive
     * @type {이벤트대상~이벤트}
     */
    this.trigger('userinactive');
  }

  /**
   * 시간 초과 값을 기준으로 사용자 활동 수신
   *
   * @사적인
   */
  listenForUserActivity_() {
    let mouseInProgress;
    lastMoveX하자;
    lastMoveY를 보자;
    const handleActivity = Fn.bind(this, this.reportUserActivity);

    const handleMouseMove = 함수(e) {
      // #1068 - mousemove 스팸 방지
      // 크롬 버그: https://code.google.com/p/chromium/issues/detail?id=366970
      if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
        lastMoveX = e.screenX;
        lastMoveY = e.screenY;
        handleActivity();
      }
    };

    const handleMouseDown = 함수() {
      handleActivity();
      // 그들이 장치를 만지거나 마우스를 누르고 있는 동안,
      // 손가락이나 마우스를 움직이지 않아도 활성 상태로 간주합니다.
      // 그래서 우리는 그들이 활성 상태임을 계속 업데이트하고 싶습니다.
      this.clearInterval(mouseInProgress);
      // 지금 userActivity=true로 설정하고 간격을 같은 시간으로 설정
      // activityCheck 간격(250)은 우리가
      // 다음 활동 확인
      mouseInProgress = this.setInterval(handleActivity, 250);
    };

    const handleMouseUpAndMouseLeave = 함수(이벤트) {
      handleActivity();
      // 마우스/터치가 눌린 경우 활동을 유지하는 간격을 중지합니다.
      this.clearInterval(mouseInProgress);
    };

    // 모든 마우스 움직임은 사용자 활동으로 간주됩니다.
    this.on('mousedown', handleMouseDown);
    this.on('mousemove', handleMouseMove);
    this.on('mouseup', handleMouseUpAndMouseLeave);
    this.on('mouseleave', handleMouseUpAndMouseLeave);

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

    // Android의 버그 수정 & iOS 어디에서 progressBar를 탭할 때(컨트롤 바가 표시될 때)
    // controlBar는 기본 시간 초과로 더 이상 숨겨지지 않습니다.
    if (컨트롤바 && !브라우저.IS_IOS && !브라우저.IS_ANDROID) {

      controlBar.on('마우스엔터', 함수(이벤트) {
        if (this.player().options_.inactivityTimeout !== 0) {
          this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
        }
        this.player().options_.inactivityTimeout = 0;
      });

      controlBar.on('마우스리브', function(event) {
        this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout;
      });

    }

    // 키보드 탐색 듣기
    // 키 반복 때문에 inProgress 간격을 사용할 필요가 없습니다.
    this.on('keydown', handleActivity);
    this.on('keyup', handleActivity);

    // 모든 것을 채우는 대신 250밀리초마다 간격을 실행합니다.
    // 성능 저하를 방지하기 위해 mousemove/touchmove 함수 자체.
    // `this.reportUserActivity`는 단순히 this.userActivity_를 true로 설정합니다.
    // 그런 다음 이 루프에 의해 선택됩니다.
    // http://ejohn.org/blog/learning-from-twitter/
    inactivityTimeout하자;

    this.setInterval(함수() {
      // 마우스/터치 활동이 발생했는지 확인
      if (!this.userActivity_) {
        반품;
      }

      // 액티비티 트래커 재설정
      this.userActivity_ = 거짓;

      // 사용자 상태가 비활성인 경우 상태를 활성으로 설정합니다.
      this.userActive(참);

      // 타이머를 다시 시작하기 위해 기존 비활성 제한 시간을 지웁니다.
      this.clearTimeout(inactivityTimeout);

      const timeout = this.options_.inactivityTimeout;

      만약 (타임아웃 < = 0) {
        반품;
      }

      // 안에 < 타임아웃> 더 이상 활동이 발생하지 않으면 밀리초
      // 사용자는 비활성 상태로 간주됩니다.
      inactivityTimeout = this.setTimeout(function() {
        // inactivityTimeout이 트리거할 수 있는 경우로부터 보호
        // 다음 사용자 활동이 활동 확인 루프에 의해 선택되기 전에
        // 깜박임 발생
        if (!this.userActivity_) {
          this.userActive(거짓);
        }
      }, 타임아웃);

    }, 250);
  }

  /**
   * 현재 재생 속도를 가져오거나 설정합니다. 재생 속도
   * 1.0은 정상 속도를 나타내고 0.5는 절반 속도를 나타냅니다.
   * 예를 들어 재생.
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
   *
   * @param {숫자} [비율]
   * 새로운 재생 속도를 설정할 수 있습니다.
   *
   * @return {숫자}
   * 현재 재생 속도를 얻거나 1.0일 때
   */
  재생 속도(속도) {
    if (속도 !== 정의되지 않음) {
      // 참고: this.cache_.lastPlaybackRate는 기술 처리기에서 설정됩니다.
      // 위에 등록된
      this.techCall_('setPlaybackRate', rate);
      반품;
    }

    만약 (이것.기술_ && this.tech_.featuresPlaybackRate) {
      this.cache_.lastPlaybackRate를 반환 || this.techGet_('재생 속도');
    }
    1.0을 반환합니다.
  }

  /**
   * 현재 기본 재생 속도를 가져오거나 설정합니다. 기본 재생 속도
   * 예를 들어 1.0은 정상 속도를 나타내고 0.5는 절반 속도 재생을 나타냅니다.
   * defaultPlaybackRate는 비디오의 초기 playbackRate가 아닌 것만 나타냅니다.
   * 현재 playbackRate가 아닙니다.
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
   *
   * @param {숫자} [비율]
   * 새로운 기본 재생 속도를 설정할 수 있습니다.
   *
   * @return {번호|플레이어}
   * - 받을 때 기본 재생 속도 또는 1.0
   * - 설정시 플레이어
   */
  defaultPlaybackRate(속도) {
    if (속도 !== 정의되지 않음) {
      return this.techCall_('setDefaultPlaybackRate', rate);
    }

    만약 (이것.기술_ && this.tech_.featuresPlaybackRate) {
      return this.techGet_('defaultPlaybackRate');
    }
    1.0을 반환합니다.
  }

  /**
   * 오디오 플래그를 가져오거나 설정합니다.
   *
   * @param {부울} 부울
   * - true는 이것이 오디오 플레이어임을 나타냅니다.
   * - 오디오 플레이어가 아니라는 잘못된 신호
   *
   * @return {부울}
   * 가져올 때 isAudio의 현재 값
   */
  isAudio(부울) {
    if (부울 !== 정의되지 않음) {
      this.isAudio_ = !!bool;
      반품;
    }

    반환 !!this.isAudio_;
  }

  enableAudioOnlyUI_() {
    // 높이를 얻을 수 있도록 컨트롤 막대를 표시하도록 스타일을 즉시 업데이트합니다.
    this.addClass('vjs-audio-only-mode');

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

    // 컨트롤 바를 제외한 모든 플레이어 구성 요소를 숨깁니다. 컨트롤 막대 구성요소
    // 비디오에만 필요한 CSS로 숨겨짐
    playerChildren.forEach(자식 => {
      if (자식 === controlBar) {
        반품;
      }

      만약 (자식.el_ && !child.hasClass('vjs-hidden')) {
        child.hide();

        this.audioOnlyCache_.hiddenChildren.push(자식);
      }
    });

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

    // 플레이어 높이를 컨트롤 바와 동일하게 설정
    this.height(controlBarHeight);
    this.trigger('audioonlymodechange');
  }

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

    // 이전에 숨겨진 플레이어 구성요소 표시
    this.audioOnlyCache_.hiddenChildren.forEach(자식 => child.show());

    // 플레이어 높이 재설정
    this.height(this.audioOnlyCache_.playerHeight);
    this.trigger('audioonlymodechange');
  }

  /**
   * 현재 audioOnlyMode 상태를 가져오거나 audioOnlyMode를 true 또는 false로 설정합니다.
   *
   * 이것을 'true'로 설정하면 컨트롤 바를 제외한 모든 플레이어 구성 요소가 숨겨집니다.
   * 비디오에만 필요한 컨트롤 막대 구성 요소도 있습니다.
   *
   * @param {부울} [값]
   * audioOnlyMode를 설정할 값.
   *
   * @return {약속|부울}
   * 상태를 설정할 때 Promise가 반환되고 가져올 때 부울이 반환됩니다.
   * 현재 상태
   */
  audioOnlyMode(값) {
    if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
      this.audioOnlyMode_를 반환합니다.
    }

    this.audioOnlyMode_ = 값;

    const PromiseClass = this.options_.Promise || 창. 약속;

    경우 (PromiseClass) {
      // 오디오 전용 모드 활성화
      경우 (값) {
        const exitPromises = [];

        // 전체 화면 및 PiP는 audioOnlyMode에서 지원되지 않으므로 필요하면 종료합니다.
        if (this.isInPictureInPicture()) {
          exitPromises.push(this.exitPictureInPicture());
        }

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

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

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

      // 오디오 전용 모드 비활성화
      반환 PromiseClass.resolve().then(() => this.disableAudioOnlyUI_());
    }

    경우 (값) {
      if (this.isInPictureInPicture()) {
        this.exitPictureInPicture();
      }

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

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

  enablePosterModeUI_() {
    // video 요소를 숨기고 포스터 이미지를 표시하여 posterModeUI를 활성화합니다.
    const 기술 = this.tech_ && this.tech_;

    tech.hide();
    this.addClass('vjs-audio-poster-mode');
    this.trigger('오디오 포스터 모드 변경');
  }

  disablePosterModeUI_() {
    // 비디오 요소를 표시하고 포스터 이미지를 숨겨 posterModeUI를 비활성화합니다.
    const 기술 = this.tech_ && this.tech_;

    tech.show();
    this.removeClass('vjs-audio-poster-mode');
    this.trigger('오디오 포스터 모드 변경');
  }

  /**
   * 현재 audioPosterMode 상태를 가져오거나 audioPosterMode를 true 또는 false로 설정
   *
   * @param {부울} [값]
   * audioPosterMode를 설정할 값.
   *
   * @return {약속|부울}
   * 상태를 설정할 때 Promise를 반환하고 가져오면 부울을 반환합니다.
   * 현재 상태
   */
  audioPosterMode(값) {

    if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
      this.audioPosterMode_를 반환합니다.
    }

    this.audioPosterMode_ = 값;

    const PromiseClass = this.options_.Promise || 창. 약속;

    경우 (PromiseClass) {

      경우 (값) {

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

          return audioOnlyModePromise.then(() => {
            // 오디오 전용 모드가 비활성화된 후 오디오 포스터 모드를 활성화합니다.
            this.enablePosterModeUI_();
          });
        }

        반환 PromiseClass.resolve().then(() => {
          // 오디오 포스터 모드 활성화
          this.enablePosterModeUI_();
        });
      }

      반환 PromiseClass.resolve().then(() => {
        // 오디오 포스터 모드 비활성화
        this.disablePosterModeUI_();
      });
    }

    경우 (값) {

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

      this.enablePosterModeUI_();
      반품;
    }

    this.disablePosterModeUI_();
  }

  /**
   * {@link TextTrack}을 우리의
   * {@link TextTrackList}.
   *
   * W3C 설정 외에도 옵션을 통해 추가 정보를 추가할 수 있습니다.
   *
   * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
   *
   * @param {문자열} [종류]
   * 추가하려는 TextTrack의 종류
   *
   * @param {문자열} [레이블]
   * TextTrack 레이블을 제공하는 레이블
   *
   * @param {문자열} [언어]
   * TextTrack에 설정할 언어
   *
   * @return {TextTrack|정의되지 않음}
   * 추가되었거나 정의되지 않은 TextTrack
   * 기술이 없을 경우
   */
  addTextTrack(종류, 라벨, 언어) {
    if (this.tech_) {
      return this.tech_.addTextTrack(종류, 라벨, 언어);
    }
  }

  /**
   * 원격 {@link TextTrack} 및 {@link HTMLTrackElement}를 생성합니다.
   * manualCleanup이 false로 설정되면 트랙이 자동으로 제거됩니다.
   * 소스 변경 시.
   *
   * @param {객체} 옵션
   * 생성 중에 {@link HTMLTrackElement}로 전달하는 옵션. 보다
   * 사용해야 하는 개체 속성에 대한 {@link HTMLTrackElement}.
   *
   * @param {boolean} [manualCleanup=true] false로 설정하면 TextTrack은
   * 소스 변경 시 제거됨
   *
   * @return {HtmlTrackElement}
   * 생성 및 추가된 HTMLTrackElement
   * HtmlTrackElementList 및 원격
   * TextTrackList
   *
   * @deprecated "manualCleanup" 매개변수의 기본값은 기본값이 됩니다.
   * 향후 버전의 Video.js에서 "false"로 변경
   */
  addRemoteTextTrack(옵션, manualCleanup) {
    if (this.tech_) {
      return this.tech_.addRemoteTextTrack(옵션, manualCleanup);
    }
  }

  /**
   * 각각의 원격 {@link TextTrack} 제거
   * {@link TextTrackList} 및 {@link HtmlTrackElementList}.
   *
   * @param {객체} 트랙
   * 제거할 원격 {@link TextTrack}
   *
   * @return {정의되지 않음}
   * 아무것도 반환하지 않습니다
   */
  removeRemoteTextTrack(obj = {}) {
    let {트랙} = obj;

    if (!트랙) {
      트랙 = obj;
    }

    // 트랙 인수를 사용하여 입력을 객체로 분해합니다. 기본값은 arguments[0]입니다.
    // 아무 것도 전달되지 않은 경우 전체 인수를 빈 객체로 기본 설정

    if (this.tech_) {
      return this.tech_.removeRemoteTextTrack(트랙);
    }
  }

  /**
   * W3C의 미디어에서 지정한 대로 사용 가능한 미디어 재생 품질 메트릭을 가져옵니다.
   * 재생 품질 API.
   *
   * @see [사양]{@link https://wicg.github.io/media-playback-quality}
   *
   * @return {객체|정의되지 않음}
   * 지원되는 미디어 재생 품질 메트릭이 있는 개체 또는 정의되지 않은 경우
   *는 기술이 없거나 기술이 지원하지 않습니다.
   */
  getVideoPlaybackQuality() {
    return this.techGet_('getVideoPlaybackQuality');
  }

  /**
   * 비디오 너비 얻기
   *
   * @return {숫자}
   * 현재 비디오 너비
   */
  비디오 너비() {
    반환합니다.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
  }

  /**
   * 비디오 높이 얻기
   *
   * @return {숫자}
   * 현재 비디오 높이
   */
  비디오 높이() {
    반환합니다.tech_ && this.tech_.video높이 && this.tech_.videoHeight() || 0;
  }

  /**
   * 플레이어의 언어 코드.
   *
   * 언어를 변경하면 트리거됩니다.
   * [언어변경]{@link Player#event:languagechange}
   * 제어 텍스트를 업데이트하는 데 사용할 수 있는 구성 요소.
   * ClickableComponent는 기본적으로 제어 텍스트를 업데이트합니다.
   * [언어변경]{@link Player#event:언어변경}.
   *
   * @fires Player#languagechange
   *
   * @param {문자열} [코드]
   * 플레이어를 설정할 언어 코드
   *
   * @return {문자열}
   * 받을 때 현재 언어 코드
   */
  언어(코드) {
    if (코드 === 정의되지 않음) {
      this.language_를 반환합니다.
    }

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

      // 처음 초기화하는 동안 이벤트가 발생하지 않을 가능성이 있습니다.
      if (isEvented(이)) {
        /**
        * 플레이어 언어가 변경되면 발생
        *
        * @event 플레이어#언어변경
        * @type {이벤트대상~이벤트}
        */
        this.trigger('언어 변경');
      }
    }
  }

  /**
   * 플레이어의 언어 사전 가져오기
   * 새로 추가된 플러그인이 언제든지 videojs.addLanguage()를 호출할 수 있으므로 매번 병합합니다.
   * 플레이어 옵션에서 직접 지정한 언어가 우선합니다.
   *
   * @return {배열}
   * 지원되는 언어 배열
   */
  언어() {
    return mergeOptions(Player.prototype.options_.languages, this.languages_);
  }

  /**
   * 현재 트랙을 나타내는 JavaScript 객체를 반환합니다.
   * 정보. **JSON으로 반환하지 않습니다**
   *
   * @return {객체}
   * 트랙 정보의 현재를 나타내는 객체
   */
  toJSON() {
    const 옵션 = mergeOptions(this.options_);
    const 트랙 = options.tracks;

    options.tracks = [];

    에 대한 (하자 i = 0; i < 트랙.길이; i++) {
      let track = 트랙[i];

      // 트랙을 완전히 병합하고 플레이어를 무효화하므로 순환 참조가 없습니다.
      트랙 = mergeOptions(트랙);
      track.player = 정의되지 않음;
      options.tracks[i] = 트랙;
    }

    반환 옵션;
  }

  /**
   * 간단한 모달 대화 상자({@link ModalDialog}
   * 구성 요소) 플레이어를 임의로 즉시 오버레이합니다.
   * 콘텐츠가 닫히면 자체적으로 제거됩니다.
   *
   * @param {문자열|함수|요소|배열|null} 내용
   * 같은 이름의 {@link ModalDialog#content}의 매개변수와 동일합니다.
   * 가장 직관적인 사용법은 문자열이나 DOM을 제공하는 것입니다.
   * 요소.
   *
   * @param {객체} [옵션]
   * {@link ModalDialog}에 전달될 추가 옵션.
   *
   * @return {모달대화상자}
   * 생성된 {@link ModalDialog}
   */
  createModal(콘텐츠, 옵션) {
    옵션 = 옵션 || {};
    options.content = 내용 || '';

    const modal = new ModalDialog(this, options);

    this.addChild(모달);
    modal.on('처리', () => {
      this.removeChild(모달);
    });

    modal.open();
    반환 모달;
  }

  /**
   * 플레이어 크기를 조정할 때 중단점 클래스를 변경합니다.
   *
   * @사적인
   */
  updateCurrentBreakpoint_() {
    if (!this.responsive()) {
      반품;
    }

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

    에 대한 (하자 i = 0; i < BREAKPOINT_ORDER.length; i++) {
      const candidateBreakpoint = BREAKPOINT_ORDER[i];
      const maxWidth = this.breakpoints_[candidateBreakpoint];

      if (전류폭 < = 최대 너비) {

        // 현재 중단점은 변경되지 않았으며 할 일이 없습니다.
        if (현재 중단점 === 후보 중단점) {
          반품;
        }

        // 현재 중단점이 있는 경우에만 클래스를 제거합니다.
        if (currentBreakpoint) {
          this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
        }

        this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
        this.breakpoint_ = 후보 중단점;
        부서지다;
      }
    }
  }

  /**
   * 현재 중단점을 제거합니다.
   *
   * @사적인
   */
  removeCurrentBreakpoint_() {
    const className = this.currentBreakpointClass();

    this.breakpoint_ = '';

    if (클래스 이름) {
      this.removeClass(className);
    }
  }

  /**
   * 플레이어에서 중단점을 가져오거나 설정합니다.
   *
   * 개체 또는 `true`와 함께 이 메서드를 호출하면 이전의 모든 항목이 제거됩니다.
   * 사용자 지정 중단점 및 기본값에서 다시 시작합니다.
   *
   * @param {Object|boolean} [중단점]
   * 객체가 주어지면 사용자 정의를 제공하는 데 사용할 수 있습니다.
   * 중단점. `true`가 주어지면 기본 중단점을 설정합니다.
   * 이 인수가 제공되지 않으면 단순히 현재 값을 반환합니다.
   * 중단점.
   *
   * @param {숫자} [중단점.작은]
   * "vjs-layout-tiny" 클래스의 최대 너비.
   *
   * @param {숫자} [중단점.xsmall]
   * "vjs-layout-x-small" 클래스의 최대 너비.
   *
   * @param {숫자} [중단점.작음]
   * "vjs-layout-small" 클래스의 최대 너비.
   *
   * @param {번호} [중단점.중간]
   * "vjs-layout-medium" 클래스의 최대 너비.
   *
   * @param {숫자} [중단점.대형]
   * "vjs-layout-large" 클래스의 최대 너비.
   *
   * @param {숫자} [중단점.xlarge]
   * "vjs-layout-x-large" 클래스의 최대 너비.
   *
   * @param {숫자} [중단점.거대한]
   * "vjs-layout-huge" 클래스의 최대 너비.
   *
   * @return {객체}
   * 개체 매핑 중단점 이름을 최대 너비 값으로 지정합니다.
   */
  중단점(중단점) {

    // 게터로 사용됩니다.
    if (중단점 === 정의되지 않음) {
      return assign(this.breakpoints_);
    }

    this.breakpoint_ = '';
    this.breakpoints_ = assign({}, DEFAULT_BREAKPOINTS, 중단점);

    // 중단점 정의가 변경되면 현재를 업데이트해야 합니다.
    // 선택된 중단점.
    this.updateCurrentBreakpoint_();

    // 반환하기 전에 중단점을 복제합니다.
    return assign(this.breakpoints_);
  }

  /**
   * 이 플레이어가 조정해야 하는지 여부를 나타내는 플래그를 가져오거나 설정합니다.
   * 크기에 따른 UI.
   *
   * @param {부울} 값
   * 플레이어가 자신의 UI를 기반으로 UI를 조정해야 하는 경우 'true'여야 합니다.
   * 치수; 그렇지 않으면 `거짓`이어야 합니다.
   *
   * @return {부울}
   * 이 플레이어가 자신의 UI를 기반으로 UI를 조정해야 하는 경우 'true'가 됩니다.
   * 치수; 그렇지 않으면 '거짓'이 됩니다.
   */
  반응형(가치) {

    // 게터로 사용됩니다.
    if (값 === 정의되지 않음) {
      this.responsive_를 반환합니다.
    }

    값 = 부울(값);
    const 현재 = this.responsive_;

    // 아무것도 바뀌지 않았다.
    if (값 === 현재) {
      반품;
    }

    // 실제로 변경된 값, 설정합니다.
    this.responsive_ = 값;

    // 중단점 수신을 시작하고 다음과 같은 경우 초기 중단점을 설정합니다.
    // 이제 플레이어가 반응합니다.
    경우 (값) {
      this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
      this.updateCurrentBreakpoint_();

    // 플레이어가 더 이상 응답하지 않으면 중단점 수신을 중지합니다.
    } else {
      this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
      this.removeCurrentBreakpoint_();
    }

    반환 값;
  }

  /**
   * 현재 중단점 이름을 가져옵니다(있는 경우).
   *
   * @return {문자열}
   * 현재 중단점이 설정되어 있으면 중단점에서 키를 반환합니다.
   * 일치하는 중단점 객체. 그렇지 않으면 빈 문자열을 반환합니다.
   */
  currentBreakpoint() {
    this.breakpoint_를 반환합니다.
  }

  /**
   * 현재 중단점 클래스 이름을 가져옵니다.
   *
   * @return {문자열}
   * 일치하는 클래스 이름(예: `"vjs-layout-tiny"` 또는
   * `"vjs-layout-large"`) 현재 중단점. 다음과 같은 경우 빈 문자열
   * 현재 중단점이 없습니다.
   */
  currentBreakpointClass() {
    return BREAKPOINT_CLASSES[this.breakpoint_] || '';
  }

  /**
   * 단일 미디어를 설명하는 개체입니다.
   *
   * 이 유형 설명의 일부가 아닌 속성은 유지됩니다. 그래서,
   * 이것은 일반적인 메타 데이터 저장 메커니즘으로도 볼 수 있습니다.
   *
   * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
   * @typedef {객체} Player~MediaObject
   *
   * @property {문자열} [앨범]
   * 이 개체가 `MediaSession`에 전달되는 경우를 제외하고 사용되지 않습니다.
   * API.
   *
   * @property {문자열} [아티스트]
   * 이 개체가 `MediaSession`에 전달되는 경우를 제외하고 사용되지 않습니다.
   * API.
   *
   * @property {객체[]} [아트워크]
   * 이 개체가 `MediaSession`에 전달되는 경우를 제외하고 사용되지 않습니다.
   * API. 지정하지 않으면 `포스터`를 통해 채워집니다.
   * 사용 가능.
   *
   * @property {문자열} [포스터]
   * 재생 전에 표시될 이미지의 URL.
   *
   * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
   * 단일 소스 객체, 소스 객체의 배열 또는 문자열
   * 미디어 소스에 대한 URL 참조. _강추합니다_
   * 객체 또는 객체 배열이 여기에서 사용되므로 소스
   * 선택 알고리즘은 `유형`을 고려할 수 있습니다.
   *
   * @property {문자열} [제목]
   * 이 개체가 `MediaSession`에 전달되는 경우를 제외하고 사용되지 않습니다.
   * API.
   *
   * @property {객체[]} [텍스트 트랙]
   * 다음과 같이 텍스트 트랙을 만드는 데 사용할 개체 배열
   * {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|네이티브 트랙 요소 형식}.
   * 쉽게 제거할 수 있도록 "원격" 텍스트로 생성됩니다.
   * 소스 변경 사항을 추적하고 자동으로 정리하도록 설정합니다.
   *
   * 이러한 개체에는 `src`, `kind`, `label`,
   * 및 `언어`는 {@link Tech#createRemoteTextTrack}을 참조하세요.
   */

  /**
   * {@link Player~MediaObject|MediaObject}를 사용하여 플레이어를 채웁니다.
   *
   * @param {Player~MediaObject} 미디어
   * 미디어 개체입니다.
   *
   * @param {함수} 준비됨
   * 플레이어가 준비되면 호출되는 콜백입니다.
   */
  loadMedia(미디어, 준비) {
    if (!미디어 || 미디어 유형 !== '개체') {
      반품;
    }

    this.reset();

    // 외부에서 변경할 수 없도록 미디어 개체를 복제합니다.
    this.cache_.media = mergeOptions(미디어);

    const {작품, 포스터, src, textTracks} = this.cache_.media;

    // `artwork`가 제공되지 않으면 `poster`를 사용하여 만듭니다.
    if (!artwork && 포스터) {
      this.cache_.media.artwork = [{
        src: 포스터,
        유형: getMimetype(포스터)
      }];
    }

    경우 (소스) {
      this.src(src);
    }

    만약 (포스터) {
      this.poster(포스터);
    }

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

    this.ready(준비);
  }

  /**
   * 이 플레이어에 대한 현재 {@link Player~MediaObject}의 클론을 가져옵니다.
   *
   * `loadMedia` 메서드를 사용하지 않은 경우 반환을 시도합니다.
   * {@link Player~MediaObject}는 플레이어의 현재 상태를 기반으로 합니다.
   *
   * @return {플레이어~미디어오브젝트}
   */
  getMedia() {
    if (!this.cache_.media) {
      const 포스터 = this.poster();
      const src = this.currentSources();
      const textTracks = Array.prototype.map.call(this.remoteTextTracks(), (tt) => ({
        종류: tt.종류,
        라벨: tt.label,
        언어: tt.언어,
        소스: tt.src
      }));

      const media = {src, textTracks};

      만약 (포스터) {
        media.poster = 포스터;
        미디어.아트워크 = [{
          src: media.poster,
          유형: getMimetype(미디어.포스터)
        }];
      }

      반환 매체;
    }

    mergeOptions(this.cache_.media)를 반환합니다.
  }

  /**
   * 태그 설정 가져오기
   *
   * @param {요소} 태그
   * 플레이어 태그
   *
   * @return {객체}
   * 모든 설정을 포함하는 객체
   * 플레이어 태그용
   */
  정적 getTagSettings(태그) {
    const baseOptions = {
      출처: [],
      트랙: []
    };

    const tagOptions = Dom.getAttributes(태그);
    const dataSetup = tagOptions['데이터 설정'];

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

    // data-setup attr이 존재하는지 확인합니다.
    if (dataSetup !== null) {
      // 파싱 옵션 JSON
      // 빈 문자열이면 구문 분석 가능한 json 객체로 만듭니다.
      const [err, data] = safeParseTuple(dataSetup || '{}');

      경우 (오류) {
        log.error(err);
      }
      assign(tagOptions, 데이터);
    }

    assign(baseOptions, tagOptions);

    // 태그 하위 설정 가져오기
    if (tag.hasChildNodes()) {
      const children = tag.childNodes;

      for (let i = 0, j = children.length; i < 제이; i++) {
        const 자식 = 자식[i];
        // 대소문자 변경 필요: http://ejohn.org/blog/nodename-case-sensitivity/
        const childName = child.nodeName.toLowerCase();

        if (아이 이름 === '출처') {
          baseOptions.sources.push(Dom.getAttributes(child));
        } 그렇지 않으면 (childName === '추적') {
          baseOptions.tracks.push(Dom.getAttributes(child));
        }
      }
    }

    baseOptions 반환;
  }

  /**
   * flexbox 지원 여부 결정
   *
   * @return {부울}
   * - flexbox가 지원되는 경우 true
   * - flexbox가 지원되지 않는 경우 false
   */
  flexNotSupported_() {
    const elem = document.createElement('i');

    // 메모: 우리는 실제로 flexBasis(또는 flexOrder)를 사용하지 않지만 더 많은 것 중 하나입니다.
    // 플렉스 지원을 확인할 때 신뢰할 수 있는 일반적인 플렉스 기능입니다.
    return !( elem.style의 'flexBasis' ||
            elem.style의 'webkitFlexBasis' ||
            elem.style의 'mozFlexBasis' ||
            elem.style의 'msFlexBasis' ||
            // IE10 전용(2012 플렉스 사양), 완전성을 위해 사용 가능
            elem.style의 'msFlexOrder');
  }

  /**
   * 정보 수준에서 로그를 활성화/비활성화하도록 디버그 모드를 설정합니다.
   *
   * @param {부울} 활성화됨
   * @fires Player#debugon
   * @fires Player#debugoff
   */
  디버그(활성화) {
    if (활성화 === 정의되지 않음) {
      this.debugEnabled_를 반환합니다.
    }
    경우 (활성화) {
      this.trigger('debugon');
      this.previousLogLevel_ = this.log.level;
      this.log.level('디버그');
      this.debugEnabled_ = 참;
    } else {
      this.trigger('debugoff');
      this.log.level(this.previousLogLevel_);
      this.previousLogLevel_ = 정의되지 않음;
      this.debugEnabled_ = 거짓;
    }

  }

  /**
   * 현재 재생 속도를 설정하거나 가져옵니다.
   * 배열을 가져와 새 항목으로 재생 속도 메뉴를 업데이트합니다.
   * 메뉴를 숨기려면 빈 배열을 전달하십시오.
   * 배열 이외의 값은 무시됩니다.
   *
   * @fires Player#playbackrateschange
   * @param {번호[]} newRates
   * 재생 속도 메뉴가 업데이트되어야 하는 새로운 속도.
   * 빈 배열은 메뉴를 숨길 것입니다.
   * @return {number[]} getter로 사용하면 현재 재생 속도를 반환합니다.
   */
  playRates(newRates) {
    if (newRates === 정의되지 않음) {
      this.cache_.playbackRates 반환;
    }

    // 배열이 아닌 값은 무시
    if (!Array.isArray(newRates)) {
      반품;
    }

    // 숫자만 포함하지 않는 배열은 무시합니다.
    if (!newRates.every((비율) => typeof rate === '숫자')) {
      반품;
    }

    this.cache_.playbackRates = newRates;

    /**
    * 플레이어의 재생 속도가 변경될 때 발생
    *
    * @event Player#playbackrateschange
    * @type {이벤트대상~이벤트}
    */
    this.trigger('재생 속도 변경');
  }
}

/**
 * {@link VideoTrackList} 받기
 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
 *
 * @return {VideoTrackList}
 * 현재 비디오 트랙 목록
 *
 * @method Player.prototype.videoTracks
 */

/**
 * {@link AudioTrackList} 받기
 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
 *
 * @return {오디오트랙리스트}
 * 현재 오디오 트랙 목록
 *
 * @method Player.prototype.audioTracks
 */

/**
 * {@link TextTrackList} 받기
 *
 * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
 *
 * @return {TextTrackList}
 * 현재 텍스트 트랙 목록
 *
 * @method Player.prototype.textTracks
 */

/**
 * 원격 {@link TextTrackList} 가져오기
 *
 * @return {TextTrackList}
 * 현재 원격 텍스트 트랙 목록
 *
 * @method Player.prototype.remoteTextTracks
 */

/**
 * 원격 {@link HtmlTrackElementList} 트랙을 가져옵니다.
 *
 * @return {HtmlTrackElementList}
 * 현재 원격 텍스트 트랙 요소 목록
 *
 * @method Player.prototype.remoteTextTrackEls
 */

TRACK_TYPES.names.forEach(기능(이름) {
  const 소품 = TRACK_TYPES[이름];

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

    // 아직 loadTech_가 없으면 {video,audio,text}Tracks_를 만듭니다.
    // 로딩하는 동안 기술자에게 전달됩니다.
    this[props.privateName] = this[props.privateName] || 새로운 props.ListClass();
    return this[props.privateName];
  };
});

/**
 * `Player`의 crossorigin 옵션을 가져오거나 설정합니다. HTML5 플레이어의 경우 이
 * `에 `crossOrigin` 속성을 설정합니다.< 동영상> ` CORS를 제어하는 태그
 * 행동.
 *
 * @see [동영상 요소 속성]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
 *
 * @param {문자열} [값]
 * `Player`의 크로스오리진을 설정할 값. 인수가
 * 주어진 'anonymous' 또는 'use-credentials' 중 하나여야 합니다.
 *
 * @return {문자열|정의되지 않음}
 * - 가져올 때 `Player`의 현재 crossorigin 값입니다.
 * - 설정 시 정의되지 않음
 */
Player.prototype.crossorigin = Player.prototype.crossOrigin;

/**
 * 플레이어의 글로벌 열거.
 *
 * 키는 플레이어 ID이고 값은 {@link Player}
 * 폐기된 플레이어에 대한 인스턴스 또는 `null`.
 *
 * @type {객체}
 */
Player.players = {};

const 네비게이터 = window.navigator;

/*
 * 옵션을 사용하여 표시되는 플레이어 인스턴스 옵션
 * 옵션 = Player.prototype.options_
 * 여기가 아닌 옵션에서 변경하십시오.
 *
 * @type {객체}
 * @사적인
 */
Player.prototype.options_ = {
  // 폴백 기술의 기본 순서
  기술 주문: Tech.defaultTechOrder_,

  HTML5: {},

  // 기본 비활성 타임아웃
  비활성 시간 초과: 2000 년,

  // 기본 재생 속도
  재생 속도: [],
  // 속도를 추가하여 재생 속도 선택 추가
  // 'playbackRates': [0.5, 1, 1.5, 2],
  liveui: 거짓,

  // 포함된 컨트롤 세트
  어린이들: [
    '미디어로더',
    '포스터 이미지',
    'textTrackDisplay',
    '로딩스피너',
    '빅플레이버튼',
    '라이브트래커',
    '컨트롤바',
    '오류 표시',
    'textTrackSettings',
    '크기 조정 관리자'
  ],

  언어: 네비게이터 && (네비게이터.언어 && navigator.languages[0] || navigator.userLanguage || navigator.언어) || '엔',

  // 로케일 및 해당 언어 번역
  언어: {},

  // 동영상을 재생할 수 없을 때 표시할 기본 메시지입니다.
  지원되지 않음메시지: '이 미디어에 대해 호환 가능한 소스를 찾을 수 없습니다.',

  normalizeAutoplay: 거짓,

  전체화면: {
    옵션: {
      navigationUI: '숨기기'
    }
  },

  중단점: {},
  반응형: 거짓,
  audioOnlyMode: 거짓,
  오디오 포스터 모드: 거짓
};

[
  /**
   * 플레이어가 "종료" 상태인지 여부를 반환합니다.
   *
   * @return {Boolean} 플레이어가 종료 상태이면 참, 아니면 거짓.
   * @method Player#ended
   */
  '종료',
  /**
   * 플레이어가 "탐색" 상태에 있는지 여부를 반환합니다.
   *
   * @return {Boolean} 플레이어가 탐색 상태에 있으면 True이고 그렇지 않으면 false입니다.
   * @method Player#seeking
   */
  '찾다',
  /**
   * 현재 사용 가능한 미디어의 TimeRanges를 반환합니다.
   * 추구하기 위해.
   *
   * @return {TimeRanges} 미디어 타임라인의 탐색 가능한 간격
   * @method Player#seekable
   */
  '찾을 수 있는',
  /**
   *에서 요소에 대한 네트워크 활동의 현재 상태를 반환합니다.
   * 아래 목록의 코드.
   * - NETWORK_EMPTY(숫자 값 0)
   * 요소가 아직 초기화되지 않았습니다. 모든 속성은
   * 그들의 초기 상태.
   * - NETWORK_IDLE(숫자 값 1)
   * 요소의 자원 선택 알고리즘이 활성화되어 있고
   * 리소스를 선택했지만 실제로는 네트워크를 사용하지 않습니다.
   * 이 시간.
   * - NETWORK_LOADING(숫자 값 2)
   * 사용자 에이전트가 적극적으로 데이터 다운로드를 시도하고 있습니다.
   * - NETWORK_NO_SOURCE(숫자 값 3)
   * 요소의 자원 선택 알고리즘이 활성화되어 있지만
   * 아직 사용할 리소스를 찾지 못했습니다.
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
   * @return {숫자} 현재 네트워크 활동 상태
   * @method Player#networkState
   */
  '네트워크 상태',
  /**
   * 요소의 현재 상태를 나타내는 값을 반환합니다.
   * 현재 재생 위치를 렌더링하는 것과 관련하여
   * 아래 목록의 코드.
   * - HAVE_NOTHING(숫자 값 0)
   * 미디어 리소스에 대한 정보가 없습니다.
   * - HAVE_METADATA(숫자 값 1)
   * 해당 기간만큼 자원을 충분히 확보했습니다.
   * 리소스를 사용할 수 있습니다.
   * - HAVE_CURRENT_DATA(숫자 값 2)
   * 바로 현재 재생 위치에 대한 데이터를 사용할 수 있습니다.
   * - HAVE_FUTURE_DATA(숫자 값 3)
   * 바로 현재 재생 위치에 대한 데이터는 다음과 같이 사용할 수 있습니다.
   * 사용자 에이전트가 현재 작업을 진행하기에 충분한 데이터
   * 재생 방향의 재생 위치.
   * - HAVE_ENOUGH_DATA(숫자 값 4)
   * 사용자 에이전트는 충분한 데이터를 사용할 수 있다고 추정합니다.
   * 재생이 중단 없이 진행됩니다.
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
   * @return {숫자} 현재 재생 렌더링 상태
   * @method Player#readyState
   */
  '준비 상태'
].forEach(함수(fn) {
  Player.prototype[fn] = 함수() {
    return this.techGet_(fn);
  };
});

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

/**
 * 플레이어에 초기 지속 시간 및 차원 정보가 있을 때 실행됩니다.
 *
 * @event Player#loadedmetadata
 * @type {이벤트대상~이벤트}
 */

/**
 * 플레이어가 현재 재생 위치에서 데이터를 다운로드했을 때 발생
 *
 * @event 플레이어#loadeddata
 * @type {이벤트대상~이벤트}
 */

/**
 * 현재 재생 위치가 변경되었을 때 발생 *
 * 재생 중에는 15-250밀리초마다 실행됩니다.
 * 사용 중인 재생 기술.
 *
 * @event 플레이어#timeupdate
 * @type {이벤트대상~이벤트}
 */

/**
 * 볼륨이 변경될 때 발생
 *
 * @event 플레이어#volumechange
 * @type {이벤트대상~이벤트}
 */

/**
 * 플레이어가 플러그인을 사용할 수 있는지 여부를 보고합니다.
 *
 * 이것은 플러그인이 초기화되었는지 여부를 보고하지 않습니다.
 * 이 플레이어에서. 이를 위해 [usingPlugin]{@link Player#usingPlugin}.
 *
 * @method Player#hasPlugin
 * @param {문자열} 이름
 * 플러그인의 이름입니다.
 *
 * @return {부울}
 * 이 플레이어가 요청한 플러그인을 사용할 수 있는지 여부.
 */

/**
 * 플레이어가 이름으로 플러그인을 사용하는지 여부를 보고합니다.
 *
 * 기본 플러그인의 경우 플러그인이 _ever_ 있었는지 여부만 보고합니다.
 * 이 플레이어에서 초기화되었습니다.
 *
 * @method Player#usingPlugin
 * @param {문자열} 이름
 * 플러그인의 이름입니다.
 *
 * @return {부울}
 * 이 플레이어가 요청된 플러그인을 사용하고 있는지 여부.
 */

Component.registerComponent('플레이어', 플레이어);
기본 플레이어 내보내기;