FFmpeg源码分析:写媒体文件尾av_write_trailer()
2022/2/13 17:45:47
本文主要是介绍FFmpeg源码分析:写媒体文件尾av_write_trailer(),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
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封装器。
这篇关于FFmpeg源码分析:写媒体文件尾av_write_trailer()的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-22程序员出海做 AI 工具:如何用 similarweb 找到最佳流量渠道?
- 2024-12-20自建AI入门:生成模型介绍——GAN和VAE浅析
- 2024-12-20游戏引擎的进化史——从手工编码到超真实画面和人工智能
- 2024-12-20利用大型语言模型构建文本中的知识图谱:从文本到结构化数据的转换指南
- 2024-12-20揭秘百年人工智能:从深度学习到可解释AI
- 2024-12-20复杂RAG(检索增强生成)的入门介绍
- 2024-12-20基于大型语言模型的积木堆叠任务研究
- 2024-12-20从原型到生产:提升大型语言模型准确性的实战经验
- 2024-12-20啥是大模型1
- 2024-12-20英特尔的 Lunar Lake 计划:一场未竟的承诺