-
video.js 源码分析(JavaScript)
组织结构
继承关系
运行机制
-
插件的运行机制
插件的定义
插件的运行
控制条是如何运行的
UI与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);})
如有疑问,请留言。