【videojs 系列01】videojs 源码分析

4,919 阅读5分钟

videojs 是广泛使用视频播放器,提供丰富的默认功能,同时扩展性极高,默认支持 HLS 和 DASH 播放,也有很多第三方开源插件,比如支持 FLV 播放的插件等。

本文源码分析基于 video.js 源码仓库 2021-4-7 的 Tag v7.12.0。

videojs 目录说明

videojs 源码根目录下有 build、docs、lang、sandbox、src、test 目录。build 是打包相关的脚本,docs 是文档,lang 是多语言,sandbox 是各种 Demo,src 是主要源码、test 是单元测试,dist 则是打包生成的目录。

我们需要关注的重点当然是 src 源码目录,需要说明的是,lang 多语言并不会被全部打包进 js 中,在打包生成的 dist 目录中,lang 文件也是单独存在的。

源码目录 src

src 源码目录中,代码和样式是分开存放的,分为 css 和 js 目录。videojs 采用 scss 写样式,在 components 中,几乎每个组件都有对应的样式文件。业内另一个开源播放器 plyr 也是类似的结构。

我们先看入口文件 index.js,代码如下:

import videojs from './video';
import '@videojs/http-streaming';
export default videojs;

可以看到 index.js 只是引入了 video.js 文件和 @videojs/http-streaming 库,这个库 video 实现的,支持 HLS 和 DASH 播放的库,与 videojs 源码是解耦的,我们可以先不管,真正的入口文件是 video.js。

如果我们先打开根目录下的 rollup.config.js,可以看到里面有个打包配置,就是以 video.js 为入口,生成 video.core.js,也就是去掉 @videojs/http-streaming 之后的 videojs 核心。

真正入口 video.js

我们在业务中使用 videojs 的时候,主要就是使用其提供的 videojs 方法,传入一些参数,获得一个 player 对象,如下所示。

this.player = videojs(this.videoNode, {
    controls: true,
    autoplay: true,
    errorDisplay: false,
    controlBar: controlBar,
    techOrder: ['html5'],
    html5: {
      hls: {
        withCredentials: false,
        overrideNative: !videojs.browser.IS_SAFARI,
        }
    },
}

而 videojs 方法就是 video.js 的默认导出。从下面的截图可以看到 video.js 代码有 500 多行,除了 videojs 方法的定义之外,videojs 还被当做一个对象,添加了很多属性和方法,如下截取部分代码,本文就不一一列举了。

videojs.players = Player.players;
videojs.getComponent = Component.getComponent;
// ...
videojs.extend = extend;
videojs.mergeOptions = mergeOptions;
videojs.bind = Fn.bind;
videojs.registerPlugin = Plugin.registerPlugin;
videojs.deregisterPlugin = Plugin.deregisterPlugin;

接下来我们主要看 videojs 方法,其中关键代码如下,由 Component 获取 PlayerComponent 类,再根据传入的参数,实例化 PlayerComponent 类的对象 player,并将其返回。

  const PlayerComponent = Component.getComponent('Player');
  player = new PlayerComponent(el, options, ready);
  return player;

其中,Component.getComponent 定义在 component.js,而 PlayerComponent 则定义在 player.js 中。

component.js 和 player.js 是 videojs 中最重要的两个文件

组件基类 component.js

component.js 中定义了 Component 类,而 Component 是所有组件的基类。

Component 中最重要的两个静态方法就是 registerComponent 和 getComponent,分别是注册组件和获取组件。

这个注册机制是很好的解耦方式,以字符串为 key,组件自注册之后,在任何地方都可以通过字符串获取对应的组件,而不需要引入对应的文件。

在 video.js 中,PlayerComponent 就是通过 Component.getComponent 获取的。

除了上面两个静态方法之外,Component 中还有一些重要的函数。

  • constructor(构造函数第一个参数是 player,这就保证所有的组件都能获取 player 对象)
  • createEl(创建 DOM)
  • el(获取组件 DOM)

子组件操作方法

  • initChildren(添加子组件到 DOM 中,组件可以无限嵌套)
  • addChild
  • removeChild
  • getChild

样式操作相关

  • buildCSSClass
  • addClass
  • removeClass
  • toggleClass
  • hasClass

类似 jQuery 的 DOM 操作方法

  • show
  • hide
  • $ 和 $$

以 videojs 中内置的 LoadingSpinner 举例说明组件如何继承 Component。

import Component from './component';
import * as dom from './utils/dom';

class LoadingSpinner extends Component {

  // 只需要重写 createEl 方法即可
  createEl() {
    const isAudio = this.player_.isAudio();
    const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player');
    const controlText = dom.createEl('span', {
      className: 'vjs-control-text',
      innerHTML: this.localize('{1} is loading.', [playerType])
    });

    const el = super.createEl('div', {
      className: 'vjs-loading-spinner',
      dir: 'ltr'
    });

    el.appendChild(controlText);

    return el;
  }
}

// 在文件中注册组件本身,这样只需全局引入一次此文件即可
Component.registerComponent('LoadingSpinner', LoadingSpinner);
export default LoadingSpinner;

播放器核心 player.js

前面已经提到过,Player 也是 Component 的派生类,但 Player 稍微有点特殊,因为它是所有其他组件的容器。

Player 是 videojs 核心中的核心,因为其代码文件就有 5000 多行。5000 多行这一点是应该批评的,维护成本必然很高,完全可以拆分成多个文件。

关于代码文件中代码行数,我一般建议代码不超过 500 行,最好在 200 行以内。

在 player.js 第 5148 行有注册 Player 的代码 Component.registerComponent('Player', Player);

在 player.js 的 createEl 函数中,会根据传入的参数 tag 来决定如何创建 video 并复制属性,关键代码如下。

player.js 中有大量对 MediaElement 的封装,通过 player 对象即可操作 MediaElement,一般就是指播放视频的 video 元素。

player.js 中还有一些比较重要的方法,src_、loadTech_、selectSource 方法等。在指定媒体播放源之后,这些方法会调用支持这些指定媒体源播放的 Tech 来播放视频,例如 HLS 或 FLV 视频源。

Tech 是 videojs 中一个重要且复杂的概念,篇幅所限,此处不过多描述,以后会专门另写文章来讲解。

videojs 的默认组件

当我们使用 videojs 的时候,videojs 本身就提供了足够丰富的功能。同时 videojs 也支持各种扩展,而其内部默认提供的功能,基本都是 Component,也就是以 player 为容器的子组件。

根据前面组件注册机制的描述,player.js 中引入了默认组件对应的文件。

注意只是引入,只是为了让组件能够自己执行 Component.registerComponent 进行注册。

import './tech/loader.js';
import './poster-image.js';
import './tracks/text-track-display.js';
import './loading-spinner.js';
import './big-play-button.js';
import './close-button.js';
import './control-bar/control-bar.js';
import './error-display.js';
import './tracks/text-track-settings.js';
import './resize-manager.js';
import './live-tracker.js';

Component 的子组件通过 children 定义,在 initChildren 方法中,会根据 children 获取对应子组件被添加到 DOM 中。

Player 也是一个 Component,其默认子组件定义如下。

Player.prototype.options_ = {
  techOrder: Tech.defaultTechOrder_,
  html5: {},
  children: [
    'mediaLoader',
    'posterImage',
    'textTrackDisplay',
    'loadingSpinner',
    'bigPlayButton',
    'liveTracker',
    'controlBar',
    'errorDisplay',
    'textTrackSettings',
    'resizeManager'
  ],
};

Player 的这些默认子组件中,有一个特殊的子组件,那就是 ControlBar,ControlBar 本身也有许多默认的子组件,比如播放按钮、音量条、进度条、全屏按钮等。在 control-bar.js 中定义了默认子组件。

ControlBar.prototype.options_ = {
  children: [
    'playToggle',
    'volumePanel',
    'currentTimeDisplay',
    'timeDivider',
    'durationDisplay',
    'progressControl',
    'liveDisplay',
    'seekToLive',
    'remainingTimeDisplay',
    'customControlSpacer',
    'playbackRateMenuButton',
    'chaptersButton',
    'descriptionsButton',
    'subsCapsButton',
    'audioTrackButton',
    'fullscreenToggle'
  ]
};

和 player 的默认子组件一样,controlBar 的子组件也是通过字符串数组来定义,数组中每个字符串都是另一个 Component 注册时使用的 key,通过这个 key 就能获得组件实例。

Component 的注册机制和 children 子组件定义是 videojs 可扩展性的重点。关于 videojs 的扩展性,后续另出文章详细讲解。