边学边做,法力无边。
先提问题
当打算用 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 是否是浏览器内核所支持,如果不支持那么在执行到addSourceBuffer
,appendBuffer
这些操作都会报错。
问题来了,当一个文件给出,怎么去获取它的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!