本片文章主要总结如何利用live555库取流,再使用ffmpeg库将H264解码为YUV420。将这个过程制作为一个demo。因为是第一次涉及视频流传输和解码,所以这种“从零开始”的开发过程可能比较适合新手。
live555是一个基于RTP/RTSP等协议开源的视频流库。而ffmpeg更像是一个视频处理工具。
编译环境配置
live555部分
live555的结构还是比较清晰的,如果需要开发一个客户端,主要关注“testProgs”和“testRtspClient”这两个文件夹即可,其中都给出了一些开发实例和源代码。
刚开始我以为client就是testRtspClient中的同名.cpp文件,后来发现,在testProgs文件夹中的 testRTSPClient.cpp 才更适合开发。
所以我复制了 testRTSPClient.cpp 的代码,放在自己的demo中,把他改名为client.cpp 进行二次开发。
Makefile可以直接参考testRtspClient文件夹中的Makefile,基本是testProgs的缩简版。
可以先从Makefile分析一下编译构成。
1 | INCLUDES = -I../UsageEnvironment/include -I../groupsock/include -I../liveMedia/include -I../BasicUsageEnvironment/include -I../openRTSP/include |
可以看到动态库就是UsageEnvironment,BasicUsageEnvironment,liveMedia,groupsock和openRTSP。所以写demo的时候需要把这几个文件夹中的include和.a文件包含进来。
文件结构:
.
├── BasicUsageEnvironment
│ ├── include
│ └── libBasicUsageEnvironment.a
├── groupsock
│ ├── include
│ └── libgroupsock.a
├── liveMedia
│ ├── include
│ └── libliveMedia.a
├── openRTSP
│ ├── include
│ └── libRtspClient.a
└── UsageEnvironment
├── include
└── libUsageEnvironment.a
这样就完成了live555的环境配置。
ffmpeg部分
ffmpeg就没这么简单了,ffmpeg的库很多,互相依赖复杂。因为这个demo中只需要将h264解码为YUV420,所以理论上只需要用到libavformat、libavcodec、libavutil这三个库。但实际上,我最后还加上了libswresample,一共四个库。
在网上下好源码包之后,应该先在文件夹下执行
./configure
make
make install
确保几个库中都编译好动态库,方便直接拷贝引用。
我的方式是直接复制上诉4个文件夹在demo文件夹中,文件夹除了包含头文件还应该包含动态库。
Makefile修改
整个demo的文件结构是这样的:
.
├── libavcodec
├── libavformat
├── libavutil
├── libswresample
├── live
├── client.cpp
live中是上述live555的库,client.cpp 是主函数。
因为ffmepg的头文件引用都是使用 #include <libavcodec/avcodec.h>
这样的方式引用,所以我把主函数和几个库文件夹放在同一个根目录下。
live555通过#include "liveMedia.hh"
引用。所以live555需要分别添加库的文件地址,而ffmpeg只需要添加根地址。
Makefile在上述testRTSPClient的Makefile中修改。
- INCLUDES 中添加ffmpeg几个库的根地址。
这里的根地址就是当前地址。
在原来的INCLUDES 后面添加-I./
即可。 - live库的引用则需要在原来的基础上加上live/。需要注意的是,还是因为目录结构的不同,原版Makefile的INCLUDES 后面有两个.. ,在我的目录结构下,只需要一个。大家要根据自己的结构修改。
- 最终版本:
INCLUDES = -I./live/UsageEnvironment/include -I./live/groupsock/include -I./live/liveMedia/include -I./live/BasicUsageEnvironment/include -I./live/openRTSP/include -I./
除了-I的修改,还有链接库需要修改。
我按照live555的写法,把ffmpeg的链接库添加在后面,具体是:1
2
3
4
5
6
7
8
9
10
11
12LIBAVCODEC_DIR = ./libavcodec
LIBAVCODEC_LIB = $(LIBAVCODEC_DIR)/libavcodec.$(LIB_SUFFIX)
LIBAVUTIL_DIR = ./libavutil
LIBAVUTIL_LIB = $(LIBAVUTIL_DIR)/libavutil.$(LIB_SUFFIX)
LIBAVFORMAT_DIR = ./libavformat
LIBAVFORMAT_LIB = $(LIBAVFORMAT_DIR)/libavformat.$(LIB_SUFFIX)
LIBSWRESAMPLE_DIR = ./libswresample
LIBSWRESAMPLE_LIB = $(LIBSWRESAMPLE_DIR)/libswresample.$(LIB_SUFFIX)
LOCAL_LIBS = $(OPENRTSP_LIB) $(LIVEMEDIA_LIB) $(GROUPSOCK_LIB) \
$(BASIC_USAGE_ENVIRONMENT_LIB) $(USAGE_ENVIRONMENT_LIB)\
$(LIBAVCODEC_LIB) $(LIBAVFORMAT_LIB) $(LIBAVUTIL_LIB) $(LIBSWRESAMPLE_LIB)
基本就是加上lib地址,然后在local_libs里面添加就可以。
这样,demo的环境就搭好了,可以修改代码,然后make编译。
代码修改
live555解读
live555的代码读起来真的是新手不友好,但是修改起来还是方便的。
找到main函数,首先初始化环境env1
2TaskScheduler* scheduler = BasicTaskScheduler::createNew();
UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);
调用OpenUrl函数打开url(rtsp://摄像头的ip, 而且url是支持用户名密码认证的:rtsp://admin:password@ip)1
2
3for (int i = 1; i <= argc-1; ++i) {
openURL(*env, argv[0], argv[i]);
}
然后就开始循环执行env下的task。1
env->taskScheduler().doEventLoop(&eventLoopWatchVariable);
task包括什么,基本就是所有调用了UsageEnvironment* env
的函数。
因为我们的目的是通过live收到一帧视频并解码,所以可以不关注连接建立的过程,主要关注收到视频帧部分的操作。
这里有一个类比较重要:1
2
3DummySink* createNew(UsageEnvironment& env,
MediaSubsession& subsession, // identifies the kind of data that's being received
char const* streamId = NULL); // identifies the stream itself (optional)
他相当于提供一个池子,接受视频帧。子函数包括:1
2
3
4
5
6DummySink* DummySink::createNew(UsageEnvironment& env, MediaSubsession& subsession, char const* streamId)
void DummySink::afterGettingFrame(void* clientData, unsigned frameSize, unsigned numTruncatedBytes,
struct timeval presentationTime, unsigned durationInMicroseconds)
Boolean DummySink::continuePlaying()
分别是构造——处理——继续的过程。
如果我们需要对视频帧处理,直接将处理函数加在afterGettingFrame
函数末尾,continuePlaying()
前面即可。
H264 与 sps pps帧
live555接受到的是H264的裸流,要获取h264格式,需要在原始数据的高为加包头。而原始数据保存在DummySink的成员函数fReceiveBuffer中,大小保存在freamesize里。
1 | unsigned char *SaveData = new unsigned char[DUMMY_SINK_RECEIVE_BUFFER_SIZE + 4]; |
DUMMY_SINK_RECEIVE_BUFFER_SIZE根据摄像头数据定义,可以定义大一些。
#define DUMMY_SINK_RECEIVE_BUFFER_SIZE 409600
解码为YUV420需要依赖sps和pps帧,在live中,这两帧分别以数组形式保存在SPropRecord中,通过下面的代码将他们取出来。
1 | unsigned int SPropRecords = -1; |
这样,我们就获得了h264包和sps pps帧数据。
ffmpeg调用
我将解码部分代码写在decodeyuv()函数中。
1 | int decoderyuv(unsigned char * inbuf, int read_size, SPropRecord &sps, SPropRecord &pps) |
h264包、包大小、sps和pps帧作为函数输入。
首先,需要对解码器初始化。但是考虑到解码过程中是收到一帧解码一帧,但对解码器的初始化只需要做一次,否则会丢失sps和pps帧和其他环境变量的依赖。
所以,我将以下初始化代码放在主函数中,将变量作为全局变量声明。
1 | AVCodec *codec; |
以下是decodeyuv函数代码。
- 首先将sps和pps帧添加到context的 extradata中。
- 然后将extradata(sps 和pps帧数据)添加到h264数据前,重构buffer。
- 使用解码器解码,解码后的数据将存储在frame->data中,而got_frame变量记录了解码数据大小。
- 将解码后的YUV数据存储到文件。
1 | int decoderyuv(unsigned char * inbuf, int read_size, SPropRecord &sps, SPropRecord &pps) |
至此,就可以完成一个从live555取流,用ffmpeg解码为YUV,再将视频存储的demo。