JavaScript video.js 源码分析
JacWaterfie
8年前
<ul> <li> <p>video.js 源码分析(JavaScript)</p> <ul> <li> <p>组织结构</p> </li> <li> <p>继承关系</p> </li> <li> <p>运行机制</p> </li> <li> <p>插件的运行机制</p> <ul> <li> <p>插件的定义</p> </li> <li> <p>插件的运行</p> </li> </ul> </li> <li> <p>控制条是如何运行的</p> </li> <li> <p>UI与JavaScript对象的衔接</p> </li> <li> <p>类的挂载方式</p> <ul> <li> <p>存储</p> </li> <li> <p>获取</p> </li> </ul> </li> </ul> </li> </ul> <h3><strong>组织结构</strong></h3> <p>以下是video.js的源码组织结构关系,涉及控制条、菜单、浮层、进度条、滑动块、多媒体、音轨字幕、辅助函数集合等等。</p> <pre> <code class="language-javascript">├── 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</code></pre> <p>video.js的JavaScript部分都是采用面向对象方式来实现的。基类是Component,所有其他的类都是直接或间接集成此类实现。语法部分采用的是ES6标准。</p> <h3><strong>继承关系</strong></h3> <p>深入源码解读需要了解类与类之间的继承关系,直接上图。</p> <ul> <li> <p>所有的继承关系</p> <img src="https://simg.open-open.com/show/46af49f9b2d6bbf104805dfd479287c0.png"></li> <li> <p>主要的继承关系</p> <img src="https://simg.open-open.com/show/bd4091270d286247af919972461a55e5.png"></li> </ul> <h3><strong>运行机制</strong></h3> <p>首先调用videojs启动播放器,videojs方法判断当前id是否已被实例化,如果没有实例化新建一个Player对象,因Player继承Component会自动初始化Component类。如果已经实例化直接返回Player对象。</p> <p>videojs方法源码如下:</p> <pre> <code class="language-javascript">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); } []()</code></pre> <p>接下来我们看下Player的构造函数,代码如下:</p> <pre> <code class="language-javascript">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_); }</code></pre> <p>在Player的构造器中有一句 super(null, options, ready); 实例化父类Component。我们来看下Component的构造函数:</p> <pre> <code class="language-javascript">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(); } }</code></pre> <h3><strong>插件的运行机制</strong></h3> <p>插件的定义</p> <pre> <code class="language-javascript">import Player from './player.js'; // 将插件种植到Player的原型链 const plugin = function(name, init) { Player.prototype[name] = init; }; // 暴露plugin接口 videojs.plugin = plugin;</code></pre> <p>插件的运行</p> <pre> <code class="language-javascript">// 在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); }</code></pre> <h3><strong>控制条是如何运行的</strong></h3> <pre> <code class="language-javascript">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.' };</code></pre> <p>Player类中有个children配置项,这里面是控制条的各个组成部分的类。各个UI类还有子类,都是通过children属性链接的。</p> <h3><strong>UI与JavaScript对象的衔接</strong></h3> <p>video.js里都是组件化实现的,小到一个按钮大到一个播放器都是一个继承了Component类的对象实例,每个对象包含一个el属性,这个el对应一个DOM实例,el是通过createEl生成的DOM实例,在Component基类中包含一个方法createEl方法,子类也可以重写该方法。类与类的从属关系是通过children属性连接。</p> <p>那么整个播放器是怎么把播放器的UI加载到HTML中的呢?在Player的构造函数里可以看到先生成el,然后初始化父类遍历Children属性,将children中的类实例化并将对应的DOM嵌入到player的el属性中,最后在Player的构造函数中直接挂载到video标签的父级DOM上。</p> <pre> <code class="language-javascript">if (tag.parentNode) { tag.parentNode.insertBefore(el, tag); }</code></pre> <p>这里的tag指的是video标签。</p> <h3><strong>类的挂载方式</strong></h3> <p>上文有提到过UI的从属关系是通过类的children方法连接的,但是所有的类都是关在Component类上的。这主要是基于对模块化的考虑,通过这种方式实现了模块之间的通信。</p> <p>存储</p> <pre> <code class="language-javascript">static registerComponent(name, comp) { if (!Component.components_) { Component.components_ = {}; } Component.components_[name] = comp; return comp; }</code></pre> <p>获取</p> <pre> <code class="language-javascript">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> <p>在Componet里有个静态方法是registerComponet,所有的组件类都注册到Componet的components_属性里。</p> <p>例如控制条类ControlBar就是通过这个方法注册的。</p> <pre> <code class="language-javascript">Component.registerComponent('ControlBar', ControlBar);</code></pre> <p>在Player的children属性里包括了controlBar类,然后通过getComponet获取这个类。</p> <pre> <code class="language-javascript">.filter((child) => { const c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name)); return c && !Tech.isTech(c); })</code></pre> <p> </p> <p>来自:https://segmentfault.com/a/1190000007131342</p> <p> </p>