JavaScript video.js 源碼分析

JacWaterfie 8年前發布 | 16K 次閱讀 源碼分析 JavaScript開發 JavaScript

  • video.js 源碼分析(JavaScript)

    • 組織結構

    • 繼承關系

    • 運行機制

    • 插件的運行機制

      • 插件的定義

      • 插件的運行

      </li>
    • 控制條是如何運行的

    • UI與JavaScript對象的銜接

    • 類的掛載方式

      • 存儲

      • 獲取

      • </ul> </li> </ul> </li> </ul>

        組織結構

        以下是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') {
                        thisname;
                    } 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>

        在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;
        

        }</code></pre>

        獲取

        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>

        在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); })</code></pre>

         

        來自:https://segmentfault.com/a/1190000007131342

         

 本文由用戶 JacWaterfie 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!