前面讲解了视频播放器的开发,初步掌握了使用FFMPEG解码音视频。
现在我们就接着讲解使用FFMPEG来编码音视频,主要是实现一个录屏软件的制作。
一个录屏软件的流程基本就是:
图像采集
图像编码
将解码好的图像封装成视频
图像的采集:
FFmpeg中有一个和多媒体设备交互的类库:Libavdevice。使用这个库可以读取电脑(或者其他设备上)的多媒体设备的数据,或者输出数据到指定的多媒体设备上。
windows上libavdevice可以采集摄像头也可以采集屏幕,这里我们先讲采集摄像头。
Windows上采集摄像头基本上就是使用dshow或vfw。(dshow和vfw就是一个采集的驱动,有点类似Linux下的v4l2)
libavdevice的使用:
首先,使用libavdevice的时候需要包含其头文件:
#include "libavdevice/avdevice.h"
然后,在程序中需要注册libavdevice:
avdevice_register_all();
接下来就可以使用libavdevice的功能了。
使用FFMPEG打开视频设备的时候,总体上和打开一个视频文件是差不多的。因为系统的设备也被FFmpeg认为是一种输入的格式(即AVInputFormat)。
前面,播放器的例子中,我们使用如下的函数来打开一个视频文件:
AVFormatContext *pFormatCtx = avformat_alloc_context(); avformat_open_input(&pFormatCtx, "E:in.mp4",NULL,NULL);
而我们使用libavdevice的时候,唯一的不同在于需要首先查找用于输入的设备。在这里使用av_find_input_format()完成:
AVFormatContext *pFormatCtx = avformat_alloc_context(); AVInputFormat *ifmt=av_find_input_format("dshow"); avformat_open_input(&pFormatCtx,"video=e2eSoft VCam",ifmt,NULL) ;
上述代码首先指定了dshow设备作为输入设备,然后指定打开名字为“e2eSoft VCam”的一个设备。
首先dshow是固定的名字,是写死的,这个没有什么疑问。
那这里的“e2eSoft VCam”要如何获得呢,下面就重点讲解下:
既然FFMPEG支持采集摄像头,那么理论上他肯定有个函数可以获取摄像头的个数以及名字吧。
开头是猜到了,但万万没想到的是FFMPEG列出设备居然也是用的avformat_open_input函数。
如下所示:
//Show Device void show_dshow_device() { AVFormatContext *pFormatCtx = avformat_alloc_context(); AVDictionary* options = NULL; av_dict_set(&options,"list_devices","true",0); AVInputFormat *iformat = av_find_input_format("dshow"); printf("Device Info============= "); avformat_open_input(&pFormatCtx,"video=dummy",iformat,&options); printf("======================== "); }
以上代码可以实现列出所有的设备,可惜的是,这个执行后的结果是将设备打印到终端上的,而且中文居然还是乱码的!
再仔细一看,这个函数那么多参数,估计有办法可以将结果保存到我们的变量中。
不过呢,我们这里就是个简单例子,能知道设备名就行了,手动复制进去也是没关系的。
因此,我们直接使用FFMPEG的命令行来获取设备名。
还记得前面下载的ffmpeg-2.5.2-win32-shared.7z吧。
他的bin目录下除了dll以外还有3个.exe文件
ffmpeg.exe就是集成了ffmpeg的所有功能的一个可执行文件,ffmpeg所有的功能都可以通过命令行实现。
下面就示范下使用命令行获取可用的设备。
首先打开命令行:
切换到ffmpeg.exe所在的目录(我的是E:MyProjectsVideoDevelopfmpeg-2.5.2-win32-sharedin)、
然后输入:ffmpeg -list_devices true -f dshow -i dummy
会发现设备列出来了,且和我们刚才使用代码列出来的是一样的。
不过这里居然也是乱码。
windows的终端使用GBK编码这个毋庸置疑了,这里乱码了,只能说明ffmpeg用的不是GBK编码了(事实上他用的是UTF-8)。
因此可以将结果输出重定向到文件:
将上面的命令改成如下:
ffmpeg -list_devices true -f dshow -i dummy 2>E:/out.txt
执行完后,会在E盘下生成一个out.txt文件,打开out.txt,里面内容如下:
哦了 可以看到不乱码的设备名了。
前期我们测试的时候就用这种方法来获取设备名了。
有时候可能手头上没有摄像头,或者想试下多个设备的感觉,但是却只有一个摄像头。
怎么办呢?
这里给大家推荐一个软件:“e2eSoft VCam”,这是一个虚拟摄像头的软件,可以将视频文件当成摄像头,对做视频开发的我们还是非常有用的。
软件自行百度下载喽。
好了,回归正题,继续我们刚才打开摄像头之后的操作。
成功打开摄像头之后,进行如下操作:
if(avformat_find_stream_info(pFormatCtx,NULL)<0) { printf("Couldn't find stream information. "); return -1; } videoindex=-1; for(i=0; i<pFormatCtx->nb_streams; i++) { if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) { videoindex=i; } } if(videoindex==-1) { printf("Couldn't find a video stream. "); return -1; } pCodecCtx=pFormatCtx->streams[videoindex]->codec; pCodec=avcodec_find_decoder(pCodecCtx->codec_id); if(pCodec==NULL) { printf("Codec not found. "); return -1; } if(avcodec_open2(pCodecCtx, pCodec,NULL)<0) { printf("Could not open codec. "); return -1; }
可以看出,这个操作和之前我们操作视频的做法是完全一样的:
都是先查找流,然后查找打开解码器。
接下来也是一样使用av_read_frame来读取:
while(1) { if(av_read_frame(pFormatCtx, packet) < 0) { break; } if(packet->stream_index==videoindex) { ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet); ... } av_free_packet(packet); }
这里的操作和之前写视频播放器的时候真的是完全一样。
所以这就是使用FFMPEG的好处。
看到这里是不是觉得很奇怪,
从摄像头采集到的数据一般都是YUV或者RGB格式(这个由摄像头决定),不管是YUV还是RGB,他们都是原始的图像数据,是没有经过压缩的,可上面为什么会有解压缩的操作。
这个问题先留着,后面如果看了ffmpeg源码后再讲。总之,不知道原因在这里是没有影响的。
这里只要知道调用了解码函数之后获取到的数据就是摄像头的原始数据了,如果摄像头支持yuv422那么得到的结果就是yuv422。
从摄像头采集到的数据格式非常多:yuv444、yuv422、yuv420等等,而我们处理中使用最多的就是yuv420的数据了,因此这里解码之后直接将他转换成yuv420,并将得到的Yuv420数据写入文件:
FILE *fp_yuv=fopen("output.yuv","wb"); AVFrame *pFrame,*pFrameYUV; pFrame=av_frame_alloc(); pFrameYUV=av_frame_alloc(); uint8_t *out_buffer=(uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height)); avpicture_fill((AVPicture *)pFrameYUV, out_buffer, PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height); struct SwsContext *img_convert_ctx; img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); ///我们就读取100张图像 for(int i=0;i<100;i++) { if(av_read_frame(pFormatCtx, packet) < 0) { break; } if(packet->stream_index==videoindex) { ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet); if(got_picture) { sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); int y_size=pCodecCtx->width*pCodecCtx->height; fwrite(pFrameYUV->data[0],1,y_size,fp_yuv); //Y fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv); //U fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv); //V } } av_free_packet(packet); }
YUV420是原始的图像数据,因此上面我们保存的out.yuv文件是可以播放出来的。
播放YUV文件可以使用:YUVPlayer 。
点我去下载:http://download.csdn.net/detail/qq214517703/9637191
点击"文件>>打开"选择我们上面保存的out.yuv。
由于YUV是原始的图像数据,是不带其他任何信息的,比如图像的宽度。
因此我们需要告诉播放器我们这个YUV图像的宽和高。
宽高信息通过pCodecCtx->width和pCodecCtx->height获得,可以加个打印信息,把他打印到终端上。
我测试用的是320x240:
注:I420就是yuv420p
既然YUV420只是原始数据,那么他更不可能带有视频的帧率信息了,因此在上面也可以看到一个帧率的设置,这个就是决定一秒钟播放多少张图像。
播放过程中会发现速度变快了好多,这个是正常现象。因为帧率是我们手动指定的。
本文代码主要是参考雷大神的博客:http://blog.csdn.net/leixiaohua1020/article/details/39702113
完整工程下载地址:http://download.csdn.net/detail/qq214517703/9637226
学习音视频技术欢迎访问 http://blog.yundiantech.com
音视频技术交流讨论欢迎加 QQ群 121376426