生而自由

自由而无用的灵魂

FFMPEG系列之一:打开摄像头

最近陆陆续续接触到了不少视频相关的知识,终于有时间可以把这些知识整理一下了。

技术选型先看的是OpenCV(之前博文也有提到OpenCV的编译),在OpenCV下打开摄像头、录制成一个视频文件的代码是非常简单的,下面的代码演示了打开系统的第1个摄像头,并将画面录制为H264视频的过程(代码是从之前的测试代码中提取出来的,不保证没有错误,可能含无用代码):

#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从摄像头获取图像并显示的示例代码如下:

#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使用示例,不一定跑得起来。

//初始化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

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code