资讯专栏INFORMATION COLUMN

video.js 源码分析(JavaScript)

SnaiLiu / 3400人阅读

摘要:语法部分采用的是标准。那么整个播放器是怎么把播放器的加载到中的呢在的构造函数里可以看到先生成,然后初始化父类遍历属性,将中的类实例化并将对应的嵌入到的属性中,最后在的构造函数中直接挂载到标签的父级上。

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

如有疑问,请留言。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/50133.html

相关文章

  • video.js 源码分析JavaScript

    摘要:语法部分采用的是标准。那么整个播放器是怎么把播放器的加载到中的呢在的构造函数里可以看到先生成,然后初始化父类遍历属性,将中的类实例化并将对应的嵌入到的属性中,最后在的构造函数中直接挂载到标签的父级上。 video.js 源码分析(JavaScript) 组织结构 继承关系 运行机制 插件的运行机制 插件的定义 插件的运行 控制条是如何运行的 UI与JavaScript对象的...

    Neilyo 评论0 收藏0
  • video.js 源码分析JavaScript

    摘要:语法部分采用的是标准。那么整个播放器是怎么把播放器的加载到中的呢在的构造函数里可以看到先生成,然后初始化父类遍历属性,将中的类实例化并将对应的嵌入到的属性中,最后在的构造函数中直接挂载到标签的父级上。 video.js 源码分析(JavaScript) 组织结构 继承关系 运行机制 插件的运行机制 插件的定义 插件的运行 控制条是如何运行的 UI与JavaScript对象的...

    simpleapples 评论0 收藏0
  • 基于 Node+express 爬虫的数据 API,爬一套自己的api数据(2)

    摘要:目前半岛局势紧张,朝鲜已进行了六次核试验,被广泛认为已经拥有了核弹头。另外朝鲜的导弹技术今年以来快速突破,成功试射了射程可覆盖美国本土的洲际弹道导弹。这个版的内容传到互联网上后,迅速刷屏,引起纷纷议论。 SplderApi2 Node-SplderApi2 第二版 基于Node 的网络爬虫 API接口 包括前端开发日报、kugou音乐、前端top框架排行、妹纸福利、搞笑视频、段子笑话、...

    beanlam 评论0 收藏0
  • 前端插件库

    摘要:原文链接前端插件库站点前端开发文档博客前端插件库前端插件库官网是的函数库,目的是强化表格操作如搜索排序,并自动加入组件引入表格中,使用非常灵活简便。由推出,灵活扎实的建议列表函数库。 原文链接:前端插件库站点:前端开发文档博客:前端插件库 前端插件库 DataTables 官网:https://www.datatables.net/ DataTables是jQuery的JavaScr...

    高胜山 评论0 收藏0

发表评论

0条评论

SnaiLiu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<