从ffmpeg源码生成wasm 获取 mime codecs

边学边做,法力无边。

先提问题

当打算用 MSE (Media Source Extension) 做一个播放器,通常一个例子程序看起来如下面链接所示 developer.mozilla.org/en-US/docs/...

js 复制代码
const assetURL = "frag_bunny.mp4";
// Need to be specific for Blink regarding codecs
// ./mp4info frag_bunny.mp4 | grep Codec
const mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
let mediaSource;

if ("MediaSource" in window && MediaSource.isTypeSupported(mimeCodec)) {
  mediaSource = getMediaSource();
  console.log(mediaSource.readyState); // closed
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener("sourceopen", sourceOpen);
} else {
  console.error("Unsupported MIME type or codec: ", mimeCodec);
}

function sourceOpen() {
  console.log(this.readyState); // open
  const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
  fetchAB(assetURL, (buf) => {
    sourceBuffer.addEventListener("updateend", () => {
      mediaSource.endOfStream();
      video.play();
      console.log(mediaSource.readyState); // ended
    });
    sourceBuffer.appendBuffer(buf);
  });
}

其中MediaSource.isTypeSupported 判断了 mimeCodec 是否是浏览器内核所支持,如果不支持那么在执行到addSourceBufferappendBuffer这些操作都会报错。

问题来了,当一个文件给出,怎么去获取它的mimecodecs。

学习编码定义和寻找解析方法

具体 mime codec 定义参看 developer.mozilla.org/en-US/docs/...

从上述链接知道,通常有多种mime 类型,不同的格式内编码也存在差别

bash 复制代码
video/mp4; codecs="avc1.64001E, mp4a.40.2"
audio/mp4; codecs=mp4a.40.42
video/webm; codecs="vp8, vorbis"

以 H264 编码为例 avc1[.PPCCLL] avc1 为实际编码的fourcc字符串,后面 PP 以十六进制表示Profile, CC以十六进制表示Constraint,LL以十六进制表示Level。

对于H265,vpx 等其他编码格式,分别有他们自己的codecs的定义。具体参看MDN文档和RFC。


知道了定义,怎么去获取。

bash 复制代码
./mp4info frag_bunny.mp4 | grep Codec

示例代码中的注释提供了上述方法,这个工具可以解析mp4文件box获取信息。

类似的,也可以使用ffmpeg 或者ffprobe 去获取,比如

bash 复制代码
ffprobe -hide_banner -loglevel fatal -show_error -show_format -show_streams -show_private_data -print_format json

可以获取到stream 和 format信息,并且以json格式输出,相比于上面的工具,输出没有那么直观,但是不仅限于mp4文件,可以应用于任何音频,视频,图片,字幕等格式的文件。

但显然二进制的实现在js代码中不方便调用,经过搜索,js有类似的包实现,命令行即可获取mimecodecs信息

bash 复制代码
npx get-video-mime frag_bunny.mp4

实际上这个命令行的包get-video-mime是对 mp4box这个包的封装。顾名思义,可以用这个包来检查分析mp4文件。

那么如果希望播放一个mp3文件呢,好在常见的mp3编码文件对应的 mimecodec比较固定,但也不能排除有其他格式。

再次经过搜索,发现有类似ffmpeg.wasm, ffprobe-wasm的项目,提供了wasm的方法来分析文件。wasm方式本质上和C方法下的ffmpeg/ffprobe行为一致。

其中ffprobe-wasm[github.com/alfg/ffprob...] 最接近想要的解决方法,可以probe一个文件并返回json文件包含stream,format信息。 其中Dockerfile编译ffmpeg并且 embind编译一个probe的函数的CPP方法很值得参考。

怎么实现一个wasm的基础在此就不再描述。直接参考这个项目。

自己的实现

ffprobe-wasm 的问题一个是并不直接返回mime codecs字符串,仍旧需要写js代码来分析,并且对于不同编码格式要实现不同的解析和拼接字符串方法。比如avc1,av01,hev1等等都定义的codecs都不一样。 另一个问题是作者错误地理解了ffmpeg的demuxer,decoder。比如如下的编译参数,

Dockerfile 复制代码
  --enable-protocol=file \
  --enable-decoder=h264,aac,pcm_s16le,mp3 \
  --enable-demuxer=mov,matroska,mp3 \
  --enable-muxer=mp4 \
  --enable-gpl \
  --enable-libx264 \
  --enable-libmp3lame \

在编译中仅打开了mov,matroska,mp3三种demuxer,muxer也只有mp4,decoder开启了几种,另外还额外编译了x264,mp3lame等包。

实际上仅仅是probe信息的话,额外的包是不需要的,并且也不需要decoder。这个项目还可以输出frame,姑且需要decoder。但额外包仅当需要encoder才需要。 有限的demuxer和muxer可以压缩产出物的大小,但限制了应对各种格式的能力。

  • 修改ffmpeg编译,仅保留avformat,avcodec,avutil 三个库,虽然对于probe仅需要avformat,但avformat本身会依赖其他两个库。禁掉encoder,decoder,仅保留demuxer,muxer用来probe。 ⚠️注意muxer要保留,否则没法利用av_guess_format 来得到mimetype信息
  • 实现C++程序,可以先native编译验证正确,主要就是 av_guess_format得到 mime_type信息,avformat_find_stream_info 中获取AVStream并且 访问 stream的 codecpar来得到profile,level信息,以及extradata。
  • 对于有些编码格式,codecpar给出的profile,level不足够输出codecs信息,需要额外解析extradata,比如对于H264,需要按照AVCDecoderConfigurationRecord来解析,extradata也会存另一种格式,直接保存SPS/PPS信息,按照AVCDecoderConfigurationRecord定义解析得到 PPCCLL 就得到最终结果。定义如下:
scss 复制代码
aligned(8) class AVCDecoderConfigurationRecord {  
    unsigned int(8) configurationVersion = 1;  
    unsigned int(8) AVCProfileIndication;  
    unsigned int(8) profile_compatibility;  
    unsigned int(8) AVCLevelIndication;  
    bit(6) reserved = '111111'b;  
    unsigned int(2) lengthSizeMinusOne;  
    bit(3) reserved = '111'b;  
    unsigned int(5) numOfSequenceParameterSets;  
    for (i=0; i< numOfSequenceParameterSets; i++) {  
        unsigned int(16) sequenceParameterSetLength ;  
        bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit;  
    }  
    unsigned int(8) numOfPictureParameterSets;  
    for (i=0; i< numOfPictureParameterSets; i++) {  
        unsigned int(16) pictureParameterSetLength;  
        bit(8*pictureParameterSetLength) pictureParameterSetNALUnit;  
    }  
}

我们可以解析AVCProfileIndication,profile_compatibility,AVCLevelIndication作为PPCCLL H265,AV1 都有自己的定义。

  • embind编译得到wasm文件,用一个Promise 和 await来封装wasm的初始化和内部函数调用,实现可以import 以及npx两种调用方式,打包发布NPM。 包地址

    npm install ffmime

仓库地址 github.com/junka/ffmim...

知识回顾

整个过程,从写 MSE 例子程序开始,学习了以下知识

  • mimetype codecs 定义,特别是H264、H265、AV1的定义
  • 调研试用了几个项目和使用的技巧,mp4box,ffprobe、ffprobe-wasm
  • ffmpeg库C++程序的编写,接口文件的学习
  • wasm编译ffmpeg,编译bind C++程序。
  • wasm模块的引用和初始化
  • npm包和命令行封装和打包发布

Enjoy the journey!

相关推荐
小猪猪屁11 天前
WebAssembly 从零到实战:前端性能革命完全指南
前端·vue.js·webassembly
pepedd86413 天前
WebAssembly简单入门
前端·webassembly·trae
受之以蒙16 天前
Rust & WebAssembly 实践:构建一个简单实时的 Markdown 编辑器
笔记·rust·webassembly
wayhome在哪17 天前
3 分钟上手!用 WebAssembly 优化前端图片处理性能(附完整代码)
javascript·性能优化·webassembly
yangholmes888820 天前
EMSCRIPTEN File System 入门
前端·webassembly
yangholmes888825 天前
如何在 web 应用中使用 GDAL (三)
前端·webassembly
yangholmes88881 个月前
如何在 web 应用中使用 GDAL (二)
前端·webassembly
yangholmes88881 个月前
如何在 web 应用中使用 GDAL (一)
webassembly
DogDaoDao1 个月前
WebAssembly技术详解:从浏览器到云原生的高性能革命
云原生·音视频·编译·wasm·webassembly·流媒体·多媒体
受之以蒙1 个月前
Rust & WebAssembly 性能调优指南:从毫秒级加速到KB级瘦身
笔记·rust·webassembly