H5 worker 系列四 flv.js原始碼之log

weixin_34292287發表於2018-10-25

H5 worker 系列三 webworkify處理音視訊解碼中提到了 flv.js使用webworkify進行音視訊解碼,那麼在解碼的worker執行緒中,如果想使用公共的Log模組,想把log日誌整合到主執行緒中,要怎麼做呢?

一、worker執行緒和主執行緒關於log的資料
//logger.js
Log.GLOBAL_TAG = 'flv.js';
Log.FORCE_GLOBAL_TAG = false;
Log.ENABLE_ERROR = true;
Log.ENABLE_INFO = true;
Log.ENABLE_WARN = true;
Log.ENABLE_DEBUG = true;
Log.ENABLE_VERBOSE = true;

Log.ENABLE_CALLBACK = false;

Log.emitter = new EventEmitter();

log.js裡有很多開關,當改變這些開關值時,需要使用logging_control.js中的一些get/set方法,比如:

//logging-control.js
static get enableWarn() {
    return Log.ENABLE_WARN;
}

static set enableWarn(enable) {
    Log.ENABLE_WARN = enable;
    LoggingControl._notifyChange();
}

static _notifyChange() {
    let emitter = LoggingControl.emitter;

    if (emitter.listenerCount('change') > 0) {
        let config = LoggingControl.getConfig();
        emitter.emit('change', config);
    }
}

static registerListener(listener) {
    LoggingControl.emitter.addListener('change', listener);
}

static removeListener(listener) {
    LoggingControl.emitter.removeListener('change', listener);
}

這裡使用了EventEmitter丟擲change事件,來通知一下registerListener中註冊的handler。關於EventEmitter基礎知識,可以參考Node.js EventEmitter

搜尋了下,使用registerListener註冊的,也只有transmuxer.js使用webworker的時候。關於webworker基礎知識,參考H5 worker 系列三 webworkify處理音視訊解碼

this.e = {
    onLoggingConfigChanged: this._onLoggingConfigChanged.bind(this)
};
LoggingControl.registerListener(this.e.onLoggingConfigChanged);

_onLoggingConfigChanged(config) {
    if (this._worker) {
        this._worker.postMessage({cmd: 'logging_config', param: config});
    }
}

然後看看worker裡怎麼處理的:

case 'logging_config': {
    let config = e.data.param;
    LoggingControl.applyConfig(config);

    if (config.enableCallback === true) {
        LoggingControl.addLogListener(logcatListener);
    } else {
        LoggingControl.removeLogListener(logcatListener);
    }
    break;

applyConfig是啥

static getConfig() {
    return {
        globalTag: Log.GLOBAL_TAG,
        forceGlobalTag: Log.FORCE_GLOBAL_TAG,
        enableVerbose: Log.ENABLE_VERBOSE,
        enableDebug: Log.ENABLE_DEBUG,
        enableInfo: Log.ENABLE_INFO,
        enableWarn: Log.ENABLE_WARN,
        enableError: Log.ENABLE_ERROR,
        enableCallback: Log.ENABLE_CALLBACK
    };
}

static applyConfig(config) {
    Log.GLOBAL_TAG = config.globalTag;
    Log.FORCE_GLOBAL_TAG = config.forceGlobalTag;
    Log.ENABLE_VERBOSE = config.enableVerbose;
    Log.ENABLE_DEBUG = config.enableDebug;
    Log.ENABLE_INFO = config.enableInfo;
    Log.ENABLE_WARN = config.enableWarn;
    Log.ENABLE_ERROR = config.enableError;
    Log.ENABLE_CALLBACK = config.enableCallback;
}

這裡就讓人費解了,在_notifyChange中丟擲的getConfig,繞到worker裡,又把配置用applyConfig寫回去了,莫非worker這個執行緒裡,和主執行緒讀取的不是一份資料?必須通過postMessage這種方式同步麼??

二、測試一下,確實不是一份資料

1.先改一下transmuxing-worker.js偵聽message方法,無論收到啥訊息,在最後console一下

self.addEventListener('message', function (e) {
    switch (e.data.cmd) {
         ...
        }
console.log('test' + Log.FORCE_GLOBAL_TAG + ',event:' + e.data.cmd);

這樣執行一下,forceGlobalTag預設值是false,沒問題
2.在transmuxer.js新增一個test方法

    test() {
        Log.FORCE_GLOBAL_TAG = true;
        // LoggingControl.forceGlobalTag = true;
        console.log('Log.FORCE_GLOBAL_TAG' + Log.FORCE_GLOBAL_TAG);
        this._worker.postMessage({cmd: 'test'});
    }

3.在flv-player.js中新增一個test方法

    test() {
        this._transmuxer.test();
    }

4.在index.html中,改一下暫停按鈕的操作

        function flv_pause() {
            player.test();
        }

5.現在結論出來了,直接改Log.FORCE_GLOBAL_TAG之後,在worker中讀取這個值,還是原先的值。而使用LoggingControl.forceGlobalTag觸發_notifyChange這種方式就可以。把LoggingControl.applyConfig(config);註釋掉,也可以證實這一點。

三、ENABLE_CALLBACK屬性

1.普通模式
從logger.js中可以看出,ENABLE_CALLBACK預設值為false,如果為true的時候,在呼叫logger的幾個輸出方法時,會拋通知出來,比如

    static v(tag, msg) {
        if (!tag || Log.FORCE_GLOBAL_TAG)
            tag = Log.GLOBAL_TAG;

        let str = `[${tag}] > ${msg}`;

        if (Log.ENABLE_CALLBACK) {
            Log.emitter.emit('log', 'verbose', str);
        }
   ...

這個事件在logging-control.js中有處理

    static addLogListener(listener) {
        Log.emitter.addListener('log', listener);
        if (Log.emitter.listenerCount('log') > 0) {
            Log.ENABLE_CALLBACK = true;
            LoggingControl._notifyChange();
        }
    }

    static removeLogListener(listener) {
        Log.emitter.removeListener('log', listener);
        if (Log.emitter.listenerCount('log') === 0) {
            Log.ENABLE_CALLBACK = false;
            LoggingControl._notifyChange();
        }
    }

使用addLogListener可以新增一個handler,這樣在Log.v等方法呼叫時,就會執行這個handler了。可以測試一下:

//transmuxer.js
    addOneLogListener() {
        LoggingControl.addLogListener(function (type, str) { 
            console.log('type:' + type + ',str:' + str); 
        });
    }

    mainThreadLogV() {
        Log.v('mainThreadLogV', 'msg:');
    }

先在html頁面上點選按鈕觸發addOneLogListener,然後點選按鈕觸發mainThreadLogV。一切工作正常!

2.worker模式
當我們執行addLogListener時,還會觸發_notifyChange,根據上面的分析,會觸發worker裡的邏輯:

case 'logging_config': {
    let config = e.data.param;
    LoggingControl.applyConfig(config);

    if (config.enableCallback === true) {
        LoggingControl.addLogListener(logcatListener);
    } else {
        LoggingControl.removeLogListener(logcatListener);
    }
    break;

這裡在worker裡,又來了一遍addLogListener或removeLogListener,難道與applyConfig一樣,worker中不但資料不同步。剛才新增的logHandler(其實就是console.log('type:' + type + ',str:' + str);這個function)也不同步麼?做個試驗,先把LoggingControl.addLogListener(logcatListener);註釋掉,然後在worker中新增一個message的響應:

//transmuxing-worker.js
            case 'wokerLogV':
                Log.v('wokerLogV', 'msg:');
                break;

然後在主執行緒中,去讓worker執行Log.v

//transmuxer.js
    wokerLogV() {
        this._worker.postMessage({cmd: 'wokerLogV'});
    }

再在html頁面上點選觸發wokerLogV,果然不出所料,自己新增的logHandler沒有執行。所以logcatListener怎麼處理的呢?參考上面的config處理,是用logging_config把資料發射出去,然後在worker執行緒中用applyConfig這種方式重寫了一遍新資料。但是logHandler就不方便發射了,只能用worker執行緒把這兩個引數轉發回去。也就是說,不管在main執行緒上使用addLogListener新增了多少個logHandler,在worker執行緒中始終只新增了一個logcatListener,這個執行緒就是負責轉發引數的,發回到main執行緒中,才是真實的logHandler去響應。

let logcatListener = onLogcatCallback.bind(this);

function onLogcatCallback(type, str) {
    self.postMessage({
        msg: 'logcat_callback',
        data: {
            type: type,
            logcat: str
        }
    });
}

主執行緒:

case 'logcat_callback':
    Log.emitter.emit('log', data.type, data.logcat);
    break;
四、總結

可能我一開始就忽略了一些事情,在this._worker = work(TransmuxingWorker);時,生成的transmuxing-worker.js就完全import了另外的副本:

import Log from '../utils/logger.js';
import LoggingControl from '../utils/logging-control.js';
import Polyfill from '../utils/polyfill.js';
import TransmuxingController from './transmuxing-controller.js';
import TransmuxingEvents from './transmuxing-events.js';

/* post message to worker:
   data: {
       cmd: string
       param: any
   }

   receive message from worker:
   data: {
       msg: string,
       data: any
   }
 */

let TransmuxingWorker = function (self) {
...

雖然transmuxer.js中import的是同樣的logger.js,並且裡面有logger.js很多static公共常量

import EventEmitter from 'events';
import Log from '../utils/logger.js';
import LoggingControl from '../utils/logging-control.js';
import TransmuxingController from './transmuxing-controller.js';
import TransmuxingEvents from './transmuxing-events.js';
import TransmuxingWorker from './transmuxing-worker.js';
import MediaInfo from './media-info.js';

class Transmuxer {
...

說得有點囉嗦,但也印證了,兩個執行緒無法共享資料,必須通過postMessage來互動,並且互動傳遞的只是字串化的副本,也就是傳統上講的深複製

相關文章