DEMO--Live555取流与ffmpeg解码——从零开始

本片文章主要总结如何利用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
INCLUDES = -I../UsageEnvironment/include -I../groupsock/include -I../liveMedia/include -I../BasicUsageEnvironment/include -I../openRTSP/include

TEST_RTSP_CLIENT_OBJS = testRtspClient.$(OBJ)


USAGE_ENVIRONMENT_DIR = ../UsageEnvironment
USAGE_ENVIRONMENT_LIB = $(USAGE_ENVIRONMENT_DIR)/libUsageEnvironment.$(libUsageEnvironment_LIB_SUFFIX)
BASIC_USAGE_ENVIRONMENT_DIR = ../BasicUsageEnvironment
BASIC_USAGE_ENVIRONMENT_LIB = $(BASIC_USAGE_ENVIRONMENT_DIR)/libBasicUsageEnvironment.$(libBasicUsageEnvironment_LIB_SUFFIX)
LIVEMEDIA_DIR = ../liveMedia
LIVEMEDIA_LIB = $(LIVEMEDIA_DIR)/libliveMedia.$(libliveMedia_LIB_SUFFIX)
GROUPSOCK_DIR = ../groupsock
GROUPSOCK_LIB = $(GROUPSOCK_DIR)/libgroupsock.$(libgroupsock_LIB_SUFFIX)
OPENRTSP_DIR = ../openRTSP
OPENRTSP_LIB = $(OPENRTSP_DIR)/libRtspClient.$(libopenrtsp_LIB_SUFFIX)
LOCAL_LIBS = $(OPENRTSP_LIB) $(LIVEMEDIA_LIB) $(GROUPSOCK_LIB) \
$(BASIC_USAGE_ENVIRONMENT_LIB) $(USAGE_ENVIRONMENT_LIB)
LIBS = $(LOCAL_LIBS) $(LIBS_FOR_CONSOLE_APPLICATION)



testRTSPClient$(EXE): $(TEST_RTSP_CLIENT_OBJS) $(LOCAL_LIBS)
$(LINK)$@ $(CONSOLE_LINK_OPTS) $(TEST_RTSP_CLIENT_OBJS) $(LIBS) -lpthread

可以看到动态库就是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中修改。

  1. INCLUDES 中添加ffmpeg几个库的根地址。
    这里的根地址就是当前地址。
    在原来的INCLUDES 后面添加 -I./ 即可。
  2. live库的引用则需要在原来的基础上加上live/。需要注意的是,还是因为目录结构的不同,原版Makefile的INCLUDES 后面有两个.. ,在我的目录结构下,只需要一个。大家要根据自己的结构修改。
  3. 最终版本:
    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
12
LIBAVCODEC_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函数,首先初始化环境env

1
2
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);

调用OpenUrl函数打开url(rtsp://摄像头的ip, 而且url是支持用户名密码认证的:rtsp://admin:password@ip)

1
2
3
for (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
3
DummySink* 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
6
DummySink* 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
2
3
4
unsigned char *SaveData = new unsigned char[DUMMY_SINK_RECEIVE_BUFFER_SIZE + 4];
char head[4] = {0x00, 0x00, 0x00, 0x01};
memcpy(SaveData, head, 4);
memcpy(SaveData+4, fReceiveBuffer,frameSize);

DUMMY_SINK_RECEIVE_BUFFER_SIZE根据摄像头数据定义,可以定义大一些。

#define DUMMY_SINK_RECEIVE_BUFFER_SIZE 409600

解码为YUV420需要依赖sps和pps帧,在live中,这两帧分别以数组形式保存在SPropRecord中,通过下面的代码将他们取出来。

1
2
3
4
5
unsigned int SPropRecords = -1;
SPropRecord *p_record = parseSPropParameterSets(fSubsession.fmtp_spropparametersets(), SPropRecords);
//sps pps 以数组的形式保存SPropRecord中
SPropRecord &sps = p_record[0];
SPropRecord &pps = p_record[1];

这样,我们就获得了h264包和sps pps帧数据。

ffmpeg调用

我将解码部分代码写在decodeyuv()函数中。

1
2
int decoderyuv(unsigned char * inbuf, int read_size, SPropRecord &sps, SPropRecord &pps)
{ }

h264包、包大小、sps和pps帧作为函数输入。

首先,需要对解码器初始化。但是考虑到解码过程中是收到一帧解码一帧,但对解码器的初始化只需要做一次,否则会丢失sps和pps帧和其他环境变量的依赖。

所以,我将以下初始化代码放在主函数中,将变量作为全局变量声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
AVCodec *codec;
AVCodecContext *c = NULL;
AVFrame *frame;
AVCodecParserContext *avParserContext;

avcodec_register_all();
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if( NULL == codec )
{
fprintf(stderr,"Codec not found\n");
exit(1);
}

c = avcodec_alloc_context3(codec);
if(NULL == c)
{
fprintf(stderr,"Could not allocate video codec context\n");
exit(1);
}

avParserContext = av_parser_init(AV_CODEC_ID_H264);
if( NULL == avParserContext)
{
fprintf(stderr,"Could not init avParserContext\n");
exit(1);
}

if(avcodec_open2(c,codec, NULL) < 0)
{
fprintf(stderr,"Could not open codec\n");
exit(1);
}

frame = av_frame_alloc();
if(NULL == frame)
{
fprintf(stderr,"Could not allocate video frame\n");
exit(1);
}

以下是decodeyuv函数代码。

  • 首先将sps和pps帧添加到context的 extradata中。
  • 然后将extradata(sps 和pps帧数据)添加到h264数据前,重构buffer。
  • 使用解码器解码,解码后的数据将存储在frame->data中,而got_frame变量记录了解码数据大小。
  • 将解码后的YUV数据存储到文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
int decoderyuv(unsigned char * inbuf, int read_size, SPropRecord &sps, SPropRecord &pps)
{
int got_frame;
if(c->extradata == NULL)
{
int totalsize = 0;
unsigned char* tmp = NULL;
unsigned char nalu_header[4] = {0x00, 0x00, 0x00, 0x01};
totalsize = 8 + sps.sPropLength + pps.sPropLength;
//在每个sps 和pps 之前加上startcode
tmp = (unsigned char*)realloc(tmp, totalsize);
memcpy(tmp, nalu_header, 4);
memcpy(tmp + 4, sps.sPropBytes, sps.sPropLength);
memcpy(tmp + 4 + sps.sPropLength, nalu_header, 4);
memcpy(tmp + 4 + sps.sPropLength + 4, pps.sPropBytes, pps.sPropLength);
//printf("sps len:%d, pps len:%d\n", sps.sPropLength, pps.sPropLength);
//将 sps 和pps 的数据给ffmpeg的h264解码器上下文
c->extradata_size = totalsize;
c->extradata = tmp;
}
//重构buf
unsigned char * poutbuf = (unsigned char*)malloc(INBUF_SIZE*2 + AV_INPUT_BUFFER_PADDING_SIZE);
int buf_size = read_size + c->extradata_size;
memcpy(poutbuf, c->extradata, c->extradata_size);
memcpy(poutbuf + c->extradata_size, inbuf, read_size);

//解码器解码
AVPacket avpkt = {0};
av_init_packet(&avpkt);
avpkt.data = poutbuf;
avpkt.size = read_size + c->extradata_size;
time_t start ,end;
start = clock();
int decode_len = avcodec_decode_video2(c,frame, &got_frame, &avpkt);
end = clock();
printf("decoded frame used %d\n",end - start);
if(decode_len < 0)
fprintf(stderr,"Error while decoding frame \n");
if(got_frame)
{
int width = frame->width;
int height = frame->height;
fprintf(stderr,"decode success\n");
printf("width:%d, height:%d\n", frame->width, frame->height);
if( savefile)
{
yuv_fp = fopen(filename, "a+b");
解码后,存储的文件
if(NULL == yuv_fp)
{
fprintf(stderr,"Open file failed\n");
exit(1);
}
unsigned char* yuv_buf = (unsigned char*)malloc(width * height *1.5);//可以放在while循环外面,这样就不用每次都申请,释放了
//unsigned char* outbuff = (unsigned char*)malloc(width * height *1.5);
int i = 0;
//把解码出来的数据存成YUV数据,方便验证解码是否正确
for(i = 0; i < height; i++)
{
memcpy(yuv_buf + width * i, frame->data[0] + frame->linesize[0]*i, width);
if(i < height >> 1)
{
memcpy(yuv_buf + width * height + width *i / 2, frame->data[1] + frame->linesize[1]*i, width / 2);
memcpy(yuv_buf + width * height * 5 /4 + width * i / 2 ,frame->data[2] + frame->linesize[2]*i, width / 2);
}
}
fwrite(yuv_buf, sizeof(unsigned char),width * height * 1.5 ,yuv_fp);
free(yuv_buf);
fclose(yuv_fp);
yuv_fp = NULL;
}
}
free(poutbuf);
//记得释放变量的空间
return 0;
}

至此,就可以完成一个从live555取流,用ffmpeg解码为YUV,再将视频存储的demo。