JavaScript video.js 源碼分析
-
video.js 源碼分析(JavaScript)
-
組織結構
-
繼承關系
-
運行機制
-
插件的運行機制
-
插件的定義
-
插件的運行
-
-
控制條是如何運行的
-
UI與JavaScript對象的銜接
-
類的掛載方式
-
存儲
-
獲取
</ul> </li>
</ul> </li>
</ul>
組織結構
以下是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') { thisname; } 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_);
}</code></pre>
在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;
}</code></pre>
獲取
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]; }
}</code></pre>
在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); })</code></pre>
來自:https://segmentfault.com/a/1190000007131342
-
-