组织结构
以下是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);
})
如有疑问,请留言。