目录
- 参考
- 概述
- mov_read_header
- mov_read_packet
- mov_read_seek
- mov_read_close
1. 参考
- [1] github.com/FFmpeg/FFmpeg/blob/master/libavformat/mov.c
- [2] wangcong02345/ffmpeg源码分析--11.mov的mov_read_header
- [3] wangcong02345/ffmpeg源码分析--12.mov的mov_read_packet
- [4] QQ音乐技术团队/关于M4A文件的随机访问
2. 概述
mp4文件格式解析 文章对mp4文件格式进行了了解,本文主要从代码角度,学习FFmpeg中对mp4的解析、读取数据和seek等操作的实现。
以下是mp4主要box的示意图,解析部分主要分析这些box。
3. mov_read_header
mov_read_header的调用时机如下图所示。
- 在调用avformat_open_input过程会对mp4文件进行解析。
mov_read_header的工作
- 检测参数的有效性。
- 比如
decryption_key_len
的范围。
- 比如
- 调用mov_read_default进行root下的box解析。
3.1 mov_read_default
- 如果box中有剩余没有读取的部分,skip剩余未读取的部分。
- 如果box读取多于了size指定的大小(这种情况理应不该发生),把读取的位置往回调。
- mov_default_parse_table是一个数组,保存了box的type对应解析使用的函数的指针。
- 在父box还没有读取完毕且还没有到文件结尾的时候,循环地去解析父box下的子box。
static int mov_read_default(MOVContext *c, AVIOContext *pb, MOVAtom atom)
{
int64_t total_size = 0;
MOVAtom a;
int i;
c->atom_depth ++;
...
while (total_size <= atom.size - 8 && !avio_feof(pb)) {
int (*parse)(MOVContext*, AVIOContext*, MOVAtom) = NULL;
a.size = atom.size;
a.type=0;
if (atom.size >= 8) {
a.size = avio_rb32(pb);
a.type = avio_rl32(pb);
...
total_size += 8;
...
}
...
a.size -= 8;
...
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;
int err = parse(c, pb, a);
if (err < 0) {
c->atom_depth --;
return err;
}
...
}
total_size += a.size;
}
...
c->atom_depth --;
return 0;
}
3.2 mov_read_ftyp
- 读取ftyp box的"major_brand"、"minor_version"、"compatible_brands"信息,保存到AVFormatContext.metadata(AVDictionary结构体)中。
- "major_brand"保存为char *类型。
- "minor_version"保存为int类型。
- "compatible_brands"保存为char *类型。
3.3 mov_read_moov
- 如果MOVContext.found_moov为1,即发现了重复的moov box(理应不该发生),直接返回。
- 如果MOVContext.found_moov为0, 调用mov_read_default()来读取子box,把MOVContext.found_moov置为1。
3.4 mov_read_mvhd
- 读取create_time,通过strftime转换为格式化的字符串的时间表示,保存在AVFormatContext.metadata中。
-
读取time_scale,保存在MOVContext.time_scale。
- 校验有效性,如果小于等于0则置为1。
- 读取duration,保存在MOVContext.duration。
- 根据获取到的time_scale和duration计算为以FFmpeg中AV_TIME_BASE为timescale的时长保存在AVFormatContext.duratinon(int64_t)。
- FFmpeg中AV_TIME_BASE定义为1000000,示例julin_5s.mp4文件的mvhd中duration为5022,timescale为1000,所以计算得到c->fc->duration=5022 * AV_TIME_BASE / 1000 = 5022000(us)
c->fc->duration = av_rescale(c->duration, AV_TIME_BASE, c->time_scale);
- 读取matrix信息保存到MOVContext.movie_display_matrix
- box中其他的字段信息未保存。
3.5 mov_read_trak
- 使用
avformat_new_stream
函数在AVFormatContext中新建一个Stream。 - 创建MOVStreamContext结构体,保存在AVStream.priv_data。
- 用于保存trak box中读取到的一些信息,
- 读取子box。
- 一些完整性检查,针对MOVStreamContext中的一些变量。
- sample_count大于0时,chunk_count不应该为0。
- chunk_count大于0时,stts_count、stsc_count不应该为0,sample_size和sample_count不应该同时为0。
- stsc_data的最后一个元素的first_chunk值不应该大于chunk_count。
- mov_build_index
- 建立包含每个sample信息的表,保存在AVStream.index_entries(AVIndexEntry数组)。
typedef struct AVIndexEntry {
int64_t pos;
int64_t timestamp; //Timestamp in AVStream.time_base units
#define AVINDEX_KEYFRAME 0x0001
#define AVINDEX_DISCARD_FRAME 0x0002
int flags:2;
int size:30;
int min_distance; //Minimum distance between this and the previous keyframe, used to avoid unneeded searching. */
} AVIndexEntry;
- pos,当前sample相对文件的偏移。
- timestamp:当前sample的dts。以AVStream.time_base为单位。
- flags:标识,是否为关键帧,或者要舍弃的帧。
- size:当前sample的字节大小。
- min_distance:距离前一个关键帧的距离,序号的偏差。
- 建立表的方式是通过遍历stsc表,结合stco、stsz、stss、stts表的信息来完成的。
- AVIndexEntry.pos:使用stco(chunk在文件中的偏移)、stsz(每个sample的字节大小)来计算得到。
- AVIndexEntry.min_distance和AVIndexEntry.flags:依据stss来获得。
- AVIndexEntry.size:通过stsz获得。
- 代码如下所示(原始代码删去了部分逻辑)
static void mov_build_index(MOVContext *mov, AVStream *st) {
MOVStreamContext *sc = st->priv_data;
int64_t current_offset;
int64_t current_dts = 0;
unsigned int stts_index = 0;
unsigned int stsc_index = 0;
unsigned int stss_index = 0;
unsigned int stps_index = 0;
if (!(st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO &&
sc->stts_count == 1 && sc->stts_data[0].duration == 1)) {
unsigned int current_sample = 0;
unsigned int stts_sample = 0;
unsigned int sample_size;
unsigned int distance = 0;
unsigned int rap_group_index = 0;
unsigned int rap_group_sample = 0;
int64_t last_dts = 0;
int64_t dts_correction = 0;
int rap_group_present = sc->rap_group_count && sc->rap_group;
int key_off = (sc->keyframe_count && sc->keyframes[0] > 0) || (sc->stps_count && sc->stps_data[0] > 0);
current_dts -= sc->dts_shift;
last_dts = current_dts;
...
for (i = 0; i < sc->chunk_count; i++) {//遍历每个chunk
int64_t next_offset = i+1 < sc->chunk_count ? sc->chunk_offsets[i+1] : INT64_MAX;
current_offset = sc->chunk_offsets[i];//当前chunk的文件位置偏移。
while (mov_stsc_index_valid(stsc_index, sc->stsc_count) &&
i + 1 == sc->stsc_data[stsc_index + 1].first)//(i+1)等于下一组chunk的first_chunk的时候,stsc_index后移指向下一组chunk。
stsc_index++;
...
for (j = 0; j < sc->stsc_data[stsc_index].count; j++) {//遍历chunk中的每个sample。
...
int keyframe = 0;
if (!sc->keyframe_absent && (!sc->keyframe_count || current_sample+key_off == sc->keyframes[stss_index])) {//判断是否是关键帧的条件。
keyframe = 1;
if (stss_index + 1 < sc->keyframe_count)
stss_index++;//stss表指向一下个关键帧。
}
...
if (sc->keyframe_absent
&& !sc->stps_count
&& !rap_group_present
&& (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO || (i==0 && j==0)))
keyframe = 1;
if (keyframe)
distance = 0;
...
e = &st->index_entries[st->nb_index_entries++];
e->pos = current_offset;
e->timestamp = current_dts;
e->size = sample_size;
e->min_distance = distance;
e->flags = keyframe ? AVINDEX_KEYFRAME : 0;
...
if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && st->nb_index_entries < 100)
ff_rfps_add_frame(mov->fc, st, current_dts);
}
current_offset += sample_size;//sample的文件位置偏移向后移动当前sample的大小
...
//更新current_dts和last_dts
current_dts += sc->stts_data[stts_index].duration;
if (!dts_correction || current_dts + dts_correction > last_dts) {
current_dts += dts_correction;
dts_correction = 0;
} else {
/* Avoid creating non-monotonous DTS */
dts_correction += current_dts - last_dts - 1;
current_dts = last_dts + 1;
}
last_dts = current_dts;
distance++;//与前一个关键帧的距离更新
stts_sample++;//stts_sample是针对stts,sample在每个entry中的序号,不是实际的序号。
current_sample++;//更新当前sample的序号。
if (stts_index + 1 < sc->stts_count && stts_sample == sc->stts_data[stts_index].count) {//stts_data[stts_index]中的sample都遍历完了。
stts_sample = 0;
stts_index++;
}
}
}
if (st->duration > 0)
st->codecpar->bit_rate = stream_size*8*sc->time_scale/st->duration;
}
...
// Update start time of the stream.
if (st->start_time == AV_NOPTS_VALUE && st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && st->nb_index_entries > 0) {
st->start_time = st->index_entries[0].timestamp + sc->dts_shift;
if (sc->ctts_data) {
st->start_time += sc->ctts_data[0].duration;
}
}
mov_estimate_video_delay(mov, st);
3.6 mov_read_tkhd
- 读取flags,如果flags开启了MOV_TKHD_FLAG_ENABLED则,AVStreamst.disposition置为AV_DISPOSITION_DEFAULT,否则置为0。 具体的作用,尚未了解
- 读取track id,保存到AVStream.id。
- 读取matrix,根据matrix和读mvhd得到的MOVContext.movie_display_matrix计算的到res_display_matrix,保存到MOVStreamContext.display_matrix。具体的作用,尚未了解
- 根据MOVStreamContext.display_matrix一通计算得到AVStream.sample_aspect_ratio。具体的作用,尚未了解
3.7 mdia(mov_read_default)
3.8 mdhd(mov_read_mdhd)
- 一些异常检查。
- AVFormatContex中没有AVStream的数量小于1,直接返回。
- 检测到重复的mdhd box,返回错误。
- version 大于1,返回错误。
- 读取creation_time,通过strftime转换为格式化的字符串的时间表示,保存在AVStream.metadata(AVDictionary结构体)中。
- 读取time_scale,保存在MOVStreamContext.time_scale。
- 校验有效性,如果小于等于0则置为1。
- 读取duration,保存在AVStream.duration。
- 读取language,保存在AVStream.metadata中,char*的形式,key值为"language"。
3.9 hdlr(mov_read_hdlr)
- 读取handler_type。
- 如果handler_type为"vide"、"soun"或"subp",设置AVStream->codecpar->codec_type为AVMEDIA_TYPE_VIDEO、AVMEDIA_TYPE_AUDIO或AVMEDIA_TYPE_SUBTITLE。
- 如果handler_type为"m1a",设置AVStream->codecpar->id为AV_CODEC_ID_MP2。
- 读取name,保存到AVStream.metadata,key为"handler_name",类型为char *。
3.10 minf(mov_read_default)
3.11 stbl(mov_read_default)
3.12 stsd(mov_read_stsd)
- 一些异常检查。
- AVFormatContex中没有AVStream的数量小于1,直接返回。
- 读取到的entry数量<=0或者超过最大可能的数,则返回错误。
- 检测到重复的mdhd box,返回错误。
- 读取version,保存到MOVStreamContext.stsd_version。
- 读取format,保存到MOVStreamContext.format。
- 根据format,找到相应的codec_id,赋值给AVStream->codecpar->codec_id。
-
对于视频(即handler_type为"vide")
- 读取width和height保存到AVStream.codecpar的width和height。
- 读取compressorname,即编码器的名称到AVStream.metadata,key为"encoder",类型为char *。
- 读取depth,即位深,保存到AVStream.codecpar->bits_per_coded_sample。
-
对于音频(即handler_type为"soun")
- 读取channelcount保存到AVStream.codecpar->channels。
- 读取samplesize,即采样比特,保存到AVStream->codecpar->bits_per_coded_sample。
- 读取audio channel id,保存到MOVStreamContext.audio_cid。
- 读取samplerate,保存到AVStream->codecpar->sample_rate。
- 读取额外的box,比如wave, alac, damr, avcC, hvcC, SMI等。
3.13 stts(mov_read_stts)
- 一些异常检查。
- 读取time to sample表到MOVContext.stts_data(MOVStts数组)。
typedef struct MOVStts {
unsigned int count;
int duration;
} MOVStts;
- 根据stts表,统计sample总数,保存到AVStream.nb_frames。
- 根据stts表,统计时长duration。
- 校正AVStream.duration,注意mdhd中有读取duration信息到AVStream.duration。
- 保存到MOVContext.track_end。
if (duration)
st->duration= FFMIN(st->duration, duration);
3.14 ctts(mov_read_ctts)
- 读取composition time to sample表到MOVContext.ctts_data(MOVStts数组)。
- 更新MOVStreamContext.dts_shift。
3.15 stss(mov_read_stss)
- 如果读取到的entries值为0且此媒体类型为video,则AVStream.need_parsing置为AVSTREAM_PARSE_HEADERS。
- 读取关键帧序号数组,保存到MOVStreamContext.keyframes(int 数组)。
- 统计到的关键帧数组元素的数量保存到MOVStreamContext.keyframe_count。
3.16 stsz/stz2(mov_read_stsz)
- 读取sample_size,保存到MOVStreamContext.stsz_sample_size。
- 如果是stz2 box,读取field_size。stsz box时field_size设置为32。
- 读取entries,保存到MOVStreamContext.sample_count。
- 如果sample_size不为0,返回。
- 保存sample_size的数组到MOVStreamContext.sample_size(int 数组)
- 总计的sample的size总和保存到MOVStreamContext.data_size。
- 统计到的sample_size数组的元素的个数保存到MOVStreamContext.sample_count。
3.17 stsc(mov_read_stsc)
- 读取sample to chunk表,保存到MOVStreamContext.stsc_data(MOVStsc数组)
typedef struct MOVStsc {
int first;
int count;
int id;
} MOVStsc;
- 统计到的MOVStsc元素的个数保存到MOVStreamContext.stsc_count。
- 对数组做一些有效性检查并校正。
3.18 stco/co64(mov_read_stco)
- 读取chunk offset 表保存到MOVStreamContext.chunk_offsets(int64_t数组)
- 数组元素的个数保存到MOVStreamContext.chunk_count。
3.19 mdat(mov_read_mdat)
- 如果body的size为0,则返回0。
- 否则,MOVContext.found_mdat置为1。
4. mov_read_packet
mov_read_packet的调用时机如下图所示:
mov_read_packet中的主要工作:
mov_find_next_sample
avio_seek
av_get_packet
pkt->dts = ...; pkt->pts = ..; ...
- mov_find_next_sample找到要读取的sample和对应的AVStream。
- avio_seek(sc->pb, sample->pos, SEEK_SET); 将文件的指针定位到文件的偏移处
- av_get_packet(sc->pb, pkt, sample->size); 从文件的偏移读取size的数据到AVPacket中。
- 计算AVPacket的dts和pts。
4.1 mov_find_next_sample
static AVIndexEntry *mov_find_next_sample(AVFormatContext *s, AVStream **st)
{
AVIndexEntry *sample = NULL;
int64_t best_dts = INT64_MAX;
int i;
for (i = 0; i < s->nb_streams; i++) {
AVStream *avst = s->streams[i];
FFStream *const avsti = ffstream(avst);
MOVStreamContext *msc = avst->priv_data;
if (msc->pb && msc->current_sample < avsti->nb_index_entries) {
AVIndexEntry *current_sample = &avsti->index_entries[msc->current_sample];
int64_t dts = av_rescale(current_sample->timestamp, AV_TIME_BASE, msc->time_scale);
av_log(s, AV_LOG_TRACE, "stream %d, sample %d, dts %"PRId64"\n", i, msc->current_sample, dts);
if (!sample || (!(s->pb->seekable & AVIO_SEEKABLE_NORMAL) && current_sample->pos < sample->pos) ||
((s->pb->seekable & AVIO_SEEKABLE_NORMAL) &&
((msc->pb != s->pb && dts < best_dts) || (msc->pb == s->pb && dts != AV_NOPTS_VALUE &&
((FFABS(best_dts - dts) <= AV_TIME_BASE && current_sample->pos < sample->pos) ||
(FFABS(best_dts - dts) > AV_TIME_BASE && dts < best_dts)))))) {
sample = current_sample;
best_dts = dts;
*st = avst;
}
}
}
return sample;
}
- 如果流的输入和文件的输入不是同一个文件,
(msc->pb != s->pb && dts < best_dts)
,则选择dts更小的流。 - 如果流的输入和文件的输入是同一个文件且dts的差异小于AV_TIME_BASE(即1s)则选择pos更小的那个流的packet,即offset更小的那个packet,如果大于1s,则选择dts更小的那个流的packet。这里的dts是算上editlist作用后的dts。
4.2 计算AVPacket的dts和pts。
//mov_read_packet()
pkt->dts = sample->timestamp;
if (sample->flags & AVINDEX_DISCARD_FRAME) {
pkt->flags |= AV_PKT_FLAG_DISCARD;
}
if (sc->ctts_data && sc->ctts_index < sc->ctts_count) {
pkt->pts = pkt->dts + sc->dts_shift + sc->ctts_data[sc->ctts_index].duration;
/* update ctts context */
sc->ctts_sample++;
if (sc->ctts_index < sc->ctts_count &&
sc->ctts_data[sc->ctts_index].count == sc->ctts_sample) {
sc->ctts_index++;
sc->ctts_sample = 0;
}
} else {
int64_t next_dts = (sc->current_sample < st->nb_index_entries) ?
st->index_entries[sc->current_sample].timestamp : st->duration;
if (next_dts >= pkt->dts)
pkt->duration = next_dts - pkt->dts;
pkt->pts = pkt->dts;
}
if (st->discard == AVDISCARD_ALL)
goto retry;
pkt->flags |= sample->flags & AVINDEX_KEYFRAME ? AV_PKT_FLAG_KEY : 0;
pkt->pos = sample->pos;
- dts为sample->timestamp。
- 如果有ctts表(创建时间与编码时间的差值表),根据ctts表计算pts。
pkt->pts=pkt->dts + sc->dts_shift + sc->ctts_data[sc->ctts_index].duration
,不知道为什么要加上sc->dts_shift? - 如果没有ctts表,
pkt->pts = pkt->dts
,并且计算pkt->duration== next_dts - pkt->dts
5. mov_read_seek
mov_read_seek的调用时机如下所示。
未完待续。。
6. mov_read_close
avformat_close_input
- AVInputFormat.read_close --> mov_read_close
- 释放各种申请的资源和内存。比如MOVContext和MOVStreamContext中申请的各种内存。
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;
...
if (!sc->pb_is_copied)
ff_format_io_close(s, &sc->pb);
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);
...
return 0;
}