mov
、FFmpeg
、mp4
mp4
文件格式是现如今网络上最常见的视频文件格式,其和mov
等格式相同都是IOS Base File Format的实现版本,其文件格式都是基于box。
在FFmpeg中mp4文件解封装的实现在libavformat\mov.c
文件中。在FFmpeg中每个封装格式都一个描述当前封装格式的结构体和其选项的AVClass
,mp4个是对应的结构体分别为ff_mov_demuxer,mov_class
。
mov_class
描述了mp4文件的基本选项信息,mov_options
是一个FFmpeg中内部定义的key-value列表,其中定义了FFmpeg中的基本选项。
ff_mov_demuxer
描述如何解封装一个mp4文件的,以及一些基本信息。该结构包含文件扩展名,选项列表,解封装的函数指针,标志位等信息。解封装时AVFormatContext
都是通过操作函数指针读取文件信息,解封装文件。
static const AVClass mov_class = {
.class_name = "mov,mp4,m4a,3gp,3g2,mj2",
.item_name = av_default_item_name,
.option = mov_options,
.version = LIBAVUTIL_VERSION_INT,
};
const AVInputFormat ff_mov_demuxer = {
.name = "mov,mp4,m4a,3gp,3g2,mj2",
.long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"),
.priv_class = &mov_class,
.priv_data_size = sizeof(MOVContext),
.extensions = "mov,mp4,m4a,3gp,3g2,mj2,psp,m4b,ism,ismv,isma,f4v,avif",
.flags_internal = FF_FMT_INIT_CLEANUP,
.read_probe = mov_probe,
.read_header = mov_read_header,
.read_packet = mov_read_packet,
.read_close = mov_read_close,
.read_seek = mov_read_seek,
.flags = AVFMT_NO_BYTE_SEEK | AVFMT_SEEK_TO_PTS | AVFMT_SHOW_IDS,
};
ff_mov_class
每一项的具体含义:
name
:由,
隔开的格式名称;long_name
:全称;priv_class
:私有的选项;priv_data_size
:私有数据的大小,一般为对应格式的Context,比如mov格式为MOVContext
;extensions
:扩展名,能够看到mov
,```mp4````等格式公用同一个解封装器;flag_internal
:内部的标志符;const char *mime_type;
:,
隔开的mime_type;read_probe
:探测当前文件是那个类型的文件的函数指针,在avformat_find_stream_info
用来探测当前文件是否为mp4
文件,以及相关流信息;read_header
:读取格式header,初始化AVFormatContext
的函数指针,avformat_open_input
时调用,用来读取文件的基本信息;read_packet
:从文件中读取一个packet的函数指针,读取未解码的数据流的函数指针,在av_read_frame
时调用;read_close
:关闭流,但是不涉及对应流的释放;read_seek
:seek到对应的位置,av_seek_frame
时调用来seek到对应的位置;flags;
:操作文件个标志符,比如是否允许按照bytes seek等。 解封装的基本流程与用AVFormatContext
解封装的基本流程相同:
mp4解封装涉及的结构体比较多,这里挑选几个重点说下。mov.c
定义了基础boxMOVAtom
的基本结构定义,以及其他接个editlist相关的结构,比如MOVStts,MOVCtts,MOVElst
等
typedef struct MOVAtom {
uint32_t type;
int64_t size; /* total size (excluding the size and type fields) */
} MOVAtom;
以及一些他描述mp4流,轨道索引等信息的结构体,比如MOVStreamContext
,MOVFragmentIndex
,MOVContext
。
mov_probe
mov_probe
会返回一个分值,该分值表示当前文件为对应文件格式的分值,分值越高该文件为对应格式的概率越高。在probe时,FFmpeg会根据文件的具体格式进行分辨,mov
格式就是检测是否存在某个box,如果无法检测到文件符合对应格式,就会退而求其次以扩展名作为依据。所以就会出现有时候检测错误的情况,比如一个随机的mp3文件,其扩展名为mp3,FFmpeg会根据mp3文件进行解封装。解码时并不是每一片都能解码成功,有几率部分片段能够解码正常,但是解码出来的数据是异常的。
#define AVPROBE_SCORE_EXTENSION 50 ///< score for file extension
#define AVPROBE_SCORE_MIME 75 ///< score for file mime type
#define AVPROBE_SCORE_MAX 100 ///< maximum score
mov_probe
探测流文件的伪代码如下,这里输入被简化为指针```p````:
int mov_probe(int *p){
int score = 0, offset = 0, moov_offset = -1;
while(1){
int64_t size = AV_RB32(p + offset); //从当前流的位置读取当前box的大小,伪代码不考虑largesize的情况
char tag[4] = AV_RL32(p + offset+ 4) //从接下来的内存中读取tag
switch(tag){
case "moov":moov_offset = offset + 4;
case "mdat":
case "pnot":
case "udta":
case "ftyp":
if(tag == "ftyp" && tag in ["jp2 " "jpx " "jxl "]){
score = std::max(score, 5);
}else{
score = AVPROBE_SCORE_MAX;
}
break;
case "ediw":
case "wide":
case "junk":
case "pict":
score = std::max(score , AVPROBE_SCORE_MAX - 5);break;
case "skip":
case "uuid":
case "prfl":
score = std::max(score, AVPROBE_SCORE_EXTENSION);break;
}
offset += size
}
if(score > AVPROBE_SCORE_MAX - 50 && moov_offset != -1){
/* moov atom in the header - we should make sure that this is not a
* MOV-packed MPEG-PS */
offset = moov_offset;
while (offset < (len(p) - 16)) { /* Sufficient space */
/* We found an actual hdlr atom */
if (AV_RL32(p->buf + offset ) == MKTAG('h','d','l','r') &&
AV_RL32(p->buf + offset + 8) == MKTAG('m','h','l','r') &&
AV_RL32(p->buf + offset + 12) == MKTAG('M','P','E','G')) {
av_log(NULL, AV_LOG_WARNING, "Found media data tag MPEG indicating this is a MOV-packed MPEG-PS.\n");
/* We found a media handler reference atom describing an
* MPEG-PS-in-MOV, return a
* low score to force expanding the probe window until
* mpegps_probe finds what it needs */
return 5;
} else {
/* Keep looking */
offset += 2;
}
}
}
return score;
}
mov_read_header
mov_read_header
是在avforamt_open_input
时调用,解析mp4文件的基本信息。经过此操作基本上从AVForamtContext
中和封装格式相关的信息比如iformat
,流信息等基本上都已经检测到。
mov_read_header
的实现过程。首先,校验一些参数,不符合要求就会返回错误。然后调用mov_read_default
解析流文件中的atom box,从box中读取相关的信息写入到MOVContext
中。
/* check MOV header *///不断嵌套读,直到读到moov box未知
do {
if (mov->moov_retry)
avio_seek(pb, 0, SEEK_SET);
if ((err = mov_read_default(mov, pb, atom)) < 0) {
av_log(s, AV_LOG_ERROR, "error reading header\n");
return err;
}
} while ((pb->seekable & AVIO_SEEKABLE_NORMAL) && !mov->found_moov && !mov->moov_retry++);
mov_read_default
中就是不断嵌套读当前atom box内部的所有box然后解析,根据type判断是否为符合要求的box,符合的话就会调用对应的解析函数去解析。结合上面的do...while
可以理解这里采用的是深度优先的解析方式。具体的解析函数是下面的一个静态函数表格,函数内会通过for循环去寻找是否为符合要求的box然后解析。说实话这样效率感人,索引表感觉更合理。
static const MOVParseTableEntry mov_default_parse_table[] = {
{ MKTAG('A','C','L','R'), mov_read_aclr },
{ MKTAG('A','P','R','G'), mov_read_avid },
{ MKTAG('A','A','L','P'), mov_read_avid },
{ MKTAG('A','R','E','S'), mov_read_ares },
{ MKTAG('a','v','s','s'), mov_read_avss },
...
{ MKTAG('m','d','c','v'), mov_read_mdcv },
{ MKTAG('c','l','l','i'), mov_read_clli },
{ MKTAG('d','v','c','C'), mov_read_dvcc_dvvc },
{ MKTAG('d','v','v','C'), mov_read_dvcc_dvvc },
{ 0, NULL }
};}
经过上面的步骤,流的基本信息已经存储在MOVContext
和MOVStreamContext
中,之后就是将解析出来的信息进行处理或者写到AVFormatContext
中。比如从side_data
中读取转换矩阵,然后解析当前视频的旋转角,读取chatper,timebase等等。
mov_read_packet
mov_read_packet
会在avformat_find_stream_info
和av_read_frame
内被调用。前者只会调用几次用来确认流数据的详细信息,而后是是从流中读取未解码的数据。
首先会调用mov_find_next_sample
根据当前读取的sample,以及其他时间戳相关的信息解析出下一帧要读取的时间戳。并进行一些size相关的检查,校正要读取的sample的大小以及改变全局的索引(FFMpeg内部的iformat有保存全部的pos索引来表示当前读取到的位置)。
sample = mov_find_next_sample(s, &st);
if (!sample || (mov->next_root_atom && sample->pos > mov->next_root_atom)) {
if (!mov->next_root_atom)
return AVERROR_EOF;
if ((ret = mov_switch_root(s, mov->next_root_atom, -1)) < 0)
return ret;
goto retry;
}
然后就是根据标志位来判断当前packet是否要丢弃,调用av_get_packet
读取数据,在进行一些size上的校正后,调用avio_read
直接读文件。而具体的读取当然不是一次性读完,因此mov中的数据是按照box存储的,因此会一直读取到满足预期的大小或者报错为止。
if (st->codecpar->codec_id == AV_CODEC_ID_EIA_608 && sample->size > 8)
ret = get_eia608_packet(sc->pb, pkt, sample->size);
else
ret = av_get_packet(sc->pb, pkt, sample->size);
最后就是填充packet
的sidedata,以及更新ctts,stsc
等相关的索引,以及一些善后的工作。
mov_read_seek
seek
的实现比较简单,大部分为计算时间戳,更新索引,调整ctts,stsc
索引等内容。
mov_read_close
mov_read_close
是在avformat_close_input
时调用,其实现比较简单就是关闭流释放各种context。
static int mov_read_close(AVFormatContext *s)
{
MOVContext *mov = s->priv_data;
int i, j;
for (i = 0; i < s->nb_streams; i++) {
AVStream *st = s->streams[i];
MOVStreamContext *sc = st->priv_data;
if (!sc)
continue;
av_freep(&sc->ctts_data);
for (j = 0; j < sc->drefs_count; j++) {
av_freep(&sc->drefs[j].path);
av_freep(&sc->drefs[j].dir);
}
av_freep(&sc->drefs);
sc->drefs_count = 0;
if (!sc->pb_is_copied)
ff_format_io_close(s, &sc->pb); //内部就是调用io_close
sc->pb = NULL;
av_freep(&sc->chunk_offsets);
av_freep(&sc->stsc_data);
av_freep(&sc->sample_sizes);
av_freep(&sc->keyframes);
av_freep(&sc->stts_data);
av_freep(&sc->sdtp_data);
av_freep(&sc->stps_data);
av_freep(&sc->elst_data);
av_freep(&sc->rap_group);
av_freep(&sc->display_matrix);
av_freep(&sc->index_ranges);
if (sc->extradata)
for (j = 0; j < sc->stsd_count; j++)
av_free(sc->extradata[j]);
av_freep(&sc->extradata);
av_freep(&sc->extradata_size);
mov_free_encryption_index(&sc->cenc.encryption_index);
av_encryption_info_free(sc->cenc.default_encrypted_sample);
av_aes_ctr_free(sc->cenc.aes_ctr);
av_freep(&sc->stereo3d);
av_freep(&sc->spherical);
av_freep(&sc->mastering);
av_freep(&sc->coll);
}
av_freep(&mov->dv_demux);
avformat_free_context(mov->dv_fctx);
mov->dv_fctx = NULL;
if (mov->meta_keys) {
for (i = 1; i < mov->meta_keys_count; i++) {
av_freep(&mov->meta_keys[i]);
}
av_freep(&mov->meta_keys);
}
av_freep(&mov->trex_data);
av_freep(&mov->bitrates);
for (i = 0; i < mov->frag_index.nb_items; i++) {
MOVFragmentStreamInfo *frag = mov->frag_index.item[i].stream_info;
for (j = 0; j < mov->frag_index.item[i].nb_stream_info; j++) {
mov_free_encryption_index(&frag[j].encryption_index);
}
av_freep(&mov->frag_index.item[i].stream_info);
}
av_freep(&mov->frag_index.item);
av_freep(&mov->aes_decrypt);
av_freep(&mov->chapter_tracks);
return 0;
}