学习视频:
链接:https://pan.baidu.com/s/1rCKRxQcE3TxaGqXJhGRY6Q
提取码:bpji
视频顺序有点乱,我是按照下面的顺序学习的,可以参考。
一、音视频基础
-
mux封装:复用,按一定格式组织原音视频流,例如进行时间同步等
demux解封装:解复用,按一定格式解出音视频流 -
ES流,原始流,直接从编码器出来的数据流。
PES流,P(packet),ES形成的分组称为PES分组,是用来传递ES的一种数据布局。
TS流:ES形成的分组称为TS分组,是用来传递ES的一种数据布局。可以被任意截断
rtsp流:RTSP(Real Time Streaming Protocol),RFC2326,实时流传输协议,是TCP/IP协议
体系中的一个应用层协议。
rtmp流:Real Time Messaging Protocol(实时消息传输协议),是abobe公司的协议
hls流:苹果流,是Apple的动态码率自适应技术。主要用于PC和Apple终端的音视频服务。
包括一个m3u8索引文件,TS媒体分片文件。 -
服务端:为客户端提供服务,提供数据服务
客户端:为客户提供本地服务
流媒体:采用酒店式传输方式在Internet播放的媒体格式。 -
推模式:当通知消息来之时,把所有样信息都通过参数的形式"推给"观察者
拉模式:当通知消息来之时,通知的函数不带任何相关的信息,而是要观察者主动
去"拉"信息
实时流:Real Time stream实时传输的音视频流
二、FFmpeg基础
- 开源库,支持Windows,Android,IOS等
- 音视频处理
- 开发语言C
- 源码下载:https://ffmpeg.zeranoe.com/builds/win64/static/
可以下载源码,动态库,静态库等 - 基础流程:
input file---demuxer--->encoded data packets---decoder--->decoded frames---encoder--->encoded data packets---muxer--->output file - 为什么用ffmpeg?例如手机获取rtsp视频流保存到本地,要实现以下方案:
1)实现rtsp客户端,接收音视频包
2)解视频包(rtp->h264)
3)解音频包(rtp->PCM(alaw))
4)音频转码(PCM->AAC)
5)重新封装音视频包
而ffmpeg的调用流程:
1)avformat_open_input 打开文件
2)avforamt_find_stream_info 读取文件格式信息
3)av_read_frame
4)avforamt_alloc_output_context2 创建输出上下文
5)av_write_frame 写输出
三、环境搭建
- 下载源码,下载windows源码及动态库
- 新建VS项目,把源码中的include文件夹下面的头文件,lib下面的静态库.lib,以及下载的动态库中的dll库文件拷贝到工程目录下
分别新建include,lib,bin目录存放。 - 项目中引入静态库:
A、添加工程的头文件目录:工程---属性---配置属性---c/c++---常规---附加包含目录:加上头文件存放目录。
B、添加文件引用的lib静态库路径:工程---属性---配置属性---链接器---常规---附加库目录:加上lib文件存放目录。
C 然后添加工程引用的lib文件名:工程---属性---配置属性---链接器---输入---附加依赖项:加上lib文件名 - 写测试代码,例如引入extern "C" {#include "libavcodec/avcodec.h"},main方法调用av_register_all(),运行报错找不到dll动态库,
这时需要把动态库拷贝到工程目录的运行目录,也就是生成.exe的目录。 - 注意,ffmpeg是C语言开发的,而VS工程是cpp语言,所以引入头文件应该使用extern "C"
Demo源码的github地址
四、保存网络流到本地
ffmpeg基本工作流程:
image.png
ffmpeg方法流程:
1. avformat_alloc_context();//创建输入上下文
2. avformat_open_input(); //打开输入流
3. avformat_find_stream_info(); //查找音视频信息
4. avforamt_alloc_output_context2(); //创建输出上下文
5. avio_open(); //打开avio
6. avformat_new_stream(); //从输入流创建输出流
avcodec_copy_context(); //拷贝到输出流,根据nb_streams循环
7. avformat_write_header(); //写头信息
8. while(true){
av_init_packet(); //初始化packet,用来存放编码过的数据
av_read_frame();//读取帧数据
av_interleaved_write_frame();//写数据
}
9. avformat_close_input();//关闭输入输出流
avcodec_close();
一些异常处理:
-
ffmpeg avformat_open_input always returns “Protocol not found” rv=(-1330794744)
,在初始化的时候忘记调用av_register_all(); -
VS引发了异常: 读取访问权限冲突
,这是数组越界报的错误,点击调用堆栈可以跳到出错位置。
关于RTSP流:
RTSP流地址可以在网上查找,应该有很多的,当然也不仅仅限于rtsp,其它形式的流也是可以的,我使用的是:rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov
,是可以使用的,但是过一段时间就不确定了,所以还是要多找找。如果不确定某个网络流是否可用,可以ping一下IP或者网址,也可以使用potPlayer打开,能ping通或者可以播放就证明流地址没问题。
下面附上本章的Demo github地址
五、网络流转发
网络流转发的基本流程与上一章的网络流保存到本地类似,只不过最后输出是网络流。基本流程如下:
image.png
转发网络流到本地UDP端口
- 基于四章的工程,只需要修改OpenOutput(outputUrl)的地址即可,代码如下:
//写到udp端口
OpenOutput("udp://127.0.0.1:1234"); //打开输出流
- 还记得刚开始下载的ffmpeg的动态库吗,里面的bin目录下有ffplay.ext文件,这时使用cmd执行
ffplay.exe udp://127.0.0.1:1234
,来测试转发,
但是报了如下错误:
[udp @ 00000173c61ab540] bind failed: Error number -10048 occurred
udp://127.0.0.1:1234: I/O error
这是因为OpenOutput方法中的如下代码导致的:
ret = avio_open2(&outputContext->pb, outputUrl.c_str(), AVIO_FLAG_READ_WRITE, nullptr, nullptr);
我们其实只需要写权限即可,改成如下:
ret = avio_open2(&outputContext->pb, outputUrl.c_str(), AVIO_FLAG_WRITE, nullptr, nullptr);
再次重新运行就能播放转发的UDP流了。如下图所示:
image.png
红箭头指的窗口即为从UDP端口读取的流数据进行播放。
-
当然也可以使用其它播放器进行测试,我使用PotPlayer测试的,如下:
打开输入地址:
image.png
播放效果:
image.png
转发为RTMP流
需要修改两个地方:
- 修改OpenInput的地址
OpenOutput("rtmp://127.0.0.1:1935/live/stream0"); //打开输出流
- 修改OpenInut方法中avformat_alloc_output_context2中的视频格式为flv
int ret = avformat_alloc_output_context2(&outputContext, nullptr, "flv", outputUrl.c_str());
另外如果要转发rtmp流,需要打开rtmp server,关于rtmp server的详情后面再述,现提供一个现成的可执行文件下载地址,打开crtmpserver.exe后,再使用ffplay.exe或者potplayer等播放器测试即可。
附上本节github源码地址,upd-stream分支是转发为udp的,master是转发为rtmp的。
使用ffmpeg的命令行实现上述功能
其实ffmpeg的命令行即可完成我们所做的保存网络流,网络流转码等功能。
进入下载的动态库的bin目录下cmd执行:
- 保存网络流命令
./ffmpeg.exe -i rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov -vcodec copy -acodec copy -f mpegts C:/Users/bian/Desktop/test2.mp4
命令解释:
-vcodec copy 表示视频格式直接拷贝,不做转码
-acodec copy 表示音频格式直播拷贝,不做转码
-f mpegts 表示输出格式
最后加保存文件路径
2.转发为UDP流
./ffmpeg.exe -i rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov -vcodec copy -acodec copy -f mpegts udp://127.0.0.1:1234
- 转发为rtmp流
注意在转发前也要先运行crtmpserver.exe
./ffmpeg.exe -i rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov -vcodec copy -acodec copy -f flv rtmp://127.0.0.1:1935/live/stream0
六、音频转码
本节将PCM转为AAC格式。再来复习一下FFmpeg的工作流程:
image.png
本次demo也是基于保存网络流到本地的基础上的,可以看出,ffmpeg的使用流程是相当固定的。
做出如下修改:
-
输入为麦克风,获取PCM数据,输入路径从设备管理器可以查看:
image.png
#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
八、搭建简单直播系统
直播架构图:
image.png
其实在讲转码的时候已经学习过了。下面学习一下使用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了。
下面附上github源码Demo地址
网友评论