FFmpeg封装格式处理3-复用例程
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10506653.html
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 验证
测试文件下载:tnmil.flv

先看一下测试用资源文件的格式:
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播放不正常,这和预期是相符的。
FFmpeg封装格式处理3-复用例程的更多相关文章
- FFmpeg封装格式处理2-解复用例程
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10506642.html FFmpeg封装格式处理相关内容分为如下几篇文章: [1]. F ...
- FFmpeg封装格式处理4-转封装例程
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10506662.html FFmpeg封装格式处理相关内容分为如下几篇文章: [1]. F ...
- FFmpeg封装格式处理
本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10506636.html FFmpeg封装格式处理相关内容分为如下几篇文章: [1]. F ...
- 音视频处理之FFmpeg封装格式20180510
一.FFMPEG的封装格式转换器(无编解码) 1.封装格式转换 所谓的封装格式转换,就是在AVI,FLV,MKV,MP4这些格式之间转换(对应.avi,.flv,.mkv,.mp4文件). 需要注意的 ...
- 最简单的基于FFmpeg的封装格式处理:视音频复用器(muxer)
===================================================== 最简单的基于FFmpeg的封装格式处理系列文章列表: 最简单的基于FFmpeg的封装格式处理 ...
- 最简单的基于FFmpeg的封装格式处理:视音频分离器(demuxer)
===================================================== 最简单的基于FFmpeg的封装格式处理系列文章列表: 最简单的基于FFmpeg的封装格式处理 ...
- 最简单的基于FFmpeg的封装格式处理:视音频分离器简化版(demuxer-simple)
===================================================== 最简单的基于FFmpeg的封装格式处理系列文章列表: 最简单的基于FFmpeg的封装格式处理 ...
- 最简单的基于FFMPEG的封装格式转换器(无编解码)
本文介绍一个基于FFMPEG的封装格式转换器.所谓的封装格式转换,就是在AVI,FLV,MKV,MP4这些格式之间转换(相应.avi,.flv,.mkv,.mp4文件).须要注意的是,本程序并不进行视 ...
- 如何查看ffmpeg支持的编码器和封装格式
查看支持的编码器(也就是-vcodec后面可以接的参数):ffmpeg -codecs 查看支持的封装格式(也就是-f后面可以接的参数):ffmpeg -formats 查看支持的滤镜(也就是-vf后 ...
随机推荐
- var
在函数中,使用var声明的变量,为局部变量,只能在函数内部访问. 不使用var声明的变量,为全局变量,在函数外边也能访问. 没有var的情况 <script type="text/ja ...
- 走进JDK(十)------HashMap
有人说HashMap是jdk中最难的类,重要性不用多说了,敲过代码的应该都懂,那么一起啃下这个硬骨头吧!一.哈希表在了解HashMap之前,先看看啥是哈希表,首先回顾下数组以及链表数组:采用一段连续的 ...
- 第40章:MongoDB-集群--Replica Sets(副本集)---副本集的管理
①以单机模式启动成员 由于很多维护的工作需要写入操作,所以不合适在副本集中操作,可以以单机模式启动成员,也就是不要使用副本的选项,就跟以前启动单独的服务器一样.一般使用一个跟副本集配置中不一样的端口号 ...
- ubuntu上vsftpd服务配置
Ubuntu上提供两种常用的ftp服务应用:vsftpd 和 tftpd,区别如下: 1)vsftpd 支持客户端上下传文件,支持浏览器显示及下载,支持用户名密码认证,支持匿名访问,默认端口TCP:2 ...
- 整理python小爬虫
编码使我快乐!!! 我也不知道为什么,遇到自己喜欢的事情,就越想做下去,可以一个月不出门,但是不能一天没有电脑 掌握程度:对python有了一个更清晰的认识,自动化运维,也许可以用python实现呢, ...
- python之路(六)-函数相关
在没有学习函数之前我们的程序是面向过程的,不停的判断,不停的循环,同样的代码重复出现在我们的代码里.函数可以更好的提高我们的 代码质量,避免同样的代码重复出现,而只需要在用的时候调用函数即可执行.此为 ...
- 23.HashMap
HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在.在HashMap中,key-value总是会当做一个整体来处理,系统会根据 ...
- 22.LinkedList
LinkedList与ArrayList一样实现List接口,只是ArrayList是List接口的大小可变数组的实现,LinkedList是List接口链表的实现.基于链表实现的方式使得Linked ...
- faster-RCNN台标检测
最近学习了faster-RCNN算法,收获不少,记此文为证.faster-RCNN是一个目标检测算法,它能够识别多个目标,对目标分类并标注位置,非常好用.它的输入样本是标注好的图片,输出是一个hdf5 ...
- 项目Alpha冲刺(团队2/10)
项目Alpha冲刺(团队2/10) 团队名称: 云打印 作业要求: 项目Alpha冲刺(团队) 作业目标: 完成项目Alpha版本 团队队员 队员学号 队员姓名 个人博客地址 备注 221600412 ...