FFmpeg在libavformat模块提供音视频的muxer封装与demuxer解封装。其中muxer封装文件包括avformat_write_header()、av_write_frame()和av_write_trailer()。本文主要探讨av_write_trailer函数如何写入文件尾,最终完成多媒体文件的封装。
关于avformat_write_header()和av_write_frame()可查看前面两篇文章:
FFmpeg源码分析:写文件头avformat_write_header()
FFmpeg源码分析:写音视频帧av_write_frame()
1、av_write_trailer
av_write_trailer()的头文件声明位于libavformat/avformat.h,描述如下:
/**
* Write the stream trailer to an output media file and free the
* file private data.
*
* May only be called after a successful call to avformat_write_header.
*
* @param s media file handle
* @return 0 if OK, AVERROR_xxx on error
*/
int av_write_trailer(AVFormatContext *s);
中文翻译大致如下:
写入音视频流的尾部到输出媒体文件,并且释放文件的私有数据。只能在avformat_write_header()调用成功后才能调该函数。
接下来,我们看看av_write_trailer()函数的实现,位于libavformat/mux.c:
int av_write_trailer(AVFormatContext *s)
{
int i, ret1, ret = 0;
AVPacket *pkt = s->internal->pkt;
av_packet_unref(pkt);
// 如果有AVBSFContext,最后写入bitstream filter数据包
for (i = 0; i < s->nb_streams; i++) {
if (s->streams[i]->internal->bsfc) {
ret1 = write_packets_from_bsfs(s, s->streams[i], pkt, 1/*interleaved*/);
if (ret1 < 0)
av_packet_unref(pkt);
if (ret >= 0)
ret = ret1;
}
}
// 填充空数据包
ret1 = interleaved_write_packet(s, NULL, 1);
if (ret >= 0)
ret = ret1;
if (s->oformat->write_trailer) {
// 写入avio的marker标志
if (!(s->oformat->flags & AVFMT_NOFILE) && s->pb)
avio_write_marker(s->pb, AV_NOPTS_VALUE, AVIO_DATA_MARKER_TRAILER);
// 调用AVOutputFormat写文件尾
if (ret >= 0) {
ret = s->oformat->write_trailer(s);
} else {
s->oformat->write_trailer(s);
}
}
// 释放muxer资源
deinit_muxer(s);
if (s->pb)
avio_flush(s->pb);
if (ret == 0)
ret = s->pb ? s->pb->error : 0;
// 释放priv_data和index_entries
for (i = 0; i < s->nb_streams; i++) {
av_freep(&s->streams[i]->priv_data);
av_freep(&s->streams[i]->index_entries);
}
if (s->oformat->priv_class)
av_opt_free(s->priv_data);
av_freep(&s->priv_data);
return ret;
}
由源码可知,写媒体文件尾主要有6个步骤:
- 如果有AVBSFContext,最后写入bitstream filter数据包;
- 填充空数据包;
- 如果有AVIOContext,写入avio的marker标志;
- 调用AVOutputFormat写文件尾;
- 释放muxer资源;
- 释放priv_data和index_entries;
第1步的write_packets_from_bsfs和第2步的interleaved_write_packet在av_write_frame()文章有介绍。我们主要来分析第3、4、5步的处理。
2、avio_write_marker
avio_write_marker位于aviobuf.c文件,函数实现如下:
void avio_write_marker(AVIOContext *s, int64_t time, enum AVIODataMarkerType type)
{
if (type == AVIO_DATA_MARKER_FLUSH_POINT) {
if (s->buf_ptr - s->buffer >= s->min_packet_size)
avio_flush(s);
return;
}
if (!s->write_data_type)
return;
if (type == AVIO_DATA_MARKER_BOUNDARY_POINT && s->ignore_boundary_point)
type = AVIO_DATA_MARKER_UNKNOWN;
if (type == AVIO_DATA_MARKER_UNKNOWN &&
(s->current_type != AVIO_DATA_MARKER_HEADER &&
s->current_type != AVIO_DATA_MARKER_TRAILER))
return;
// 判断header和trailer的marker是否相同
switch (type) {
case AVIO_DATA_MARKER_HEADER:
case AVIO_DATA_MARKER_TRAILER:
if (type == s->current_type)
return;
break;
}
// If we've reached here, we have a new, noteworthy marker.
// Flush the previous data and mark the start of the new data.
avio_flush(s);
s->current_type = type;
s->last_time = time;
}
3、s->oformat->write_trailer
以mp4的封装格式为例,位于libavformat/movenc.c文件,对应的AVOutputFormat如下:
AVOutputFormat ff_mp4_muxer = {
.name = "mp4",
.long_name = NULL_IF_CONFIG_SMALL("MP4 (MPEG-4 Part 14)"),
.mime_type = "video/mp4",
.extensions = "mp4",
.priv_data_size = sizeof(MOVMuxContext),
.audio_codec = AV_CODEC_ID_AAC,
.video_codec = CONFIG_LIBX264_ENCODER ?
AV_CODEC_ID_H264 : AV_CODEC_ID_MPEG4,
.init = mov_init,
.write_header = mov_write_header,
.write_packet = mov_write_packet,
.write_trailer = mov_write_trailer,
.deinit = mov_free,
.flags = AVFMT_GLOBALHEADER | AVFMT_ALLOW_FLUSH | AVFMT_TS_NEGATIVE,
.codec_tag = mp4_codec_tags_list,
.check_bitstream = mov_check_bitstream,
.priv_class = &mp4_muxer_class,
};
此时,write_trailer函数指针指向mov_write_trailer(),我们来看看该函数实现:
static int mov_write_trailer(AVFormatContext *s)
{
MOVMuxContext *mov = s->priv_data;
AVIOContext *pb = s->pb;
int res = 0;
int i;
int64_t moov_pos;
// 判断是否要重写extradata
if (mov->need_rewrite_extradata) {
for (i = 0; i < s->nb_streams; i++) {
MOVTrack *track = &mov->tracks[i];
AVCodecParameters *par = track->par;
track->vos_len = par->extradata_size;
av_freep(&track->vos_data);
track->vos_data = av_malloc(track->vos_len + AV_INPUT_BUFFER_PADDING_SIZE);
if (!track->vos_data)
return AVERROR(ENOMEM);
memcpy(track->vos_data, par->extradata, track->vos_len);
memset(track->vos_data + track->vos_len, 0, AV_INPUT_BUFFER_PADDING_SIZE);
}
mov->need_rewrite_extradata = 0;
}
// 如果存在字幕轨,需要写结束包
for (i = 0; i < mov->nb_streams; i++) {
MOVTrack *trk = &mov->tracks[i];
if (trk->par->codec_id == AV_CODEC_ID_MOV_TEXT &&
!trk->last_sample_is_subtitle_end) {
mov_write_subtitle_end_packet(s, i, trk->track_duration);
trk->last_sample_is_subtitle_end = 1;
}
}
// 判断是否需要写chapter track
if (!mov->chapter_track && !(mov->flags & FF_MOV_FLAG_FRAGMENT)) {
if (mov->mode & (MODE_MP4|MODE_MOV|MODE_IPOD) && s->nb_chapters) {
mov->chapter_track = mov->nb_streams++;
if ((res = mov_create_chapter_track(s, mov->chapter_track)) < 0)
return res;
}
}
if (!(mov->flags & FF_MOV_FLAG_FRAGMENT)) {
moov_pos = avio_tell(pb);
// 写入mdat tag和size
if (mov->mdat_size + 8 <= UINT32_MAX) {
avio_seek(pb, mov->mdat_pos, SEEK_SET);
avio_wb32(pb, mov->mdat_size + 8);
} else {
/* overwrite 'wide' placeholder atom */
avio_seek(pb, mov->mdat_pos - 8, SEEK_SET);
/* special value: real atom size will be 64 bit value after tag field */
avio_wb32(pb, 1);
ffio_wfourcc(pb, "mdat");
avio_wb64(pb, mov->mdat_size + 16);
}
avio_seek(pb, mov->reserved_moov_size > 0 ? mov->reserved_header_pos : moov_pos, SEEK_SET);
// 写入moov tag
if (mov->flags & FF_MOV_FLAG_FASTSTART) {
res = shift_data(s);
if (res < 0)
return res;
avio_seek(pb, mov->reserved_header_pos, SEEK_SET);
if ((res = mov_write_moov_tag(pb, mov, s)) < 0)
return res;
} else if (mov->reserved_moov_size > 0) {
int64_t size;
if ((res = mov_write_moov_tag(pb, mov, s)) < 0)
return res;
size = mov->reserved_moov_size - (avio_tell(pb) - mov->reserved_header_pos);
if (size < 8){
return AVERROR(EINVAL);
}
avio_wb32(pb, size);
ffio_wfourcc(pb, "free");
ffio_fill(pb, 0, size - 8);
avio_seek(pb, moov_pos, SEEK_SET);
} else {
if ((res = mov_write_moov_tag(pb, mov, s)) < 0)
return res;
}
res = 0;
} else {
mov_auto_flush_fragment(s, 1);
for (i = 0; i < mov->nb_streams; i++)
mov->tracks[i].data_offset = 0;
// 写入sidx tag
if (mov->flags & FF_MOV_FLAG_GLOBAL_SIDX) {
int64_t end;
res = shift_data(s);
if (res < 0)
return res;
end = avio_tell(pb);
avio_seek(pb, mov->reserved_header_pos, SEEK_SET);
mov_write_sidx_tags(pb, mov, -1, 0);
avio_seek(pb, end, SEEK_SET);
}
// 写入mfra tag
if (!(mov->flags & FF_MOV_FLAG_SKIP_TRAILER)) {
avio_write_marker(s->pb, AV_NOPTS_VALUE, AVIO_DATA_MARKER_TRAILER);
res = mov_write_mfra_tag(pb, mov);
if (res < 0)
return res;
}
}
return res;
}
由源码可知,mp4格式写文件尾主要有7个步骤:
- 判断是否要重写extradata;
- 如果存在字幕轨,需要写结束包;
- 判断是否需要写chapter track;
- 写入mdat tag和size;
- 写入moov tag;
- 写入sidx tag;
- 写入mfra tag;
4、deinit_muxer
deinit_muxer()函数负责是否muxer内存资源,具体如下:
static void deinit_muxer(AVFormatContext *s)
{
if (s->oformat && s->oformat->deinit && s->internal->initialized)
s->oformat->deinit(s);
s->internal->initialized =
s->internal->streams_initialized = 0;
}
同样地,以mp4封装格式为例。由前面的ff_mp4_muxer结构体可知,deinit函数指针指向mov_free()。因此,s->oformat->deinit()最终调用到mov_free()来释放资源。函数实现如下:
static void mov_free(AVFormatContext *s)
{
MOVMuxContext *mov = s->priv_data;
int i;
av_packet_free(&mov->pkt);
if (!mov->tracks)
return;
if (mov->chapter_track) {
avcodec_parameters_free(&mov->tracks[mov->chapter_track].par);
}
// 遍历所有nb_streams,释放相关资源
for (i = 0; i < mov->nb_streams; i++) {
if (mov->tracks[i].tag == MKTAG('r','t','p',' '))
ff_mov_close_hinting(&mov->tracks[i]);
else if (mov->tracks[i].tag == MKTAG('t','m','c','d') && mov->nb_meta_tmcd)
av_freep(&mov->tracks[i].par);
av_freep(&mov->tracks[i].cluster);
av_freep(&mov->tracks[i].frag_info);
av_packet_free(&mov->tracks[i].cover_image);
if (mov->tracks[i].eac3_priv) {
struct eac3_info *info = mov->tracks[i].eac3_priv;
av_packet_free(&info->pkt);
av_freep(&mov->tracks[i].eac3_priv);
}
if (mov->tracks[i].vos_len)
av_freep(&mov->tracks[i].vos_data);
ff_mov_cenc_free(&mov->tracks[i].cenc);
ffio_free_dyn_buf(&mov->tracks[i].mdat_buf);
}
av_freep(&mov->tracks);
ffio_free_dyn_buf(&mov->mdat_buf);
}
至此,av_write_trailer()写媒体文件尾函数分析完毕。结合前面的文章:avformat_write_header()写媒体文件头和av_write_frame()写媒体数据包,可以完整地实现muxer封装器。