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
}
|
文章作者
shandowc
上次更新
2020-07-15
(7a1c413)