video.js 源码分析(JavaScript)

article/2025/10/29 15:31:17
  • 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,不是所有的类需要传playerif (!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);
})

如有疑问,请留言。


http://chatgpt.dhexx.cn/article/oI375A0Y.shtml

相关文章

在网站中使用VideoJs视频播放器播放视频

之前使用videojs用来网页中播放视频,现在做一下总结 (这里把插件下载及演示地址、使用方法及demo放出来) 视频播放插件Video.js 插件下载地址: http://www.jq22.com/jquery-info404 演示播放视频地址:http://www.jq…

video.js的使用,打造自定义视频播放器(综合详解,可收藏)

video.js的使用,打造自定义视频播放器(综合详解,可收藏) 一、视频初始化:1、直接在viedo的HTML标签中 初始化,标签里面加上 class"video-js" 和 data-setup{} 属性,例如;2、使用js初…

vue 视频播放插件vue-video-player自定义样式、自动播放设置、设置一开始全屏播放视频、监听全屏事件

1、背景 项目中有涉及视频播放的需求,并且UI设计了样式,与原生的视频video组件有差异,所以使用了vue-video-player插件,并对vue-video-player进行样式改造,自定义播放暂停按钮、全屏按钮、时间进度条样式等&#xff0…

关于谷歌浏览器62版本之后引用video.js不能自动播放的问题(Cross-origin plugin content from http://vjs.zencdn.net/swf/5.0.0-rc

Cross-origin plugin content from http://vjs.zencdn.net/swf/5.0.0-rc1/video-js.swf must have a visible size larger than 400 x 300 pixels, or it will be blocked. Invisible content is always blocked.这句话的意思是:来自http://vjs.zencdn.net/swf/5.0.…

videojs播放器插件使用详解

HTTP stream是各家自己定义的http流,应用于国内点播视频网站。 HLS是苹果公司实现的基于 HTTP 的流媒体传输协议,全称 HTTP Live Streaming,可支持流媒体的直播和点播,主要应用在 iOS 系统,为 iOS 设备(如…

videojs-flash.min.js 报错 this.el_.vjs_getProperty is not a function

videojs-flash.min.js 报错 this.el_.vjs_getProperty is not a function 没发现有什么好的解决方法,目前我的问题,就是切换页面的时候或出现这个问题,我猜测是因为this指向变了导致的,我的处理方法就是开个定时器,定时…

this.el_.vjs_getProperty is not a function

前言: 在使用video.js播放rtmp视频中切换页面,如果是弹框引入,关闭弹框时,必然会发现控制台报错这个,这是谈谈我这边遇到的这个问题,以及怎么解决的。 问题: 在说这个问题怎么解决之前&#xf…

Video.js使用教程一(详解)

在项目上遇到类似于直播的场景所以需要前端播放后台的视频流这个问题,所以就此问题记录一下, 在上一篇文章中写到了怎样搭建nginx流媒体服务器以及怎样使用ffmpeg进行推流,那么现在就缺前端拉流这一部分,通过这两个天的辛勤劳动终…

MySQL 导出 CSV 乱码

2019独角兽企业重金招聘Python工程师标准>>> 从MySQL导出数据到 csv 文件后,有时会发现用 excel 打开该导出 csv 文件显示的是乱码(不得不说WPS是神器,打开不会乱码 office打开会乱码)。这个问题是 csv 文件本身的文本…

Mac中解决CSV乱码问题

新建一个Excel文件,选择文件–》导入 2.选择CSV文件导入 3.“文件导入向导-第1步”弹框中“文件原始格式”选择"Unicode(UTF-8)",点击下一步 4.“文件导入向导-第2步”中勾选“逗号” 5.“文件导入向导-第3步”保持默认选项,点击…

jemter读取csv乱码

目录 【问题描述】【解决方案】 【问题描述】 已经根据 jmeter读取csv报错配置后,执行压测脚本,jmeter读取csv乱码,请求信息中为乱码,但是不影响交易结果(后续有导致交易失败,不知道是否因为乱码引起&…

java csv导出 乱码_java导出csv乱码解决方法介绍

1、问题 将查询的数据以xls文件导出时(UTF-8编码),数据正常;但以CSV文件导出时,文件中的中文乱码,同样是UTF-8编码,改成GBK编码导出时,中文显示正常。 本以为问题解决,后面导出含拉丁字符(如)的…

win10打开csv乱码

解决方法&#xff1a; 1、打开Excel&#xff1b; 2、点击任务栏的<数据>&#xff1b; 3、点击<从文本/CSV>选择csv文件&#xff1b; 4、修改文件原始格式为&#xff1a;65001&#xff1a;Unicode(UTF-8); 5、点击加载&#xff0c;即可&#xff1b; 6、将文件另存为…

python写入csv中文乱码

利用python写入csv文件打开发现中文乱码。查找资料发现需要修改编码格式&#xff0c;我原来的写法是&#xff1a; f open(51招聘数据-金融分析2.csv,modea,encodingutf-8,newline)试了一下网上的修改encoding为utf-8-sig和gbk&#xff0c;最终发现’gbk’格式的编码是可行的。…

【csv乱码】csv文件打开乱码的情况

先选择“记事本”打开方式打开&#xff0c;打开肯定还是乱码的状态。 不用慌&#xff0c;点击 选择“另存为”该记事本为csv格式&#xff0c;并且选择编码格式为“带有BOM的UTF-8”&#xff08;默认会选择UTF-8&#xff0c;并不能解决乱码的问题&#xff09;。打开另存为的cs…

mysql导入CSV乱码问题解决

错误出现页面 MySQL在导入CSV的时候&#xff0c;其中的中文乱码 解决方案 1.右键该CSV&#xff0c;用记事本打开 2.点击文件–>另存为—>修改编码为UTF-8,再导入新的的CSV就可以了

python写入csv乱码问题

原代码&#xff1a; with open(csv_path, open_mode, newline)as f:f_csv_write csv.writer(f)f_csv_write.writerow(row) 乱码截图&#xff1a; 解决办法&#xff1a; 因为python创建csv文件默认的是ansi格式的&#xff0c;而程序中是utf-8的格式进行保存的&#xff0c;所以…

csv乱码问题

pgAdmin 导出CSV文件&#xff0c;直接打开出现乱码。 解决方法&#xff1a;使用excel导入该csv文件。 step1&#xff1a;资料 > 从文字档 step2&#xff1a; 选择CSV step3&#xff1a;档案原始格式改为&#xff1a;65001&#xff1a;Unicode&#xff08;UTF-8&#xff09;…

unity显示csv乱码

vscode添加插件&#xff0c;vs2022添加插件&#xff0c;都没用&#xff0c;发现是&#xff0c; excel保存为csv时选择上面那个。

php csv乱码问题,如何解决php csv乱码问题

php csv乱码的解决办法:首先重写fputcsv方法;然后添加转码功能,代码如“function fputcsv2($handle, array $fields, $delimiter = "){...}”。 PHP导出CSV中文乱码的解决方法:UTF-8转GB2312 一、背景 因项目需求,要导出Excel表格数据,使用fputcsv方法导出数据遇到中…