webassembly在flv编解码中的实战

2,265 阅读6分钟

pc端直播平台的播放器大部分已FLV 直播流格式为主。FLV 直播流可以嵌套在swf文件当中,可以用flash播放。

前几个月,我们刚刚完成了对直播播放器的重构,引入了现有的bilibili开源的flv.js,接入了flv h5播放器。向下兼容了flash播放器。

1

h5播放器的原理

各大直播平台纷纷介入h5播放器,增强用户体验。v8对ArrayBuffer的操作性能很好。可以很快的将flv格式的数据转换成MP4格式的数据类型,供MSE使用。flv依赖 HTTP FLV 或者 WebSocket 中的一种协议。其中HTTP FLV需通过流式IO去拉取音视频原数据,通过原生的JS去解码FLV数据,再通过Media Source Extensions API作为桥梁将组装好的MP4格式数据传给原生HTML5 Video标签 。h5实现的播放器的流程如下:

2

具体flv编解码流程如下:

3

介入webassembly

最近,WebAssembly 在 JavaScript 圈非常的火。很多浏览器加入了即时编译器(JITs),听说比JavaScript快好几倍。尤其是高密集的二进制流的操作。刚好我们h5播放器的解封装和封装的过程都涉及到了大量的二进制转换和计算的工作。引入webassembly开发remux和demux模块有利于提升直播流编解码的速度。

WebAssembly的编译过程:

4

WebAssembly是一个二进制文件。可由一种高级编程语言转换成中间代码(IR),再由IR转换成wasm文件(wasm是一个概念机上的机器语言不是在一个真正存在的物理机上运行的机器语言,不直接和特定硬件的特定机器代码对应),再由wasm转换成目标机器的汇编代码(运行时转换)。 我选择了Emscripten作为生产wasm文件的工具链。选择c++作为高级语言。

浏览器执行wasm优势:

  • 编译和优化 - 编译和优化所需的时间较少,因为在将文件推送到服务器之前已经进行了更多优化,JavaScript 需要为动态类型多次编译代码
  • 重新优化 - WebAssembly 代码不需要重新优化,因为编译器有足够的信息可以在第一次运行时获得正确的代码
  • 执行 - 执行可以更快,WebAssembly 指令更接近机器码

c++ 编写中的感受

c++作为强类型语言,对于用惯了JavaScript开发的开发者来说, 显得不那么自由了,编写起来也要考虑的更加谨慎,很有可能就会产生未知的内存泄漏。拿起一本c++ primer,学习了一周就可以上手了。 首先了解该语言的基本数据类型,基本语法和主要语言构造。其次掌握了数组和其他集合类的使用, 简单字符串处理, 基本面向对象或者函数式编程的特征,基本输入输出和文件处理,输入输出流类的组织就ok了。

  • 垃圾回收
    在 JavaScript 中,开发者不需要担心内存中无用变量的回收。JS 引擎使用一个叫垃圾回收器的东西来自动进行垃圾回收处理。
    这对于控制性能可能并不是一件好事。你并不能控制垃圾回收时机,所以它可能在非常重要的时间去工作,从而影响性能。
    编写C++就不同了,你需要手动的把分配的内存销毁掉。(还好c++11的智能指针可以帮我减少不必要的麻烦)

  • 类继承的多态
    多态只在强类型语言中需要考虑的。当无法在编译时确定一个对象的类型时,只能在运行时确定一个类型。实现多态有四种方式:虚函数,抽象类,覆盖,模板:
    js不存在多态问题,它可以接受任意的参数数量和任意的类型,因此,当 JIT 在执行 JS 阶段发现变量类型不合理,就会丢弃优化代码重新进行 重新优化。而 WebAssembly 中的变量类型都是确定的,JIT 不需要检查变量类型的合理性,因此并不用重新优化。

c++代码中FLV文件格式解析Script Tag Data结构的抽象类与虚函数:

// 基类
struct Script_data_base {
    Script_data_base() : type(kNull) {};
    explicit Script_data_base(enum Script_data_type t) : type(t) {}

    enum Script_data_type type;
    virtual std::string to_json() const { return std::string(""); };
    virtual ~Script_data_base() {}
};

// 子类
struct Script_data_number : public Script_data_base {
    explicit Script_data_number(double n)
            : Script_data_base(kNumber),
              num_value(n) {};

    std::string to_json() const {
        std::stringstream ss;
        ss << num_value;
        return ss.str();
    }

    double num_value;
};

Script_data_base *data_num = new Script_data_number(0);

// 调用子类方法
data_num->to_json();

c++中,使用模板来定义函数和类,可以在声明时,动态的实例化指定类型的变量,用户提供不同的类型参数,就会实例化出不同的代码。

例如FLV 的Script Tag Data Value结构,可以包含很多的类型如,Script_data_number, Script_data_String。我们不用关心具体包含的类型,只关注公共的逻辑,如下定义:

// 定义Script_data_value
template<typename T>
struct Script_data_value : public Script_data_base {
    explicit Script_data_value(Script_data_type _type, T ptr)
            : Script_data_base(_type),
              script_data_value(ptr),
              ecma_array_length(0) {}

    virtual std::string to_json() const {
        std::stringstream ss;
        ss << script_data_value;
        return ss.str();
    };

    T script_data_value;
    uint32_t ecma_array_length;
};

// 实例化
Script_data_value <std::shared_ptr<Script_data_number>> value(KNumber, data_num);

value->to_json();

c++与js通信

由于flv的解封装和封装用c++实现,只需要将二进制数据传入到c++中。在把二级制数据返还给js就好了。目前,WebAssembly 中的函数只能使用 整数或浮点数作为参数或返回值。

5

Emscripten产出的js帮我们封装了一些方法Module.ccallModule.cwrap,它们可以直接调用c++暴露出的接口。

c++可以直接调用js的全局变量通过头文件<emscripten.h>EM_ASM_ARGS等一些方法。

那么如何将js的Uint8Array传给c++呢?

js初始化的时候会为wasm分配一块固定的内存空间。我们只要通过js调用Emscripten的方法将Uint8Array插入到这块空间当中。并获得typedArray在这块内存空间的位置。将位置byteOffset传入给c++,c++可通过uint8_t *类型的形参接收参数。

6

js代码:

// 插入空间中
function _arrayToHeap(typedArray) {
  var numBytes = typedArray.length * typedArray.BYTES_PER_ELEMENT;
  var ptr = Module._malloc(numBytes);
  var heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes);
  heapBytes.set(new Uint8Array(typedArray.buffer));
  return heapBytes;
}

// 释放指定空间
function _freeArray(heapBytes) {
  Module._free(heapBytes.byteOffset);
}

// 调用c++暴露的方法
Module.ccall(
      'setflv',
      'number',
      ['number', 'number'],
      [heapBytes.byteOffset, len]
);

c++代码:

uint32_t setflv(uint8_t *ptr, uint32_t length) {
    // ...
}

c++的uint8_t 数组数据传给js

原理其实是类似的,我们把uint8_t数组的指针(位置)直接传给js。js从wasm固定的内存空间找到对应想要Uint8Array

c++代码:

uint8_t ptr = segment->data->get_buf_ptr();

#ifdef __EMSCRIPTEN__

EM_ASM_ARGS({
        window['receiveMediaSegment']($0, $1);
     }, ptr, segment_buffer_len);

#endif

js代码:

window['receiveMediaSegment'] = ( ptr, len) => {
  const heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, len);

  const typeArray = heapBytes.slice(0, len);
  // ...
};

最后

很顺利的将wasm集成到了flv当中。为了向下兼容,我通过Emscripten将c++代码分别产出了两份。一份是asm.js一份是wasm.js和.wasm文件。通过wasm实现的flv编解码,在web端调试二进制流也是个复杂困难的过程,浏览器内存的消耗相对之前而言要好很多。最重要的是flv编解过程可以让编解码组来维护了,可以相应的实现一些视频追帧等策略,后面的效果还在测试与观察~~~ ☕️