video.js 源碼分析(JavaScript)

組織結構

如下是video.js的源碼組織結構關係,涉及控制條、菜單、浮層、進度條、滑動塊、多媒體、音軌字幕、輔助函數集合等等。

├── control-bar
│   ├── audio-track-controls
│   │   ├── audio-track-button.js
│   │   └── audio-track-menu-item.js
│   ├── playback-rate-menu
│   │   ├── playback-rate-menu-button.js
│   │   └── playback-rate-menu-item.js
│   ├── progress-control
│   │   ├── load-progress-bar.js
│   │   ├── mouse-time-display.js
│   │   ├── play-progress-bar.js
│   │   ├── progress-control.js
│   │   ├── seek-bar.js
│   │   └── tooltip-progress-bar.js
│   ├── spacer-controls
│   │   ├── custom-control-spacer.js
│   │   └── spacer.js
│   ├── text-track-controls
│   │   ├── caption-settings-menu-item.js
│   │   ├── captions-button.js
│   │   ├── chapters-button.js
│   │   ├── chapters-track-menu-item.js
│   │   ├── descriptions-button.js
│   │   ├── off-text-track-menu-item.js
│   │   ├── subtitles-button.js
│   │   ├── text-track-button.js
│   │   └── text-track-menu-item.js
│   ├── time-controls
│   │   ├── current-time-display.js
│   │   ├── duration-display.js
│   │   ├── remaining-time-display.js
│   │   └── time-divider.js
│   ├── volume-control
│   │   ├── volume-bar.js
│   │   ├── volume-control.js
│   │   └── volume-level.js
│   ├── control-bar.js
│   ├── fullscreen-toggle.js
│   ├── live-display.js
│   ├── mute-toggle.js
│   ├── play-toggle.js
│   ├── track-button.js
│   └── volume-menu-button.js
├── menu
│   ├── menu-button.js
│   ├── menu-item.js
│   └── menu.js
├── popup
│   ├── popup-button.js
│   └── popup.js
├── progress-bar
│   ├── progress-control
│   │   ├── load-progress-bar.js
│   │   ├── mouse-time-display.js
│   │   ├── play-progress-bar.js
│   │   ├── progress-control.js
│   │   ├── seek-bar.js
│   │   └── tooltip-progress-bar.js
│   └── progress-bar.js
├── slider
│   └── slider.js
├── tech
│   ├── flash-rtmp.js
│   ├── flash.js
│   ├── html5.js
│   ├── loader.js
│   └── tech.js
├── tracks
│   ├── audio-track-list.js
│   ├── audio-track.js
│   ├── html-track-element-list.js
│   ├── html-track-element.js
│   ├── text-track-cue-list.js
│   ├── text-track-display.js
│   ├── text-track-list-converter.js
│   ├── text-track-list.js
│   ├── text-track-settings.js
│   ├── text-track.js
│   ├── track-enums.js
│   ├── track-list.js
│   ├── track.js
│   ├── video-track-list.js
│   └── video-track.js
├── utils
│   ├── browser.js
│   ├── buffer.js
│   ├── dom.js
│   ├── events.js
│   ├── fn.js
│   ├── format-time.js
│   ├── guid.js
│   ├── log.js
│   ├── merge-options.js
│   ├── stylesheet.js
│   ├── time-ranges.js
│   ├── to-title-case.js
│   └── url.js
├── big-play-button.js
├── button.js
├── clickable-component.js
├── close-button.js
├── component.js
├── error-display.js
├── event-target.js
├── extend.js
├── fullscreen-api.js
├── loading-spinner.js
├── media-error.js
├── modal-dialog.js
├── player.js
├── plugins.js
├── poster-image.js
├── setup.js
└── video.js

video.js的JavaScript部分都是採用面向對象方式來實現的。基類是Component,全部其餘的類都是直接或間接集成此類實現。語法部分採用的是ES6標準。

繼承關係

深刻源碼解讀須要瞭解類與類之間的繼承關係,直接上圖。

  • 全部的繼承關係

  • 主要的繼承關係

運行機制

首先調用videojs啓動播放器,videojs方法判斷當前id是否已被實例化,若是沒有實例化新建一個Player對象,因Player繼承Component會自動初始化Component類。若是已經實例化直接返回Player對象。

videojs方法源碼以下:

function videojs(id, options, ready) {
let tag;
// id能夠是選擇器也能夠是DOM節點
if (typeof id === 'string') {
    if (id.indexOf('#') === 0) {
        id = id.slice(1);
    }
    //檢查播放器是否已被實例化
    if (videojs.getPlayers()[id]) {
        if (options) {
            log.warn(`Player "${id}" is already initialised. Options will not be applied.`);
        }
        if (ready) {
            videojs.getPlayers()[id].ready(ready);
        }
        return videojs.getPlayers()[id];
    }
    // 若是播放器沒有實例化,返回DOM節點
    tag = Dom.getEl(id);
} else {
    // 若是是DOM節點直接返回
    tag = id;
}
if (!tag || !tag.nodeName) {
    throw new TypeError('The element or ID supplied is not valid. (videojs)');
}
// 返回播放器實例
return tag.player || Player.players[tag.playerId] || new Player(tag, options, ready);
}
[]()

接下來咱們看下Player的構造函數,代碼以下:

constructor(tag, options, ready) {
    // 注意這個tag是video原生標籤
    tag.id = tag.id || `vjs_video_${Guid.newGUID()}`;
    // 選項配置的合併
    options = assign(Player.getTagSettings(tag), options);
    // 這個選項要關掉不然會在父類自動執行加載子類集合
    options.initChildren = false;
    // 調用父類的createEl方法
    options.createEl = false;
    // 在移動端關掉手勢動做監聽
    options.reportTouchActivity = false;
    // 檢查播放器的語言配置
    if (!options.language) {
        if (typeof tag.closest === 'function') {
            const closest = tag.closest('[lang]');
            if (closest) {
                options.language = closest.getAttribute('lang');
            }
        } else {
            let element = tag;
            while (element && element.nodeType === 1) {
                if (Dom.getElAttributes(element).hasOwnProperty('lang')) {
                    options.language = element.getAttribute('lang');
                    break;
                }
                element = element.parentNode;
            }
        }
    }
    // 初始化父類
    super(null, options, ready);
    // 檢查當前對象必須包含techOrder參數
    if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {
        throw new Error('No techOrder specified. Did you overwrite ' +
            'videojs.options instead of just changing the ' +
            'properties you want to override?');
    }
    // 存儲當前已被實例化的播放器
    this.tag = tag;
    // 存儲video標籤的各個屬性
    this.tagAttributes = tag && Dom.getElAttributes(tag);
    // 將默認的英文切換到指定的語言
    this.language(this.options_.language);
    if (options.languages) {
        const languagesToLower = {};
        Object.getOwnPropertyNames(options.languages).forEach(function(name) {
            languagesToLower[name.toLowerCase()] = options.languages[name];
        });
        this.languages_ = languagesToLower;
    } else {
        this.languages_ = Player.prototype.options_.languages;
    }
    // 緩存各個播放器的各個屬性.
    this.cache_ = {};
    // 設置播放器的貼片
    this.poster_ = options.poster || '';
    // 設置播放器的控制
    this.controls_ = !!options.controls;
    // 默認是關掉控制
    tag.controls = false;
    this.scrubbing_ = false;
    this.el_ = this.createEl();
    const playerOptionsCopy = mergeOptions(this.options_);
    // 自動加載播放器插件
    if (options.plugins) {
        const plugins = options.plugins;
        Object.getOwnPropertyNames(plugins).forEach(function(name) {
            if (typeof this[name] === 'function') {
                this[name](plugins[name]);
            } else {
                log.error('Unable to find plugin:', name);
            }
        }, this);
    }
    this.options_.playerOptions = playerOptionsCopy;
    this.initChildren();
    // 判斷是否是音頻
    this.isAudio(tag.nodeName.toLowerCase() === 'audio');
    if (this.controls()) {
        this.addClass('vjs-controls-enabled');
    } else {
        this.addClass('vjs-controls-disabled');
    }
    this.el_.setAttribute('role', 'region');
    if (this.isAudio()) {
        this.el_.setAttribute('aria-label', 'audio player');
    } else {
        this.el_.setAttribute('aria-label', 'video player');
    }
    if (this.isAudio()) {
        this.addClass('vjs-audio');
    }
    if (this.flexNotSupported_()) {
        this.addClass('vjs-no-flex');
    }

    if (!browser.IS_IOS) {
        this.addClass('vjs-workinghover');
    }
    Player.players[this.id_] = this;
    this.userActive(true);
    this.reportUserActivity();
    this.listenForUserActivity_();
    this.on('fullscreenchange', this.handleFullscreenChange_);
    this.on('stageclick', this.handleStageClick_);
}

在Player的構造器中有一句super(null, options, ready);實例化父類Component。咱們來看下Component的構造函數:

constructor(player, options, ready) {
    // 以前說過全部的類都是繼承Component,不是全部的類須要傳player
    if (!player && this.play) {
        // 這裏判斷調用的對象是否是Player自己,是自己只須要返回本身
        this.player_ = player = this; // eslint-disable-line
    } else {
        this.player_ = player;
    }
    this.options_ = mergeOptions({}, this.options_);
    options = this.options_ = mergeOptions(this.options_, options);
    this.id_ = options.id || (options.el && options.el.id);
    if (!this.id_) {
        const id = player && player.id && player.id() || 'no_player';
        this.id_ = `${id}_component_${Guid.newGUID()}`;
    }
    this.name_ = options.name || null;
    if (options.el) {
        this.el_ = options.el;
    } else if (options.createEl !== false) {
        this.el_ = this.createEl();
    }
    this.children_ = [];
    this.childIndex_ = {};
    this.childNameIndex_ = {};
    // 知道Player的構造函數爲啥要設置initChildren爲false了吧
    if (options.initChildren !== false) {
        // 這個initChildren方法是將一個類的子類都實例化,一個類都對應着本身的el(DOM實例),經過這個方法父類和子類的DOM繼承關係也就實現了
        this.initChildren();
    }
    this.ready(ready);
    if (options.reportTouchActivity !== false) {
        this.enableTouchActivity();
    }
}

插件的運行機制

插件的定義

import Player from './player.js';
// 將插件種植到Player的原型鏈
const plugin = function(name, init) {
  Player.prototype[name] = init;
};
// 暴露plugin接口
videojs.plugin = plugin;

插件的運行

// 在Player的構造函數裏判斷是否使用了插件,若是有遍歷執行
if (options.plugins) {
    const plugins = options.plugins;
    Object.getOwnPropertyNames(plugins).forEach(function(name) {
    if (typeof this[name] === 'function') {
        this[name](plugins[name]);
    } else {
        log.error('Unable to find plugin:', name);
    }
    }, this);
}

控制條是如何運行的

Player.prototype.options_ = {
  // 此處表示默認使用html5的video標籤
  techOrder: ['html5', 'flash'],
  html5: {},
  flash: {},
  // 默認的音量,官方代碼該配置無效有bug,咱們已修復,
  defaultVolume: 0.85,
  // 用戶的交互時長,好比超過這個時間表示失去焦點
  inactivityTimeout: 2000,
  playbackRates: [],
  // 這是控制條各個組成部分,做爲Player的子類
  children: [
    'mediaLoader',
    'posterImage',
    'textTrackDisplay',
    'loadingSpinner',
    'bigPlayButton',
    'progressBar',
    'controlBar',
    'errorDisplay',
    'textTrackSettings'
  ],
  language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',
  languages: {},
  notSupportedMessage: 'No compatible source was found for this media.'
};

Player類中有個children配置項,這裏面是控制條的各個組成部分的類。各個UI類還有子類,都是經過children屬性連接的。

UI與JavaScript對象的銜接

video.js裏都是組件化實現的,小到一個按鈕大到一個播放器都是一個繼承了Component類的對象實例,每一個對象包含一個el屬性,這個el對應一個DOM實例,el是經過createEl生成的DOM實例,在Component基類中包含一個方法createEl方法,子類也能夠重寫該方法。類與類的從屬關係是經過children屬性鏈接。

那麼整個播放器是怎麼把播放器的UI加載到HTML中的呢?在Player的構造函數裏能夠看到先生成el,而後初始化父類遍歷Children屬性,將children中的類實例化並將對應的DOM嵌入到player的el屬性中,最後在Player的構造函數中直接掛載到video標籤的父級DOM上。

if (tag.parentNode) {
  tag.parentNode.insertBefore(el, tag);
}

這裏的tag指的是video標籤。

類的掛載方式

上文有提到過UI的從屬關係是經過類的children方法鏈接的,可是全部的類都是關在Component類上的。這主要是基於對模塊化的考慮,經過這種方式實現了模塊之間的通訊。

存儲

static registerComponent(name, comp) {
    if (!Component.components_) {
      Component.components_ = {};
    }

    Component.components_[name] = comp;
    return comp;
}

獲取

static getComponent(name) {
    if (Component.components_ && Component.components_[name]) {
      return Component.components_[name];
    }

    if (window && window.videojs && window.videojs[name]) {
      log.warn(`The ${name} component was added to the videojs object when it should be registered using videojs.registerComponent(name, component)`);
      return window.videojs[name];
    }
}

在Componet裏有個靜態方法是registerComponet,全部的組件類都註冊到Componet的components_屬性裏。

例如控制條類ControlBar就是經過這個方法註冊的。

Component.registerComponent('ControlBar', ControlBar);

在Player的children屬性裏包括了controlBar類,而後經過getComponet獲取這個類。

.filter((child) => {
  const c = Component.getComponent(child.opts.componentClass ||
                                 toTitleCase(child.name));

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

若有疑問,請留言。

相關文章
相關標籤/搜索