一、简介:
MP4封装格式以其跨平台特性而成为当前最常见的媒体封装格式之一。MP4文件由多个box组成,每个box存储不同的信息,且box之间会出现嵌套。MP4的box有很多,但最重要的顶层box主要有如下三个:
ftyp:File Type Box,描述文件遵从的MP4规范与版本
moov:Movie Box,媒体的metadata信息,有且仅有一个
mdat:Media Data Box,存放实际的媒体数据,一般有多个
每个box有两部分组成:box header 和 box body。
box header:box的元数据,比如box type、box size。
box body:box的数据部分,实际存储的内容跟box类型有关,比如mdat中body部分存储的媒体数据。
当box body中嵌套其他box时,这样的box叫做container box。
二、重要的box:
ftyp
mdat
moov
-mvhd
-(time_scale):1s包含的时间单位
-(duration):影片时长,等于最长trak的duration
-trak
-tkhd:单个track的metadata
-(id):当前track的唯一标识
-(duration):当前track的持续时间,FFmpeg忽略了此值
-(width):视频宽
-(height):视频高
-mdia:描述当前track的一些信息
-hdlr:声明当前的track类型
*vide:视频track
*soun:音频track
*m1a :MP2
*subp/clcp:字幕
-stbl:媒体数据的索引及时间信息(非常重要)
-stsd:确认当前trak的format,匹配FFmpeg中的codec_id和codec_type等
-stts:每个帧的时长
-stss:该trak中关键帧的个数及序号
-ctts:记录dts与pts的差值,仅B帧存在的码流才需要
-stsc:每个chunk的sample数
-stsz:当前trak包含的sample数
-stco:chunk在文件中的偏移量
-chunk_offsets:每个chunk相对于文件整体的偏移量
三、FFmpeg中对MP4相关box的解析:
FFmpeg源码中,解析MP4格式的demuxer是mov,文件所在路径为:
libavformat/mov.c
看一下结构体各成员定义:
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",
.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,
};
各个box的解析是在mov_read_header中完成的:
static int mov_read_header(AVFormatContext *s)
{
MOVContext *mov = s->priv_data;
AVIOContext *pb = s->pb;
int j, err;
/* atmo为box解析中的最小单位 */
MOVAtom atom = { AV_RL32("root") };
...
/* check MOV header */
do {
if (mov->moov_retry)
avio_seek(pb, 0, SEEK_SET);
/* 读取box中内容,有嵌套的话持续往下读 */
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++);
if (!mov->found_moov) { //是否读取完的标志位
av_log(s, AV_LOG_ERROR, "moov atom not found\n");
return AVERROR_INVALIDDATA;
}
...
}
看一下mov_read_default:
static int mov_read_default(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
int64_t total_size = 0;
MOVAtom a;
int i;
/* 记录atom的嵌套层数 */
if (c->atom_depth > 10) {
av_log(c->fc, AV_LOG_ERROR, "Atoms too deeply nested\n");
return AVERROR_INVALIDDATA;
}
c->atom_depth ++;
if (atom.size < 0)
atom.size = INT64_MAX;
while (total_size <= atom.size - 8 && !avio_feof(pb)) {
/* parse函数指针用于指向各个解析box */
int (*parse)(MOVContext*, AVIOContext*, MOVAtom) = NULL;
...
/* 遍历各个数组,找根据type找到对应的box函数进行解析 */
for (i = 0; mov_default_parse_table[i].type; i++)
if (mov_default_parse_table[i].type == a.type) {
parse = mov_default_parse_table[i].parse;
break;
}
...
if (!parse) { /* skip leaf atoms data */
avio_skip(pb, a.size);
} else {
int64_t start_pos = avio_tell(pb);
int64_t left;
/* 调用对应的box解析函数 */
int err = parse(c, pb, a);
if (err < 0) {
c->atom_depth --;
return err;
}
...
}
...
}
...
}
MP4文件默认的box解析数组为mov_default_parse_table:
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('a','v','1','C'), mov_read_glbl },
...
}
四、MP4的box解析工具:
1.mp4info:
方便获取一些box的基本信息,但无法完全显示box中一些关键信息。
2.MP4 exploer:
会比较详细的列出每个box的关键信息,有助于我们分析码流,下载地址。
五、MP4的一些常见问题分析:
1.Samba等共享访问时起播时间较长:
这个主要是因为记录metadata的box-moov在文件末尾的,需要将整个片源都下载下来之后才会获取metadata,解析出解码相关信息。
这种问题的解决方案为将moov移动到片源开始位置,对于本地片源,可以使用FFmpeg进行转码:
ffmpeg -i xxx.mp4 -codec copy -movflags faststart output.mp4
对于短视频类,建议在上传的时候统一进行moov的位移,如抖音等,对于线上moov已经在文件末尾的码流,可以考虑使用云端转码,以实现秒起播。