WXInlinePlayer的js和cpp代码怎么沟通的?

先来看看lib/codec/build.sh, 这里编译了解码库, 我们挑一个解码库的编译过程看下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
node ../tool/compile.js wasm h265         # 根据条件生成CMakeLists.txt和config.h
emcmake cmake ..
emmake make -j 4
mv ../bin/prod.js ../bin/h265.wasm.js     # 将生成的js文件重命名

node ../tool/compile.js asm h265          # 根据条件生成CMakeLists.txt和config.h
emcmake cmake ..
emmake make -j 4
mv ../bin/prod.js ../bin/h265.asm.js      # 将生成的js文件重命名

node ../tool/compile.js                   # 重置CMakeLists.txt和config.h
node ../tool/wrapper.js ../bin/h265.wasm.js h265.wasm # 通过模板,给各个编码器套上一些公用代码,并压缩js
node ../tool/wrapper.js ../bin/h265.asm.js h265.asm   # 通过模板,给各个编码器套上一些公用代码,并压缩js

然后我们看下lib/tool/compile.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const fs = require('fs');
const path = require('path');
const filepath = path.join(__dirname, '../CMakeLists.txt');
const confpath = path.join(__dirname, '../src/config.h');
const cmds = fs
  .readFileSync(filepath)
  .toString('UTF8')
  .split(/\r?\n/g);

['wasm', 'asm'].forEach(v => {
  let index = cmds.indexOf(`#<-----${v}----->`);
  if (index > -1) {
    if (cmds[index + 1].indexOf('#') != 0) {
      cmds[index + 1] = `#${cmds[index + 1]}`;    // 同时注释掉wasm和asm.js两种解码器
    }
  }
});

let type = process.argv[2];
let isBaseline = process.argv[3] == 'baseline';
let isH265 = process.argv[3] == 'h265';
fs.writeFileSync(confpath, `
#ifndef CODEC_CONFIG_H
#define CODEC_CONFIG_H

${isH265 ? '' : '//'}#define USE_OPEN_H265
${isBaseline ? '//' : ''}#define USE_OPEN_H264

#endif //CODEC_CONFIG_H
`);                                               // 根据配置,确定本次编译的解码器支不支持H264和H265

index = cmds.indexOf(`#<-----${type}----->`);
if (index > -1) {
  cmds[index + 1] = cmds[index + 1].replace('#', ''); // 本次编译只遍wasm或者asm.js目标文件, 把一开始加的注释取消掉
}

fs.writeFileSync(filepath, cmds.join('\n'));          // 写回CMakeLists.txt

上面提到的注释内容是下面两句

1
2
3
4
#<-----wasm----->
set(EM_CONFIG_PARAM "-O3 -s ENVIRONMENT=\"web,worker\" -s SINGLE_FILE=1 -s WASM=1 -s FETCH=0 -s DISABLE_EXCEPTION_CATCHING=0 -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s NO_EXIT_RUNTIME=0 -s FILESYSTEM=0 -s INVOKE_RUN=0 -s ASSERTIONS=1 -s TOTAL_MEMORY=16777216 -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS=\"['_codecInit', '_codecSetBridgeName', '_codecDecode', '_codecSetVideoBuffer', '_codecSetAudioBuffer', '_codecTry2Seek', '_codecFree']\"")
#<-----asm----->
set(EM_CONFIG_PARAM "-O3 -s ENVIRONMENT=\"web,worker\" -s SINGLE_FILE=1 -s WASM=0 -s FETCH=0 -s DISABLE_EXCEPTION_CATCHING=0 -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s NO_EXIT_RUNTIME=0 -s FILESYSTEM=0 -s TOTAL_MEMORY=16777216 -s ALLOW_MEMORY_GROWTH=1 -s INVOKE_RUN=0 -s LEGACY_VM_SUPPORT=1 -s MEM_INIT_METHOD=0 -s ELIMINATE_DUPLICATE_FUNCTIONS=1 -s ASSERTIONS=1 -s EXPORTED_FUNCTIONS=\"['_codecInit', '_codecSetBridgeName', '_codecDecode', '_codecSetVideoBuffer', '_codecSetAudioBuffer', '_codecTry2Seek', '_codecFree']\"")

参考lib/codec/src/main.cpp,我们看到,c部分的代码暴露了如下方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 初始化编码器, 初始化全局变量
void codecInit(void);
// 设置cpp代码回调目标,后面有讲解
void codecSetBridgeName(char *bridgeName);
// 设置音频解封装数据存储块
void codecSetAudioBuffer(char *buffer);
// 设置视频帧数据存储块
void codecSetVideoBuffer(char *buffer);
// 解封装/解码一段数据
void codecDecode(uint8_t *bytes, uint32_t length);
// 未用上, 暂不讨论
int codecTry2Seek(char *buffer, uint32_t length);
// 释放解码器, 销毁全局变量
void codecFree(void);

wrapper.js的内容则是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const fs = require('fs');
const path = require('path');
const argv = process.argv;
const UglifyJS = require('uglify-js');

const GLUE_PATH = path.join(__dirname, '../combine/glue.js');   //胶水代码
const CODEC_PATH = path.join(__dirname, argv[2]);               //上面编译出来的代码

const glueCodeStr = fs.readFileSync(GLUE_PATH).toString();
const codecCodeStr = fs.readFileSync(CODEC_PATH).toString();

let content = `此处先省略代码`

fs.writeFileSync(
  path.join(__dirname, `../combine/prod.${argv[3]}.combine.js`),
  UglifyJS.minify(content).code                                 //压缩代码体积
);

从上述简化代码看出,主要是将胶水代码和上面编译的代码拼接起来,我们看下content里面的内容,看下里面干了些啥

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
var WORKER_ENABLED = !!(window.URL && window.Blob && window.Worker);

function __GLUE_EXEC__(Module){
    ${glueCodeStr};                     // 把胶水代码封到函数中
};

function __CODEC_EXEC__(Module){
    ${codecCodeStr}                     // 把解码器代码放到函数中
};

var H264Codec = null;
if(!WORKER_ENABLED){
  // 限于篇幅,大部分浏览器都是支持web worker的,不介绍这部分的代码
}else{
    H264Codec = function(){
        var _me = this;
        this.destroied = false;
        var glueCodeStr = __GLUE_EXEC__.toString();
        var codecCodeStr = __CODEC_EXEC__.toString();
        var blob = new Blob([[
            'var Module = {};',
            glueCodeStr,
            codecCodeStr,
            ';__GLUE_EXEC__(Module);__CODEC_EXEC__(Module);'
        ].join(';')], {type:'text/javascript'});            // 把胶水代码和解码器代码拼接起来

        this.url = URL.createObjectURL(blob);
        this.worker = new Worker(this.url);                 // 拼接起来的代码在新worker中执行
        this.worker.onmessage = function(msg){              // 收到胶水代码返回来的消息的处理方式
            var data = msg.data;
            if(typeof _me.onmessage == "function"){
                _me.onmessage(data);               // 收到解码器的事件,如果设置了onmessage函数,透传到上层,processor通过这种方式拿到解码器事件
                if(data.type == 'destroy' && typeof _me.onterminate == 'function'){
                    _me.onterminate();
                    _me.worker.terminate();
                    _me.worker = null;
                }
            }
        }

        this.worker.onterminate = function(){
            
        }

        this.onmessage = function(){};
        this.onterminate = function(){};
    };

    H264Codec.prototype.decode = function(buffer){    // 对外api,解码一段数据
        if(this.worker){
            this.worker.postMessage({
                type: 'decode',
                buffer: buffer,
            });
        }
    }

    H264Codec.prototype.destroy = function(){         // 对外api,销毁解码器
        this.destroied = true;
        if(this.worker){
            window.URL.revokeObjectURL(this.url);
            this.worker.postMessage({type: 'destroy'});
        }
    }
}

window.H264Codec = H264Codec;

接下来, 是时候看胶水代码glue.js怎么写的了, 先看glue.js的第一部分, 胶水代码接收外部请求, 然后处理.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Module.onmessage = function(msg) {
  var data = msg.data;
  switch (data.type) {
    case "decode": {                            // 收到外部需要解码一段数据的请求
      var buffer = new Uint8Array(data.buffer);
      var data = Module._malloc(buffer.length); // 在HEAP上分配空间
      Module.HEAPU8.set(buffer, data);

      var now = +new Date();
      Module.audioTimestamps = [];
      Module.videoTimestamps = [];
      Module._codecDecode(data, buffer.length); // 调用emscripten编译出来的代码解码

      var ats = Module.audioTimestamps;
      var vts = Module.videoTimestamps;
      Module.postMessage({                      // 返回解码耗时, 以及本段数据可播放的长度
        type: "decode",
        data: {
          consume: +new Date() - now,
          duration: Math.max(
            ats.length > 0 ? ats[ats.length - 1] - ats[0] : 0,
            vts.length > 0 ? vts[vts.length - 1] - vts[0] : 0
          )
        }
      });

      Module._free(data);
      break;
    }
    case "destroy": {
      if (Module.audioBuffer) {
        Module._free(Module.audioBuffer);
      }
      if (Module.videoBuffer) {
        Module._free(Module.videoBuffer);
      }
      Module._codecFree();
      Module.postMessage({ type: "destroy" });
      break;
    }
  }
};

if (isWorker) {
  self.onmessage = Module.onmessage;
}

其实就是web worker的经典套路, onmessage接收指令, 执行代码, 然后postmessage返回结果, 整个过程在子线程中完成, 不占用主线程时间和资源. 接下来, 需要关注的是Module这个对象的赋值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var T = {
  audioTimestamps: [],
  videoTimestamps: [],
  audioBufferSize: 0,
  videoBufferSize: 0,
  audioBuffer: null,
  videoBuffer: null,
  postMessage: isWorker ? postMessage.bind(self) : function() {}, 
  onRuntimeInitialized: function() {                    // wasm加载完毕, 执行一些操作
    Module._codecInit();                                // 调用cpp代码的codecInit方法初始化解码器
    var callbackStr = bridgeName.split("");
    callbackStr = callbackStr
      .map(function(v) {
        return v.charCodeAt(0);
      })
      .concat(0);                                       // 这些代码成迷,为何不通过allocateUTF8传字符串进去

    var callbackStrData = Module._malloc(callbackStr.length - 1);
    Module.HEAPU8.set(callbackStr, callbackStrData);
    Module._codecSetBridgeName(callbackStrData);        // 重点, 先留意, 主要是设置透音视频等数据给上层的方法

    Module.postMessage({ type: "ready" });              // 告诉上层, 解码器准备好了
  }
};

Module = Module || {};

for (var key in T) {
  if (T.hasOwnProperty(key)) {      // 只赋值T自己拥有的属性方法, 不管继承属性
    Module[key] = T[key];
  }
}

这种赋值的技巧似乎也不错, 这样就不用Model.xxx这样给一个个属性赋值了, 可能这样看着更整洁.

最后, 胶水代码最重要的一部分, 视频和音频数据是怎么透到上层的?

1
2
3
4
5
6
7
8
var isWorker = typeof importScripts == "function";
var bridgeName = "__CODE_BRIDGE__" + +new Date();         // 刚才提到的codecSetBridgeName, 入参就是这个
(isWorker ? self : window)[bridgeName] = {
  onHeader: function(header) {
    Module.postMessage({ type: "header", data: header });
  },
  // 省略其它方法
};

然后我们在lib/codec/src/factor/codec_factor.cpp中找到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void CodecFactor::recvHeaderValue(HeaderValue &value) {
#ifdef __EMSCRIPTEN__
  EM_ASM({
    var isWorker = typeof importScripts == "function";
    var bridge = (isWorker ? self : window)[UTF8ToString($0)];    // 获得当前的bridge, 等于是解码器的桥, 桥接模式?
    if(bridge && typeof bridge["onHeader"] == "function"){        // 调用这个方法
      bridge["onHeader"]({
        "hasAudio": $1,
        "hasVideo": $2,
      });
    }
  }, _codec->bridgeName.c_str(), value.hasAudio, value.hasVideo);
#endif
}