video.js 源碼分析(JavaScript)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屬性連接的。
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); })
若有疑問,請留言。