Skip to content
Junjie edited this page Apr 28, 2024 · 1 revision

Welcome to the ffmime wiki!

边学边做,法力无边。

先提问题

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

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 定义参看 https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter

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

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。


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

./mp4info frag_bunny.mp4 | grep Codec

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

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

ffprobe -hide_banner -loglevel fatal -show_error -show_format -show_streams -show_private_data -print_format json

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

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

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

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

自己的实现

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

  --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 就得到最终结果。定义如下:
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 

仓库地址 https://github.com/junka/ffmime

知识回顾

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

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

Enjoy the journey!

Clone this wiki locally