/**
 * @file player.js
 */
// 子類組件
從 './component.js' 導入組件;

從'../../package.json'導入{version};
從“全局/文檔”導入文檔;
從“全局/窗口”導入窗口;
從'./mixins/evented'導入事件;
從 './mixins/evented' 導入 {isEvented, addEventedCallback};
import * as Events from './utils/events.js';
import * as Dom from './utils/dom.js';
從 './utils/fn.js' 導入 * 作為 Fn;
從 './utils/guid.js' 導入 * 作為 Guid;
import * as browser from './utils/browser.js';
從 './utils/browser.js' 導入 {IE_VERSION, IS_CHROME, IS_WINDOWS};
從'./utils/log.js'導入日誌,{createLogger};
從 './utils/string-cases.js' 導入 {toTitleCase, titleCaseEquals};
從'./utils/time-ranges.js'導入{createTimeRange};
從'./utils/buffer.js'導入{bufferedPercent};
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 as TRACK_TYPES};
從 './utils/filter-source' 導入 filterSource;
從 './utils/mimetypes' 導入 {getMimetype, findMimetype};
從 './utils/hooks' 導入 {hooks};
從 './utils/obj' 導入 {isObject};
從“鍵碼”導入鍵碼;

// 下面的imports只是用來保證對應的模塊
// 始終包含在 video.js 包中。導入模塊將
// 執行它們,它們將自己註冊到 video.js。
導入'./tech/loader.js';
導入“./poster-image.js”;
導入“./tracks/text-track-display.js”;
導入'./loading-spinner.js';
導入'./big-play-button.js';
導入'./close-button.js';
導入'./control-bar/control-bar.js';
導入'./error-display.js';
導入“./tracks/text-track-settings.js”;
導入'./resize-manager.js';
導入'./live-tracker.js';

// 引入 Html5 技術,至少是為了處理原始視頻標籤。
導入'./tech/html5.js';

// 以下技術事件只是重新觸發
// 當它們發生時在播放器上
const TECH_EVENTS_RETRIGGER = [
  /**
   * 在用戶代理下載媒體數據時觸發。
   *
   * @event Player#progress
   * @type {EventTarget~Event}
   */
  /**
   * 重新觸發由 {@link Tech} 觸發的 `progress` 事件。
   *
   * @私人的
   * @method Player#handleTechProgress_
   * @fires Player#progress
   * @listens Tech#progress
   */
  '進步',

  /**
   * 當音頻/視頻加載中止時觸發。
   *
   * @event Player#中止
   * @type {EventTarget~Event}
   */
  /**
   * 重新觸發由 {@link Tech} 觸發的 `abort` 事件。
   *
   * @私人的
   * @method Player#handleTechAbort_
   * @fires Player#中止
   * @listens Tech#abort
   */
  '中止',

  /**
   * 當瀏覽器有意不獲取媒體數據時觸發。
   *
   * @event Player#暫停
   * @type {EventTarget~Event}
   */
  /**
   * 重新觸發由 {@link Tech} 觸發的 `suspend` 事件。
   *
   * @私人的
   * @method Player#handleTechSuspend_
   * @fires Player#暫停
   * @listens 技術#suspend
   */
  '暫停',

  /**
   * 當前播放列表為空時觸發。
   *
   * @event Player#emptied
   * @type {EventTarget~Event}
   */
  /**
   * 重新觸發由 {@link Tech} 觸發的 `emptied` 事件。
   *
   * @私人的
   * @method Player#handleTechEmptied_
   * @fires Player#emptied
   * @listens Tech#emptied
   */
  '清空',
  /**
   * 當瀏覽器試圖獲取媒體數據但數據不可用時觸發。
   *
   * @event Player#stalled
   * @type {EventTarget~Event}
   */
  /**
   * 重新觸發由 {@link Tech} 觸發的 `stalled` 事件。
   *
   * @私人的
   * @method Player#handleTechStalled_
   * @fires Player#stalled
   * @listens Tech#stalled
   */
  '停滯不前',

  /**
   * 當瀏覽器加載了音頻/視頻的元數據時觸發。
   *
   * @event Player#loadedmetadata
   * @type {EventTarget~Event}
   */
  /**
   * 重新觸發由 {@link Tech} 觸發的 `loadedmetadata` 事件。
   *
   * @私人的
   * @method Player#handleTechLoadedmetadata_
   * @fires Player#loadedmetadata
   * @listens Tech#loadedmetadata
   */
  '加載元數據',

  /**
   * 當瀏覽器加載了音頻/視頻的當前幀時觸發。
   *
   * @event Player#loadeddata
   * @type {事件}
   */
  /**
   * 重新觸發由 {@link Tech} 觸發的 `loadeddata` 事件。
   *
   * @私人的
   * @method Player#handleTechLoaddeddata_
   * @fires Player#loadeddata
   * @listens Tech#loadeddata
   */
  '加載數據',

  /**
   * 當前播放位置改變時觸發。
   *
   * @event Player#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#調整大小
   * @listens Tech#resize
   */
  '調整大小',

  /**
   * 音量改變時觸發
   *
   * @event Player#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
   */
  'texttrackchange'
];

// 當播放速率為零時要排隊的事件
// 這是一個哈希,其唯一目的是映射非駝峰事件名稱
// 駝峰式函數名
const TECH_EVENTS_QUEUE = {
  可以玩:'可以玩',
  可以玩:'可以播放',
  播放:'玩',
  尋求:'尋求'
};

const BREAKPOINT_ORDER = [
  '微小的',
  'xsmall',
  '小的',
  '中等的',
  '大的',
  '超大',
  '巨大的'
];

const BREAKPOINT_CLASSES = {};

// grep: vjs-layout-tiny
// grep: vjs-layout-x-small
// grep: vjs-layout-small
// grep: vjs-layout-medium
// grep: vjs-layout-large
// grep: vjs-layout-x-large
// grep: vjs-layout-huge
BREAKPOINT_ORDER.forEach(k => {
  常量 v = K 字符(0)=== 'X'?`x $ {K. 子字符串(1)} `:k;

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

const DEFAULT_BREAKPOINTS = {
  微小的:210,
  x小:320,
  小的:425、
  中等的:768,
  大的:1440,
  超大:2560,
  巨大的:無窮
};

/**
 * 當任何 Video.js 設置方法時,都會創建 `Player` 類的實例
 * 用於初始化視頻。
 *
 * 創建實例後,可以通過兩種方式全局訪問它:
 * 1。通過調用 '視頻 JS(' 例如視頻 1 '); `
 * 2。通過使用它直接通過視頻播放器示例 _ 視頻 1; `
 *
 * @extends 組件
 */
類播放器擴展組件{

  /**
   * 創建此類的一個實例。
   *
   * @param {元素} 標籤
   * 用於配置選項的原始視頻 DOM 元素。
   *
   * @param {對象} [選項]
   * 選項名稱和值的對象。
   *
   * @param {Component~ReadyCallback} [就緒]
   * 準備回調函數。
   */
  構造函數(標籤,選項,準備就緒){
    // 確保標籤 ID 存在
    tag.id = tag.id ||選項.id || `vjs_video_${Guid.newGUID()}`;

    // 設置選項
    // 選項參數覆蓋視頻標籤中設置的選項
    // 覆蓋全局設置的選項。
    // 後半部分與加載順序一致
    //(標籤必須存在於播放器之前)
    options = assign(Player.getTagSettings(tag), options);

    // 延遲孩子的初始化,因為我們需要設置
    // 播放器屬性優先,不能在 `super()` 之前使用 `this`
    options.initChildren = false;

    // 與創建元素相同
    options.createEl = false;

    // 不要自動混合事件混合
    options.evented = false;

    // 我們不希望播放器報告自己的觸摸活動
    // 查看組件中的 enableTouchActivity
    options.reportTouchActivity = false;

    // 如果未設置語言,則獲取最接近的 lang 屬性
    如果(!options.language){
      如果(typeof tag.closest ==='函數'){
        const closest = tag.closest('[lang]');

        如果(最接近的 && closest.getAttribute){
          options.language = closest.getAttribute('lang');
        }
      }其他{
        讓元素=標籤;

        while (element && element.nodeType === 1) {
          如果 (Dom.getAttributes(element).hasOwnProperty('lang')) {
            options.language = element.getAttribute('lang');
            休息;
          }
          element = 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);

    // 默認 isFullscreen_ 為 false
    this.isFullscreen_ = false;

    // 創建記錄器
    this.log = createLogger(this.id_);

    // 保留我們自己對全屏 api 的引用,以便可以在測試中對其進行模擬
    this.fsApi_ = FullscreenApi;

    // 跟踪技術何時更改海報
    this.isPosterFromTech_ = false;

    // 保存播放速率為零時排隊的回調信息
    // 並且正在尋找
    this.queuedCallbacks_ = [];

    // 關閉 API 訪問,因為我們正在加載一項可能異步加載的新技術
    this.isReady_ = false;

    // 初始化狀態 hasStarted_
    this.hasStarted_ = false;

    // 初始化狀態 userActive_
    this.userActive_ = false;

    // 初始化 debugEnabled_
    this.debugEnabled_ = false;

    // 初始化狀態audioOnlyMode_
    this.audioOnlyMode_ = false;

    // 初始化狀態audioPosterMode_
    this.audioPosterMode_ = false;

    // 初始化狀態audioOnlyCache_
    this.audioOnlyCache_ = {
      播放器高度:空,
      隱藏的孩子:[]
    };

    // 如果全局選項對像被不小心吹走了
    // 某人,由於信息性錯誤而提早退出
    如果(!this.options_ ||
        !這個。選項 _ 技術訂單 ||
        !這個選項 _. 技術訂單. 長度) {
      throw new Error('沒有指定 techOrder。你覆蓋了'+
                      'videojs.options 而不是僅僅改變 ' +
                      '你想覆蓋的屬性?');
    }

    // 存儲用於設置選項的原始標籤
    this.tag = 標籤;

    // 存儲用於恢復html5元素的標籤屬性
    this.tagAttributes = tag && Dom.getAttributes(tag);

    // 更新當前語言
    this.language(this.options_.language);

    // 更新支持的語言
    如果(選項。語言){
      // 將播放器選項語言規範化為小寫
      const languagesToLower = {};

      Object.getOwnPropertyNames(options.languages).forEach(函數(名稱){
        languagesToLower[name.toLowerCase()] = options.languages[name];
      });
      this.languages_ = languagesToLower;
    }其他{
      this.languages_ = Player.prototype.options_.languages;
    }

    這個.resetCache_();

    // 設置海報
    this.poster_ = options.poster || '';

    // 設置控件
    這個。控制 _ =!!選項。控件;

    // 選項中存儲的原始標籤設置
    // 現在立即刪除,這樣本機控件就不會閃爍。
    // 如果 nativeControlsForTouch 為真,HTML5 技術可能會重新啟用
    tag.controls = false;
    tag.removeAttribute('控件');

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

    // 屬性覆蓋選項
    如果(tag.hasAttribute('自動播放')){
      這個。自動播放(真);
    }其他{
      // 否則使用 setter 來驗證和
      // 設置正確的值。
      this.autoplay(this.options_.autoplay);
    }

    // 檢查插件
    如果(選項。插件){
      Object.keys(options.plugins).forEach((name) => {
        如果(鍵入此 [名稱]!== '功能') {
          throw new Error(`插件“${name}”不存在`);
        }
      });
    }

    /*
     * 存儲擦洗的內部狀態
     *
     * @私人的
     * @return {Boolean} 如果用戶正在擦洗則為真
     */
    this.scrubbing_ = false;

    this.el_ = this.createEl();

    // 將其設為事件對象並使用 `el_` 作為其事件總線。
    事件(這,{eventBusKey:'el_'});

    // 監聽 document 和 player fullscreenchange 處理程序以便我們接收這些事件
    // 在用戶可以接收它們之前,我們可以適當地更新 isFullscreen。
    // 確保我們先監聽 fullscreenchange 事件,以確保
    // 我們的 isFullscreen 方法針對內部組件和外部組件進行了正確更新。
    如果(this.fsApi_.requestFullscreen){
      Events.on(文檔, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
      this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
    }

    如果(this.fluid_){
      this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
    }
    // 我們還想將原始播放器選項傳遞給每個組件和插件
    // 也這樣他們以後就不需要再回到播放器中獲取選項了。
    // 我們還需要復制 this.options_ 所以我們不會以
    // 無限循環。
    const playerOptionsCopy = mergeOptions(this.options_);

    //加載插件
    如果(選項。插件){
      Object.keys(options.plugins).forEach((name) => {
        這個[名稱](選項。插件[名稱]);
      });
    }

    // 啟用調試模式以觸發所有插件的 debugon 事件。
    如果(選項。調試){
      這個。調試(真);
    }

    this.options_.playerOptions = playerOptionsCopy;

    this.middleware_ = [];

    this.playbackRates(options.playbackRates);

    這個.initChildren();

    // 根據是否使用音頻標籤設置 isAudio
    this.isAudio(tag.nodeName.toLowerCase() === '音頻');

    // 更新控件類名。最初控制時無法執行此操作
    // 設置是因為該元素尚不存在。
    如果(this.controls()){
      this.addClass('vjs-controls-enabled');
    }其他{
      this.addClass('vjs-controls-disabled');
    }

    // 根據播放器類型設置 ARIA 標籤和區域角色
    this.el_.setAttribute('角色', '區域');
    如果(this.isAudio()){
      this.el_.setAttribute('aria-label', this.localize('Audio Player'));
    }其他{
      this.el_.setAttribute('aria-label', this.localize('Video Player'));
    }

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

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

    // 去做:讓這更聰明。在觸摸/鼠標之間切換用戶狀態
    // 使用事件,因為設備可以同時具有觸摸和鼠標事件。
    // 去做:當窗口在監視器之間切換時再次執行此檢查
    //(參見 https://github.com/videojs/video.js/issues/5683)
    如果(瀏覽器.TOUCH_ENABLED){
      this.addClass('vjs-touch-enabled');
    }

    // iOS Safari 中斷了懸停處理
    如果(!瀏覽器。IS_IOS){
      this.addClass('vjs-workinghover');
    }

    // 讓玩家可以通過 ID 輕鬆找到
    Player.players[this.id_] = this;

    // 添加一個主要版本類來幫助插件中的 css
    const majorVersion = version.split('.')[0];

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

    // player第一次初始化時,觸發activity so組件
    // 如果需要,就像控制欄顯示自己一樣
    這個。userActive(真);
    這個.reportUserActivity();

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

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

    // 播放器完全播放後調用兩個音頻模式方法
    // 設置為能夠監聽它們觸發的事件
    this.on('準備好', () => {
      // 首先調用 audioPosterMode 方法,這樣
      // 當兩個選項都設置為 true 時,audioOnlyMode 可以優先
      this.audioPosterMode(this.options_.audioPosterMode);
      this.audioOnlyMode(this.options_.audioOnlyMode);
    });
  }

  /**
   * 銷毀視頻播放器並進行任何必要的清理。
   *
   * 如果您動態添加和刪除視頻,這將特別有用
   * 到/從 DOM。
   *
   * @fires Player#dispose
   */
  處置(){
    /**
     * 播放器被處理時調用。
     *
     * @event Player#dispose
     * @type {EventTarget~Event}
     */
    this.trigger('處置');
    // 防止 dispose 被調用兩次
    this.off('處置');

    // 確保所有播放器特定的文檔偵聽器都是未綁定的。這是
    Events.off(文檔,this.fsApi_.fullscreenchange,this.boundDocumentFullscreenChange_);
    Events.off(文檔, 'keydown', this.boundFullWindowOnEscKey_);

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

    // 殺死對這個玩家的引用
    Player.players[this.id_] = null;

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

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

    如果(this.tech_){
      這個.tech_.dispose();
      this.isPosterFromTech_ = false;
      this.poster_ = '';
    }

    如果(this.playerElIngest_){
      this.playerElIngest_ = null;
    }

    如果(這個。標籤){
      this.tag = null;
    }

    middleware.clearCacheForPlayer(this);

    // 刪除軌道列表的所有事件處理程序
    // 所有軌道和軌道偵聽器都被移除
    // 技術處置
    TRACK_TYPES.names.forEach((名稱) => {
      const props = TRACK_TYPES[名稱];
      const list = this[props.getterName]();

      // 如果它不是原生列表
      // 我們必須手動移除事件監聽器
      如果(列表&& list.off){
        列表.off();
      }
    });

    //實際的 .el_ 在這裡被刪除,或者如果更換
    super.dispose({restoreEl: this.options_.restoreEl});
  }

  /**
   * 創建 `Player` 的 DOM 元素。
   *
   * @return {元素}
   * 創建的 DOM 元素。
   */
  創建El() {
    讓 tag = this.tag;
    讓埃爾;
    讓 playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
    const divEmbed = this.tag.tagName.toLowerCase() === 'video-js';

    如果(playerElIngest){
      el = this.el_ = tag.parentNode;
    } 否則如果(!divEmbed){
      el = this.el_ = super.createEl('div');
    }

    // 複製標籤的所有屬性,包括 ID 和類
    // ID 現在將引用播放器框,而不是視頻標籤
    const attrs = Dom.getAttributes(標籤);

    如果(divEmbed){
      el = this.el_ = 標籤;
      tag = this.tag = document.createElement('video');
      而(el.children.length){
        tag.appendChild(el.firstChild);
      }

      如果(!DOM.Hasclass (埃爾, '視頻 JS')) {
        Dom.addClass(el, 'video-js');
      }

      el.appendChild(標籤);

      playerElIngest = this.playerElIngest_ = el;
      // 將屬性從我們自定義的 `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 的行為
    如果 (IE_VERSION || (IS_CHROME && IS_WINDOWS)) {
      tag.setAttribute('角色', '應用程序');
      attrs.role = '應用程序';
    }

    // 從標籤中移除寬度/高度屬性,這樣 CSS 就可以使它成為 100% 寬度/高度
    tag.removeAttribute('寬度');
    tag.removeAttribute('高度');

    如果(屬性中的“寬度”){
      刪除 attrs.width;
    }
    如果(屬性中的“高度”){
      刪除 attrs.height;
    }

    Object.getOwnPropertyNames(屬性).forEach(函數(屬性){
      // 當我們在 div 嵌入中時,不要將 class 屬性複製到播放器元素
      // 該類已經在 divEmbed 案例中正確設置
      // 我們要確保 `video-js` 類不會丟失
      如果(!(潛入 && 屬性 === '類')) {
        埃爾。設置屬性(屬性,屬性 [ATTR]);
      }

      如果(divEmbed){
        tag.setAttribute(attr, attr[attr]);
      }
    });

    // 更新標籤 id/class 以用作 HTML5 播放技術
    //可能認為我們應該在嵌入容器之後這樣做,以便 .vjs-tech 類
    //不會閃爍 100% 的寬度/高度,但類只適用於 .視頻/js 父
    tag.playerId = tag.id;
    tag.id += '_html5_api';
    tag.className = 'vjs-tech';

    // 讓玩家在元素上可被找到
    tag.player = el.player = this;
    // 視頻的默認狀態是暫停
    this.addClass('vjs-paused');

    // 在我們將用於設置寬度/高度的播放器中添加一個樣式元素
    // 播放器的方式仍然可以被 CSS 覆蓋,就像
    // 視頻元素
    如果(窗口。視頻 _ 動態風格!真){
      this.styleEl_ = stylesheet.createStyleElement('vjs-styles-dimensions');
      const defaultsStyleEl = Dom.$('.vjs-styles-defaults');
      const head = Dom.$('head');

      頭。插入之前(這個。樣式 _,默認樣式?默認樣式。下一個兄弟姐妹:頭。第一個孩子);
    }

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

    // 傳入 width/height/aspectRatio 選項,這將更新樣式 el
    this.width(this.options_.width);
    this.height(this.options_.height);
    this.fill(this.options_.fill);
    this.fluid(this.options_.fluid);
    this.aspectRatio(this.options_.aspectRatio);
    // 同時支持 crossOrigin 和 crossorigin 以減少圍繞名稱的混淆和問題
    this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);

    // 隱藏視頻/音頻標籤中的任何鏈接,
    // 因為 IE 不會對屏幕閱讀器完全隱藏它們。
    const links = tag.getElementsByTagName('a');

    對於(讓我 = 0; 我 < 鏈接. 長度; 我 ++){
      const linkEl = links.item(i);

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

    // insertElFirst 似乎導致 networkState 從 3 閃爍到 2,所以
    // 跟踪原始文件以備後用,以便我們知道源文件最初是否失敗
    tag.initNetworkState_ = tag.networkState;

    // 在 div (el/box) 容器中包裝視頻標籤
    如果(標記. 父節點 &&!最重要的) {
      tag.parentNode.insertBefore(el, tag);
    }

    // 插入標籤作為播放器元素的第一個子元素
    // 然後手動將它添加到 children 數組中,這樣 this.addChild
    // 將對其他組件正常工作
    //
    // 破壞 iPhone,已在 HTML5 設置中修復。
    Dom.prependTo(tag, el);
    this.children_.unshift(標籤);

    //在播放器上設置郎屬性,以確保 CSS:Lang()與玩家保持一致
    // 如果它被設置為與文檔不同的東西
    this.el_.setAttribute('lang', this.language_);

    this.el_.setAttribute('翻譯', '否');

    this.el_ = el;

    返回 el;
  }

  /**
   * 獲取或設置 `Player` 的 crossOrigin 選項。對於 HTML5 播放器,這
   * 在 `<video>` 標籤上設置 `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 值。
   * - 設置時未定義
   */
  交叉來源(價值){
    如果(!值){
      返回 this.techGet_('crossOrigin');
    }

    如果(值!== '匿名' && 值!== '使用憑據') {
      log.warn(`crossOrigin 必須是“匿名”或“使用憑據”,給定“${value}”`);
      返回;
    }

    this.techCall_('setCrossOrigin', value);

    返回;
  }

  /**
   * `Player` 寬度的 getter/setter。返回玩家的配置值。
   * 要獲取當前寬度,請使用 `currentWidth()`。
   *
   * @param {number} [值]
   * 將 `Player` 的寬度設置為的值。
   *
   * @return {數字}
   * 獲取時`Player`的當前寬度。
   */
  寬度(值){
    返回 this.dimension('width', value);
  }

  /**
   * `Player` 高度的 getter/setter。返回玩家的配置值。
   * 要獲取當前高度,請使用 currentheight() 。
   *
   * @param {number} [值]
   * 將 `Player` 的高度設置為的值。
   *
   * @return {數字}
   * 獲取時`Player`的當前高度。
   */
  高度(值){
    返回 this.dimension('height', value);
  }

  /**
   * `Player` 的寬度和高度的 getter/setter。
   *
   * @param {string} 維度
   * 這個字符串可以是:
   * - '寬度'
   * - '高度'
   *
   * @param {number} [值]
   * 第一個參數中指定的維度值。
   *
   * @return {數字}
   * 獲取(寬度/高度)時的尺寸參數值。
   */
  維度(維度,價值){
    const privDimension = 維度 + '_';

    如果(值===未定義){
      返回這個[privDimension] || 0;
    }

    如果(值===''||值==='自動'){
      // 如果給出空字符串,則將維度重置為自動
      這個 [privDimension] = 未定義;
      this.updateStyleEl_();
      返回;
    }

    const parsedVal = parseFloat(值);

    如果 (isNaN(parsedVal)) {
      log.error(`為 ${dimension} 提供的不正確值“${value}”`);
      返回;
    }

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

  /**
   * `Player` 上 vjs-fluid `className` 的 getter/setter/toggler。
   *
   * 打開此項將關閉填充模式。
   *
   * @param {布爾} [布爾]
   * - 值為 true 添加類。
   * - 值為 false 會刪除該類。
   * - 沒有任何價值會成為吸氣劑。
   *
   * @return {布爾值|未定義}
   * - 獲得時的流體值。
   * - 設置時為`undefined`。
   */
  流體(布爾){
    如果(布爾===未定義){
      返回!!這個. 流體 _;
    }

    這個. 流體 _ =!!布爾;

    如果(isEvented(這個)){
      this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_);
    }
    如果(布爾){
      this.addClass('vjs-fluid');
      這個.填充(假);
      添加事件回調(這個,()=> {
        this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
      });
    }其他{
      this.removeClass('vjs-fluid');
    }

    this.updateStyleEl_();
  }

  /**
   * `Player` 上 vjs-fill `className` 的 getter/setter/toggler。
   *
   * 打開此功能將關閉流體模式。
   *
   * @param {布爾} [布爾]
   * - 值為 true 添加類。
   * - 值為 false 會刪除該類。
   * - 沒有任何價值會成為吸氣劑。
   *
   * @return {布爾值|未定義}
   * - 獲得時的流體值。
   * - 設置時為`undefined`。
   */
  填充(布爾){
    如果(布爾===未定義){
      返回!!這個。填充 _;
    }

    這個填充 _ =!!布爾;

    如果(布爾){
      this.addClass('vjs-fill');
      這個。流體(假);
    }其他{
      this.removeClass('vjs-fill');
    }
  }

  /**
   * 獲取/設置縱橫比
   *
   * @param {string} [比率]
   *播放器的縱橫比
   *
   * @return {字符串|未定義}
   * 獲取時返回當前縱橫比
   */

  /**
   * `Player` 縱橫比的 getter/setter。
   *
   * @param {string} [比率]
   * 將 `Player` 的縱橫比設置為的值。
   *
   * @return {字符串|未定義}
   * - 獲取時 `Player` 的當前縱橫比。
   * - 設置時未定義
   */
  aspectRatio(比率){
    如果(比率===未定義){
      返回this.aspectRatio_;
    }

    // 檢查寬度:高度格式
    如果(!(/^\ d+\:\ D+$/). 測試 (比率)) {
      throw new Error('為寬高比提供的值不正確。格式應該是寬度:高度,例如 16:9。');
    }
    this.aspectRatio_ = 比率;

    // 我們假設如果您設置縱橫比,您需要流體模式,
    // 因為在固定模式下你可以自己計算寬度和高度。
    這個。流體(真);

    this.updateStyleEl_();
  }

  /**
   * 更新 `Player` 元素的樣式(高度、寬度和縱橫比)。
   *
   * @私人的
   * @listens Tech#loadedmetadata
   */
  updateStyleEl_() {
    如果(window.VIDEOJS_NO_DYNAMIC_STYLE === true){
      常量寬度 = 這個。寬度 _ === '數字' 的類型?這個寬度 _:這個。選項 _ 寬度;
      常量高度 = 此類型。高度 _ === '數字'?這個。高度 _:這個。選項 _ 高度;
      const techEl = this.tech_ && this.tech_.el();

      如果(技術){
        如果(寬度 >= 0){
          techEl.width = 寬度;
        }
        如果(高度 >= 0){
          techEl.height = 高度;
        }
      }

      返回;
    }

    讓寬度;
    讓身高;
    讓縱橫比;
    讓 idClass;

    // 縱橫比要么直接使用,要么計算寬高。
    如果(這個。方面的 _!== 未定義 && 這個。方面的 _!== '自動'){
      // 使用任何專門設置的 aspectRatio
      aspectRatio = this.aspectRatio_;
    } else if (this.videoWidth() > 0) {
      // 否則嘗試從視頻元數據中獲取縱橫比
      aspectRatio = this.videoWidth() + ':' + this.videoHeight();
    }其他{
      // 或者使用默認值。視頻元素的是 2:1,但 16:9 更常見。
      相關交易 = '16:9';
    }

    // 獲取我們可以用來計算尺寸的小數比率
    const ratioParts = aspectRatio.split(':');
    const ratioMultiplier = ratioParts[1] / ratioParts[0];

    如果(this.width_!==未定義){
      // 使用專門設置的任何寬度
      width = this.width_;
    } 否則,如果(這個。高度 _!== 未定義){
      // 或者,如果已設置高度,則根據縱橫比計算寬度
      width = this.height_ / ratioMultiplier;
    }其他{
      // 或者使用視頻的元數據,或者使用視頻el默認的300
      寬度 = this.videoWidth() || 300;
    }

    如果(this.height_!==未定義){
      // 使用任何專門設置的高度
      height = this.height_;
    }其他{
      // 否則根據比率和寬度計算高度
      高度 = 寬度 * 比率乘數;
    }

    // 通過以字母字符開頭來確保 CSS 類有效
    如果 ((/^[^a-zA-Z]/).test(this.id())) {
      idClass = '維度-' + this.id();
    }其他{
      idClass = this.id() + '-尺寸';
    }

    // 確保樣式元素的正確類仍在播放器上
    this.addClass(idClass);

    stylesheet.setTextContent(this.styleEl_, `
      .${idClass} {
        寬度:${width}px;
        高度:${height}px;
      }

      。$ {ID類}. VJ-流體:不 (. VJS-音頻僅模式) {
        填充頂部:${ratioMultiplier * 100}%;
      }
    `);
  }

  /**
   *加載/創建播放實例{@link Tech}包括元素
   * 和 API 方法。然後將 Tech 元素附加到 Player 中作為子元素。
   *
   * @param {string} 技術名稱
   * 播放技術名稱
   *
   * @param {string} 來源
   * 視頻源
   *
   * @私人的
   */
  loadTech_(技術名稱,來源){

    // 暫停並移除當前播放技術
    如果(this.tech_){
      這個.unloadTech_();
    }

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

    // 一旦我們使用另一種技術,就擺脫 HTML5 視頻標籤
    如果(標題技術名稱!== 'HTML5' && 這個。標籤){
      Tech.getTech('Html5').disposeMediaElement(this.tag);
      this.tag.player = null;
      this.tag = null;
    }

    this.techName_ = titleTechName;

    // 關閉 API 訪問,因為我們正在加載一項可能異步加載的新技術
    this.isReady_ = false;

    讓 autoplay = this.autoplay();

    // 如果自動播放是一個字符串(或帶有 normalizeAutoplay: true 的 `true`),我們將 false 傳遞給技術人員
    // 因為播放器將在 `loadstart` 上處理自動播放
    if (typeof this.autoplay() === 'string' || this.autoplay() === true && 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.language(),
      'playerElIngest': this.playerElIngest_ ||錯誤的,
      'vtt.js': this.options_['vtt.js'],
      「罐頭儲罐器」:!!這個。選項 _。技術海報,
      'enableSourceset': this.options_.enableSourceset,
      '承諾':this.options_.Promise
    };

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

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

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

    如果(這個。標籤){
      techOptions.tag = this.tag;
    }

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

    // 初始化技術實例
    const TechClass = Tech.getTech(技術名稱);

    如果(!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_, event, (e) => this[`handleTech${toTitleCase(event)}_`](e));
    });

    Object.keys(TECH_EVENTS_QUEUE).forEach((event) => {
      this.on(this.tech_, event, (eventObj) => {
        如果 (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
          this.queuedCallbacks_.push({
            回調:this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
            事件:事件對象
          });
          返回;
        }
        這個[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
      });
    });

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

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

    如果(這個。控件()&&!這個。使用本地控制()){
      this.addTechControlsListeners_();
    }

    // 如果 tech 元素不存在,則在 DOM 中添加它
    // 如果使用 Html5,請確保不要插入原始視頻元素
    如果(這個 .tech_el()。父節點!== 這個 .el () && (標題技術名稱!「HTML5」||!這個。標籤)){
      Dom.prependTo(this.tech_.el(), this.el());
    }

    // 在加載第一個技術後去掉原始視頻標籤引用
    如果(這個。標籤){
      this.tag.player = null;
      this.tag = null;
    }
  }

  /**
   * 卸載並處理當前播放 {@link Tech}。
   *
   * @私人的
   */
  unloadTech_() {
    // 保存當前的文本軌道,以便我們可以在下一個技術中重複使用相同的文本軌道
    TRACK_TYPES.names.forEach((名稱) => {
      const props = TRACK_TYPES[名稱];

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

    this.isReady_ = false;

    這個.tech_.dispose();

    this.tech_ = false;

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

    this.isPosterFromTech_ = false;
  }

  /**
   * 返回對當前 {@link Tech} 的引用。
   * 默認情況下,它會打印一條關於直接使用該技術的危險的警告
   * 但是傳入的任何參數都會使警告靜音。
   *
   * @param {*} [安全]
   * 傳入的任何內容都會使警告靜音
   *
   * @return {技術}
   *技術
   */
  技術(安全){
    如果(安全===未定義){
      log.warn('直接使用技術可能很危險。我希望你知道你在做什麼。\\n' +
        '有關詳細信息,請參閱 https://github.com/videojs/video.js/issues/2617。\\n');
    }

    返回this.tech_;
  }

  /**
   * 為播放元素設置點擊和触摸監聽器
   *
   * - 在台式機上:點擊視頻本身將切換播放
   * - 在移動設備上:點擊視頻切換控件
   * 這是通過在活動和之間切換用戶狀態來完成的
   * 無效
   * - 點擊可以表示用戶已激活或已變為非活動狀態
   * 例如,快速點擊一部 iPhone 電影應該會顯示控件。其他
   * 快速點擊應該再次隱藏它們(表示用戶處於非活動狀態
   * 查看狀態)
   * - 除此之外,我們仍然希望用戶在之後被視為不活躍
   * 幾秒鐘的不活動。
   *
   * > 注意:iOS 交互的唯一部分我們無法通過此設置模仿
   * 觸摸並按住算作活動的視頻元素,以便
   * 保持控件顯示,但這應該不是問題。觸摸並按住
   * 在任何控件上仍然會讓用戶保持活動狀態
   *
   * @私人的
   */
  addTechControlsListeners_() {
    // 確保移除所有之前的監聽器,以防我們被多次調用。
    this.removeTechControlsListeners_();

    this.on(this.tech_, 'click', 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_);

    // tap 監聽器需要在 touchend 監聽器之後,因為 tap
    // 偵聽器在設置 userActive(false) 時取消任何 reportedUserActivity
    this.on(this.tech_, 'tap', this.boundHandleTechTap_);
  }

  /**
   * 刪除用於點擊和點擊控件的偵聽器。這是需要的
   * 切換到禁用的控件,點擊/觸摸應該什麼都不做。
   *
   * @私人的
   */
  removeTechControlsListeners_() {
    // 我們不想只使用 `this.off()` 因為可能還有其他需要
    // 由擴展它的技術人員添加的偵聽器。
    this.off(this.tech_, 'tap', this.boundHandleTechTap_);
    this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
    this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
    this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
    this.off(this.tech_, 'click', this.boundHandleTechClick_);
    this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
  }

  /**
   * 玩家等待技術準備就緒
   *
   * @私人的
   */
  handleTechReady_() {
    this.triggerReady();

    // 保持和之前一樣的音量
    如果(this.cache_.volume){
      this.techCall_('setVolume', this.cache_.volume);
    }

    // 查看技術人員在加載時是否找到更高分辨率的海報
    this.handleTechPosterChange_();

    // 如果可用,更新持續時間
    this.handleTechDurationChange_();
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的 `loadstart` 事件。這個
   * 如果它是第一個加載開始,函數也會觸發 {@link Player#firstplay}
   * 對於視頻。
   *
   * @fires Player#loadstart
   * @fires Player#firstplay
   * @listens Tech#loadstart
   * @私人的
   */
  handleTechLoadStart_() {
    // 去做:更新為使用“emptied”事件。參見#1277。

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

    // 重置錯誤狀態
    這個錯誤(空);

    // 更新時長
    this.handleTechDurationChange_();

    // 如果它已經在播放,我們現在想觸發一個 firstplay 事件。
    // firstplay 事件同時依賴於 play 和 loadstart 事件
    // 對於新源,這可以以任何順序發生
    如果(!this.paused()){
      /**
       * 當用戶代理開始尋找媒體數據時觸發
       *
       * @event Player#loadstart
       * @type {EventTarget~Event}
       */
      this.trigger('loadstart');
      this.trigger('firstplay');
    }其他{
      // 重置 hasStarted 狀態
      這個.hasStarted(false);
      this.trigger('loadstart');
    }

    // 自動播放發生在瀏覽器加載開始之後,
    // 所以我們模仿這種行為
    手動自動播放 _(這個自動播放()=== 真 && 這個。選項 _。正常自動播放?'播放':這個。自動播放());
  }

  /**
   * 處理自動播放字符串值,而不是典型的布爾值
   * 應該由技術處理的值。請注意,這不是
   * 任何規範的一部分。有效值及其作用可以是
   * 在 Player#autoplay() 的自動播放吸氣器上找到
   */
  手動自動播放_(類型){
    如果(!這種技術 _ || 類型的類型!== '字符串'){
      返回;
    }

    // 保存原始的 muted() 值,將 muted 設置為 true,並嘗試播放 ()。
    // 在 promise 被拒絕時,從保存的值中恢復靜音
    const resolveMuted = () => {
      const previouslyMuted = this.muted();

      這個。靜音(真);

      const restoreMuted = () => {
        這個。靜音(以前靜音);
      };

      // 在播放終止時恢復靜音
      this.playTerminatedQueue_.push(restoreMuted);

      const mutedPromise = this.play();

      如果(!isPromise(mutedPromise)){
        返回;
      }

      返回 mutedPromise.catch(err => {
        恢復靜音();
        throw new Error(`Rejection at manualAutoplay.恢復靜音值。 ${錯了嗎?錯誤:''}`);
      });
    };

    讓承諾;

    // 如果靜音默認為 true
    // 我們唯一能做的就是調用 play
    如果(鍵入 === '任何' &&!這個。靜音()){
      promise = this.play();

      如果(isPromise(承諾)){
        promise = promise.catch(resolveMuted);
      }
    } 否則,如果(類型 === '靜音' &&!這個。靜音()){
      承諾 = resolveMuted();
    }其他{
      promise = this.play();
    }

    如果(!isPromise(承諾)){
      返回;
    }

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

  /**
   *更新內部源緩存,以便我們從中返回正確的源
   * `src()`、`currentSource()` 和 `currentSources()`。
   *
   * > 注意:如果傳入的源存在,則不會更新 `currentSources`
   * 在當前的 `currentSources` 緩存中。
   *
   *
   * @param {Tech~SourceObject} srcObj
   * 將我們的緩存更新為的字符串或對像源。
   */
  updateSourceCaches_(srcObj = '') {

    讓 src = srcObj;
    讓類型 = '';

    如果(類型的 src!== '字符串'){
      src = srcObj.src;
      type = srcObj.type;
    }

    // 確保所有緩存都設置為默認值
    // 防止空檢查
    this.cache_.source = this.cache_.source || {};
    this.cache_.sources = this.cache_.sources || [];

    // 嘗試獲取傳入的 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 = [];

    對於(讓我 = 0; 我 < 來源。長度; 我 ++){
      const sourceObj = Dom.getAttributes(sourceEls[i]);

      sourceElSources.push(sourceObj);

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

    // 如果我們有匹配的源 el 但沒有匹配的源
    // 當前源緩存不是最新的
    如果(匹配源代碼。長度 &&!匹配源. 長度) {
      this.cache_.sources = sourceElSources;
    // 如果我們沒有匹配的源或源 els 設置
    // 源緩存到 `currentSource` 緩存
    } 否則,如果(!匹配源. 長度) {
      this.cache_.sources = [this.cache_.source];
    }

    // 更新技術 `src` 緩存
    this.cache_.src = src;
  }

  /**
   * *EXPERIMENTAL* 在 {@link Tech} 上設置或更改源時觸發
   * 導致媒體元素重新加載。
   *
   * 它將為初始源和每個後續源觸發。
   * 此事件是來自 Video.js 的自定義事件,由 {@link Tech} 觸發。
   *
   * 此事件的事件對象包含一個 `src` 屬性,該屬性將包含源
   * 事件觸發時可用。這通常只有在 Video.js 時才有必要
   * 在更改源時正在切換技術。
   *
   * 當在播放器(或媒體元素)上調用 `load` 時它也會被觸發
   * 因為 {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|`load` 的規範
   * 表示資源選擇算法需要中止並重新啟動。
   * 在這種情況下,很可能會將 `src` 屬性設置為
   * 空字符串 `""` 表示我們不知道源是什麼但是
   *它正在改變。
   *
   * *此事件目前仍處於試驗階段,可能會在次要版本中發生變化。*
   * __要使用它,請將 `enableSourceset` 選項傳遞給播放器.__
   *
   * @event Player#sourceset
   * @type {EventTarget~Event}
   * @prop {string} 源
   * 觸發 `sourceset` 時可用的源 url。
   * 如果我們不知道來源是什麼,它將是一個空字符串
   * 但知道來源會改變。
   */
  /**
   * 重新觸發由 {@link Tech} 觸發的 `sourceset` 事件。
   *
   * @fires Player#sourceset
   * @listens Tech#sourceset
   * @私人的
   */
  handleTechSourceset_(事件){
    // 只在有源時更新源緩存
    // 未使用播放器 api 更新
    如果(!this.changingSrc_){
      讓 updateSourceCaches = (src) => this.updateSourceCaches_(src);
      const playerSrc = this.currentSource().src;
      const eventSrc = event.src;

      // 如果我們有一個不是 blob 的 playerSrc 和一個是 blob 的 tech src
      如果(播放器 SRC &&!測試(玩家 SRC)&&(/^ BLOB:/)。測試(事件 RC)){

        // 如果技術源和播放器源都更新了,我們假設
        // 像@videojs/http-streaming 這樣的東西做了源集並跳過更新源緩存。
        如果(!這個最後來源 _ ||(這是最後來源 _ 技術!== 事件 && 這個。最後來源 _. 播放器!== 播放器 SRC)) {
          updateSourceCaches = () => {};
        }
      }

      // 立即將源更新為初始源
      // 在某些情況下,這將是空字符串
      updateSourceCaches(eventSrc);

      // 如果 `sourceset` `src` 是一個空字符串
      // 等待 `loadstart` 將緩存更新為 `currentSrc`。
      // 如果 sourceset 發生在 `loadstart` 之前,我們重置狀態
      如果(!event.src){
        this.tech_.any(['sourceset', 'loadstart'], (e) => {
          // 如果源集發生在 `loadstart` 之前
          // 與這個 `handleTechSourceset_` 無關
          // 將再次調用,這將在那里處理。
          如果(e.type === 'sourceset'){
            返回;
          }

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

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

    這個。觸發({
      來源:事件.src,
      類型:'源集'
    });
  }

  /**
   * 添加/刪除 vjs-has-started 類
   *
   * @fires Player#firstplay
   *
   * @param {boolean} 請求
   * - 真:添加類
   * - false:刪除類
   *
   * @return {布爾值}
   * hasStarted_ 的布爾值
   */
  已開始(請求){
    如果(請求===未定義){
      // 充當 getter,如果我們沒有更改請求
      返回 this.hasStarted_;
    }

    如果(請求=== this.hasStarted_){
      返回;
    }

    this.hasStarted_ = 請求;

    如果(this.hasStarted_){
      this.addClass('vjs-has-started');
      this.trigger('firstplay');
    }其他{
      this.removeClass('vjs-has-started');
    }
  }

  /**
   * 媒體開始或恢復播放時觸發
   *
   * @see [規範]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
   * @fires Player#play
   * @listens Tech#play
   * @私人的
   */
  handleTechPlay_() {
    this.removeClass('vjs-ended');
    this.removeClass('vjs-paused');
    this.addClass('vjs-playing');

    // 當用戶點擊播放時隱藏海報
    這個.hasStarted(真);
    /**
     * 每當 {@link Tech#play} 事件發生時觸發。表示
     * 播放已開始或恢復。
     *
     * @event 播放器#play
     * @type {EventTarget~Event}
     */
    this.trigger('播放');
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的 `ratechange` 事件。
   *
   * 如果播放速率為零時有任何事件排隊,則觸發
   * 現在的那些事件。
   *
   * @私人的
   * @method Player#handleTechRateChange_
   * @fires Player#ratechange
   * @listens Tech#ratechange
   */
  handleTechRateChange_() {
    如果 (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
      this.queuedCallbacks_.forEach((queued) => queued.callback(queued.event));
      this.queuedCallbacks_ = [];
    }
    this.cache_.lastPlaybackRate = this.tech_.playbackRate();
    /**
     * 當音頻/視頻的播放速度改變時觸發
     *
     * @event Player#ratechange
     * @type {事件}
     */
    this.trigger('ratechange');
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的“等待”事件。
   *
   * @fires Player#waiting
   * @listens Tech#waiting
   * @私人的
   */
  handleTechWaiting_() {
    this.addClass('vjs-waiting');
    /**
     * DOM 元素上的 readyState 更改導致播放停止。
     *
     * @event Player#waiting
     * @type {EventTarget~Event}
     */
    this.trigger('等待');

    // 瀏覽器可能會在等待事件後發出一個 timeupdate 事件。為了阻止
    // 過早移除等待類,等待時間改變。
    const timeWhenWaiting = this.currentTime();
    const timeUpdateListener = () => {
      如果(等待時間!== 這個。當前時間()){
        this.removeClass('vjs-waiting');
        this.off('timeupdate', timeUpdateListener);
      }
    };

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

  /**
   * 重新觸發由 {@link Tech} 觸發的 `canplay` 事件。
   * > 注意:這在瀏覽器之間不一致。見#1351
   *
   * @fires Player#canplay
   * @listens Tech#canplay
   * @私人的
   */
  handleTechCanPlay_() {
    this.removeClass('vjs-waiting');
    /**
     * 媒體的 readyState 為 HAVE_FUTURE_DATA 或更高。
     *
     * @event Player#canplay
     * @type {EventTarget~Event}
     */
    this.trigger('canplay');
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的 `canplaythrough` 事件。
   *
   * @fires Player#canplaythrough
   * @listens Tech#canplaythrough
   * @私人的
   */
  handleTechCanPlayThrough_() {
    this.removeClass('vjs-waiting');
    /**
     * 媒體的 readyState 為 HAVE_ENOUGH_DATA 或更高。這意味著
     * 無需緩衝即可播放整個媒體文件。
     *
     * @event Player#canplaythrough
     * @type {EventTarget~Event}
     */
    this.trigger('canplaythrough');
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的“播放”事件。
   *
   * @fires Player#playing
   * @listens Tech#playing
   * @私人的
   */
  handleTechPlaying_() {
    this.removeClass('vjs-waiting');
    /**
     * 媒體不再被阻止播放,並開始播放。
     *
     * @event Player#playing
     * @type {EventTarget~Event}
     */
    this.trigger('播放');
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的 `seeking` 事件。
   *
   * @fires Player#seeking
   * @listens Tech#seeking
   * @私人的
   */
  handleTechSeeking_() {
    this.addClass('vjs-seeking');
    /**
     * 每當玩家跳到新時間時觸發
     *
     * @event Player#seeking
     * @type {EventTarget~Event}
     */
    this.trigger('尋求');
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的 `seeked` 事件。
   *
   * @fires Player#seeked
   * @listens Tech#seeked
   * @私人的
   */
  handleTechSeeked_() {
    this.removeClass('vjs-seeking');
    this.removeClass('vjs-ended');
    /**
     * 當玩家完成跳轉到新時間時觸發
     *
     * @event Player#seeked
     * @type {EventTarget~Event}
     */
    this.trigger('seeked');
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的“firstplay”事件。
   *
   * @fires Player#firstplay
   * @listens Tech#firstplay
   * @deprecated 自 6.0 起,firstplay 事件已棄用。
   * 從 6.0 開始,將 `starttime` 選項傳遞給玩家和 firstplay 事件已被棄用。
   * @私人的
   */
  handleTechFirstPlay_() {
    // 如果指定了第一個開始時間屬性
    // 然後我們將以秒為單位從給定的偏移量開始
    如果(this.options_.starttime){
      log.warn('將 `starttime` 選項傳遞給播放器將在 6.0 中被棄用');
      this.currentTime(this.options_.starttime);
    }

    this.addClass('vjs-has-started');
    /**
     * 第一次播放視頻時觸發。不是 HLS 規範的一部分,這是
     * 可能還不是最好的實現,所以請謹慎使用。如果你沒有
     * 阻止播放的原因,使用「我的圖層。一('播放'); `代替。
     *
     * @event Player#firstplay
     * @deprecated 自 6.0 起,firstplay 事件已棄用。
     * @type {EventTarget~Event}
     */
    this.trigger('firstplay');
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的“暫停”事件。
   *
   * @fires Player#暫停
   * @listens Tech#pause
   * @私人的
   */
  handleTechPause_() {
    this.removeClass('vjs-playing');
    this.addClass('vjs-paused');
    /**
     * 媒體暫停時觸發
     *
     * @event Player#暫停
     * @type {EventTarget~Event}
     */
    this.trigger('暫停');
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的 `ended` 事件。
   *
   * @fires Player#ended
   * @listens Tech#ended
   * @私人的
   */
  handleTechEnded_() {
    this.addClass('vjs-ended');
    this.removeClass('vjs-waiting');
    如果(this.options_.loop){
      這個.currentTime(0);
      這個.play();
    } 否則,如果(!這個。暫停()){
      這個。暫停();
    }

    /**
     * 到達媒體資源末尾時觸發(currentTime == duration)
     *
     * @event Player#ended
     * @type {EventTarget~Event}
     */
    this.trigger('結束');
  }

  /**
   * 當媒體資源的持續時間首次已知或更改時觸發
   *
   * @listens Tech#durationchange
   * @私人的
   */
  handleTechDurationChange_() {
    this.duration(this.techGet_('duration'));
  }

  /**
   * 處理媒體元素上的點擊以播放/暫停
   *
   * @param {EventTarget~Event} 事件
   * 導致該函數觸發的事件
   *
   * @listens Tech#click
   * @私人的
   */
  handleTechClick_(事件){
    // 當控件被禁用時,點擊不應切換播放,因為
    // 點擊被認為是一個控件
    如果(!this.controls_){
      返回;
    }

    如果 (
      this.options_ === undefined ||
      this.options_.userActions === undefined ||
      this.options_.userActions.click === undefined ||
      此選項 _. 使用者動作。按一下!== 假
    ) {

      如果 (
        這個。選項 _!== 未定義的 &&
        這個。選項 _. 用戶操作!== 未定義的 &&
        typeof this.options_.userActions.click === '函數'
      ) {

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

      } else if (this.paused()) {
        silencePromise(this.play());
      }其他{
        這個。暫停();
      }
    }
  }

  /**
   *處理雙擊媒體元素進入/退出全屏
   *
   * @param {EventTarget~Event} 事件
   * 導致該函數觸發的事件
   *
   * @listens Tech#dblclick
   * @私人的
   */
  handleTechDoubleClick_(事件){
    如果(!this.controls_){
      返回;
    }

    // 我們不想切換全屏狀態
    // 在控制欄或模式框內雙擊時
    const inAllowedEls = Array.prototype.some.call(
      這個 .$$('.vJS-控制欄,.vJS 模態對話 '),
      el => el.contains(event.target)
    );

    如果(!inAllowedEls){
      /*
       * options.userActions.doubleClick
       *
       * 如果“undefined”或“true”,如果控件存在,雙擊切換全屏
       * 設置為“false”以禁用雙擊處理
       * 設置為替代外部雙擊處理程序的函數
       */
      如果 (
        this.options_ === undefined ||
        this.options_.userActions === undefined ||
        this.options_.userActions.doubleClick === undefined ||
        這個。選項 _. 用戶操作。雙擊!== 假
      ) {

        如果 (
          這個。選項 _!== 未定義的 &&
          這個。選項 _. 用戶操作!== 未定義的 &&
          typeof this.options_.userActions.doubleClick === '函數'
        ) {

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

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

  /**
   * 處理媒體元素上的點擊。它將切換用戶
   *活動狀態,隱藏和顯示控件。
   *
   * @listens Tech#tap
   * @私人的
   */
  handleTechTap_() {
    這個。用戶激活(!這個。用戶激活());
  }

  /**
   *處理觸摸開始
   *
   * @listens Tech#touchstart
   * @私人的
   */
  handleTechTouchStart_() {
    this.userWasActive = this.userActive();
  }

  /**
   *手柄觸摸移動
   *
   * @listens Tech#touchmove
   * @私人的
   */
  handleTechTouchMove_() {
    如果(this.userWasActive){
      這個.reportUserActivity();
    }
  }

  /**
   * 處理觸摸結束
   *
   * @param {EventTarget~Event} 事件
   * 觸發的touchend事件
   * 這個函數
   *
   * @listens Tech#touchend
   * @私人的
   */
  handleTechTouchEnd_(事件){
    // 阻止鼠標事件的發生
    如果(事件。可取消){
      事件.preventDefault();
    }
  }

  /**
   * SWF 上的原生點擊事件不會在 IE11、Win8.1RT 上觸發
   * 改為使用從 SWF 內部觸發的 stageclick 事件
   *
   * @私人的
   * @listens stageclick
   */
  handleStageClick_() {
    這個.reportUserActivity();
  }

  /**
   * @私人的
   */
  toggleFullscreenClass_() {
    如果(this.isFullscreen()){
      this.addClass('vjs-fullscreen');
    }其他{
      this.removeClass('vjs-fullscreen');
    }
  }

  /**
   * 當文檔 fschange 事件觸發時調用它
   */
  documentFullscreenChange_(e) {
    const targetPlayer = e.target.player;

    // 如果另一個玩家是全屏的
    // 對 targetPlayer 進行空檢查,因為舊的 firefox 會將文檔作為 e.target
    如果(目標播放器 && 目標播放器!== 這個){
      返回;
    }

    const el = this.el();
    讓 isFs = 文檔 [this.fsApi_.fullscreenElement] === el;

    如果(!isFs && el.matches){
      isFs = el.matches(':' + this.fsApi_.fullscreen);
    } 否則,如果(!ISFS & El.ms 匹配選擇器) {
      isFs = el.msMatchesSelector(':' + this.fsApi_.fullscreen);
    }

    this.isFullscreen(isFs);
  }

  /**
   *處理技術全屏變化
   *
   * @param {EventTarget~Event} 事件
   * 觸發此功能的全屏更改事件
   *
   * @param {Object} 數據
   * 隨事件發送的數據
   *
   * @私人的
   * @listens Tech#fullscreenchange
   * @fires Player#fullscreenchange
   */
  handleTechFullscreenChange_(事件,數據){
    如果(數據){
      如果(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('fullscreenerror', err);
  }

  /**
   * @私人的
   */
  togglePictureInPictureClass_() {
    如果 (this.isInPictureInPicture()) {
      this.addClass('vjs-畫中畫');
    }其他{
      this.removeClass('vjs-畫中畫');
    }
  }

  /**
   * Handle Tech 輸入畫中畫。
   *
   * @param {EventTarget~Event} 事件
   * 觸發該函數的enterpictureinpicture事件
   *
   * @私人的
   * @listens Tech#enterpictureinpicture
   */
  handleTechEnterPictureInPicture_(事件){
    this.isInPictureInPicture(true);
  }

  /**
   * 處理技術休假畫中畫。
   *
   * @param {EventTarget~Event} 事件
   * 觸發該函數的畫中畫事件
   *
   * @私人的
   * @listens Tech#leavepictureinpicture
   */
  handleTechLeavePictureInPicture_(事件){
    this.isInPictureInPicture(false);
  }

  /**
   * 在加載音頻/視頻期間發生錯誤時觸發。
   *
   * @私人的
   * @listens 技術#error
   */
  handleTechError_() {
    const error = this.tech_.error();

    這個錯誤(錯誤);
  }

  /**
   * 重新觸發由 {@link Tech} 觸發的 `textdata` 事件。
   *
   * @fires Player#textdata
   * @listens Tech#textdata
   * @私人的
   */
  handleTechTextData_() {
    讓數據=空;

    如果(參數長度 > 1){
      數據=參數[1];
    }

    /**
     * 當我們從 tech 獲得文本數據事件時觸發
     *
     * @event Player#textdata
     * @type {EventTarget~Event}
     */
    this.trigger('文本數據', 數據);
  }

  /**
   * 獲取緩存值的對象。
   *
   * @return {對象}
   * 獲取當前對象緩存
   */
  getCache() {
    返回this.cache_;
  }

  /**
   * 重置內部緩存對象。
   *
   * 在播放器構造函數或重置方法之外使用此函數可能
   * 有意想不到的副作用。
   *
   * @私人的
   */
  重置緩存_() {
    this.cache_ = {

      // 現在,currentTime 並沒有_真正_緩存,因為它總是
      // 從技術中檢索(參見:currentTime)。但是,為了完整性,
      // 我們在這裡將其設置為零以確保如果我們確實開始實際緩存
      // 它,我們將它與其他所有內容一起重置。
      當前時間:0,
      初始化時間:0,
      inactivityTimeout:this.options_.inactivityTimeout,
      持續時間:南,
      最後一卷:1、
      lastPlaybackRate: this.defaultPlaybackRate(),
      媒體:空,
      來源:'',
      來源: {},
      資料來源:[],
      回放率:[],
      體積:1
    };
  }

  /**
   * 將值傳遞給播放技術
   *
   * @param {字符串} [方法]
   * 調用的方法
   *
   * @param {對象} 參數
   * 要傳遞的參數
   *
   * @私人的
   */
  techCall_(方法,參數){
    // 如果還沒有準備好,就在準備好時調用方法

    這個。準備好(功能(){
      如果(中間件中的方法。allowedSetters){
        返回 middleware.set(this.middleware_, this.tech_, method, arg);

      } else if (middleware.allowedMediators 中的方法) {
        返回 middleware.mediate(this.middleware_, this.tech_, method, arg);
      }

      嘗試{
        如果(this.tech_){
          this.tech_[方法](arg);
        }
      } 抓住 (e) {
        日誌(e);
        扔 e;
      }
    }, 真的);
  }

  /**
   * 接聽電話等不及技術人員,有時甚至不需要。
   *
   * @param {string} 方法
   * 技術方法
   *
   * @return {函數|未定義}
   * 方法或未定義
   *
   * @私人的
   */
  techGet_(方法){
    如果(!this.tech_ || !this.tech_.isReady_){
      返回;
    }

    如果(中間件中的方法。allowedGetters){
      返回 middleware.get(this.middleware_, this.tech_, method);

    } else if (middleware.allowedMediators 中的方法) {
      返回 middleware.mediate(this.middleware_, this.tech_, method);
    }

    // Flash 喜歡在您隱藏或重新定位時死掉並重新加載。
    // 在這些情況下,對象方法消失,我們得到錯誤。
    // 去做:這對 Flash 以外的技術人員來說是必需的嗎?
    // 當這種情況發生時,我們將捕獲錯誤並通知技術人員它還沒有準備好。
    嘗試{
      返回 this.tech_[方法]();
    } 抓住 (e) {

      // 當構建額外的技術庫時,可能還沒有定義預期的方法
      如果(this.tech_[方法] ===未定義){
        log(`Video.js: ${method} 方法沒有為 ${this.techName_} 播放技術定義。`, e);
        扔 e;
      }

      // 當方法在對像上不可用時,它會拋出 TypeError
      如果(e.name === 'TypeError'){
        log(`Video.js: ${method} 在 ${this.techName_} 播放技術元素上不可用。`, e);
        this.tech_.isReady_ = false;
        扔 e;
      }

      // 如果錯誤未知,只記錄並拋出
      日誌(e);
      扔 e;
    }
  }

  /**
   * 嘗試在第一時間開始播放。
   *
   * @return {承諾|未定義}
   * 如果瀏覽器支持 Promise(或一個
   * 作為選項傳入)。這個承諾將在
   * 播放的返回值。如果這是未定義的,它將滿足
   * promise 鏈 否則 promise 鏈將在
   * 遊戲中的承諾已兌現。
   */
  玩() {
    const PromiseClass = this.options_.Promise || window.Promise;

    如果(承諾類){
      返回新的 PromiseClass((resolve) => {
        this.play_(解決);
      });
    }

    返回 this.play_();
  }

  /**
   * 播放的實際邏輯,需要一個將在
   * 播放的返回值。如果有的話,這使我們能夠解決遊戲承諾
   * 是現代瀏覽器上的一個。
   *
   * @私人的
   * @param {函數} [回調]
   * 實際調用技術人員播放時應調用的回調
   */
  play_(callback = silencePromise) {
    this.playCallbacks_.push(回調);

    常量是基本 = 布爾值 (!這個。更改 SRC_ &&(這個 .src()|| 這個 .CurrentsRC()));

    // 對 play_ 的調用有點像 `one` 事件函數
    如果(this.waitToPlay_){
      this.off(['ready', 'loadstart'], this.waitToPlay_);
      this.waitToPlay_ = null;
    }

    // 如果播放器/技術沒有準備好或者 src 本身沒有準備好
    // 排隊調用以在 `ready` 或 `loadstart` 播放
    如果(!這是。島上 _ ||!伊斯蘭迪) {
      this.waitToPlay_ = (e) => {
        這個.play_();
      };
      this.one(['ready', 'loadstart'], this.waitToPlay_);

      // 如果我們在 Safari 中,loadstart 很有可能會在手勢時間段後觸發
      // 在這種情況下,我們需要通過調用 load 來準備視頻元素,以便它能及時準備好
      如果(!這是一個 && (瀏覽器. 是任何 Safari 瀏覽器 || 瀏覽器.
        這個.load();
      }
      返回;
    }

    // 如果播放器/技術準備就緒並且我們有源,我們可以嘗試播放。
    const val = this.techGet_('play');

    // 如果返回值為 null,則播放終止
    如果(val === null){
      this.runPlayTerminatedQueue_();
    }其他{
      this.runPlayCallbacks_(val);
    }
  }

  /**
   * 這些功能將在播放終止時運行。如果玩
   * runPlayCallbacks_ 運行這些函數將不會運行。這讓我們
   * 區分終止播放和實際調用播放。
   */
  runPlayTerminatedQueue_() {
    const queue = this.playTerminatedQueue_.slice(0);

    this.playTerminatedQueue_ = [];

    queue.forEach(函數(q){
      q();
    });
  }

  /**
   * 當回調播放延遲時,我們必須運行這些
   * 在技術上實際調用播放時的回調。這個功能
   * 運行延遲的回調並接受返回值
   * 來自技術。
   *
   * @param {undefined|Promise} 值
   * 技術的返回值。
   */
  runPlayCallbacks_(val) {
    const callbacks = this.playCallbacks_.slice(0);

    this.playCallbacks_ = [];
    // 清除播放終止隊列,因為我們完成了真正的播放
    this.playTerminatedQueue_ = [];

    回調.forEach(函數(cb){
      cb(val);
    });
  }

  /**
   * 暫停視頻播放
   *
   * @return {玩家}
   * 調用此函數的播放器對象的引用
   */
  暫停() {
    this.techCall_('暫停');
  }

  /**
   * 檢查播放器是否暫停或尚未播放
   *
   * @return {布爾值}
   * - false:如果媒體當前正在播放
   * - 真:如果媒體當前沒有播放
   */
  暫停(){
    // paused 的初始狀態應該是 true(在 Safari 中它實際上是 false)
    返回 (this.techGet_('paused') === false) ?假:真;
  }

  /**
   * 獲取表示用戶當前時間範圍的 TimeRange 對象
   * 有玩過。
   *
   * @return {TimeRange}
   * 一個時間範圍對象,表示具有的所有時間增量
   * 已播放。
   */
  播放(){
    返回 this.techGet_('played') ||創建時間範圍(0,0);
  }

  /**
   * 返回用戶是否正在“清理”。擦洗是
   * 當用戶點擊了進度條句柄並且是
   * 沿著進度條拖動它。
   *
   * @param {boolean} [isScrubbing]
   * 用戶是否正在擦洗
   *
   * @return {布爾值}
   * 獲取時擦洗的值
   */
  擦洗(isScrubbing){
    如果(typeof isScrubbing === 'undefined'){
      返回 this.scrubbing_;
    }
    這個。擦洗 _ =!!擦洗;
    this.techCall_('setScrubbing', this.scrubbing_);

    如果(isScrubbing){
      this.addClass('vjs-scrubbing');
    }其他{
      this.removeClass('vjs-scrubbing');
    }
  }

  /**
   * 獲取或設置當前時間(以秒為單位)
   *
   * @param {number|string} [秒]
   * 以秒為單位尋找的時間
   *
   * @return {數字}
   * - 獲取時的當前時間(以秒為單位)
   */
  當前時間(秒){
    如果(類型秒!== '未定義') {
      如果 (秒 <  0) {
        秒 = 0;
      }
      如果(!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_){
        this.cache_.initTime = seconds;
        this.off('canplay', this.boundApplyInitTime_);
        this.one('canplay', this.boundApplyInitTime_);
        返回;
      }
      this.techCall_('setCurrentTime', seconds);
      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 {number} [秒數]
   * 以秒為單位設置的視頻時長
   *
   * @return {數字}
   * - 獲取時視頻的持續時間(以秒為單位)
   */
  持續時間(秒){
    如果(秒 === 未定義){
      // 如果持續時間未知,則返回 NaN
      返回這個。緩存 _ 持續時間!== 未定義?這個. 緩存 _. 持續時間:南;
    }

    秒 = parseFloat(秒);

    // 對 Infinity 進行標準化以發出視頻信號
    如果 (秒 <  0) {
      秒 = 無限;
    }

    如果(秒!== this.cache_.duration){
      // 緩存優化清理的最後一個設置值(尤其是閃光)
      // 去做:是否需要 Flash 以外的技術?
      this.cache_.duration = 秒;

      如果(秒===無限){
        this.addClass('vjs-live');
      }其他{
        this.removeClass('vjs-live');
      }
      如果(!isNaN(秒)){
        // 除非持續時間值已知,否則不要觸發 durationchange。
        // @see [規範]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}

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

  /**
   * 計算視頻剩餘時間。不屬於
   * 本機視頻 API。
   *
   * @return {數字}
   * 剩餘時間以秒為單位
   */
  剩餘時間() {
    返回 this.duration() - this.currentTime();
  }

  /**
   * 打算在以下情況下使用的剩餘時間功能
   * 時間將直接顯示給用戶。
   *
   * @return {數字}
   * 以秒為單位的剩餘時間
   */
  剩餘時間顯示(){
    返回 Math.floor(this.duration()) - Math.floor(this.currentTime());
  }

  //
  // 有點像已下載視頻部分的數組。

  /**
   * 獲取包含視頻時間數組的 TimeRange 對象
   * 已下載。如果你只想要百分比
   * 已下載的視頻,使用 bufferedPercent。
   *
   * @see [緩衝規範]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
   *
   * @return {TimeRange}
   * 模擬 TimeRange 對象(遵循 HTML 規範)
   */
  緩衝的() {
    讓 buffered = this.techGet_('buffered');

    如果(!緩衝||!緩衝長度){
      緩衝 = createTimeRange(0, 0);
    }

    返回緩衝;
  }

  /**
   * 獲取已下載視頻的百分比(小數形式)。
   * 此方法不是本機 HTML 視頻 API 的一部分。
   *
   * @return {數字}
   * 0 到 1 之間的小數,表示百分比
   * 緩衝的 0 為 0%,1 為 100%
   */
  緩衝百分比(){
    返回 bufferedPercent(this.buffered(), this.duration());
  }

  /**
   * 獲取最後一次緩衝時間範圍的結束時間
   * 這個是用在進度條裡面,封裝所有的時間範圍。
   *
   * @return {數字}
   * 最後緩衝時間範圍的結束
   */
  緩衝結束(){
    const buffered = this.buffered();
    const duration = this.duration();
    let end = buffered.end(buffered.length - 1);

    如果(結束>持續時間){
      結束=持續時間;
    }

    返回端;
  }

  /**
   * 獲取或設置媒體的當前音量
   *
   * @param {number} [percentAsDecimal]
   * 以小數百分比表示的新體積:
   * - 0 靜音/0%/關閉
   * - 1.0 是 100%/滿
   * - 0.5 是體積的一半或 50%
   *
   * @return {數字}
   * 獲取時的當前音量百分比
   */
  體積(percentAsDecimal){
    讓卷;

    如果(百分比十進制!== 未定義){
      // 強制值在 0 和 1 之間
      vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal)));
      this.cache_.volume = vol;
      this.techCall_('setVolume', vol);

      如果(音量 > 0){
        this.lastVolume_(vol);
      }

      返回;
    }

    // 返回當前音量時默認為 1。
    vol = parseFloat(this.techGet_('volume'));
    返回(isNaN(vol))?1:體積;
  }

  /**
   * 獲取當前靜音狀態,或者開啟或關閉靜音
   *
   * @param {boolean} [靜音]
   * - 靜音
   * - false 取消靜音
   *
   * @return {布爾值}
   * - 如果靜音開啟並獲得,則為真
   * - 如果靜音關閉並獲取則為 false
   */
  靜音(靜音){
    如果(靜音!==未定義){
      this.techCall_('setMuted', 靜音);
      返回;
    }
    返回 this.techGet_('muted') ||錯誤的;
  }

  /**
   * 獲取當前 defaultMuted 狀態,或打開或關閉 defaultMuted。默認靜音
   * 表示初始播放時靜音狀態。
   *
   *```js
   * var myPlayer = videojs('some-player-id');
   *
   * myPlayer.src("http://www.example.com/path/to/video.mp4");
   *
   * // 得到,應該是假的
   * console.log(myPlayer.defaultMuted());
   * // 設置為真
   * myPlayer.defaultMuted(true);
   * // 得到應該是真的
   * console.log(myPlayer.defaultMuted());
   *```
   *
   * @param {boolean} [默認靜音]
   * - 靜音
   * - false 取消靜音
   *
   * @return {布爾值|玩家}
   * - 如果 defaultMuted 開啟並獲取則為真
   * - 如果 defaultMuted 關閉並獲取則為 false
   * - 設置時對當前播放器的引用
   */
  默認靜音(默認靜音){
    如果(默認靜音!==未定義){
      返回 this.techCall_('setDefaultMuted', defaultMuted);
    }
    返回 this.techGet_('defaultMuted') ||錯誤的;
  }

  /**
   * 獲取最後一個音量,或設置它
   *
   * @param {number} [percentAsDecimal]
   * 新的最後一個體積為小數百分比:
   * - 0 靜音/0%/關閉
   * - 1.0 是 100%/滿
   * - 0.5 是體積的一半或 50%
   *
   * @return {數字}
   * 獲取時 lastVolume 的當前值作為百分比
   *
   * @私人的
   */
  lastVolume_(percentAsDecimal) {
    如果(百分比十進制!== 未定義 && 百分比十進制!== 0) {
      this.cache_.lastVolume = percentAsDecimal;
      返回;
    }
    返回 this.cache_.lastVolume;
  }

  /**
   * 檢查當前技術是否支持原生全屏
   *(例如,內置控件,如 iOS)
   *
   * @return {布爾值}
   * 如果支持原生全屏
   */
  支持全屏(){
    返回 this.techGet_('supportsFullScreen') ||錯誤的;
  }

  /**
   * 檢查播放器是否處於全屏模式或告訴播放器它
   * 是否處於全屏模式。
   *
   * > 注意:從最新的 HTML5 規範開始,isFullscreen 不再是官方的
   * 屬性,而不是使用 document.fullscreenElement。但是是全屏是
   * 仍然是內部玩家工作的寶貴財產。
   *
   * @param {boolean} [isFS]
   * 設置播放器當前全屏狀態
   *
   * @return {布爾值}
   * - 如果全屏打開並獲取則為真
   * - 如果全屏關閉並獲取則為 false
   */
  isFullscreen(isFS) {
    如果(isFS !==未定義){
      const oldValue = this.isFullscreen_;

      this.isFullscreen_ = Boolean(isFS);

      // 如果我們改變全屏狀態並且我們處於前綴模式,觸發 fullscreenchange
      // 這是我們為舊瀏覽器觸發 fullscreenchange 事件的唯一地方
      // fullWindow 模式被視為一個前綴事件,也會得到一個 fullscreenchange 事件
      如果(這個。全屏 _!== 舊值 && 這個 .fsapi_. 前綴) {
        /**
           * @event Player#fullscreenchange
           * @type {EventTarget~Event}
           */
        this.trigger('fullscreenchange');
      }

      this.toggleFullscreenClass_();
      返回;
    }
    返回 this.isFullscreen_;
  }

  /**
   * 將視頻的大小增加到全屏
   * 部分瀏覽器原生不支持全屏,所以進入
   *“全窗口模式”,視頻填滿瀏覽器窗口。
   * 在支持原生全屏的瀏覽器和設備中,有時
   * 將顯示瀏覽器的默認控件,而不是 Video.js 自定義皮膚。
   * 這包括大多數移動設備(iOS、Android)和舊版本的
   * 蘋果瀏覽器。
   *
   * @param {Object} [全屏選項]
   *覆蓋播放器全屏選項
   *
   * @fires Player#fullscreenchange
   */
  請求全屏(全屏選項){
    const PromiseClass = this.options_.Promise || window.Promise;

    如果(承諾類){
      常量自我=這個;

      返回新的 PromiseClass((解決, 拒絕) => {
        函數 offHandler() {
          self.off('fullscreenerror', errorHandler);
          self.off('fullscreenchange', changeHandler);
        }
        函數 changeHandler() {
          關閉處理程序();
          解決();
        }
        函數 errorHandler(e, err) {
          關閉處理程序();
          拒絕(錯誤);
        }

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

        const promise = self.requestFullscreenHelper_(fullscreenOptions);

        如果(承諾){
          promise.then(offHandler, offHandler);
          promise.then(解決,拒絕);
        }
      });
    }

    返回 this.requestFullscreenHelper_();
  }

  requestFullscreenHelper_(fullscreenOptions) {
    讓 fsOptions;

    // 僅在符合規範的瀏覽器中將全屏選項傳遞給 requestFullscreen。
    // 除非直接傳遞給此方法,否則使用默認值或播放器配置選項。
    如果(!this.fsApi_.prefixed){
      fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
      如果(全屏選項!==未定義){
        fsOptions = 全屏選項;
      }
    }

    // 這個方法的工作原理如下:
    // 1. 如果全屏 api 可用,則使用它
    // 1. 使用潛在選項調用 requestFullscreen
    // 2. 如果我們從上面得到一個承諾,用它來更新 isFullscreen()
    // 2. 否則,如果技術支持全屏,則在其上調用 `enterFullScreen`。
    // 這特別適用於 iPhone、舊版 iPad 和 iOS 上的非 Safari 瀏覽器。
    // 3. 否則,使用“fullWindow”模式
    如果(this.fsApi_.requestFullscreen){
      const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions);

      如果(承諾){
        promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false));
      }

      回報承諾;
    } 否則,如果(這個。技術支持全屏()&&!首選窗口 === 真){
      // 我們不能讓 video.js 控件全屏,但我們可以全屏
      // 使用原生控件
      this.techCall_('enterFullScreen');
    }其他{
      // 不支持全屏,所以我們只是將視頻元素拉伸到
      // 填充視口
      這個.enterFullWindow();
    }
  }

  /**
   * 在全屏模式下將視頻恢復到正常大小
   *
   * @fires Player#fullscreenchange
   */
  退出全屏(){
    const PromiseClass = this.options_.Promise || window.Promise;

    如果(承諾類){
      常量自我=這個;

      返回新的 PromiseClass((解決, 拒絕) => {
        函數 offHandler() {
          self.off('fullscreenerror', errorHandler);
          self.off('fullscreenchange', changeHandler);
        }
        函數 changeHandler() {
          關閉處理程序();
          解決();
        }
        函數 errorHandler(e, err) {
          關閉處理程序();
          拒絕(錯誤);
        }

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

        const promise = self.exitFullscreenHelper_();

        如果(承諾){
          promise.then(offHandler, offHandler);
          // 將 promise 映射到我們的 resolve/reject 方法
          promise.then(解決,拒絕);
        }
      });
    }

    返回 this.exitFullscreenHelper_();
  }

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

      如果(承諾){
        // 我們在這里拆分承諾,所以,我們想抓住
        // 潛在的錯誤,這樣這條鏈就沒有未處理的錯誤
        silencePromise(promise.then(() => this.isFullscreen(false)));
      }

      回報承諾;
    } 否則,如果(這個。技術支持全屏()&&!首選窗口 === 真){
      this.techCall_('exitFullScreen');
    }其他{
      這個.exitFullWindow();
    }
  }

  /**
   * 當不支持全屏時,我們可以拉伸
   * 視頻容器的寬度與瀏覽器允許的一樣寬。
   *
   * @fires Player#enterFullWindow
   */
  enterFullWindow() {
    這是全屏(真);
    this.isFullWindow = true;

    // 存儲原始文檔溢出值以在全屏關閉時返回
    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 {EventTarget~Event}
     */
    this.trigger('enterFullWindow');
  }

  /**
   * 檢查呼叫退出全窗口或
   * ESC 鍵全屏
   *
   * @param {string} 事件
   * 檢查按鍵的事件
   */
  fullWindowOnEscKey(事件){
    如果 (keycode.isEventKey(event, 'Esc')) {
      如果(this.isFullscreen()===真){
        如果(!this.isFullWindow){
          this.exitFullscreen();
        }其他{
          這個.exitFullWindow();
        }
      }
    }
  }

  /**
   * 退出全窗口
   *
   * @fires Player#exitFullWindow
   */
  exitFullWindow() {
    this.isFullscreen(false);
    this.isFullWindow = false;
    Events.off(文檔, 'keydown', this.boundFullWindowOnEscKey_);

    //取消隱藏滾動條。
    document.documentElement.style.overflow = this.docOrigOverflow;

    // 移除全屏樣式
    Dom.removeClass(document.body, 'vjs-full-window');

    // 將盒子、控制器和海報調整為原始大小
    // this.positionAll();
    /**
     * @event Player#exitFullWindow
     * @type {EventTarget~Event}
     */
    this.trigger('exitFullWindow');
  }

  /**
   * 禁用畫中畫模式。
   *
   * @param {boolean} 值
   * - true 將禁用畫中畫模式
   * - false 將啟用畫中畫模式
   */
  禁用畫中畫(值){
    如果(值===未定義){
      返回 this.techGet_('disablePictureInPicture');
    }
    this.techCall_('setDisablePictureInPicture', value);
    this.options_.disablePictureInPicture = 值;
    this.trigger('禁用畫中畫更改');
  }

  /**
   * 檢查播放器是否處於畫中畫模式或告訴播放器它
   * 是否處於畫中畫模式。
   *
   * @param {boolean} [isPiP]
   * 設置播放器當前畫中畫狀態
   *
   * @return {布爾值}
   * - 如果畫中畫開啟並獲取則為真
   * - 如果畫中畫關閉並獲取則為 false
   */
  畫中畫(畫中畫){
    如果(iSPIP!== 未定義){
      這是。無畫內景 _ =!!國際互動協會;
      this.togglePictureInPictureClass_();
      返回;
    }
    返回!!這是。無畫像文化 _;
  }

  /**
   * 創建一個始終位於其他窗口之上的浮動視頻窗口,以便用戶可以
   * 在與其他內容網站互動時繼續消費媒體,或
   * 他們設備上的應用程序。
   *
   * @see [規範]{@link https://wicg.github.io/picture-in-picture}
   *
   * @fires Player#enterpictureinpicture
   *
   * @return {承諾}
   * 帶有畫中畫窗口的承諾。
   */
  請求畫中畫(){
    if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) {
      /**
       * 當玩家進入畫中畫模式時觸發此事件
       *
       * @event Player#enterpictureinpicture
       * @type {EventTarget~Event}
       */
      返回 this.techGet_('requestPictureInPicture');
    }
  }

  /**
   * 退出畫中畫模式。
   *
   * @see [規範]{@link https://wicg.github.io/picture-in-picture}
   *
   * @fires Player#leavepictureinpicture
   *
   * @return {承諾}
   * 一個承諾。
   */
  退出畫中畫(){
    如果(文檔中的“pictureInPictureEnabled”){
      /**
       * 當玩家離開畫中畫模式時觸發此事件
       *
       * @event Player#leavepictureinpicture
       * @type {EventTarget~Event}
       */
      返回 document.exitPictureInPicture();
    }
  }

  /**
   * 當這個 Player 有焦點並且按下一個鍵時調用,或者當
   * 該播放器的任何組件都會收到它無法處理的按鍵。
   * 這允許播放器範圍內的熱鍵(定義如下,或可選
   * 通過外部函數)。
   *
   * @param {EventTarget~Event} 事件
   * 導致調用此函數的 `keydown` 事件。
   *
   * @listens 按鍵
   */
  handleKeyDown(事件){
    const {userActions} = this.options_;

    // 如果未配置熱鍵,則退出。
    如果(!userActions ||!userActions.hotkeys){
      返回;
    }

    // 確定是否從中排除元素的函數
    // 熱鍵處理。
    const excludeElement = (el) => {
      const tagName = el.tagName.toLowerCase();

      // 第一個也是最簡單的測試是針對 `contenteditable` 元素。
      如果(el.isContentEditable){
        返回真;
      }

      // 匹配這些類型的輸入仍然會觸發熱鍵處理
      // 它們不是文本輸入。
      const allowedInputTypes = [
        '按鈕',
        '複選框',
        '隱',
        '收音機',
        '重置',
        '提交'
      ];

      如果(標籤名稱==='輸入'){
        返回 allowedInputTypes.indexOf(el.type) === -1;
      }

      // 最後的測試是通過標籤名。這些標籤將被完全排除。
      const excludedTags = ['textarea'];

      返回排除的標籤。索引(標籤名)!== -1;
    };

    // 如果用戶專注於交互式表單元素,則退出。
    如果(排除元素(this.el_.ownerDocument.activeElement)){
      返回;
    }

    如果(typeof userActions.hotkeys ==='函數'){
      userActions.hotkeys.call(this, event);
    }其他{
      這個.handleHotkeys(事件);
    }
  }

  /**
   * 當此 Player 收到熱鍵按鍵事件時調用。
   * 支持的播放器範圍的熱鍵是:
   *
   * f - 切換全屏
   * m - 切換靜音
   * k 或 Space - 切換播放/暫停
   *
   * @param {EventTarget~Event} 事件
   * 導致調用此函數的 `keydown` 事件。
   */
  handleHotkeys(事件){
    常量熱鍵 = 這個。選項 _.這個. 選項 _. 用戶操作. 熱鍵:{};

    // 從 `hotkeys` 設置 fullscreenKey, muteKey, playPauseKey,如果沒有設置則使用默認值
    常數 {
      fullscreenKey = keydownEvent => keycode.isEventKey(keydownEvent, 'f'),
      muteKey = keydownEvent => keycode.isEventKey(keydownEvent, 'm'),
      playPauseKey = keydownEvent => (keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, 'Space'))
    } = 熱鍵;

    如果 (fullscreenKey.call(this, event)) {
      事件.preventDefault();
      事件.stopPropagation();

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

      如果(文檔 [這個 .FSAPI _. 全屏啟用]!== 假){
        FSToggle.prototype.handleClick.call(this, event);
      }

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

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

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

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

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

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

  /**
   * 檢查播放器是否可以播放給定的 mimetype
   *
   * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
   *
   * @param {string} 類型
   * 要檢查的 mimetype
   *
   * @return {字符串}
   *“可能”、“也許”或“”(空字符串)
   */
  canPlayType(類型){
    讓可以;

    // 循環遍歷選項順序中的每一種播放技術
    為 (讓我 = 0, j = 這個. 選項 _. 技術訂單; I  <  J. 長度; 我 ++) {
      const 技術名稱 = j[i];
      讓 tech = Tech.getTech(techName);

      // 支持將技術註冊為組件的舊行為。
      // 一旦刪除了不推薦使用的行為,就刪除。
      如果(!技術){
        tech = Component.getComponent(techName);
      }

      // 在繼續之前檢查當前技術是否已定義
      如果(!技術){
        log.error(`“${techName}”技術未定義。跳過該技術的瀏覽器支持檢查。`);
        繼續;
      }

      // 檢查瀏覽器是否支持該技術
      如果(技術支持()){
        can = tech.canPlayType(類型);

        如果能) {
          退貨罐;
        }
      }
    }

    返回 '';
  }

  /**
   * 根據技術順序或來源順序選擇來源
   * 如果 `options.sourceOrder` 為真,則使用源順序選擇。否則,
   * 默認為技術訂單選擇
   *
   * @param {Array} 來源
   * 媒體資產的來源
   *
   * @return {對象|布爾}
   * 來源對象和技術順序或錯誤
   */
  選擇來源(來源){
    // 僅獲取 `techOrder` 中指定的存在且受其支持的技術
    // 當前平台
    const 技術人員 =
      this.options_.techOrder
        .map((技術名稱) => {
          返回 [技術名稱, Tech.getTech(技術名稱)];
        })
        .filter(([技術名稱, 技術]) => {
          // 在繼續之前檢查當前技術是否已定義
          如果(技術){
            // 檢查瀏覽器是否支持該技術
            返回 tech.isSupported();
          }

          log.error(`“${techName}”技術未定義。跳過該技術的瀏覽器支持檢查。`);
          返回假;
        });

    // 對每個 `outerArray` 元素迭代一次每個 `innerArray` 元素並執行
    // `tester` 兩者都有。如果 `tester` 返回一個非假值,提前退出並返回
    //那個值。
    const findFirstPassingTechSourcePair = function(outerArray, innerArray, tester) {
      讓我們發現;

      outerArray.some((outerChoice) => {
        返回 innerArray.some((innerChoice) => {
          發現=測試者(outerChoice,innerChoice);

          如果(找到){
            返回真;
          }
        });
      });

      返回找到;
    };

    讓 foundSourceAndTech;
    const flip = (fn) => (a, b) => fn(b, a);
    const finder = ([技術名稱, 技術], 來源) => {
      if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
        返回 {source, tech: techName};
      }
    };

    // 根據 `options.sourceOrder` 的真實性,我們交換技術和來源的順序
    // 根據優先級從中選擇。
    如果(this.options_.sourceOrder){
      // 源優先順序
      foundSourceAndTech = findFirstPassingTechSourcePair(資源,技術,翻轉(查找器));
    }其他{
      // 技術優先排序
      foundSourceAndTech = findFirstPassingTechSourcePair(技術,資源,查找器);
    }

    返回 foundSourceAndTech ||錯誤的;
  }

  /**
   * 執行源設置和獲取邏輯
   *
   * @param {Tech~SourceObject|Tech~SourceObject[]|string} [來源]
   * 一個 SourceObject、一個 SourceObjects 數組或一個字符串引用
   * 指向媒體源的 URL。_強烈推薦_一個對象
   * 或此處使用對像數組,以便選擇源
   * 算法可以考慮“類型”。
   *
   * 如果未提供,則此方法充當吸氣劑。
   * @param {boolean} isRetry
   * 指示是否由於重試而在內部調用
   *
   * @return {字符串|未定義}
   * 如果缺少 `source` 參數,則返回當前源
   * 網址。否則,不返回任何內容/未定義。
   */
  handleSrc_(source, isRetry) {
    // 吸氣劑用法
    如果(源類型==='未定義'){
      返回 this.cache_.src || '';
    }

    // 重置新源的重試行為
    如果(this.resetRetryOnError_){
      this.resetRetryOnError_();
    }

    // 過濾掉無效源並將我們的源轉換為
    // 源對像數組
    const sources = filterSource(來源);

    // 如果傳入了一個源,那麼它是無效的,因為
    // 它被過濾為零長度數組。所以我們必須
    // 顯示錯誤
    如果(!sources.length){
      this.setTimeout(函數() {
        這個。錯誤({代碼:4、消息:this.options_.notSupportedMessage});
      }, 0);
      返回;
    }

    // 初始資源
    this.changingSrc_ = true;

    // 如果我們在錯誤後不重試新源,則只更新緩存的源列表,
    // 因為在那種情況下我們希望在緩存中包含失敗的源
    如果(!isRetry){
      this.cache_.sources = 來源;
    }

    this.updateSourceCaches_(sources[0]);

    // middlewareSource 是經過中間件改變後的源
    middleware.setSource(this, sources[0], (middlewareSource, mws) => {
      this.middleware_ = mws;

      // 由於 sourceSet 是異步的,我們必須在選擇源後再次更新緩存,因為
      // 所選源可能與此回調上方的緩存更新順序不一致。
      如果(!isRetry){
        this.cache_.sources = 來源;
      }

      this.updateSourceCaches_(middlewareSource);

      const err = this.src_(middlewareSource);

      如果(錯誤){
        如果(來源。長度> 1){
          返回 this.handleSrc_(sources.slice(1));
        }

        this.changingSrc_ = false;

        // 我們需要將其包裝在超時中,讓人們有機會添加錯誤事件處理程序
        this.setTimeout(函數() {
          這個。錯誤({代碼:4、消息:this.options_.notSupportedMessage});
        }, 0);

        // 我們找不到合適的技術,但我們仍然通知委託人就是這樣
        // 這需要更好地說明為什麼需要這樣做
        this.triggerReady();

        返回;
      }

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

    // 如果這個源在播放前失敗,請嘗試另一個可用的源。
    如果(this.options_.retryOnError && sources.length > 1){
      const 重試 = () => {
        // 刪除錯誤模態
        這個錯誤(空);
        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、一個 SourceObjects 數組或一個字符串引用
   * 指向媒體源的 URL。_強烈推薦_一個對象
   * 或此處使用對像數組,以便選擇源
   * 算法可以考慮“類型”。
   *
   * 如果未提供,則此方法充當吸氣劑。
   *
   * @return {字符串|未定義}
   * 如果缺少 `source` 參數,則返回當前源
   * 網址。否則,不返回任何內容/未定義。
   */
  來源(來源){
    返回 this.handleSrc_(source, false);
  }

  /**
   * 在技術上設置源對象,返回一個布爾值,指示是否
   *有技術能不能播放源碼
   *
   * @param {Tech~SourceObject} 來源
   * 要在 Tech 上設置的源對象
   *
   * @return {布爾值}
   * - 如果沒有技術播放此源則為真
   * - 否則為假
   *
   * @私人的
   */
  src_(來源){
    const sourceTech = this.selectSource([source]);

    如果(!sourceTech){
      返回真;
    }

    如果(!標題資訊(來源技術,這個。技術名稱 _)){
      this.changingSrc_ = true;
      // 使用選擇的源加載此技術
      this.loadTech_(sourceTech.tech, sourceTech.source);
      this.tech_.ready(() => {
        this.changingSrc_ = false;
      });
      返回假;
    }

    // 等到技術人員準備好設置源
    // 並儘可能同步設置它 (#2326)
    這個。準備好(功能(){

      // setSource 技術方法添加了源處理程序
      // 所以老技術不會支持它
      // 我們需要檢查子類的情況下的直接原型
      // 該技術不支持源處理程序
      如果 (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
        this.techCall_('setSource', 來源);
      }其他{
        this.techCall_('src', source.src);
      }

      this.changingSrc_ = false;
    }, 真的);

    返回假;
  }

  /**
   * 開始加載 src 數據。
   */
  加載() {
    this.techCall_('load');
  }

  /**
   * 重置播放器。加載 techOrder 中的第一個技術,
   * 刪除現有 `tech` 中的所有文本軌道,
   * 並在 `tech` 上調用 `reset`。
   */
  重置() {
    const PromiseClass = this.options_.Promise || window.Promise;

    如果(這個。暫停()||!責任級別) {
      這個.doReset_();
    }其他{
      const playPromise = this.play();

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

  doReset_() {
    如果(this.tech_){
      this.tech_.clearTracks('text');
    }
    這個.resetCache_();
    這個。海報('');
    this.loadTech_(this.options_.techOrder[0], null);
    this.techCall_('重置');
    this.resetControlBarUI_();
    如果(isEvented(這個)){
      this.trigger('playerreset');
    }
  }

  /**
   * 通過調用重置的子方法重置控制欄的 UI
   * 控制欄的所有組件
   */
  resetControlBarUI_() {
    this.resetProgressBar_();
    this.resetPlaybackRate_();
    this.resetVolumeBar_();
  }

  /**
   * 重置技術進度,以便在 UI 中重置進度條
   */
  resetProgressBar_() {
    這個.currentTime(0);

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

    如果(持續時間顯示){
      durationDisplay.updateContent();
    }

    如果(剩餘時間顯示){
      remainingTimeDisplay.updateContent();
    }
  }

  /**
   * 重置播放比例
   */
  resetPlaybackRate_() {
    this.playbackRate(this.defaultPlaybackRate());
    this.handleTechRateChange_();
  }

  /**
   *重置音量條
   */
  resetVolumeBar_() {
    這個.volume(1.0);
    this.trigger('volumechange');
  }

  /**
   * 返回所有當前源對象。
   *
   * @return {Tech~SourceObject[]}
   * 當前源對象
   */
  當前來源(){
    const source = this.currentSource();
    const 來源 = [];

    // 假設 `{}` 或 `{ src }`
    如果(對象鍵(源)。== 0) {
      來源.推送(來源);
    }

    返回 this.cache_.sources ||來源;
  }

  /**
   * 返回當前源對象。
   *
   * @return {Tech~SourceObject}
   * 當前源對象
   */
  當前來源(){
    返回 this.cache_.source || {};
  }

  /**
   * 返回當前源值的完全限定 URL,例如 http://mysite.com/video.mp4
   * 可與 `currentType` 結合使用,以協助重建當前源對象。
   *
   * @return {字符串}
   * 當前源
   */
  當前源(){
    返回 this.currentSource() && this.currentSource().src || '';
  }

  /**
   * 獲取當前源類型,例如 video/mp4
   * 這可以讓你重建當前的源對象,這樣你就可以加載相同的
   * 稍後的來源和技術
   *
   * @return {字符串}
   * 源 MIME 類型
   */
  當前類型(){
    返回 this.currentSource() && this.currentSource().type || '';
  }

  /**
   * 獲取或設置預加載屬性
   *
   * @param {boolean} [值]
   * - true 意味著我們應該預加載
   * - false 意味著我們不應該預加載
   *
   * @return {字符串}
   * 獲取時的preload屬性值
   */
  預加載(值){
    如果(值!==未定義){
      this.techCall_('setPreload', value);
      this.options_.preload = 值;
      返回;
    }
    返回 this.techGet_('preload');
  }

  /**
   * 獲取或設置自動播放選項。當這是一個布爾值時,它將
   * 修改技術屬性。當這是一個字符串時,屬性
   * 該技術將被刪除,`Player` 將在加載開始時處理自動播放。
   *
   * @param {boolean|string} [值]
   * - true:使用瀏覽器行為自動播放
   * - false:不自動播放
   * - 'play': 在每次加載開始時調用 play()
   * - 'muted': 在每次加載開始時調用 muted() 然後 play()
   * - 'any':在每次加載開始時調用 play()。如果失敗調用 muted() 然後 play()。
   * - *:此處列出的值以外的值將設置為 `autoplay` 為 true
   *
   * @return {布爾值|字符串}
   * 獲取時自動播放的當前值
   */
  自動播放(值){
    // 吸氣劑用法
    如果(值===未定義){
      返回 this.options_.autoplay ||錯誤的;
    }

    讓技術自動播放;

    // 如果該值是一個有效的字符串,則將其設置為該值,或者在需要時將 `true` 規範化為 'play'
    if (typeof value === 'string' && (/(any|play|muted)/).test(value) || value === true && this.options_.normalizeAutoplay) {
      this.options_.autoplay = 值;
      這是。手動自動播放 _(值的類型 === '字符串'?價值:'玩');
      技術自動播放=假;

    // 任何 falsy 值都會在瀏覽器中將自動播放設置為 false,
    // 讓我們做同樣的事情
    } 否則如果(!價值){
      this.options_.autoplay = false;

    // 任何其他值(即 truthy)將自動播放設置為 true
    }其他{
      this.options_.autoplay = true;
    }

    技術自動播放 = 技術自動播放 ===「未定義」的類型?這個選項 _. 自動播放:技術自動播放;

    // 如果我們沒有技術那麼我們就不會排隊
    // 技術就緒時的 setAutoplay 調用。我們這樣做是因為
    // 自動播放選項將在構造函數中傳遞,我們
    // 不需要設置兩次
    如果(this.tech_){
      this.techCall_('setAutoplay', techAutoplay);
    }
  }

  /**
   * 設置或取消設置 playsinline 屬性。
   * Playsinline 告訴瀏覽器首選非全屏播放。
   *
   * @param {boolean} [值]
   * - true 意味著我們應該默認嘗試內聯播放
   * - false 表示我們應該使用瀏覽器的默認播放模式,
   * 在大多數情況下是內聯的。 iOS Safari 是一個明顯的例外
   * 並默認全屏播放。
   *
   * @return {字符串|播放器}
   * - playsinline 的當前值
   * - 設置時的播放器
   *
   * @see [規範]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
   */
  在線播放(值){
    如果(值!==未定義){
      this.techCall_('setPlaysinline', value);
      this.options_.playsinline = 值;
      歸還這個;
    }
    返回 this.techGet_('playsinline');
  }

  /**
   * 獲取或設置視頻元素的循環屬性。
   *
   * @param {boolean} [值]
   * - true 表示我們應該循環播放視頻
   * - false 表示我們不應該循環播放視頻
   *
   * @return {布爾值}
   * 獲取時循環的當前值
   */
  循環(值){
    如果(值!==未定義){
      this.techCall_('setLoop', value);
      this.options_.loop = 值;
      返回;
    }
    返回 this.techGet_('loop');
  }

  /**
   * 獲取或設置海報圖片來源url
   *
   * @fires Player#posterchange
   *
   * @param {字符串} [來源]
   * 海報圖片來源網址
   *
   * @return {字符串}
   * 獲得時海報的當前值
   */
  海報(src){
    如果(源===未定義){
      返回this.poster_;
    }

    // 刪除海報的正確方法是設置為空字符串
    // 其他假值會拋出錯誤
    如果(!src){
      來源 = '';
    }

    如果(src === this.poster_){
      返回;
    }

    // 更新內部海報變量
    this.poster_ = src;

    // 更新技術海報
    this.techCall_('setPoster', src);

    this.isPosterFromTech_ = false;

    // 提醒組件海報已設置
    /**
     * 當播放器上的海報圖像發生變化時會觸發此事件。
     *
     * @event Player#posterchange
     * @type {EventTarget~Event}
     */
    this.trigger('posterchange');
  }

  /**
   * 一些技術人員(例如 YouTube)可以在
   * 異步方式。我們希望海報組件使用它
   *海報來源,以便它掩蓋技術的控制。
   *(YouTube 的播放按鈕)。但是我們只想用這個
   * 如果播放器用戶沒有通過設置海報
   * 正常的 API。
   *
   * @fires Player#posterchange
   * @listens Tech#posterchange
   * @私人的
   */
  handleTechPosterChange_() {
    如果((!this.poster_ || this.options_.techCanOverridePoster)&& this.tech_ && this.tech_.poster){
      const newPoster = this.tech_.poster() || '';

      如果(新海報!== this.poster_){
        this.poster_ = newPoster;
        this.isPosterFromTech_ = true;

        // 讓組件知道海報已經改變
        this.trigger('posterchange');
      }
    }
  }

  /**
   * 獲取或設置控件是否顯示。
   *
   * @fires Player#controlsenabled
   *
   * @param {布爾} [布爾]
   * - true 打開控件
   * - false 關閉控件
   *
   * @return {布爾值}
   * 獲取時控件的當前值
   */
  控制(布爾){
    如果(布爾===未定義){
      返回!!這個。控制 _;
    }

    布爾 =!!布爾;

    // 不要觸發更改事件,除非它確實發生了變化
    如果(this.controls_ === bool){
      返回;
    }

    this.controls_ = bool;

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

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

  /**
   * 打開/關閉本機控件。本機控件是內置於
   * 設備(例如默認的 iPhone 控件)或其他技術
   *(例如 Vimeo 控件)
   * **這應該只能由當前技術設置,因為只有技術知道
   * 如果它可以支持原生控件**
   *
   * @fires Player#usingnativecontrols
   * @fires Player#usingcustomcontrols
   *
   * @param {布爾} [布爾]
   * - true 打開本機控件
   * - false 關閉本機控件
   *
   * @return {布爾值}
   * 獲取時原生控件的當前值
   */
  使用本機控件(布爾){
    如果(布爾===未定義){
      返回!!這是。使用本地控制 _;
    }

    布爾 =!!布爾;

    // 不要觸發更改事件,除非它確實發生了變化
    如果(this.usingNativeControls_ === bool){
      返回;
    }

    this.usingNativeControls_ = bool;

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

      /**
       * 播放器正在使用本機設備控件
       *
       * @event Player#usingnativecontrols
       * @type {EventTarget~Event}
       */
      this.trigger('usingnativecontrols');
    }其他{
      this.removeClass('vjs-using-native-controls');

      /**
       * 播放器正在使用自定義 HTML 控件
       *
       * @event Player#usingcustomcontrols
       * @type {EventTarget~Event}
       */
      this.trigger('usingcustomcontrols');
    }
  }

  /**
   * 設置或獲取當前的MediaError
   *
   * @fires Player#錯誤
   *
   * @param {MediaError|string|number} [錯誤]
   * MediaError 或要轉換的字符串/數字
   * 變成一個 MediaError
   *
   * @return {MediaError|null}
   * 獲取(或null)時的當前MediaError
   */
  錯誤(錯誤){
    如果(錯誤===未定義){
      返回 this.error_ ||無效的;
    }

    // 允許鉤子修改錯誤對象
    鉤子('beforeeror').forEach((hookFunction)=> {
      const newErr = hookFunction(this, err);

      如果 (!(
        (隔離物件 (更新者) &&!陣列. 伊斯陣列(新版))||
        typeof newErr === '字符串' ||
        typeof newErr === '數字' ||
        newErr === 空
      )) {
        this.log.error('請在 beforeerror 鉤子中返回 MediaError 期望的值');
        返回;
      }

      錯誤=新錯誤;
    });

    // 抑制不兼容源的第一條錯誤消息,直到
    // 用戶交互
    如果(this.options_.suppressNotSupportedError &&
        錯誤 && 錯誤代碼 === 4
    ) {
      const triggerSuppressedError = function() {
        這個錯誤(錯誤);
      };

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

    //恢復默認
    如果(錯誤=== null){
      this.error_ = 錯誤;
      this.removeClass('vjs-error');
      如果(this.errorDisplay){
        這個.errorDisplay.close();
      }
      返回;
    }

    this.error_ = new MediaError(err);

    // 將 vjs-error 類名添加到播放器
    this.addClass('vjs-error');

    // 記錄錯誤類型的名稱和任何消息
    //IE11 日誌「[對象對象]」,並要求您展開消息以查看錯誤對象
    日誌錯誤(`(代碼:$ {這個 .error_.code} $ {媒體錯誤。錯誤類型 [這個 .error_.code]})`,這個 .error_.message,這個 .error_);

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

    // 通知每個玩家錯誤的鉤子
    hooks('error').forEach((hookFunction) => hookFunction(this, this.error_));

    返回;
  }

  /**
   * 報告用戶活動
   *
   * @param {Object} 事件
   * 事件對象
   */
  報告用戶活動(事件){
    this.userActivity_ = true;
  }

  /**
   * 獲取/設置用戶是否活躍
   *
   * @fires Player#useractive
   * @fires Player#userinactive
   *
   * @param {布爾} [布爾]
   * - 如果用戶處於活動狀態則為真
   * - 如果用戶處於非活動狀態則為 false
   *
   * @return {布爾值}
   * 獲取時userActive的當前值
   */
  userActive(布爾){
    如果(布爾===未定義){
      返回這個。userActive_;
    }

    布爾 =!!布爾;

    如果(bool === this.userActive_){
      返回;
    }

    this.userActive_ = bool;

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

    // Chrome/Safari/IE 有錯誤,當你改變光標時它可以
    // 觸發鼠標移動事件。當您隱藏時,這會導致問題
    // 用戶不活動時的光標,鼠標移動信號用戶
    // 活動。使其無法進入非活動模式。具體來說
    // 當我們真的需要隱藏光標時,這會在全屏模式下發生。
    //
    // 當這在所有瀏覽器中得到解決時,它可以被刪除
    // https://code.google.com/p/chromium/issues/detail?id=103041
    如果(this.tech_){
      this.tech_.one('mousemove', function(e) {
        e.stopPropagation();
        e.preventDefault();
      });
    }

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

  /**
   * 根據超時值監聽用戶活動
   *
   * @私人的
   */
  listenForUserActivity_() {
    讓 mouseInProgress;
    讓 lastMoveX;
    讓 lastMoveY;
    const handleActivity = Fn.bind(this, this.reportUserActivity);

    const handleMouseMove = function(e) {
      // #1068 - 防止 mousemove 垃圾郵件
      // Chrome 錯誤:https://code.google.com/p/chromium/issues/detail?id=366970
      如果(。屏幕!最後一次移動== 拉斯莫維) {
        lastMoveX = e.screenX;
        lastMoveY = e.screenY;
        處理活動();
      }
    };

    const handleMouseDown = function() {
      處理活動();
      // 只要他們正在觸摸設備或按下鼠標,
      // 即使他們沒有移動手指或鼠標,我們也認為他們是活躍的。
      // 所以我們想繼續更新他們是活躍的
      this.clearInterval(mouseInProgress);
      // 現在設置 userActivity=true 並將間隔設置為同一時間
      // 因為 activityCheck 間隔 (250) 應該確保我們不會錯過
      // 下一個活動檢查
      mouseInProgress = this.setInterval(handleActivity, 250);
    };

    const handleMouseUpAndMouseLeave = 函數(事件){
      處理活動();
      // 如果鼠標/觸摸按下,則停止保持活動的間隔
      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 上點擊進度條時(顯示控制欄時)的錯誤
    // controlBar 將不再被默認超時隱藏。
    如果(控制欄 &&!瀏覽器 .iOS&&!瀏覽器安卓) {

      controlBar.on('mouseenter', function(event) {
        如果(這個。播放器()。選項 _ 不活動超時!== 0) {
          this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
        }
        this.player().options_.inactivityTimeout = 0;
      });

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

    }

    // 監聽鍵盤導航
    // 由於鍵重複,不需要使用 inProgress 間隔
    this.on('keydown', handleActivity);
    this.on('keyup', handleActivity);

    // 每 250 毫秒運行一次間隔,而不是將所有內容都塞入
    // mousemove/touchmove 函數本身,以防止性能下降。
    // `this.reportUserActivity` 只是將 this.userActivity_ 設置為 true,這
    // 然後被這個循環拾取
    // http://ejohn.org/blog/learning-from-twitter/
    讓不活動超時;

    this.setInterval(函數() {
      // 檢查鼠標/觸摸活動是否發生
      如果(!this.userActivity_){
        返回;
      }

      // 重置活動跟踪器
      this.userActivity_ = false;

      // 如果用戶狀態為非活動狀態,則將狀態設置為活動狀態
      這個。userActive(真);

      // 清除任何現有的不活動超時以重新啟動計時器
      this.clearTimeout(inactivityTimeout);

      const timeout = this.options_.inactivityTimeout;

      如果(超時 <= 0){
        返回;
      }

      // 在 <timeout> 毫秒內,如果沒有更多的活動發生
      // 用戶將被視為不活躍
      inactivityTimeout = this.setTimeout(函數() {
        // 防止 inactivityTimeout 可以觸發的情況
        // 在活動檢查循環獲取下一個用戶活動之前
        // 引起閃爍
        如果(!this.userActivity_){
          this.userActive(false);
        }
      }, 暫停);

    }, 250);
  }

  /**
   * 獲取或設置當前播放速率。播放速率為
   * 1.0 代表正常速度,0.5 代表半速
   * 播放,例如。
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
   *
   * @param {number} [率]
   * 要設置的新播放速率。
   *
   * @return {數字}
   * 獲取或1.0時的當前播放速率
   */
  回放率(速率){
    如果(率!==未定義){
      // 注意:this.cache_.lastPlaybackRate 是從技術處理程序中設置的
      //上面註冊的
      this.techCall_('setPlaybackRate', 速率);
      返回;
    }

    如果(this.tech_ && this.tech_.featuresPlaybackRate){
      返回 this.cache_.lastPlaybackRate || this.techGet_('playbackRate');
    }
    返回 1.0;
  }

  /**
   * 獲取或設置當前默認播放速率。默認播放速率
   * 例如,1.0 表示正常速度,0.5 表示半速播放。
   * defaultPlaybackRate 將僅代表視頻的初始播放速率,而不是
   * 不是當前播放速率。
   *
   * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
   *
   * @param {number} [率]
   * 要設置的新默認播放速率。
   *
   * @return {number|玩家}
   * - 獲取或1.0時的默認播放速率
   * - 設置時的播放器
   */
  defaultPlaybackRate(速率){
    如果(率!==未定義){
      返回 this.techCall_('setDefaultPlaybackRate', rate);
    }

    如果(this.tech_ && this.tech_.featuresPlaybackRate){
      返回 this.techGet_('defaultPlaybackRate');
    }
    返回 1.0;
  }

  /**
   * 獲取或設置音頻標誌
   *
   * @param {boolean} 布爾
   * - true 表示這是一個音頻播放器
   * - 錯誤信號表明這不是音頻播放器
   *
   * @return {布爾值}
   * 獲取時isAudio的當前值
   */
  isAudio(布爾){
    如果(布爾!==未定義){
      這是音頻 _ =!!布爾;
      返回;
    }

    返回!!這是音頻 _;
  }

  enableAudioOnlyUI_() {
    // 立即更新樣式以顯示控制欄,這樣我們就可以得到它的高度
    this.addClass('vjs-audio-only-mode');

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

    // 隱藏除控制欄之外的所有播放器組件。控制欄組件
    // 只有視頻需要用 CSS 隱藏
    playerChildren.forEach(孩子 => {
      如果(孩子=== controlBar){
        返回;
      }

      如果(孩子的孩子 _ &&!兒童. 有類 ('VJS-隱藏')) {
        孩子。隱藏();

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

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

    // 設置播放器高度與控制條相同
    this.height(controlBarHeight);
    this.trigger('audioonlymodechange');
  }

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

    // 顯示之前隱藏的播放器組件
    this.audioOnlyCache_.hiddenChildren.forEach(child => child.show());

    // 重置玩家高度
    this.height(this.audioOnlyCache_.playerHeight);
    this.trigger('audioonlymodechange');
  }

  /**
   * 獲取當前的 audioOnlyMode 狀態或將 audioOnlyMode 設置為 true 或 false。
   *
   * 將此設置為 `true` 將隱藏除控制欄之外的所有播放器組件,
   * 以及僅視頻所需的控制欄組件。
   *
   * @param {boolean} [值]
   * 將 audioOnlyMode 設置為的值。
   *
   * @return {承諾|布爾}
   * 設置狀態時返回一個Promise,獲取時返回一個布爾值
   * 目前的狀態
   */
  audioOnlyMode(值){
    如果(類型值!== '布爾值' || 值 === 這個。音頻專用模式 _) {
      返回this.audioOnlyMode_;
    }

    this.audioOnlyMode_ = 值;

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

    如果(承諾類){
      //啟用純音頻模式
      如果(值){
        const exitPromises = [];

        // audioOnlyMode 不支持全屏和畫中畫,所以如果需要請退出。
        如果 (this.isInPictureInPicture()) {
          exitPromises.push(this.exitPictureInPicture());
        }

        如果 (this.isFullscreen()) {
          exitPromises.push(this.exitFullscreen());
        }

        如果(this.audioPosterMode()){
          exitPromises.push(this.audioPosterMode(false));
        }

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

      // 禁用純音頻模式
      返回 PromiseClass.resolve().then(() => this.disableAudioOnlyUI_());
    }

    如果(值){
      如果 (this.isInPictureInPicture()) {
        this.exitPictureInPicture();
      }

      如果(this.isFullscreen()){
        this.exitFullscreen();
      }

      this.enableAudioOnlyUI_();
    }其他{
      this.disableAudioOnlyUI_();
    }
  }

  enablePosterModeUI_() {
    // 隱藏視頻元素並顯示海報圖像以啟用 posterModeUI
    const tech = this.tech_ && this.tech_;

    技術隱藏();
    this.addClass('vjs-audio-poster-mode');
    this.trigger('audiopostermodechange');
  }

  disablePosterModeUI_() {
    // 顯示視頻元素並隱藏海報圖像以禁用 posterModeUI
    const tech = this.tech_ && this.tech_;

    技術展示();
    this.removeClass('vjs-audio-poster-mode');
    this.trigger('audiopostermodechange');
  }

  /**
   * 獲取當前audioPosterMode狀態或設置audioPosterMode為true或false
   *
   * @param {boolean} [值]
   * 將 audioPosterMode 設置為的值。
   *
   * @return {承諾|布爾}
   * 設置狀態時返回一個 Promise,獲取時返回一個布爾值
   * 目前的狀態
   */
  audioPosterMode(值){

    如果(類型值!== '布爾' || 值 === 這個. 音頻海報模式 _) {
      返回this.audioPosterMode_;
    }

    this.audioPosterMode_ = 值;

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

    如果(承諾類){

      如果(值){

        如果(this.audioOnlyMode()){
          const audioOnlyModePromise = this.audioOnlyMode(false);

          返回 audioOnlyModePromise.then(() => {
            // 在禁用純音頻模式後啟用音頻海報模式
            this.enablePosterModeUI_();
          });
        }

        返回 PromiseClass.resolve().then(() => {
          //啟用音頻海報模式
          this.enablePosterModeUI_();
        });
      }

      返回 PromiseClass.resolve().then(() => {
        // 禁用音頻海報模式
        this.disablePosterModeUI_();
      });
    }

    如果(值){

      如果(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 {string} [種類]
   * 您要添加的 TextTrack 類型
   *
   * @param {字符串} [標籤]
   * 給TextTrack標籤的標籤
   *
   * @param {字符串} [語言]
   * 在 TextTrack 上設置的語言
   *
   * @return {TextTrack|undefined}
   * 添加或未定義的 TextTrack
   * 如果沒有技術
   */
  addTextTrack(種類,標籤,語言){
    如果(this.tech_){
      返回 this.tech_.addTextTrack(種類、標籤、語言);
    }
  }

  /**
   * 創建一個遠程 {@link TextTrack} 和一個 {@link HTMLTrackElement}。
   * 當manualCleanup設置為false時,track會被自動清除
   * 源更改。
   *
   * @param {Object} 選項
   * 在創建過程中傳遞給 {@link HTMLTrackElement} 的選項。看
   * {@link HTMLTrackElement} 用於您應該使用的對象屬性。
   *
   * @param {boolean} [manualCleanup=true] 如果設置為 false,TextTrack 將是
   * 在源更改時刪除
   *
   * @return {HtmlTrackElement}
   * 創建並添加的 HTMLTrackElement
   * 到 HtmlTrackElementList 和遠程
   * 文本軌道列表
   *
   * @deprecated “manualCleanup”參數的默認值將默認
   * 在即將推出的 Video.js 版本中設置為“false”
   */
  addRemoteTextTrack(選項,manualCleanup){
    如果(this.tech_){
      返回 this.tech_.addRemoteTextTrack(選項,manualCleanup);
    }
  }

  /**
   *從各自的刪除遠程{@link TextTrack}
   * {@link TextTrackList} 和 {@link HtmlTrackElementList}。
   *
   * @param {Object} 軌道
   * 遠程 {@link TextTrack} 刪除
   *
   * @return {未定義}
   * 不返回任何東西
   */
  removeRemoteTextTrack(obj = {}) {
    讓 {track} = obj;

    如果(!跟踪){
      軌道=對象;
    }

    // 將輸入解構為一個帶有 track 參數的對象,默認為 arguments[0]
    // 如果沒有傳入任何內容,則將整個參數默認為一個空對象

    如果(this.tech_){
      返回 this.tech_.removeRemoteTextTrack(track);
    }
  }

  /**
   * 獲取 W3C 媒體指定的可用媒體播放質量指標
   * 播放質量 API。
   *
   * @see [規範]{@link https://wicg.github.io/media-playback-quality}
   *
   * @return {對象|未定義}
   * 具有支持的媒體播放質量指標或未定義的對象(如果有)
   * 沒有技術或技術不支持它。
   */
  getVideoPlaybackQuality() {
    返回 this.techGet_('getVideoPlaybackQuality');
  }

  /**
   * 獲取視頻寬度
   *
   * @return {數字}
   * 當前視頻寬度
   */
  視頻寬度(){
    返回 this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
  }

  /**
   * 獲取視頻高度
   *
   * @return {數字}
   * 當前視頻高度
   */
  視頻高度(){
    返回 this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0;
  }

  /**
   * 玩家的語言代碼。
   *
   * 更改語言會觸發
   * [語言更改] {@link 播放器 # 事件:語言更改}
   * 哪些組件可以用來更新控製文本。
   * ClickableComponent 將默認更新其控件文本
   * [語言更改] {@link 播放器 # 事件:語言更改}。
   *
   * @fires Player#languagechange
   *
   * @param {字符串} [代碼]
   * 設置播放器的語言代碼
   *
   * @return {字符串}
   * 獲取時的當前語言代碼
   */
  語言(代碼){
    如果(代碼===未定義){
      返回 this.language_;
    }

    如果(這個語言 _!== 字符串(代碼)。小寫()){
      this.language_ = String(code).toLowerCase();

      // 在第一次初始化期間,有些事情可能不會被觸發
      如果(isEvented(這個)){
        /**
        * 當玩家語言改變時觸發
        *
        * @event Player#languagechange
        * @type {EventTarget~Event}
        */
        this.trigger('languagechange');
      }
    }
  }

  /**
   * 獲取玩家的語言詞典
   * 每次合併,因為新添加的插件可能隨時調用 videojs.addLanguage()
   * 直接在播放器選項中指定的語言優先
   *
   * @return {數組}
   *一系列支持的語言
   */
  語言(){
    返回 mergeOptions(Player.prototype.options_.languages, this.languages_);
  }

  /**
   * 返回代表當前曲目的 JavaScript 對象
   * 信息。 **不將其作為 JSON 返回**
   *
   * @return {對象}
   * 表示當前軌道信息的對象
   */
  toJSON() {
    const options = mergeOptions(this.options_);
    const tracks = options.tracks;

    選項.tracks = [];

    對於(讓我 = 0; 我 < 跟踪。長度; 我 ++){
      讓 track = tracks[i];

      // 深度合併音軌並清空播放器,因此沒有循環引用
      track = mergeOptions(track);
      track.player = undefined;
      options.tracks[i] = track;
    }

    退貨選項;
  }

  /**
   * 創建一個簡單的模式對話框({@link ModalDialog} 的一個實例
   * component) 立即覆蓋播放器
   * 內容並在關閉時自行刪除。
   *
   * @param {string|Function|Element|Array|null} 內容
   * 與 {@link ModalDialog#content} 的同名參數相同。
   * 最直接的用法是提供字符串或 DOM
   * 元素。
   *
   * @param {對象} [選項]
   * 將傳遞給 {@link ModalDialog} 的額外選項。
   *
   * @return {ModalDialog}
   * 創建的 {@link ModalDialog}
   */
  createModal(內容,選項){
    選項=選項|| {};
    options.content = 內容 || '';

    const modal = new ModalDialog(this, options);

    this.addChild(模態);
    modal.on('處置', () => {
      this.removeChild(模態);
    });

    模態.open();
    返回模態;
  }

  /**
   * 當播放器調整大小時更改斷點類。
   *
   * @私人的
   */
  updateCurrentBreakpoint_() {
    如果(!this.responsive()){
      返回;
    }

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

    對於(讓我 = 0; 我 < 斷點 _ 順序。長度; 我 ++){
      const candidateBreakpoint = BREAKPOINT_ORDER[i];
      const maxWidth = this.breakpoints_[candidateBreakpoint];

      如果(當前寬度 <= 最大寬度){

        // 當前斷點沒有改變,不用做什麼。
        if (currentBreakpoint === candidateBreakpoint) {
          返回;
        }

        // 如果存在當前斷點,則只刪除一個類。
        如果(當前斷點){
          this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
        }

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

  /**
   * 刪除當前斷點。
   *
   * @私人的
   */
  removeCurrentBreakpoint_() {
    const className = this.currentBreakpointClass();

    this.breakpoint_ = '';

    如果(類名){
      this.removeClass(類名);
    }
  }

  /**
   * 在播放器上獲取或設置斷點。
   *
   * 使用對像或 `true` 調用此方法將刪除任何先前的
   * 自定義斷點並再次從默認值開始。
   *
   * @param {Object|boolean} [斷點]
   * 如果給定一個對象,它可以用來提供自定義
   *斷點。如果給出 true ,將設置默認斷點。
   * 如果沒有給出這個參數,將簡單地返回當前
   *斷點。
   *
   * @param {number} [斷點.tiny]
   * “vjs-layout-tiny”類的最大寬度。
   *
   * @param {number} [斷點.xsmall]
   * “vjs-layout-x-small”類的最大寬度。
   *
   * @param {number} [斷點.small]
   * “vjs-layout-small”類的最大寬度。
   *
   * @param {number} [斷點.medium]
   * “vjs-layout-medium”類的最大寬度。
   *
   * @param {number} [breakpoints.large]
   * “vjs-layout-large”類的最大寬度。
   *
   * @param {number} [斷點.xlarge]
   * “vjs-layout-x-large”類的最大寬度。
   *
   * @param {number} [breakpoints.huge]
   * “vjs-layout-huge”類的最大寬度。
   *
   * @return {對象}
   * 將斷點名稱映射到最大寬度值的對象。
   */
  斷點(斷點){

    // 用作吸氣劑。
    如果(斷點===未定義){
      返回分配(this.breakpoints_);
    }

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

    // 當斷點定義改變時,我們需要更新當前
    // 選定的斷點。
    this.updateCurrentBreakpoint_();

    // 返回前克隆斷點。
    返回分配(this.breakpoints_);
  }

  /**
   * 獲取或設置一個標誌,指示該玩家是否應該調整
   * 其 UI 基於其尺寸。
   *
   * @param {boolean} 值
   * 如果播放器應根據其調整其 UI,則應為 `true`
   * 尺寸;否則,應該是「假」。
   *
   * @return {布爾值}
   * 如果此播放器應根據其調整其 UI,則為“true”
   * 尺寸; 否則,將是 '假'。
   */
  響應(值){

    // 用作吸氣劑。
    如果(值===未定義){
      返回this.responsive_;
    }

    值 = 布爾值(值);
    const current = this.responsive_;

    // 沒有改變。
    如果(值===當前){
      返回;
    }

    // 值實際改變了,設置它。
    this.responsive_ = 值;

    // 開始偵聽斷點並設置初始斷點
    // 播放器現在響應。
    如果(值){
      this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
      this.updateCurrentBreakpoint_();

    // 如果播放器不再響應,則停止監聽斷點。
    }其他{
      this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
      this.removeCurrentBreakpoint_();
    }

    返回值;
  }

  /**
   * 獲取當前斷點名稱,如果有的話。
   *
   * @return {字符串}
   * 如果當前設置了斷點,則返回一個來自
   * 斷點對象匹配它。否則,返回一個空字符串。
   */
  當前斷點(){
    返回 this.breakpoint_;
  }

  /**
   * 獲取當前斷點類名。
   *
   * @return {字符串}
   * 匹配的類名(例如 `"vjs-layout-tiny"` 或
   * `"vjs-layout-large"`) 當前斷點。空字符串如果
   * 沒有當前斷點。
   */
  當前斷點類(){
    返回 BREAKPOINT_CLASSES[this.breakpoint_] || '';
  }

  /**
   * 描述單個媒體的對象。
   *
   * 不屬於該類型描述的屬性將被保留;所以,
   * 這也可以被視為一種通用的元數據存儲機制。
   *
   * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
   * @typedef {Object} Player~MediaObject
   *
   * @property {字符串} [專輯]
   * 未使用,除非將此對像傳遞給“MediaSession”
   * API。
   *
   * @property {string} [藝術家]
   * 未使用,除非將此對像傳遞給“MediaSession”
   * API。
   *
   * @property {對象[]} [藝術品]
   * 未使用,除非將此對像傳遞給“MediaSession”
   * API。如果未指定,將通過“海報”填充,如果
   * 可用的。
   *
   * @property {string} [海報]
   * 將在播放前顯示的圖像的 URL。
   *
   * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
   * 單個源對象、源對像數組或字符串
   * 引用指向媒體源的 URL。這是_強烈推薦_
   * 這裡使用了一個對像或對像數組,所以源
   * 選擇算法可以考慮“類型”。
   *
   * @property {字符串} [標題]
   * 未使用,除非將此對像傳遞給“MediaSession”
   * API。
   *
   * @property {Object[]} [textTracks]
   * 一組用於創建文本軌道的對象,如下
   * {@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 {Function} 準備好了
   * 播放器準備就緒時調用的回調。
   */
  loadMedia(媒體,準備就緒){
    如果(!媒體 || 類型的媒體!== '對象'){
      返回;
    }

    這個。重置();

    // 克隆媒體對象,使其不能從外部改變。
    this.cache_.media = mergeOptions(媒體);

    const {藝術品、海報、src、textTracks} = this.cache_.media;

    // 如果未給出 `artwork`,則使用 `poster` 創建它。
    如果(!藝術品和海報){
      this.cache_.media.artwork = [{
        來源:海報,
        類型:getMimetype(海報)
      }];
    }

    如果(源){
      這個.src(src);
    }

    如果(海報){
      這個。海報(海報);
    }

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

    this.ready(準備就緒);
  }

  /**
   * 為該播放器獲取當前 {@link Player~MediaObject} 的克隆。
   *
   * 如果沒有使用 `loadMedia` 方法,將嘗試返回一個
   * {@link Player~MediaObject} 基於播放器的當前狀態。
   *
   * @return {播放器~MediaObject}
   */
  得到媒體(){
    如果(!this.cache_.media){
      const poster = this.poster();
      const src = this.currentSources();
      const textTracks = Array.prototype.map.call(this.remoteTextTracks(), (tt) => ({
        種類:tt.kind,
        標籤:tt.label,
        語言:tt.language,
        來源:tt.src
      }));

      const media = {src, textTracks};

      如果(海報){
        media.poster = 海報;
        媒體.藝術品= [{
          來源:media.poster,
          類型:getMimetype(media.poster)
        }];
      }

      返回媒體;
    }

    返回合併選項(this.cache_.media);
  }

  /**
   * 獲取標籤設置
   *
   * @param {元素} 標籤
   * 玩家標籤
   *
   * @return {對象}
   * 包含所有設置的對象
   * 對於玩家標籤
   */
  靜態 getTagSettings(標籤){
    const 基礎選項 = {
      資料來源:[],
      曲目:[]
    };

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

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

    // 檢查數據設置屬性是否存在。
    如果(數據設置!== null){
      // 解析選項 JSON
      // 如果為空字符串,將其設為可解析的 json 對象。
      const [err, data] = safeParseTuple(dataSetup || '{}');

      如果(錯誤){
        日誌錯誤(錯誤);
      }
      分配(標籤選項,數據);
    }

    分配(基礎選項,標籤選項);

    // 獲取標籤子設置
    如果(tag.hasChildNodes()){
      const children = tag.childNodes;

      對於 (讓我 = 0, j = 孩子. 長度; 我 <  ; 我 ++) {
        const child = children[i];
        // 需要更改大小寫:http://ejohn.org/blog/nodename-case-sensitivity/
        const childName = child.nodeName.toLowerCase();

        如果 (childName === '來源') {
          baseOptions.sources.push(Dom.getAttributes(child));
        } else if (childName === 'track') {
          baseOptions.tracks.push(Dom.getAttributes(child));
        }
      }
    }

    返回基本選項;
  }

  /**
   * 判斷是否支持flexbox
   *
   * @return {布爾值}
   * - 如果支持 flexbox 則為真
   * - 如果不支持 flexbox,則為 false
   */
  flexNotSupported_() {
    const elem = document.createElement('i');

    // 筆記:我們實際上並沒有使用 flexBasis(或 flexOrder),但它是其中之一
    // 在檢查 flex 支持時我們可以依賴的常見 flex 特性。
    返回!(元素風格 || 中的「柔性基礎」
            elem.style 中的 'webkitFlexBasis' ||
            elem.style 中的 'mozFlexBasis' ||
            elem.style 中的 'msFlexBasis' ||
            // 特定於 IE10(2012 flex 規範),可用於完整性
            elem.style 中的“msFlexOrder”);
  }

  /**
   * 設置調試模式以在信息級別啟用/禁用日誌。
   *
   * @param {boolean} 啟用
   * @fires Player#debugon
   * @fires Player#debugoff
   */
  調試(啟用){
    如果(啟用===未定義){
      返回 this.debugEnabled_;
    }
    如果(啟用){
      this.trigger('debugon');
      this.previousLogLevel_ = this.log.level;
      this.log.level('調試');
      this.debugEnabled_ = true;
    }其他{
      this.trigger('debugoff');
      this.log.level(this.previousLogLevel_);
      this.previousLogLevel_ = undefined;
      this.debugEnabled_ = false;
    }

  }

  /**
   * 設置或獲取當前播放速率。
   * 採用數組並使用新項目更新播放速率菜單。
   * 傳入一個空數組以隱藏菜單。
   * 忽略數組以外的值。
   *
   * @fires Player#playbackrateschange
   * @param {number[]} newRates
   * 播放速率菜單應更新到的新速率。
   * 空數組將隱藏菜單
   * @return {number[]} 當用作 getter 時將返回當前播放速率
   */
  回放率(新率){
    如果(新利率===未定義){
      返回 this.cache_.playbackRates;
    }

    // 忽略任何不是數組的值
    如果(!Array.isArray(newRates)){
      返回;
    }

    // 忽略任何不僅僅包含數字的數組
    如果(!每((率)=> 類型率 ===「數字」){
      返回;
    }

    this.cache_.playbackRates = newRates;

    /**
    * 當播放器中的播放速率改變時觸發
    *
    * @event Player#playbackrateschange
    * @type {EventTarget~Event}
    */
    this.trigger('playbackrateschange');
  }
}

/**
 * 獲取 {@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 {AudioTrackList}
 * 當前音軌列表
 *
 * @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 props = TRACK_TYPES[名稱];

  Player.prototype[props.getterName] = function() {
    如果(this.tech_){
      返回 this.tech_[props.getterName]();
    }

    //如果我們還沒有 LoadTech_,我們創建 {視頻,音頻,文本} 軌道 _
    // 這些將在加載期間傳遞給技術人員
    這個[props.privateName] = 這個[props.privateName] ||新道具.ListClass();
    返回這個[props.privateName];
  };
});

/**
 * 獲取或設置 `Player` 的 crossorigin 選項。對於 HTML5 播放器,這
 * 在 `<video>` 標籤上設置 `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 值。
 * - 設置時未定義
 */
Player.prototype.crossorigin = Player.prototype.crossOrigin;

/**
 * 玩家的全球枚舉。
 *
 * 鍵是玩家 ID,值是 {@link Player}
 * 已處置玩家的實例或 `null`。
 *
 * @type {對象}
 */
Player.players = {};

const navigator = window.navigator;

/*
 *播放器實例選項,使用選項浮出水面
 * options = Player.prototype.options_
 * 在選項中進行更改,不在此處。
 *
 * @type {對象}
 * @私人的
 */
Player.prototype.options_ = {
  // 回退技術的默認順序
  技術訂單:Tech.defaultTechOrder_,

  html5:{},

  // 默認不活動超時
  不活動超時:2000年

  // 默認播放速率
  回放率:[],
  // 通過添加速率添加播放速率選擇
  // 'playbackRates': [0.5, 1, 1.5, 2],
  liveui:假的,

  // 包含的控制集
  孩子們: [
    '媒體加載器',
    '海報圖片',
    'textTrackDisplay',
    '加載微調器',
    '大播放按鈕',
    'liveTracker',
    '控制欄',
    '錯誤顯示',
    'textTrackSettings',
    '調整大小管理器'
  ]、

  語言:導航器 && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || '恩',

  // 語言環境及其語言翻譯
  語言:{},

  // 無法播放視頻時顯示的默認消息。
  不支持消息:'找不到此媒體的兼容源。',

  正常化自動播放:假,

  全屏: {
    選項: {
      navigationUI: '隱藏'
    }
  },

  斷點:{},
  回應:假的,
  audioOnlyMode:假,
  audioPosterMode: false
};

[
  /**
   * 返回播放器是否處於“結束”狀態。
   *
   * @return {Boolean} 如果玩家處於結束狀態則為真,否則為假。
   * @method 播放器#ended
   */
  '結束',
  /**
   * 返回玩家是否處於“尋找”狀態。
   *
   * @return {Boolean} 如果玩家處於搜索狀態則為真,否則為假。
   * @method Player#seeking
   */
  '尋求',
  /**
   * 返回當前可用媒體的時間範圍
   * 尋求。
   *
   * @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 {number} 當前網絡活動狀態
   * @method 播放器#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 {number} 當前播放渲染狀態
   * @method 播放器#readyState
   */
  '就緒狀態'
].forEach(函數(fn) {
  Player.prototype[fn] = function() {
    返回 this.techGet_(fn);
  };
});

TECH_EVENTS_RETRIGGER.forEach(函數(事件){
  Player.prototype[`handleTech${toTitleCase(event)}_`] = function() {
    返回 this.trigger(event);
  };
});

/**
 * 當玩家有初始持續時間和維度信息時觸發
 *
 * @event Player#loadedmetadata
 * @type {EventTarget~Event}
 */

/**
 * 當播放器在當前播放位置下載數據時觸發
 *
 * @event Player#loadeddata
 * @type {EventTarget~Event}
 */

/**
 *當前播放位置改變時觸發*
 * 在播放期間,每 15-250 毫秒觸發一次,具體取決於
 * 使用的回放技術。
 *
 * @event Player#timeupdate
 * @type {EventTarget~Event}
 */

/**
 * 音量變化時觸發
 *
 * @event Player#volumechange
 * @type {EventTarget~Event}
 */

/**
 * 報告玩家是否有可用的插件。
 *
 * 這不會報告插件是否已經初始化
 * 在這個播放器上。為此,[usingPlugin]{@link Player#usingPlugin}。
 *
 * @method 播放器#hasPlugin
 * @param {string} 名稱
 * 插件名稱。
 *
 * @return {布爾值}
 * 該播放器是否有可用的請求插件。
 */

/**
 * 報告玩家是否按名稱使用插件。
 *
 * 對於基本插件,這只報告插件是否_曾經_被使用過
 * 在此播放器上初始化。
 *
 * @method 播放器#usingPlugin
 * @param {string} 名稱
 * 插件名稱。
 *
 * @return {布爾值}
 * 此播放器是否正在使用請求的插件。
 */

Component.registerComponent('Player', Player);
導出默認播放器;