/**
 * @file tech.js
 */

從 '../component' 導入組件;
從 '../utils/merge-options.js' 導入 mergeOptions;
從 '../utils/fn.js' 導入 * 作為 Fn;
從 '../utils/log.js' 導入日誌;
從'../utils/time-ranges.js'導入{createTimeRange};
從'../utils/buffer.js'導入{bufferedPercent};
從 '../media-error.js' 導入 MediaError;
從“全局/窗口”導入窗口;
從“全局/文檔”導入文檔;
從 '../utils/obj' 導入 {isPlain};
從 '../tracks/track-types' 導入 * 作為 TRACK_TYPES;
從 '../utils/string-cases.js' 導入 {toTitleCase, toLowerCase};
從 'videojs-vtt.js' 導入 vtt;
從 '../utils/guid.js' 導入 * 作為 Guid;

/**
 * 包含如下結構的對象:`{src: 'url', type: 'mimetype'}` 或字符串
 * 僅包含 src url。
 * * `變種源對象 = {SRC:'http://ex.com/video.mp4',類型:「視頻/MP4」};`
   * `var SourceString = 'http://example.com/some-video.mp4';`
 *
 * @typedef {Object|string} Tech~SourceObject
 *
 * @property {string} 源代碼
 * 源地址
 *
 * @property {string} 類型
 * 源的 mime 類型
 */

/**
 * {@link Tech} 用來創建新的 {@link TextTrack} 的函數。
 *
 * @私人的
 *
 * @param {Tech} 自我
 * Tech 類的一個實例。
 *
 * @param {string} 種類
 * `TextTrack` 類型(字幕、字幕、說明、章節或元數據)
 *
 * @param {字符串} [標籤]
 * 用於識別文本軌道的標籤
 *
 * @param {字符串} [語言]
 * 兩個字母的語言縮寫
 *
 * @param {對象} [選項={}]
 * 具有附加文本軌道選項的對象
 *
 * @return {TextTrack}
 * 創建的文本軌道。
 */
function createTrackHelper(self, kind, label, language, options = {}) {
  const tracks = self.textTracks();

  options.kind = 種類;

  如果(標籤){
    options.label = 標籤;
  }
  如果(語言){
    options.language = 語言;
  }
  options.tech = self;

  const track = new TRACK_TYPES.ALL.text.TrackClass(選項);

  tracks.addTrack(軌道);

  返回軌道;
}

/**
 * 這是媒體播放技術控制器的基類,例如
 * {@link HTML5}
 *
 * @extends 組件
 */
類技術擴展組件{

  /**
  * 創建該技術的一個實例。
  *
  * @param {對象} [選項]
  * 播放器選項的鍵/值存儲。
  *
  * @param {Component~ReadyCallback} 準備好了
  * `HTML5` 技術準備就緒時調用的回調函數。
  */
  構造函數(選項={},就緒=函數(){}){
    // 我們不希望技術自動報告用戶活動。
    // 這是在 addControlsListeners 中手動完成的
    options.reportTouchActivity = false;
    超級(空,選項,準備就緒);

    this.onDurationChange_ = (e) => this.onDurationChange(e);
    this.trackProgress_ = (e) => this.trackProgress(e);
    this.trackCurrentTime_ = (e) => this.trackCurrentTime(e);
    this.stopTrackingCurrentTime_ = (e) => this.stopTrackingCurrentTime(e);
    this.disposeSourceHandler_ = (e) => this.disposeSourceHandler(e);

    this.queuedHanders_ = new Set();

    // 跟踪當前源是否已經播放到
    // 實現一個非常有限的 played()
    this.hasStarted_ = false;
    this.on('播放', function() {
      this.hasStarted_ = true;
    });
    this.on('loadstart', function() {
      this.hasStarted_ = false;
    });

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

      如果(選項 && 選項[props.getterName]){
        this[props.privateName] = options[props.getterName];
      }
    });

    // 在瀏覽器/技術未報告的情況下手動跟踪進度。
    如果(!this.featuresProgressEvents){
      這個.manualProgressOn();
    }

    // 在瀏覽器/技術未報告的情況下手動跟踪時間更新。
    如果(!this.featuresTimeupdateEvents){
      this.manualTimeUpdatesOn();
    }

    ['文本', '音頻', '視頻'].forEach((track) => {
      如果(選項[`native${track}Tracks`] === false){
        這個[`featuresNative${track}Tracks`] = false;
      }
    });

    如果(options.nativeCaptions === false || options.nativeTextTracks === false){
      this.featuresNativeTextTracks = false;
    } else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
      this.featuresNativeTextTracks = true;
    }

    如果(!this.featuresNativeTextTracks){
      this.emulateTextTracks();
    }

    預加載文本軌道 = 選項。預加載文本軌道!== 假的;

    this.autoRemoteTextTracks_ = new TRACK_TYPES.ALL.text.ListClass();

    this.initTrackListeners();

    // 僅在不使用原生控件時開啟組件點擊事件
    如果(!options.nativeControlsForTouch){
      this.emitTapEvents();
    }

    如果(這個。構造函數){
      this.name_ = this.constructor.name || “未知技術”;
    }
  }

  /**
   * 一種特殊的觸發源設置的功能,允許播放器
   * 如果玩家或技術人員尚未準備好,則重新觸發。
   *
   * @fires Tech#sourceset
   * @param {string} src 源更改時的源字符串。
   */
  triggerSourceset(源){
    如果(!this.isReady_){
      // 在初始準備時,我們必須觸發源集
      // 準備好後 1 毫秒,以便玩家可以觀看。
      this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
    }

    /**
     * 當源設置在導致媒體元素的技術上時觸發
     * 重新加載。
     *
     * @see {@link 播放器 # 事件:源集}
     * @event Tech#sourceset
     * @type {EventTarget~Event}
     */
    這個。觸發({
      來源,
      類型:'源集'
    });
  }

  /* 不支持的事件類型的回退
  ================================================ ============================== */

  /**
   * 為原生不支持的瀏覽器填充 `progress` 事件。
   *
   * @see {@link Tech#trackProgress}
   */
  manualProgressOn() {
    this.on('durationchange', this.onDurationChange_);

    this.manualProgress = true;

    // 當源開始加載時觸發進度監視
    this.one('準備好了', this.trackProgress_);
  }

  /**
   * 關閉在中創建的 `progress` 事件的 polyfill
   * {@link Tech#manualProgressOn}
   */
  manualProgressOff() {
    this.manualProgress = false;
    this.stopTrackingProgress();

    this.off('durationchange', this.onDurationChange_);
  }

  /**
   * 這用於在緩衝的百分比發生變化時觸發“進度”事件。它
   * 設置一個間隔函數,每 500 毫秒調用一次以檢查是否
   * 緩衝區結束百分比已更改。
   *
   * > 此函數由 {@link Tech#manualProgressOn} 調用
   *
   * @param {EventTarget~Event} 事件
   * 導致此運行的“就緒”事件。
   *
   * @listens Tech#ready
   * @fires Tech#progress
   */
  跟踪進度(事件){
    this.stopTrackingProgress();
    this.progressInterval = this.setInterval(Fn.bind(this, function() {
      // 除非緩衝量大於上次,否則不觸發

      const numBufferedPercent = this.bufferedPercent();

      如果(這個。自助百分比 _!== 數字自助百分比) {
        /**
         *參見{@link Player#progress}
         *
         * @event 技術#progress
         * @type {EventTarget~Event}
         */
        this.trigger('進度');
      }

      this.bufferedPercent_ = numBufferedPercent;

      如果(numBufferedPercent === 1){
        this.stopTrackingProgress();
      }
    }), 500);
  }

  /**
   * 通過調用在 `durationchange` 事件上更新我們的內部持續時間
   * {@link Tech#duration}。
   *
   * @param {EventTarget~Event} 事件
   * 導致此運行的 `durationchange` 事件。
   *
   * @listens Tech#durationchange
   */
  onDurationChange(事件){
    this.duration_ = this.duration();
  }

  /**
   * 獲取並創建用於緩衝的 `TimeRange` 對象。
   *
   * @return {TimeRange}
   * 創建的時間範圍對象。
   */
  緩衝的() {
    返回 createTimeRange(0, 0);
  }

  /**
   * 獲取當前緩衝的當前視頻的百分比。
   *
   * @return {數字}
   * 一個從 0 到 1 的數字,表示小數點的百分比
   * 緩衝的視頻。
   *
   */
  緩衝百分比(){
    返回 bufferedPercent(this.buffered(), this.duration_);
  }

  /**
   * 關閉在中創建的 `progress` 事件的 polyfill
   * {@link Tech#manualProgressOn}
   * 通過清除設置的間隔來停止手動跟踪進度事件
   * {@link Tech#trackProgress}。
   */
  stopTrackingProgress() {
    this.clearInterval(this.progressInterval);
  }

  /**
   * 為不支持它的瀏覽器填充 `timeupdate` 事件。
   *
   * @see {@link Tech#trackCurrentTime}
   */
  manualTimeUpdatesOn() {
    this.manualTimeUpdates = true;

    this.on('播放', this.trackCurrentTime_);
    this.on('暫停', this.stopTrackingCurrentTime_);
  }

  /**
   * 關閉在中創建的 `timeupdate` 事件的 polyfill
   * {@link Tech#manualTimeUpdatesOn}
   */
  manualTimeUpdatesOff() {
    this.manualTimeUpdates = false;
    this.stopTrackingCurrentTime();
    this.off('播放', this.trackCurrentTime_);
    this.off('暫停', this.stopTrackingCurrentTime_);
  }

  /**
   * 設置一個間隔函數來跟踪當前時間並每隔一段時間觸發 `timeupdate`
   * 250 毫秒。
   *
   * @listens Tech#play
   * @triggers Tech#timeupdate
   */
  trackCurrentTime() {
    如果(this.currentTimeInterval){
      this.stopTrackingCurrentTime();
    }
    this.currentTimeInterval = this.setInterval(函數() {
      /**
       * 以 250 毫秒的間隔觸發,表示時間正在視頻中流逝。
       *
       * @event 技術#timeupdate
       * @type {EventTarget~Event}
       */
      this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });

    // 42 = 24 fps // Webkit 使用 250 // FF 使用 15
    }, 250);
  }

  /**
   * 停止在 {@link Tech#trackCurrentTime} 中創建的間隔函數,以便
   * 不再觸發 `timeupdate` 事件。
   *
   * @listens {技術#暫停}
   */
  stopTrackingCurrentTime() {
    this.clearInterval(this.currentTimeInterval);

    // #1002 - 如果視頻在下一次時間更新發生之前結束,
    // 進度條不會一直走到最後
    this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
  }

  /**
   * 關閉所有事件 polyfill,清除 `Tech` 的 {@link AudioTrackList},
   * {@link VideoTrackList} 和 {@link TextTrackList},並處理此技術。
   *
   * @fires 組件#dispose
   */
  處置(){

    // 清除所有軌道,因為我們不能在技術人員之間重複使用它們
    this.clearTracks(TRACK_TYPES.NORMAL.names);

    // 關閉任何手動進度或時間更新跟踪
    如果(this.manualProgress){
      這個.manualProgressOff();
    }

    如果(this.manualTimeUpdates){
      this.manualTimeUpdatesOff();
    }

    super.dispose();
  }

  /**
   * 清除單個 `TrackList` 或一組 `TrackLists` 給定的名稱。
   *
   * > 注意:沒有源處理程序的技術人員應該在“視頻”的源之間調用它
   * & `audio` 曲目。你不想在曲目之間使用它們!
   *
   * @param {string[]|string} 類型
   * 要清除的 TrackList 名稱,有效名稱為 `video`、`audio` 和
   *`文本`。
   */
  clearTracks(類型){
    類型 = [].concat(類型);
    // 清除所有軌道,因為我們不能在技術人員之間重複使用它們
    types.forEach((type) => {
      const list = this[`${type}Tracks`]() || [];
      讓我= list.length;

      當我 - ) {
        const track = 列表[i];

        如果(類型==='文本'){
          this.removeRemoteTextTrack(track);
        }
        list.removeTrack(track);
      }
    });
  }

  /**
   * 刪除通過 addRemoteTextTrack 添加的任何 TextTracks
   * 標記為自動垃圾收集
   */
  cleanupAutoTextTracks() {
    const list = this.autoRemoteTextTracks_ || [];
    讓我= list.length;

    當我 - ) {
      const track = 列表[i];

      this.removeRemoteTextTrack(track);
    }
  }

  /**
   * 重置技術,這將刪除所有來源並重置內部 readyState。
   *
   * @抽象的
   */
  重置() {}

  /**
   * 從技術中獲取 `crossOrigin` 的值。
   *
   * @抽象的
   *
   * @see {Html5#crossOrigin}
   */
  交叉來源(){}

  /**
   * 在技術上設置 `crossOrigin` 的值。
   *
   * @抽象的
   *
   * @param {字符串} 交叉起源值
   * @see {Html5#setCrossOrigin}
   */
  setCrossOrigin() {}

  /**
   * 獲取或設置技術錯誤。
   *
   * @param {MediaError} [錯誤]
   * 技術設置錯誤
   *
   * @return {MediaError|null}
   * 技術上的當前錯誤對象,如果沒有則為 null。
   */
  錯誤(錯誤){
    如果(錯誤!==未定義){
      this.error_ = new MediaError(err);
      this.trigger('錯誤');
    }
    返回這個。錯誤_;
  }

  /**
   * 返回當前源已播放的 `TimeRange`。
   *
   * > 注意:這個實現是不完整的。它不跟踪播放的“TimeRange”。
   * 它只檢查源是否播放過。
   *
   * @return {TimeRange}
   * - 單個時間範圍(如果該視頻已播放)
   * - 如果不是,則為一組空範圍。
   */
  播放(){
    如果(this.hasStarted_){
      返回 createTimeRange(0, 0);
    }
    返回創建時間範圍();
  }

  /**
   * 開始播放
   *
   * @抽象的
   *
   * @see {Html5#play}
   */
  玩() {}

  /**
   * 設置我們是否正在擦洗
   *
   * @抽象的
   *
   * @see {Html5#setScrubbing}
   */
  setScrubbing() {}

  /**
   * 獲取我們是否正在擦洗
   *
   * @抽象的
   *
   * @see {Html5#scrubbing}
   */
  擦洗(){}

  /**
   *如果{@link Tech#manualTimeUpdatesOn}是,則會導致手動時間更新
   * 之前調用過。
   *
   * @fires Tech#timeupdate
   */
  設置當前時間(){
    // 提高手動時間更新的準確性
    如果(this.manualTimeUpdates){
      /**
       * 手動 `timeupdate` 事件。
       *
       * @event 技術#timeupdate
       * @type {EventTarget~Event}
       */
      this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true });
    }
  }

  /**
   * 打開 {@link VideoTrackList}、{@link {AudioTrackList} 和
   * {@link TextTrackList} 事件。
   *
   * 這為 `addtrack` 和 `removetrack` 添加了 {@link EventTarget~EventListeners}。
   *
   * @fires Tech#audiotrackchange
   * @fires Tech#videotrackchange
   * @fires Tech#texttrackchange
   */
  initTrackListeners() {
    /**
      * 在 Tech {@link AudioTrackList} 上添加或刪除曲目時觸發
      *
      * @event Tech#audiotrackchange
      * @type {EventTarget~Event}
      */

    /**
      * 在 Tech {@link VideoTrackList} 上添加或刪除曲目時觸發
      *
      * @event Tech#videotrackchange
      * @type {EventTarget~Event}
      */

    /**
      * 在 Tech {@link TextTrackList} 上添加或刪除曲目時觸發
      *
      * @event Tech#texttrackchange
      * @type {EventTarget~Event}
      */
    TRACK_TYPES.NORMAL.names.forEach((名稱) => {
      const props = TRACK_TYPES.NORMAL[名稱];
      const trackListChanges = () => {
        this.trigger(`${name}trackchange`);
      };

      const tracks = this[props.getterName]();

      tracks.addEventListener('removetrack', trackListChanges);
      tracks.addEventListener('addtrack', trackListChanges);

      this.on('處置', () => {
        tracks.removeEventListener('removetrack', trackListChanges);
        tracks.removeEventListener('addtrack', trackListChanges);
      });
    });
  }

  /**
   * 如有必要,使用 vtt.js 模擬 TextTracks
   *
   * @fires Tech#vttjsloaded
   * @fires Tech#vttjserror
   */
  addWebVttScript_() {
    如果(窗口。WebVTT){
      返回;
    }

    // 最初,Tech.el_ 是 dummy-div 的子級,等待組件系統
    // 表示 Tech 已準備就緒,此時 Tech.el_ 是 DOM 的一部分
    // 在插入 WebVTT 腳本之前
    如果 (document.body.contains(this.el())) {

      // 如果可用且未傳入 vtt.js 腳本位置,則通過 require 加載
      // 作為一個選項。 novtt 構建會將上面的 require 調用變成一個空對象
      // 這將導致這個 if 檢查總是失敗。
      如果(!這個。選項 _ ['vtt.js'] && 艾斯普林(VTT)和對象。鍵(VTT)。
        this.trigger('vttjsloaded');
        返回;
      }

      // 通過腳本位置選項加載 vtt.js 或沒有位置的 cdn 是
      //傳入
      const script = document.createElement('腳本');

      script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
      script.onload = () => {
        /**
         * 加載 vtt.js 時觸發。
         *
         * @event Tech#vttjsloaded
         * @type {EventTarget~Event}
         */
        this.trigger('vttjsloaded');
      };
      script.onerror = () => {
        /**
         * 由於錯誤而未加載 vtt.js 時觸發
         *
         * @event Tech#vttjsloaded
         * @type {EventTarget~Event}
         */
        this.trigger('vttjserror');
      };
      this.on('處置', () => {
        腳本.onload = null;
        script.onerror = null;
      });
      // 但是還沒有加載,我們在註入之前將它設置為 true,這樣
      // 如果它立即加載,我們不會覆蓋注入的 window.WebVTT
      window.WebVTT = true;
      this.el().parentNode.appendChild(腳本);
    }其他{
      this.ready(this.addWebVttScript_);
    }

  }

  /**
   * 模擬文本軌道
   *
   */
  emulateTextTracks() {
    const tracks = this.textTracks();
    const remoteTracks = this.remoteTextTracks();
    const handleAddTrack = (e) => tracks.addTrack(e.track);
    const handleRemoveTrack = (e) => tracks.removeTrack(e.track);

    remoteTracks.on('addtrack', handleAddTrack);
    remoteTracks.on('removetrack', handleRemoveTrack);

    this.addWebVttScript_();

    const updateDisplay = () => this.trigger('texttrackchange');

    const textTracksChanges = () => {
      更新顯示();

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

        track.removeEventListener('cuechange', updateDisplay);
        如果(track.mode ==='顯示'){
          track.addEventListener('cuechange', updateDisplay);
        }
      }
    };

    textTracksChanges();
    tracks.addEventListener('change', textTracksChanges);
    tracks.addEventListener('addtrack', textTracksChanges);
    tracks.addEventListener('removetrack', textTracksChanges);

    this.on('處置', function() {
      remoteTracks.off('addtrack', handleAddTrack);
      remoteTracks.off('removetrack', handleRemoveTrack);
      tracks.removeEventListener('change', textTracksChanges);
      tracks.removeEventListener('addtrack', textTracksChanges);
      tracks.removeEventListener('removetrack', textTracksChanges);

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

        track.removeEventListener('cuechange', updateDisplay);
      }
    });
  }

  /**
   * 創建並返回一個遠程 {@link TextTrack} 對象。
   *
   * @param {string} 種類
   * `TextTrack` 類型(字幕、字幕、說明、章節或元數據)
   *
   * @param {字符串} [標籤]
   * 用於識別文本軌道的標籤
   *
   * @param {字符串} [語言]
   * 兩個字母的語言縮寫
   *
   * @return {TextTrack}
   * 創建的 TextTrack。
   */
  addTextTrack(種類,標籤,語言){
    如果(!種類){
      throw new Error('需要 TextTrack 類型但未提供');
    }

    返回 createTrackHelper(this, kind, label, language);
  }

  /**
   * 創建一個模擬的 TextTrack 供 addRemoteTextTrack 使用
   *
   * 這旨在被繼承自的類覆蓋
   * 技術以創建本機或自定義 TextTrack。
   *
   * @param {Object} 選項
   * 該對象應包含用於初始化 TextTrack 的選項。
   *
   * @param {string} [options.kind]
   * `TextTrack` 類型(字幕、字幕、說明、章節或元數據)。
   *
   * @param {string} [選項.標籤]。
   * 用於識別文本軌道的標籤
   *
   * @param {string} [選項.語言]
   * 兩個字母的語言縮寫。
   *
   * @return {HTMLTrackElement}
   * 創建的軌道元素。
   */
  createRemoteTextTrack(選項){
    const track = mergeOptions(選項, {
      技術:這個
    });

    返回新的 TRACK_TYPES.REMOTE.remoteTextEl.TrackClass(track);
  }

  /**
   * 創建一個遠程文本軌道對象並返回一個 html 軌道元素。
   *
   * > 注意:這可以是模擬的 {@link HTMLTrackElement} 或本機的。
   *
   * @param {Object} 選項
   * 有關更多詳細屬性,請參閱 {@link Tech#createRemoteTextTrack}。
   *
   * @param {boolean} [manualCleanup=true]
   * - 如果為 false:TextTrack 將自動從視頻中刪除
   * 每當源更改時的元素
   * - 當為真時:必須手動清理 TextTrack
   *
   * @return {HTMLTrackElement}
   * 一個 Html 軌道元素。
   *
   * @deprecated 這個函數的默認功能是等價的
   * 將來改為“manualCleanup=false”。手動清除參數將
   * 也被刪除。
   */
  addRemoteTextTrack(選項 = {},manualCleanup){
    const htmlTrackElement = this.createRemoteTextTrack(選項);

    如果(手動清理!== 真正的 && 手動清理!== 假){
      //棄用警告
      log.warn('在未將“manualCleanup”參數顯式設置為“true”的情況下調用 addRemoteTextTrack 已被棄用,在未來版本的 video.js 中默認為“false”');
      手動清理 = 真;
    }

    // 將 HTMLTrackElement 和 TextTrack 存儲到遠程列表
    this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
    this.remoteTextTracks().addTrack(htmlTrackElement.track);

    如果(手動清理!==真){
      // 如果 TextTrackList 不存在則創建它
      this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
    }

    返回 htmlTrackElement;
  }

  /**
   * 從遠程 `TextTrackList` 中刪除遠程文本軌道。
   *
   * @param {TextTrack} 軌道
   * `TextTrack` 從 `TextTrackList` 中移除
   */
  removeRemoteTextTrack(軌道){
    const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);

    // 從遠程列表中移除 HTMLTrackElement 和 TextTrack
    this.remoteTextTrackEls().removeTrackElement_(trackElement);
    this.remoteTextTracks().removeTrack(track);
    this.autoRemoteTextTracks_.removeTrack(track);
  }

  /**
   * 獲取 W3C 媒體指定的可用媒體播放質量指標
   * 播放質量 API。
   *
   * @see [規範]{@link https://wicg.github.io/media-playback-quality}
   *
   * @return {對象}
   * 具有支持的媒體播放質量指標的對象
   *
   * @抽象的
   */
  getVideoPlaybackQuality() {
    返回 {};
  }

  /**
   * 嘗試創建一個始終位於其他窗口之上的浮動視頻窗口
   * 以便用戶在與其他人互動時可以繼續消費媒體
   * 內容網站或他們設備上的應用程序。
   *
   * @see [規範]{@link https://wicg.github.io/picture-in-picture}
   *
   * @return {承諾|未定義}
   * 如果瀏覽器支持,承諾帶有畫中畫窗口
   * 承諾(或一個作為選項傳入)。它返回未定義
   * 否則。
   *
   * @抽象的
   */
  請求畫中畫(){
    const PromiseClass = this.options_.Promise || window.Promise;

    如果(承諾類){
      返回 PromiseClass.reject();
    }
  }

  /**
   * 一種檢查“disablePictureInPicture”<video> 屬性值的方法。
   * 默認為 true,因為如果技術不支持 pip,則應將其視為已禁用
   *
   * @抽象的
   */
  禁用畫中畫(){
    返回真;
  }

  /**
   * 設置或取消設置 'disablePictureInPicture' <video> 屬性的方法。
   *
   * @抽象的
   */
  setDisablePictureInPicture() {}

  /**
   * 使用 requestAnimationFrame 的 requestVideoFrameCallback 回退實現
   *
   * @param {函數} cb
   * @return {number} 請求ID
   */
  requestVideoFrameCallback(cb) {
    const id = Guid.newGUID();

    如果(!this.isReady_ || this.paused()){
      this.queuedHanders_.add(id);
      this.one('播放', () => {
        如果(this.queuedHanders_.has(id)){
          this.queuedHanders_.delete(id);
          cb();
        }
      });
    }其他{
      this.requestNamedAnimationFrame(id, cb);
    }

    返回ID;
  }

  /**
   * cancelVideoFrameCallback 的回退實現
   *
   * @param {number} id 要取消的回調id
   */
  cancelVideoFrameCallback(id) {
    如果(this.queuedHanders_.has(id)){
      this.queuedHanders_.delete(id);
    }其他{
      this.cancelNamedAnimationFrame(id);
    }
  }

  /**
   * 一種從 `Tech` 設置海報的方法。
   *
   * @抽象的
   */
  設置海報(){}

  /**
   * 一種檢查 'playsinline' <video> 屬性是否存在的方法。
   *
   * @抽象的
   */
  playsinline() {}

  /**
   * 設置或取消設置 'playsinline' <video> 屬性的方法。
   *
   * @抽象的
   */
  setPlaysinline() {}

  /**
   * 嘗試強制覆蓋本機音軌。
   *
   * @param {boolean} override - 如果設置為 true 原生音頻將被覆蓋,
   *否則可能會使用本機音頻。
   *
   * @抽象的
   */
  overrideNativeAudioTracks() {}

  /**
   * 嘗試強制覆蓋本機視頻軌道。
   *
   * @param {boolean} override - 如果設置為 true 原生視頻將被覆蓋,
   *否則可能會使用原生視頻。
   *
   * @抽象的
   */
  overrideNativeVideoTracks() {}

  /*
   * 檢查技術是否可以支持給定的 mime 類型。
   *
   * 基礎技術不支持任何類型,但源處理程序可能
   * 覆蓋這個。
   *
   * @param {string} 類型
   * 用於檢查支持的 mimetype
   *
   * @return {字符串}
   * 'probably', 'maybe' 或空字符串
   *
   * @see [規範]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
   *
   * @抽象的
   */
  canPlayType() {
    返回 '';
  }

  /**
   * 檢查該技術是否支持該類型。
   *
   * 基礎技術不支持任何類型,但源處理程序可能
   * 覆蓋這個。
   *
   * @param {string} 類型
   * 要檢查的媒體類型
   * @return {string} 返回原生視頻元素的響應
   */
  靜態 canPlayType() {
    返回 '';
  }

  /**
   * 檢查技術是否可以支持給定的來源
   *
   * @param {對象} srcObj
   * 源對象
   * @param {Object} 選項
   * 傳遞給技術的選項
   * @return {string} 'probably', 'maybe', or '' (空字符串)
   */
  靜態 canPlaySource(srcObj,選項){
    返回 Tech.canPlayType(srcObj.type);
  }

  /*
   * 返回參數是否為 Tech。
   * 可以傳遞類似 `Html5` 的類或類似 `player.tech_` 的實例
   *
   * @param {Object} 組件
   * 要檢查的項目
   *
   * @return {布爾值}
   * 是否是技術
   * - 如果是技術則為真
   * - 如果不是則為假
   */
  靜態技術(組件){
    返回 component.prototype instanceof Tech ||
           技術組件實例 ||
           組件 === 技術;
  }

  /**
   * 將 `Tech` 註冊到 videojs 的共享列表中。
   *
   * @param {string} 名稱
   * 要註冊的 `Tech` 的名稱。
   *
   * @param {Object} 技術
   * 要註冊的 `Tech` 類。
   */
  靜態註冊技術(名稱,技術){
    如果(!Tech.techs_){
      Tech.techs_ = {};
    }

    如果(!Tech.isTech(技術)){
      throw new Error(`Tech ${name} 必須是 Tech`);
    }

    如果(!Tech.canPlayType){
      throw new Error('Techs must have a static canPlayType method on them');
    }
    如果(!Tech.canPlaySource){
      throw new Error('Techs must have a static canPlaySource method on them');
    }

    名稱 = toTitleCase(名稱);

    Tech.techs_[名稱] = 技術;
    Tech.techs_[toLowerCase(name)] = tech;
    如果(名稱!=='技術'){
      // 駝峰式大小寫在 techOrder 中使用的 techName
      Tech.defaultTechOrder_.push(name);
    }
    返回技術;
  }

  /**
   * 按名稱從共享列表中獲取 `Tech`。
   *
   * @param {string} 名稱
   * 要獲取的 Tech 的 `camelCase` 或 `TitleCase` 名稱
   *
   * @return {技術|未定義}
   * 如果沒有具有請求名稱的技術,則為 `Tech` 或未定義。
   */
  靜態getTech(名稱){
    如果(!名稱){
      返回;
    }

    if (Tech.techs_ && Tech.techs_[name]) {
      返回 Tech.techs_[名稱];
    }

    名稱 = toTitleCase(名稱);

    如果(窗口 && window.videojs && window.videojs [名稱]){
      log.warn(`${name} 技術在應該使用 videojs.registerTech(name, tech) 註冊時被添加到 videojs 對像中`);
      返回 window.videojs[名稱];
    }
  }
}

/**
 * 獲取 {@link VideoTrackList}
 *
 * @returns {VideoTrackList}
 * @method Tech.prototype.videoTracks
 */

/**
 *獲取{@link AudioTrackList}
 *
 * @returns {AudioTrackList}
 * @method Tech.prototype.audioTracks
 */

/**
 *獲取{@link TextTrackList}
 *
 * @returns {TextTrackList}
 * @method Tech.prototype.textTracks
 */

/**
 * 獲取遠程元素 {@link TextTrackList}
 *
 * @returns {TextTrackList}
 * @method Tech.prototype.remoteTextTracks
 */

/**
 * 獲取遠程元素 {@link HtmlTrackElementList}
 *
 * @returns {HtmlTrackElementList}
 * @method Tech.prototype.remoteTextTrackEls
 */

TRACK_TYPES.ALL.names.forEach(函數(名稱){
  const props = TRACK_TYPES.ALL[名稱];

  Tech.prototype[props.getterName] = function() {
    這個[props.privateName] = 這個[props.privateName] ||新道具.ListClass();
    返回這個[props.privateName];
  };
});

/**
 *相關文本軌道列表
 *
 * @type {TextTrackList}
 * @私人的
 * @property Tech#textTracks_
 */

/**
 * 相關音軌列表。
 *
 * @type {AudioTrackList}
 * @私人的
 * @property Tech#audioTracks_
 */

/**
 * 相關視頻軌道列表。
 *
 * @type {VideoTrackList}
 * @私人的
 * @property Tech#videoTracks_
 */

/**
 * 布爾值表示 `Tech` 是否支持音量控制。
 *
 * @type {布爾}
 * @默認
 */
Tech.prototype.featuresVolumeControl = true;

/**
 * 表示 `Tech` 是否支持靜音音量的布爾值。
 *
 * @type {布爾值}
 * @默認
 */
Tech.prototype.featuresMuteControl = true;

/**
 * 布爾值,指示 `Tech` 是否支持全屏調整大小控制。
 * 使用請求全屏調整插件大小會重新加載插件
 *
 * @type {布爾}
 * @默認
 */
Tech.prototype.featuresFullscreenResize = false;

/**
 * 表示 `Tech` 是否支持改變視頻播放速度的布爾值
 * 播放。範例:
 * - 設置播放器以 2 倍(兩倍)的速度播放
 * - 設置播放器播放速度為 0.5 倍(一半)
 *
 * @type {布爾}
 * @默認
 */
Tech.prototype.featuresPlaybackRate = false;

/**
 * 布爾值表示 `Tech` 是否支持 `progress` 事件。這是目前
 * 不是由 video-js-swf 觸發的。這將用於確定是否
 * {@link Tech#manualProgressOn} 應該被調用。
 *
 * @type {布爾}
 * @默認
 */
Tech.prototype.featuresProgressEvents = false;

/**
 * 表示 `Tech` 是否支持 `sourceset` 事件的布爾值。
 *
 * 技術人員應將其設置為“true”,然後使用 {@link Tech#triggerSourceset}
 * 在獲取後最早時間觸發 {@link 技術 # 事件:源集}
 * 一個新的來源。
 *
 * @type {布爾}
 * @默認
 */
Tech.prototype.featuresSourceset = false;

/**
 * 表示 `Tech` 是否支持 `timeupdate` 事件的布爾值。這是目前
 * 不是由 video-js-swf 觸發的。這將用於確定是否
 * {@link Tech#manualTimeUpdates} 應該被調用。
 *
 * @type {布爾}
 * @默認
 */
Tech.prototype.featuresTimeupdateEvents = false;

/**
 * 布爾值,表示 `Tech` 是否支持原生的 `TextTrack`。
 * 如果瀏覽器支持,這將幫助我們與原生 `TextTrack` 集成。
 *
 * @type {布爾}
 * @默認
 */
Tech.prototype.featuresNativeTextTracks = false;

/**
 * 表示 `Tech` 是否支持 `requestVideoFrameCallback` 的布爾值。
 *
 * @type {布爾}
 * @默認
 */
Tech.prototype.featuresVideoFrameCallback = false;

/**
 * 為想要使用源處理程序模式的技術人員提供的功能性混合。
 * 源處理程序是用於處理特定格式的腳本。
 * 源處理程序模式用於自適應格式(HLS、DASH)
 * 手動加載視頻數據並將其送入源緩衝區(媒體源擴展)
 * 示例:帶有來源處理程序的技術。呼叫(MyTech); `
 *
 * @param {技術} _技術
 * 添加源處理函數的技術。
 *
 * @mixes Tech~SourceHandlerAdditions
 */
Tech.withSourceHandlers = function(_Tech) {

  /**
   * 註冊一個源處理器
   *
   * @param {函數}處理程序
   * 源處理器類
   *
   * @param {number} [索引]
   * 在以下索引處註冊
   */
  _Tech.registerSourceHandler = 函數(處理程序,索引){
    讓處理程序 = _Tech.sourceHandlers;

    如果(!處理程序){
      handlers = _Tech.sourceHandlers = [];
    }

    如果(索引===未定義){
      // 添加到列表的末尾
      index = handlers.length;
    }

    handlers.splice(index, 0, handler);
  };

  /**
   * 檢查技術是否可以支持給定的類型。還檢查
   * Techs sourceHandlers。
   *
   * @param {string} 類型
   * 要檢查的 mimetype。
   *
   * @return {字符串}
   *“可能”、“也許”或“”(空字符串)
   */
  _Tech.canPlayType = 函數(類型){
    常量處理程序 = _Tech.sourceHandlers || [];
    讓可以;

    對於(讓我 = 0; 我 < 處理器。長度; 我 ++){
      can = handlers[i].canPlayType(類型);

      如果能) {
        退貨罐;
      }
    }

    返回 '';
  };

  /**
   * 返回支持該源的第一個源處理程序。
   *
   * 去做:回答問題:“可能”應該優先於“也許”
   *
   * @param {Tech~SourceObject} 來源
   * 源對象
   *
   * @param {Object} 選項
   * 傳遞給技術的選項
   *
   * @return {SourceHandler|null}
   * 支持源的第一個源處理程序,如果為 null
   * 沒有 SourceHandler 支持源
   */
  _Tech.selectSourceHandler = 函數(源,選項){
    常量處理程序 = _Tech.sourceHandlers || [];
    讓可以;

    對於(讓我 = 0; 我 < 處理器。長度; 我 ++){
      can = handlers[i].canHandleSource(source, options);

      如果能) {
        返回處理程序[i];
      }
    }

    返回空值;
  };

  /**
   * 檢查技術是否可以支持給定的來源。
   *
   * @param {Tech~SourceObject} srcObj
   * 源對象
   *
   * @param {Object} 選項
   * 傳遞給技術的選項
   *
   * @return {字符串}
   *“可能”、“也許”或“”(空字符串)
   */
  _Tech.canPlaySource = 函數(srcObj,選項){
    const sh = _Tech.selectSourceHandler(srcObj, options);

    如果(噓){
      返回 sh.canHandleSource(srcObj,選項);
    }

    返回 '';
  };

  /**
   * 使用源處理程序時,更喜歡它的實現
   * 通常由技術人員提供的任何功能。
   */
  const 可延遲 = [
    '可尋',
    '尋求',
    '期間'
  ];

  /**
   * 圍繞 {@link Tech#seekable} 的包裝器,它將調用 `SourceHandler` 的 seekable
   * 函數(如果存在),回退到 Techs 可搜索函數。
   *
   * @method _Tech.seekable
   */

  /**
   * 圍繞 {@link Tech#duration} 的包裝器,它將調用 `SourceHandler` 的持續時間
   * 函數(如果存在),否則它將回退到技術持續時間函數。
   *
   * @method _Tech.duration
   */

  deferrable.forEach(函數(fnName){
    const originalFn = this[fnName];

    如果(原始類型 FN!== '功能') {
      返回;
    }

    這個 [fnName] = 函數 () {
      如果(this.sourceHandler_ && this.sourceHandler_[fnName]){
        返回 this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments);
      }
      返回 originalFn.apply(this, arguments);
    };
  }, _Tech.prototype);

  /**
   * 創建一個使用源對象設置源的函數
   * 和源處理程序。
   * 除非找到源處理程序,否則永遠不應調用。
   *
   * @param {Tech~SourceObject} 來源
   * 帶有 src 和 type 鍵的源對象
   */
  _Tech.prototype.setSource = 函數(來源){
    讓 sh = _Tech.selectSourceHandler(source, this.options_);

    如果(!sh){
      // 當不支持的源出現時回退到本地源處理程序
      //故意設置
      如果(_Tech.nativeSourceHandler){
        sh = _Tech.nativeSourceHandler;
      }其他{
        log.error('沒有找到當前源的源處理程序。');
      }
    }

    // 處置任何現有的源處理程序
    this.disposeSourceHandler();
    this.off('dispose', this.disposeSourceHandler_);

    如果(SH!== _ 技術。本地化處理程序){
      this.currentSource_ = 來源;
    }

    this.sourceHandler_ = sh.handleSource(source, this, this.options_);
    this.one('dispose', this.disposeSourceHandler_);
  };

  /**
   * 在處理 Tech 時清理所有現有的 SourceHandlers 和偵聽器。
   *
   * @listens Tech#dispose
   */
  _Tech.prototype.disposeSourceHandler = function() {
    // 如果我們有一個源並得到另一個
    // 然後我們加載新的東西
    // 而不是清除我們當前的所有曲目
    如果(this.currentSource_){
      this.clearTracks(['音頻', '視頻']);
      this.currentSource_ = null;
    }

    // 總是清理自動文本軌道
    this.cleanupAutoTextTracks();

    如果(this.sourceHandler_){

      如果(this.sourceHandler_.dispose){
        this.sourceHandler_.dispose();
      }

      this.sourceHandler_ = null;
    }
  };

};

// 基礎 Tech 類需要註冊為組件。這是唯一的
// 可以註冊為組件的技術。
Component.registerComponent('技術', 技術);
Tech.registerTech('技術', 技術);

/**
 * 應添加到玩家 techOrder 的技術列表
 *
 * @私人的
 */
Tech.defaultTechOrder_ = [];

導出默認技術;