0%

FFMPEG系列之一:打开摄像头

最近陆陆续续接触到了不少视频相关的知识,终于有时间可以把这些知识整理一下了。 技术选型先看的是OpenCV(之前博文也有提到OpenCV的编译),在OpenCV下打开摄像头、录制成一个视频文件的代码是非常简单的,下面的代码演示了打开系统的第1个摄像头,并将画面录制为H264视频的过程(代码是从之前的测试代码中提取出来的,不保证没有错误,可能含无用代码):

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
#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

using namespace cv;

int main(int argc, char *argv[])
{
VideoCapture cap(0); // open the default camera
if(!cap.isOpened()) // check if succeeded
return -1;

//帧率相关参数
double capRate = cap.get(CV_CAP_PROP_FPS);
cout<<"rate:"<<capRate;

cap.set(CV_CAP_PROP_FPS, 25);

//分辨率相关参数
cap.set(CV_CAP_PROP_FRAME_WIDTH, 1024);
cap.set(CV_CAP_PROP_FRAME_HEIGHT, 768);

double capWidth = cap.get(CV_CAP_PROP_FRAME_WIDTH);
double capHeight = cap.get(CV_CAP_PROP_FRAME_HEIGHT);
cout<<"width:"<<capWidth<<", height:"<<capHeight;

//目标帧率
int targetRate = 20;

//视频录制
Size videoSize( capWidth, capHeight );
VideoWriter writer;

writer.open("result.avi", CV_FOURCC('X','2','6','4'), targetRate, videoSize);

// the camera will be deinitialized automatically in VideoCapture destructor
return 0;
}
但是简单归简单,其实OpenCV视频相关的功能也是在FFMPEG的基础上进行了封装,而且封装后可以控制的东西就变少了,尤其是现在要求在录制的同时有实时预览,OpenCV倒是可以预览摄像头画面,但是似乎同时就无法进行视频编码了--这个没有深研过,总之评估下来在项目中使用OpenCV是不太合适的。另外使用OpenCV从摄像头获取图像并显示的示例代码如下:
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
#include "opencv2/opencv.hpp"
#include "opencv2/highgui.hpp"

using namespace cv;

int main(int argc, char *argv[])
{
VideoCapture cap(0); // open the default camera
if(!cap.isOpened()) // check if succeeded
return -1;

Mat frame;

namedWindow("edges",1);

for(;;)
{
cap >> frame; // get a new frame from camera

imshow("edges", frame);
}

// the camera will be deinitialized automatically in VideoCapture destructor
return 0;
}

因此开始改看FFMPEG了,现在回头来看,过程真是极其痛苦的。视频的领域知识比较深;FFMPEG文档又比较少且纯C的API新手看起来既复杂又生涩,而且网上有关FFMPEG的资料大多数都是直接使用FFMPEG的命令行工具的、用FFMPEG库做开发的比较少,中文资料就更少了,基本只有CSDN的雷霄骅博士--大家都称作雷博--在写,但是刚看他的博客不久,忽然有一天惊闻他去世的消息,唉,一路走好。

雷博这里有一篇FFMPEG编码入门的文章:http://blog.csdn.net/leixiaohua1020/article/details/15811977/ ,可以作为入门的基础。FFMPEG开发库如果想在Windows下编译是非常麻烦的,mingw、各种依赖、路径问题,所以最好直接下载官方推荐的编译好的库:http://ffmpeg.zeranoe.com/builds/ ,做开发dev版(含.h、lib文件)、shared版(含dll动态链接库)都要下载。另外我的这系列文章,使用的都是2.8.6版本,现在已经到3.x版本了,API有了比较大的变化,这点需要注意。

言归正传,FFMPEG库下载好后,在项目中设置好头文件include和库lib,就可以开始编码了,下面直接给出FFMPEG从指定摄像头获取AVFrame的示例代码,注意代码只是API使用示例,不一定跑得起来。

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
//初始化FFMPEG
avdevice_register_all();
avcodec_register_all();
av_register_all();

const char *pcstrDeviceName = "Integrated Camera"; //注意如果有中文要用UTF-8编码
const char *pcstrResolution = "640x480"; //指定分辨率

AVFormatContext *pFormatCtx = avformat_alloc_context();
AVInputFormat *pInputFormat = av_find_input_format("dshow");

AVDictionary *options = NULL;
av_dict_set(&options, "video_size", pcstrResolution, 0);

//打开输入流
if ( avformat_open_input(&pFormatCtx, pcstrDeviceName, pInputFormat, &options) != 0 ){
av_dict_free(&options);
return -1;
}

av_dict_free(&options);

//获取流信息
if ( avformat_find_stream_info(pFormatCtx, NULL) < 0 )
{
avformat_close_input(&pFormatCtx);
pFormatCtx = NULL;
return -1;
}

//输出调试信息:tbr代表帧率;tbn代表文件层(st)的时间精度,即1S=1200k,和duration相关;tbc代表视频层(st->codec)的时间精度,即1S=XX,和stream->duration和时间戳相关。
av_dump_format(pFormatCtx, 0, pcstrDeviceName, 0);

//寻找视频流,有麦克风的摄像头同时还有音频流输入
int videoStreamIndex = -1;

for(unsigned int i=0; i < pFormatCtx->nb_streams; i++)
{
if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStreamIndex = i;
break;
}
}

if( -1 == videoStreamIndex)
{
avformat_close_input(&pFormatCtx);
pFormatCtx = NULL;
return -1;
}

//获取帧率信息,注意输入流里面用r_frame_rate不用avg_frame_rate
double dInputFps = pFormatCtx->streams[videoStreamIndex]->r_frame_rate.num * 1.0 / pFormatCtx->streams[videoStreamIndex]->r_frame_rate.den;
int inputFps = dInputFps; //帧率转为整型
cout<<"输入帧率:"<< inputFps;

//获取AVCodecContext,并获取分辨率信息
AVCodecContext *pCodecCtx = pFormatCtx->streams[videoStreamIndex]->codec;
cout<<"输入视频高度:"<<pCodecCtx->height;
cout<<"输入视频宽度:"<<pCodecCtx->width;

//寻找视频流解码器
AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
if ( NULL == pCodec )
{
avformat_close_input(&pFormatCtx);
pFormatCtx = NULL;
pCodecCtx = NULL;

return -1;
}

//打开视频流解码器
if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
{
avformat_close_input(&pFormatCtx);
pFormatCtx = NULL;
pCodecCtx = NULL;

return -1;
}

//----------从摄像头中获取帧----------
AVPacket packet;
int gotPicture;

AVFrame *pFrame = av_frame_alloc(); //注意av_frame_alloc并未实际分配空间

while( av_read_frame(pFormatCtx, &packet) >= 0 )
{
if( packet.stream_index == videoStreamIndex )
{
//返回的pFrame由decoder管理
avcodec_decode_video2(pCodecCtx, pFrame, &gotPicture, &packet);

//Do something

if(gotPicture)
{
av_frame_unref(pFrame);
}
}

av_free_packet(&packet);
}

//----------清理----------
if ( NULL != pCodecCtx )
{
avcodec_close(pCodecCtx);
pCodecCtx = NULL;
}

if ( NULL != pFormatCtx )
{
avformat_close_input(&pFormatCtx);
pFormatCtx = NULL;
}
最后需要注意一点,虽然可以从输入流中获取到帧率,但实际上摄像头输入的帧率是不固定的,比如环境光线越暗,CMOS需要的曝光时间就越长,相应的帧率也就会随之降低,如果需要获得固定的输入帧率,就需要就是插帧或者抛弃多余帧了。

附参考文档: * http://blog.csdn.net/nonmarking/article/details/48022387