JavaScript video.js 源码分析

JacWaterfie 8年前
   <ul>     <li> <p>video.js 源码分析(JavaScript)</p>      <ul>       <li> <p>组织结构</p> </li>       <li> <p>继承关系</p> </li>       <li> <p>运行机制</p> </li>       <li> <p>插件的运行机制</p>        <ul>         <li> <p>插件的定义</p> </li>         <li> <p>插件的运行</p> </li>        </ul> </li>       <li> <p>控制条是如何运行的</p> </li>       <li> <p>UI与JavaScript对象的衔接</p> </li>       <li> <p>类的挂载方式</p>        <ul>         <li> <p>存储</p> </li>         <li> <p>获取</p> </li>        </ul> </li>      </ul> </li>    </ul>    <h3><strong>组织结构</strong></h3>    <p>以下是video.js的源码组织结构关系,涉及控制条、菜单、浮层、进度条、滑动块、多媒体、音轨字幕、辅助函数集合等等。</p>    <pre>  <code class="language-javascript">├── 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</code></pre>    <p>video.js的JavaScript部分都是采用面向对象方式来实现的。基类是Component,所有其他的类都是直接或间接集成此类实现。语法部分采用的是ES6标准。</p>    <h3><strong>继承关系</strong></h3>    <p>深入源码解读需要了解类与类之间的继承关系,直接上图。</p>    <ul>     <li> <p>所有的继承关系</p> <img src="https://simg.open-open.com/show/46af49f9b2d6bbf104805dfd479287c0.png"></li>     <li> <p>主要的继承关系</p> <img src="https://simg.open-open.com/show/bd4091270d286247af919972461a55e5.png"></li>    </ul>    <h3><strong>运行机制</strong></h3>    <p>首先调用videojs启动播放器,videojs方法判断当前id是否已被实例化,如果没有实例化新建一个Player对象,因Player继承Component会自动初始化Component类。如果已经实例化直接返回Player对象。</p>    <p>videojs方法源码如下:</p>    <pre>  <code class="language-javascript">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);  }  []()</code></pre>    <p>接下来我们看下Player的构造函数,代码如下:</p>    <pre>  <code class="language-javascript">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_);  }</code></pre>    <p>在Player的构造器中有一句 super(null, options, ready); 实例化父类Component。我们来看下Component的构造函数:</p>    <pre>  <code class="language-javascript">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();      }  }</code></pre>    <h3><strong>插件的运行机制</strong></h3>    <p>插件的定义</p>    <pre>  <code class="language-javascript">import Player from './player.js';  // 将插件种植到Player的原型链  const plugin = function(name, init) {    Player.prototype[name] = init;  };  // 暴露plugin接口  videojs.plugin = plugin;</code></pre>    <p>插件的运行</p>    <pre>  <code class="language-javascript">// 在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);  }</code></pre>    <h3><strong>控制条是如何运行的</strong></h3>    <pre>  <code class="language-javascript">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.'  };</code></pre>    <p>Player类中有个children配置项,这里面是控制条的各个组成部分的类。各个UI类还有子类,都是通过children属性链接的。</p>    <h3><strong>UI与JavaScript对象的衔接</strong></h3>    <p>video.js里都是组件化实现的,小到一个按钮大到一个播放器都是一个继承了Component类的对象实例,每个对象包含一个el属性,这个el对应一个DOM实例,el是通过createEl生成的DOM实例,在Component基类中包含一个方法createEl方法,子类也可以重写该方法。类与类的从属关系是通过children属性连接。</p>    <p>那么整个播放器是怎么把播放器的UI加载到HTML中的呢?在Player的构造函数里可以看到先生成el,然后初始化父类遍历Children属性,将children中的类实例化并将对应的DOM嵌入到player的el属性中,最后在Player的构造函数中直接挂载到video标签的父级DOM上。</p>    <pre>  <code class="language-javascript">if (tag.parentNode) {    tag.parentNode.insertBefore(el, tag);  }</code></pre>    <p>这里的tag指的是video标签。</p>    <h3><strong>类的挂载方式</strong></h3>    <p>上文有提到过UI的从属关系是通过类的children方法连接的,但是所有的类都是关在Component类上的。这主要是基于对模块化的考虑,通过这种方式实现了模块之间的通信。</p>    <p>存储</p>    <pre>  <code class="language-javascript">static registerComponent(name, comp) {      if (!Component.components_) {        Component.components_ = {};      }        Component.components_[name] = comp;      return comp;  }</code></pre>    <p>获取</p>    <pre>  <code class="language-javascript">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];      }  }</code></pre>    <p>在Componet里有个静态方法是registerComponet,所有的组件类都注册到Componet的components_属性里。</p>    <p>例如控制条类ControlBar就是通过这个方法注册的。</p>    <pre>  <code class="language-javascript">Component.registerComponent('ControlBar', ControlBar);</code></pre>    <p>在Player的children属性里包括了controlBar类,然后通过getComponet获取这个类。</p>    <pre>  <code class="language-javascript">.filter((child) => {    const c = Component.getComponent(child.opts.componentClass ||                                   toTitleCase(child.name));      return c && !Tech.isTech(c);  })</code></pre>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000007131342</p>    <p> </p>