博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
video.js 源码分析(JavaScript)
阅读量:5930 次
发布时间:2019-06-19

本文共 11295 字,大约阅读时间需要 37 分钟。

  • 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);})

如有疑问,请留言。

转载地址:http://otutx.baihongyu.com/

你可能感兴趣的文章
第二届网易前端技术大会-启航
查看>>
基于Vert.x和RxJava 2构建通用的爬虫框架
查看>>
bootstrap基本布局
查看>>
老牌语言依然强势,GO、Kotlin 等新语言为何不能破局?
查看>>
RxJava2系列之相较RxJava1的更新之处(二)
查看>>
JavaEE进阶知识学习-----SpringCloud(九)Zuul路由网关
查看>>
浏览器是多进程
查看>>
安全令牌JWT
查看>>
Redux框架之applyMiddleware()讲解
查看>>
“寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
查看>>
基于RxJava2+Retrofit2实现简单易用的网络请求框架
查看>>
iOS自定义对象的读写怎么保证线程安全问题
查看>>
PHPExcel(更新中)
查看>>
Android不透明度对应的16进制值
查看>>
AppDelegate解耦
查看>>
突破Android P非SDK API限制的几种代码实现
查看>>
270行代码实现一个AMD模块加载器
查看>>
【译】GraphQL 初学者指南
查看>>
pkg版本规范管理自动化最佳实践
查看>>
iOS检测系统弹窗并自动关闭
查看>>