六、音频转码
本节将PCM转为AAC格式。再来复习一下FFmpeg的工作流程:
本次demo也是基于保存网络流到本地的基础上的,可以看出,ffmpeg的使用流程是相当固定的。
做出如下修改:
- 输入为麦克风,获取PCM数据,输入路径从设备管理器可以查看:
#include <dshow> //字符转码 static char *dup_wchar_to_utf8(const wchar_t *w) { char *s = NULL; int l = WideCharToMultiByte(CP_UTF8, 0, w, -1, 0, 0, 0, 0); s = (char *)av_malloc(l); if (s) WideCharToMultiByte(CP_UTF8, 0, w, -1, s, l, 0, 0); return s; } //设置输入路径 //避免找不到中文符号 string fileAudioInput = dup_wchar_to_utf8(L"麦克风阵列=Realtek High Definition Audio"); int ret = OpenInput(fileAudioInput);
2. 初始化的方法中添加设备注册
//ffmpeg初始化 void Init() { av_register_all(); avcodec_register_all(); avfilter_register_all(); avformat_network_init(); avdevice_register_all();//设备注册 av_log_set_level(AV_LOG_ERROR); }
3. 输出文件修改
OpenOutput("C:/Users/bian/Desktop/aac.ts");
4. 初始化CodecFilter,Codec编码器等,代码太多,不过多描述
下面附上github源码地址
做了几个简单的demo程序了,感觉好多方法不知道参数的意义,没有入门理解ffmpeg,后面要了解一引起基本原理了。
七、Ffmpeg重要数据结构
本章简单探索一下ffmpeg的一些数据结构,这样在后面的学习过程中可以有自己的思考,更能深入得理解为什么要这样写,有新的需求该如何写。
重点学习以下几个结构体:
- AVFormatContext
- AVFrame
- AVPacket
八、搭建简单直播系统
直播架构图:
其实在讲转码的时候已经学习过了。下面学习一下使用wireshark抓取rtp包来分析。
首先安装WireShark和npcap。
打开WireShark,选择本地连接即可。
设置使用UDP还是TCP传输:
AVDictionary *options = nullptr; //参数设置使用UDP传输 av_dict_set(&options, "rstp_transport", "udp", 0); int ret = avformat_open_input(&inputContext, inputUrl.c_str(), nullptr, &options);
八、音频裁剪
使用音频裁剪的流程与前面的工程类似,初始化,打开输入流,打开输出流,不同的地方在于把指定要裁剪的数据写入到输出文件。
理解本项目的关键在于理解AVPacket和AVRational的数据结构,尤其是pts和dts的理解,下面简单讲一下这两个数据结构的关键内容。
AVPacket的dts和pts
FFmpeg里有两种时间戳:DTS(Decoding Time Stamp)和PTS(Presentation Time Stamp)。 顾名思义,前者是解码的时间,后者是显示的时间。要仔细理解这两个概念,需要先了解FFmpeg中的packet和frame的概念。
FFmpeg中用AVPacket结构体来描述解码前或编码后的压缩包,用AVFrame结构体来描述解码后或编码前的信号帧。 对于视频来说,AVFrame就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的PTS。DTS是AVPacket里的一个成员,表示这个压缩包应该什么时候被解码。 如果视频里各帧的编码是按输入顺序(也就是显示顺序)依次进行的,那么解码和显示时间应该是一致的。可事实上,在大多数编解码标准(如H.264或HEVC)中,编码顺序和输入顺序并不一致,于是才会需要PTS和DTS这两种不同的时间戳。
每个AVPacket的时间戳间隔是固定的,那么这个间隔如何计算呢?答案是denominator/帧率
,那时间戳间隔如何用秒或者微秒表示呢?
在FFMPEG中有三种时间单位:秒、微秒和dts/pts。从dts/pts转化为微秒公式:
dts* AV_TIME_BASE/ denominator
其中AV_TIME_BASE为1,000,000,denominator为90,000。
现在就更好的理解了,denominator其实就是把一秒等分的个数。例如帧率为30,也就是一秒30帧,denominator为90000,一帧其实是90000/30=3000个,即(1秒/90000)*3000。
denominator是AVRational结构体变量:
typedef struct AVRational{ int num; ///< Numerator int den; ///< Denominator } AVRational;
同时根据上面所述,dts是某一帧的解码时间,pts是显示时间,那么pts肯定要大于dts的,因为要先解码才能播放吧,这一点一定要弄清楚。
好下,下面上主程序:
int main() { //定义裁剪位置,1)按包个数来裁剪,对应时间与帧率有关;2)根据时间戳裁剪 int startPacketNum = 200; //开始裁剪位置 int discardPacketNum = 200; //裁剪的包个数 int count = 0; //记录startPacketNum最后一个包的pts和dts,本例中的视频源pts和dts是不同的 int64_t lastPacketPts = AV_NOPTS_VALUE; int64_t lastPacketDts = AV_NOPTS_VALUE; //时间戳间隔,不同的视频源可能不同,我的是3000 int timerIntervel = 3000; int64_t lastPts = AV_NOPTS_VALUE; Init(); //只支持视频流,不能带有音频流 int ret = OpenInput("gaoxiao-v.mp4");//视频流,注意输入如果带有音频会失败,导致音频与视频不同步 if (ret >= 0) { ret = OpenOutput("gaoxiao-v-caijian.mp4"); } if (ret < 0) goto Error; while (true) { count++; auto packet = ReadPacketFromSource(); if (packet) { if (count <= startPacketNum || count > startPacketNum + discardPacketNum) { if (count >= startPacketNum + discardPacketNum) { //需要调整dts和pts,调整策略和视频源的pts和dts的规律有关,不是固定的 packet->dts = -6000 + (count - 1 - discardPacketNum) * timerIntervel; if (count % 4 == 0) { packet->pts = packet->dts; } else if (count % 4 == 1) { packet->pts = packet->dts + 3000; } else if (count % 4 == 2) { packet->pts = packet->dts + 15000; } else if (count % 4 == 3) { packet->pts = packet->dts + 6000; } } ret = WritePacket(packet); } } else { break; } } cout << "cut file end\n" << endl; Error: CloseInput(); CloseOutput(); ... return 0; }
关于上面代码中不理解的地方可能就是调整dts和pts的策略了,注释中也说明了,这跟视频的dts和pts的策略有关,我为什么这样写呢?下面我打印了原始视频的dts和pts
pakcet.pts=0,pakcet.dts=-6000 //第一个packet,count=1 pakcet.pts=12000,pakcet.dts=-3000 //count=2 pakcet.pts=6000,pakcet.dts=0 //count=3 pakcet.pts=3000,pakcet.dts=3000 //count=4 pakcet.pts=9000,pakcet.dts=6000 //count=5 pakcet.pts=24000,pakcet.dts=9000 //count=6 pakcet.pts=18000,pakcet.dts=12000 //count=7 pakcet.pts=15000,pakcet.dts=15000 //count=8 pakcet.pts=21000,pakcet.dts=18000 pakcet.pts=36000,pakcet.dts=21000 pakcet.pts=30000,pakcet.dts=24000 pakcet.pts=27000,pakcet.dts=27000 pakcet.pts=33000,pakcet.dts=30000 pakcet.pts=48000,pakcet.dts=33000 pakcet.pts=42000,pakcet.dts=36000 pakcet.pts=39000,pakcet.dts=39000 pakcet.pts=45000,pakcet.dts=42000 pakcet.pts=60000,pakcet.dts=45000 pakcet.pts=54000,pakcet.dts=48000 pakcet.pts=51000,pakcet.dts=51000 pakcet.pts=57000,pakcet.dts=54000 pakcet.pts=72000,pakcet.dts=57000 pakcet.pts=66000,pakcet.dts=60000 pakcet.pts=63000,pakcet.dts=63000 ...
从上面的Log中可以看到dts是非常有规律的,起始为-6000,按等差3000递增。而pts看着似乎有点乱,但是有有一定的规律,就是在特定packet包ptd=dts,这个packet的个数为4的整倍数,而不是4的整倍数的也很容易找出规律,模4余1时,pts=dts+3000,余2时pts=dts+15000,余3时pts=dts+6000。这样就OK了。