ffmpeg和Opencv结合进行视频解码播放

43 篇文章 1 订阅
35 篇文章 1 订阅




引子

OpenCV中有自己的用于处理图片和视频的类VideoCapture,可以很方便的读入文件和显示。
现在视频数据流是ffmpeg解码h264文件得到的,由于要依赖该数据源进行相应的后续处理,所以需要将ffmpeg中得到的数据缓存转换成可以被OpenCV处理的Mat类对象。

ffmpeg介绍

FFmpeg是一个开源免费跨平台的视频和音频流方案,属于自由软件,采用LGPL或GPL许可证(依据你选择的组件)。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多codec都是从头开发的。
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它包括了目前领先的音/视频编码库libavcodec。 FFmpeg是在Linux下开发出来的,但它可以在包括Windows在内的大多数操作系统中编译。

FFmpeg的组成结构

FFmpeg主要由一下几个部分组成:

  • libavcodec:一个包含了所有FFmpeg音视频编解码器的库。 为了保证最优性能和高可复用性,大多数编解码器从头开发的。
  • libavformat:一个包含了所有的普通音视格式的解析器和 产生器的库。
  • 三个实例程序,这三个实例较为复杂,基本可以作为API使用手册:
    ffmpeg:命令行的视频格式转换程序。
    ffplay:视频播放程序。(需要SDL支持)
    ffserver:多媒体服务器

了解完组成结构后,你会发现,如果你在寻找一种视频格式转换的方式,那FFmpeg绝对是你的第一选择,libavcodec 则又是重 中之重。如果遇上API不会使用的情况,可以参考ffmpeg.c、ffplay.c、 ffserver.c、apiexample.c(解码)和output_example.c(编码)。

ffmpeg使用说明

ffmpeg库的接口都是c函数,其头文件也没有extern “C”的声明,所以在cpp文件里调用ffmpeg函数要注意了。
一般来说,一个用C写成的库如果想被C/C++同时可以使用,那在头文件应该加上

     
     
1
2
3
4
5
6
     
     
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
} // endof extern "C"
#endif

如果在.cpp里调用av_register_all()在链接时将找到不符号,因为.cpp要求的符号名
和ffmpeg库提供的符号名不一致。
可以这么解决:

     
     
1
2
3
4
5
6
     
     
extern "C"
{
#include <libavutil/avutil.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}

使用ffmpeg SDK解码流数据过程

以H264视频流为例,讲解解码流数据的步骤。

准备变量

定义AVCodec,AVCodec *变量为解码器指针。
定义AVCodecContext,使用该变量可以将其定义为ffmpeg解码类的类成员。
定义AVFrame,AVFrame描述一个多媒体帧。解码后的数据将被放在其中。
定义AVFormatContext变量,AVFormatContext用于保存视频流的有效信息。

     
     
1
2
3
4
     
     
AVCodec *pCodec;
AVCodecContext * pCodecCtx;
AVFrame * pAvFrame;
AVFormatContext *pFormatCtx;

初始化解码器

第一件事情就是初始化libavformat/libavcodec:
ffmpeg注册复用器,编码器等的函数av_register_all()。

     
     
1
     
     
av_register_all();

这一步注册库中含有的所有可用的文件格式和编码器,这样当打开一个文件时,它们才能够自动选择相应的文件格式和编码器。要注意你只需调用一次 av_register_all(),所以,尽可能的在你的初始代码中使用它。这里注册了所有的文件格式和编解码器的库,所以它们将被自动的使用在被打开的合适格式的文件上。注意你只需要调用 av_register_all()一次,因此我们在主函数main()中来调用它。如果你喜欢,也可以只注册特定的格式和编解码器,但是通常你没有必要这样做。

打开视频文件,取出包含在文件中的流信息

     
     
1
2
3
     
     
// 打开视频文件
if(av_open_input_file( &pFormatCtx, filename, NULL, 0, NULL) != 0)
handle_error(); // 不能打开此文件

这个函数读取文件的头部并且把信息保存到我们给的AVFormatContext结构体中。
最后三个参数描述了文件格式,缓冲区大小(size)和格式参数;我们通过简单地指明NULL或0告诉 libavformat 去自动探测文件格式并且使用默认的缓冲区大小。

     
     
1
2
3
     
     
// 取出流信息
if( av_find_stream_info(pFormatCtx)<0)
handle_error(); // 不能够找到流信息

查找文件的流信息,avformat_open_input函数只是检测了文件的头部,接着要检查在文件中的流的信息。
这一步会用有效的信息把 AVFormatContext 的流域(streams field)填满。作为一个可调试的诊断,我们会将这些信息全盘输出到标准错误输出中,不过你在一个应用程序的产品中并不用这么做。

我们仅仅处理视频流,而不是音频流。为了让这件事情更容易理解,我们只简单使用我们发现的第一种视频流。

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
     
     
//遍历文件的各个流,找到第一个视频流,并记录该流的编码信息
videoindex = - 1;
for(i= 0; i<pFormatCtx->nb_streams; i++)
{
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
videoindex=i;
break;
}
}
if(videoindex==- 1)
{
printf( "Didn't find a video stream.\n");
return;
}
pCodecCtx=pFormatCtx->streams[videoindex]->codec;

我们已经得到了一个指向视频流的称之为上下文的指针。接下来,我们需要找到真正的编码器打开它。

寻找视频流的解码器

在库里面查找支持该格式的解码器

     
     
1
2
3
     
     
pCodec = avcodec_find_decoder(pCodecCtx ->codec_id);
if(pCodec == NULL)
handle_error(); // 找不到解码器

打开解码器

     
     
1
2
     
     
if( avcodec_open(pCodecCtx, pCodec)<0)
handle_error();

给视频帧分配空间,以便存储解码后的图片数据

     
     
1
     
     
pAvFrame = avcodec_alloc_frame();

解码视频帧就像我前面提到过的,视频文件包含数个音频和视频流,并且他们各个独自被分开存储在固定大小的包里。我们要做的就是使用libavformat依次读取这些包,过滤掉所有那些视频流中我们不感兴趣的部分,并把它们交给libavcodec进行解码处理。

进行解码

通过该api读入一帧

     
     
1
     
     
result = av_read_frame(pFormatCtx, packet);

通过下面的api进行解码一帧数据,将有效的图像数据存储到pAvFrame成员变量中

     
     
1
     
     
ret = avcodec_decode_video2(pCodecCtx, pAvFrame, &got_picture, packet);

下面是ffmpeg解码的API:

将YUV420p颜色编码转换成BGR颜色编码

首先得到图片转换上下文img_convert_ctx,这里注意的是,opencv的RGB编码顺序为BGR,所以选用AV_PIX_FMT_BGR24的编码方式。

     
     
1
2
3
4
5
6
     
     
//根据编码信息设置渲染格式
if(img_convert_ctx == NULL){
img_convert_ctx = sws_getContext(pCodecCtx ->width, pCodecCtx ->height,
pCodecCtx ->pix_fmt, pCodecCtx ->width, pCodecCtx ->height,
AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
}

再得到为BGR格式帧分配内存

     
     
1
2
3
4
5
6
7
8
     
     
AVFrame *pFrameRGB = NULL;
uint8_t *out_bufferRGB = NULL;
pFrameRGB = avcodec_alloc_frame();
//给pFrameRGB帧加上分配的内存;
int size = avpicture_get_size(AV_PIX_FMT_BGR24, pCodecCtx ->width, pCodecCtx ->height);
out_bufferRGB = new uint8_t[size];
avpicture_fill((AVPicture *)pFrameRGB, out_bufferRGB, AV_PIX_FMT_BGR24, pCodecCtx ->width, pCodecCtx ->height);

最后进行转换

     
     
1
     
     
sws_scale(img_convert_ctx, pFrame -> data, pFrame ->linesize, 0, pCodecCtx ->height, pFrameRGB -> data, pFrameRGB ->linesize);

OpenCV Mat数据复制

cv::Mat对象中有data指针,指向内存中存放矩阵数据的一块内存 (uchar* data)。
所以,要将ffmpeg解码之后得到的RGB色彩的帧数据复制给该指针,这样就实现了ffmpeg解码数据到opencv中Mat格式的转换,进而就可以对Mat对象进行相应的处理。

代码示例

ffmpegDecode.h文件

     
     
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
     
     
#ifndef __FFMPEG_DECODE_H__
#define __FFMPEG_DECODE_H__
#include <opencv2/core/core.hpp>
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
//图像转换结构需要引入的头文件
#include "libswscale/swscale.h"
};
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avformat.lib ")
#pragma comment(lib, "avutil.lib ")
#pragma comment(lib, "avdevice.lib ")
#pragma comment(lib, "avfilter.lib ")
#pragma comment(lib, "postproc.lib ")
#pragma comment(lib, "swresample.lib")
#pragma comment(lib, "swscale.lib ")
class ffmpegDecode
{
public:
ffmpegDecode( char * file = NULL);
~ffmpegDecode();
cv::Mat getDecodedFrame();
cv::Mat getLastFrame();
int readOneFrame();
int getFrameInterval();
private:
AVFrame *pAvFrame;
AVFormatContext *pFormatCtx;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
int i;
int videoindex;
char *filepath;
int ret, got_picture;
SwsContext *img_convert_ctx;
int y_size;
AVPacket *packet;
cv::Mat *pCvMat;
void init();
void openDecode();
void prepare();
void get(AVCodecContext *pCodecCtx, SwsContext *img_convert_ctx,AVFrame *pFrame);
};
#endif

ffmpegDecode.cpp文件

     
     
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
     
     
#include "ffmpegDecode.h"
ffmpegDecode :: ~ffmpegDecode()
{
pCvMat->release();
//释放本次读取的帧内存
av_free_packet(packet);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
}
ffmpegDecode :: ffmpegDecode(char * file)
{
pAvFrame = NULL/**pFrameRGB = NULL*/;
pFormatCtx = NULL;
pCodecCtx = NULL;
pCodec = NULL;
pCvMat = new cv::Mat();
i=0;
videoindex=0;
ret = 0;
got_picture = 0;
img_convert_ctx = NULL;
y_size = 0;
packet = NULL;
if (NULL == file)
{
filepath = "opencv.h264";
}
else
{
filepath = file;
}
init();
openDecode();
prepare();
return;
}
void ffmpegDecode :: init()
{
//ffmpeg注册复用器,编码器等的函数av_register_all()。
//该函数在所有基于ffmpeg的应用程序中几乎都是第一个被调用的。只有调用了该函数,才能使用复用器,编码器等。
//这里注册了所有的文件格式和编解码器的库,所以它们将被自动的使用在被打开的合适格式的文件上。注意你只需要调用 av_register_all()一次,因此我们在主函数main()中来调用它。如果你喜欢,也可以只注册特定的格式和编解码器,但是通常你没有必要这样做。
av_register_all();
//pFormatCtx = avformat_alloc_context();
//打开视频文件,通过参数filepath来获得文件名。这个函数读取文件的头部并且把信息保存到我们给的AVFormatContext结构体中。
//最后2个参数用来指定特殊的文件格式,缓冲大小和格式参数,但如果把它们设置为空NULL或者0,libavformat将自动检测这些参数。
if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0)
{
printf( "无法打开文件\n");
return;
}
//查找文件的流信息,avformat_open_input函数只是检测了文件的头部,接着要检查在文件中的流的信息
if(av_find_stream_info(pFormatCtx)<0)
{
printf( "Couldn't find stream information.\n");
return;
}
return;
}
void ffmpegDecode :: openDecode()
{
//遍历文件的各个流,找到第一个视频流,并记录该流的编码信息
videoindex = -1;
for(i=0; i<pFormatCtx->nb_streams; i++)
{
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
videoindex=i;
break;
}
}
if(videoindex==-1)
{
printf( "Didn't find a video stream.\n");
return;
}
pCodecCtx=pFormatCtx->streams[videoindex]->codec;
//在库里面查找支持该格式的解码器
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL)
{
printf( "Codec not found.\n");
return;
}
//打开解码器
if(avcodec_open2(pCodecCtx, pCodec,NULL) < 0)
{
printf( "Could not open codec.\n");
return;
}
}
void ffmpegDecode :: prepare()
{
//分配一个帧指针,指向解码后的原始帧
pAvFrame=avcodec_alloc_frame();
y_size = pCodecCtx->width * pCodecCtx->height;
//分配帧内存
packet=(AVPacket *)av_malloc(sizeof(AVPacket));
av_new_packet(packet, y_size);
//输出一下信息-----------------------------
printf( "文件信息-----------------------------------------\n");
av_dump_format(pFormatCtx,0,filepath,0);
//av_dump_format只是个调试函数,输出文件的音、视频流的基本信息了,帧率、分辨率、音频采样等等
printf( "-------------------------------------------------\n");
}
int ffmpegDecode :: readOneFrame()
{
int result = 0;
result = av_read_frame(pFormatCtx, packet);
return result;
}
cv::Mat ffmpegDecode :: getDecodedFrame()
{
if(packet->stream_index==videoindex)
{
//解码一个帧
ret = avcodec_decode_video2(pCodecCtx, pAvFrame, &got_picture, packet);
if(ret < 0)
{
printf( "解码错误\n");
return cv::Mat();
}
if(got_picture)
{
//根据编码信息设置渲染格式
if(img_convert_ctx == NULL){
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height,
AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
}
//----------------------opencv
if (pCvMat->empty())
{
pCvMat->create(cv::Size(pCodecCtx->width, pCodecCtx->height),CV_8UC3);
}
if(img_convert_ctx != NULL)
{
get(pCodecCtx, img_convert_ctx, pAvFrame);
}
}
}
av_free_packet(packet);
return *pCvMat;
}
cv::Mat ffmpegDecode :: getLastFrame()
{
ret = avcodec_decode_video2(pCodecCtx, pAvFrame, &got_picture, packet);
if(got_picture)
{
//根据编码信息设置渲染格式
img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);
if(img_convert_ctx != NULL)
{
get(pCodecCtx, img_convert_ctx,pAvFrame);
}
}
return *pCvMat;
}
void ffmpegDecode :: get(AVCodecContext * pCodecCtx, SwsContext * img_convert_ctx, AVFrame * pFrame)
{
if (pCvMat->empty())
{
pCvMat->create(cv::Size(pCodecCtx->width, pCodecCtx->height),CV_8UC3);
}
AVFrame *pFrameRGB = NULL;
uint8_t *out_bufferRGB = NULL;
pFrameRGB = avcodec_alloc_frame();
//给pFrameRGB帧加上分配的内存;
int size = avpicture_get_size(AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);
out_bufferRGB = new uint8_t[size];
avpicture_fill((AVPicture *)pFrameRGB, out_bufferRGB, AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);
//YUV to RGB
sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize);
memcpy(pCvMat->data,out_bufferRGB,size);
delete[] out_bufferRGB;
av_free(pFrameRGB);
}

转载请注明作者Jason Ding及其出处
Github主页(http://jasonding1354.github.io/)
CSDN博客(http://blog.csdn.net/jasonding1354)
简书主页(http://www.jianshu.com/users/2bd9b48f6ea8/latest_articles)





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值