FFMPEG 封装格式处理相关内容分为如下几篇文章:
[1]. FFMPEG 封装格式处理 - 简介
[2]. FFMPEG 封装格式处理 - 解复用例程
[3]. FFMPEG 封装格式处理 - 复用例程
[4]. FFMPEG 封装格式处理 - 转封装例程
4. 复用例程
复用 (mux), 是 multiplex 的缩写, 表示将多路流(视频, 音频, 字幕等) 混入一路输出中(普通文件, 流等).
本例实现, 提取第一路输入文件中的视频流和第二路输入文件中的音频流, 将这两路流混合, 输出到一路输出文件中.
本例不支持裸流输入, 是因为裸流不包含时间戳信息(时间戳信息一般由容器提供), 为裸流生成时间戳信息会增加示例代码的复杂性. 因此输入文件有特定要求, 第一路输入文件应包含至少一路视频流, 第二路输入文件应包含至少一路音频流, 且输入文件必须包含封装格式, 以便能取得时间戳信息, 从而可根据时间戳信息对音视频帧排序; 另外, 为了观测输出文件的音画效果, 第一路输入中的视频和第二路输入中的音频最好有一定的关系关系, 本例中即是先从一个电影片段中分离出视频和音频, 用作测试输入.
4.1 源码
源码实现步骤如注释所述.
- #include <stdbool.h>
- #include <libavformat/avformat.h>
- /*
- FFMPEG -i tnmil.flv -c:v copy -an tnmil_v.flv
- FFMPEG -i tnmil.flv -c:a copy -vn tnmil_a.flv
- ./muxing tnmil_v.flv tnmil_a.flv tnmil_av.flv
- */
- int main (int argc, char **argv)
- {
- if (argc != 4)
- {
- fprintf(stderr, "usage: %s test.h264 test.aac test.ts\n", argv[0]);
- exit(1);
- }
- const char *input_v_fname = argv[1];
- const char *input_a_fname = argv[2];
- const char *output_fname = argv[3];
- int ret = 0;
- // 1 打开两路输入
- // 1.1 打开第一路输入, 并找到一路视频流
- AVFormatContext *v_ifmt_ctx = NULL;
- ret = avformat_open_input(&v_ifmt_ctx, input_v_fname, NULL, NULL);
- ret = avformat_find_stream_info(v_ifmt_ctx, NULL);
- int video_idx = av_find_best_stream(v_ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
- AVStream *in_v_stream = v_ifmt_ctx->streams[video_idx];
- // 1.2 打开第二路输入, 并找到一路音频流
- AVFormatContext *a_ifmt_ctx = NULL;
- ret = avformat_open_input(&a_ifmt_ctx, input_a_fname, NULL, NULL);
- ret = avformat_find_stream_info(a_ifmt_ctx, NULL);
- int audio_idx = av_find_best_stream(a_ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
- AVStream *in_a_stream = a_ifmt_ctx->streams[audio_idx];
- av_dump_format(v_ifmt_ctx, 0, input_v_fname, 0);
- av_dump_format(a_ifmt_ctx, 1, input_a_fname, 0);
- if (video_idx <0 || audio_idx < 0)
- {
- printf("find stream failed: %d %d\n", video_idx, audio_idx);
- return -1;
- }
- // 2 打开输出, 并向输出中添加两路流, 一路用于存储视频, 一路用于存储音频
- AVFormatContext *ofmt_ctx = NULL;
- ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, output_fname);
- AVStream *out_v_stream = avformat_new_stream(ofmt_ctx, NULL);
- ret = avcodec_parameters_copy(out_v_stream->codecpar, in_v_stream->codecpar);
- AVStream *out_a_stream = avformat_new_stream(ofmt_ctx, NULL);
- ret = avcodec_parameters_copy(out_a_stream->codecpar, in_a_stream->codecpar);
- if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) // TODO: 研究 AVFMT_NOFILE 标志
- {
- ret = avio_open(&ofmt_ctx->pb, output_fname, AVIO_FLAG_WRITE);
- }
- av_dump_format(ofmt_ctx, 0, output_fname, 1);
- // 3 写输入文件头
- ret = avformat_write_header(ofmt_ctx, NULL);
- AVPacket vpkt;
- av_init_packet(&vpkt);
- vpkt.data = NULL;
- vpkt.size = 0;
- AVPacket apkt;
- av_init_packet(&apkt);
- apkt.data = NULL;
- apkt.size = 0;
- AVPacket *p_pkt = NULL;
- int64_t vdts = 0;
- int64_t adts = 0;
- bool video_finished = false;
- bool audio_finished = false;
- bool v_or_a = false;
- // 4 从两路输入依次取得 packet, 交织存入输出中
- printf("V/A\tPTS\tDTS\tSIZE\n");
- while (1)
- {
- if (vpkt.data == NULL && (!video_finished))
- {
- while (1) // 取出一个 video packet, 退出循环
- {
- ret = av_read_frame(v_ifmt_ctx, &vpkt);
- if ((ret == AVERROR_EOF) || avio_feof(v_ifmt_ctx->pb))
- {
- printf("video finished\n");
- video_finished = true;
- vdts = AV_NOPTS_VALUE;
- break;
- }
- else if (ret <0)
- {
- printf("video read error\n");
- goto end;
- }
- if (vpkt.stream_index == video_idx)
- {
- // 更新 packet 中的 pts 和 dts. 关于 AVStream.time_base 的说明:
- // 输入: 输入流中含有 time_base, 在 avformat_find_stream_info()中可取到每个流中的 time_base
- // 输出: avformat_write_header()会根据输出的封装格式确定每个流的 time_base 并写入文件中
- // AVPacket.pts 和 AVPacket.dts 的单位是 AVStream.time_base, 不同的封装格式其 AVStream.time_base 不同
- // 所以输出文件中, 每个 packet 需要根据输出封装格式重新计算 pts 和 dts
- av_packet_rescale_ts(&vpkt, in_v_stream->time_base, out_v_stream->time_base);
- vpkt.pos = -1; // 让 muxer 根据重新将 packet 在输出容器中排序
- vpkt.stream_index = 0;
- vdts = vpkt.dts;
- break;
- }
- av_packet_unref(&vpkt);
- }
- }
- if (apkt.data == NULL && (!audio_finished))
- {
- while (1) // 取出一个 audio packet, 退出循环
- {
- ret = av_read_frame(a_ifmt_ctx, &apkt);
- if ((ret == AVERROR_EOF) || avio_feof(a_ifmt_ctx->pb))
- {
- printf("audio finished\n");
- audio_finished = true;
- adts = AV_NOPTS_VALUE;
- break;
- }
- else if (ret <0)
- {
- printf("audio read error\n");
- goto end;
- }
- if (apkt.stream_index == audio_idx)
- {
- ret = av_compare_ts(vdts, out_v_stream->time_base, adts, out_a_stream->time_base);
- apkt.pos = -1;
- apkt.stream_index = 1;
- adts = apkt.dts;
- break;
- }
- av_packet_unref(&apkt);
- }
- }
- if (video_finished && audio_finished)
- {
- printf("all read finished. flushing queue.\n");
- //av_interleaved_write_frame(ofmt_ctx, NULL); // 冲洗交织队列
- break;
- }
- else // 音频或视频未读完
- {
- if (video_finished) // 视频读完, 音频未读完
- {
- v_or_a = false;
- }
- else if (audio_finished) // 音频读完, 视频未读完
- {
- v_or_a = true;
- }
- else // 音频视频都未读完
- {
- // video pakect is before audio packet?
- ret = av_compare_ts(vdts, in_v_stream->time_base, adts, in_a_stream->time_base);
- v_or_a = (ret <= 0);
- }
- p_pkt = v_or_a ? &vpkt : &apkt;
- printf("%s\t%3"PRId64"\t%3"PRId64"\t%-5d\n", v_or_a ? "vp" : "ap",
- p_pkt->pts, p_pkt->dts, p_pkt->size);
- //ret = av_interleaved_write_frame(ofmt_ctx, p_pkt);
- ret = av_write_frame(ofmt_ctx, p_pkt);
- if (p_pkt->data != NULL)
- {
- av_packet_unref(p_pkt);
- }
- }
- }
- // 5 写输出文件尾
- av_write_trailer(ofmt_ctx);
- printf("Muxing succeeded.\n");
- end:
- avformat_close_input(&v_ifmt_ctx);
- avformat_close_input(&a_ifmt_ctx);
- avformat_free_context(ofmt_ctx);
- return 0;
- }
注意两点:
4.1.1 音视频帧交织问题
音频流视频流混合进输出媒体时, 需要确保音频帧和视频帧按照 dts 递增的顺序交错排列, 这就是交织 (interleaved) 问题. 如果我们使用 av_interleaved_write_frame(), 这个函数会缓存一定数量的帧, 将将缓存的帧按照 dts 递增的顺序写入输出媒体, 用户 (调用者) 不必关注交织问题 (当然, 因为缓存帧数量有限, 用户不可能完全不关注交织问题, 小范围的 dts 顺序错误问题这个函数可以修正). 如果我们使用 av_write_frame(), 这个函数会直接将帧写入输出媒体, 用户(必须) 自行处理交织问题, 确保写帧的顺序严格按照 dts 递增的顺序.
代码中, 通过 av_compare_ts()比较视频帧 dts 和音频帧 dts 哪值小, 将值小的帧调用 av_write_frame()先输出.
运行测试命令(详细测试方法在 4.3 节描述):
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.flv
抓取一段打印看一下:
- V/A PTS DTS SIZE
- vp 80 0 12840
- ap 0 0 368
- ap 23 23 364
- vp 240 40 4346
- ap 46 46 365
- ap 70 70 365
- vp 160 80 1257
- ap 93 93 368
- ap 116 116 367
- vp 120 120 626
- ap 139 139 367
- vp 200 160 738
- ap 163 163 367
- ap 186 186 367
- vp 400 200 4938
可以看到, 第三列 DTS, 数值逐行递增.
4.1.2 时间域转换问题
在代码中, 读取音频帧或视频帧后, 调用了 av_packet_rescale_ts()将帧中的时间相关值 (pts,dts,duration) 进行了时基转换, 从输入流的时基转换为输出流的时间基 (time_base).pts/dts 的单位是 time_base,pts/dts 的值乘以 time_base 表示时刻值. 不同的封装格式, 其时间基(time_base) 不同, 所以需要进行转换. 当然, 如果输出封装格式和输入封装格式相同, 那不调用 av_packet_rescale_ts()也可以.
封装格式中的时间基就是流中的时间基 AVStream.time_base, 关于 AVStream.time_base 的说明:
输入: 输入流中含有 time_base, 在 avformat_find_stream_info()中可取到每个流中的 time_base
输出: avformat_write_header()会根据输出的封装格式确定每个流的 time_base 并写入文件中
我们对比看一下, ts 封装格式和 flv 封装格式的不同, 运行测试命令(详细测试方法在 4.3 节描述):
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.ts
看一下前 15 帧的打印信息:
- V/A PTS DTS SIZE
- vp 7200 0 12840
- ap 0 0 368
- ap 2070 2070 364
- vp 21600 3600 4346
- ap 4140 4140 365
- ap 6300 6300 365
- vp 14400 7200 1257
- ap 8370 8370 368
- ap 10440 10440 367
- vp 10800 10800 626
- ap 12510 12510 367
- vp 18000 14400 738
- ap 14670 14670 367
- ap 16740 16740 367
- vp 36000 18000 4938
和上一节 flv 封装格式打印信息对比一下, 不同封装格式中同样的一帧数据, 其解码时刻和播放时刻肯定是一样的, 但其 PTS/DTS 值是不同的, 说明它们的时间单位不同.
4.2 编译
源文件为 muxing.c, 在 SHELL 中执行如下编译命令:
gcc -o muxing muxing.c -lavformat -lavcodec -lavutil -g
生成可执行文件 muxing
4.3 验证
测试文件下载:
先看一下测试用资源文件的格式:
- think@opensuse> ffprobe tnmil.flv
- ffprobe version 4.1 Copyright (c) 2007-2018 the FFMPEG developers
- Input #0, flv, from 'tnmil.flv':
- Metadata:
- encoder : Lavf58.20.100
- Duration: 00:00:54.52, start: 0.000000, bitrate: 611 kb/s
- Stream #0:0: Video: h264 (High), yuv420p(progressive), 784x480, 25 fps, 25 tbr, 1k tbn, 50 tbc
- Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp
可以看到视频文件'tnmil.flv'封装格式为 flv, 包含一路 h264 编码的视频流和一路 aac 编码的音频流.
运行如下两条命令, 处理一下, 生成只含一路视频流的文件, 和只含一路音频流的文件, 文件封装格式均为 FLV. 这两个文件用于下一步的测试.
- FFMPEG -i tnmil.flv -c:v copy -an tnmil_v.flv
- FFMPEG -i tnmil.flv -c:a copy -vn tnmil_a.flv
不输出裸流, 而输出带封装格式的流, 就是为了利用封装格式中携带的时间戳信息, 简化本例程.
运行如下命令进行测试:
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.flv
使用 ffprobe 检测输出文件正常. 使用 ffplay 播放输出文件正常, 播放效果和原始的测试文件一致.
输出另外一路封装格式的文件再测试一下, 运行如下命令:
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.ts
使用 ffprobe 检测输出文件正常. 使用 ffplay 播放输出文件正常, 播放效果和原始的测试文件一致.
如果我们改一下代码, 将 av_packet_rescale_ts()注释掉, 再测上述两条指令, 发现 tnmil_av.flv 播放正常, tnmil_av.ts 播放不正常, 这和预期是相符的.
来源: https://www.cnblogs.com/leisure_chn/p/10506653.html