上次介绍了视频聊天软件的界面、文字聊天、文件传输部分,这此介绍视频聊天功能,这算是音视频领域一个很广的应用。首先视频聊天的双方需要有一个USB摄像头(或者笔记本摄像头),在windows系统下,一个完整的视频流程应该有如下步骤:
采集摄像头数据--> 视频帧编码 --> 码流网络传输 --> 解码 --> 播放
然后按流程来选择相应的工具分块实现,串联起来,就可以聊天了。本次视频聊天使用的工具如下:
vs2010;windows; VFW视频采集、FFmpeg编解码、Socket网络传输、VFW播放
效果如下,因为只有一个摄像头,只做了发送方的视频采集和接收方的显示视频。
目前市场上常用的视频采集工具有VFW、DirectShow,FFmpeg也可以采集视频。其中VFW(Video for Windows)是微软公司92年推出的数字视频软件包,很古老的技术,目前已不再更新了,DirectShow是微软公司在VFW的基础上推出的新一代基于COM(Component Object Model)的流媒体处理的开发包,功能比VFW更强大、效果更好。但VFW调用特别方便,如果对视频采集不高,VFW还是不错的选择。
VFW是WIN32 SDK 中多媒体编程SDK 的视频开发工具,在微软的Visual C ++中提供了Vedio for Windows 的头文件vfw.h 和库文件vfw32.lib,只需在StdAfx.h 中加入以下内容:
#include < vfw.h >
#pragma comment(lib,"vfw32.lib")
视频采集阶段的任务有两个:1. 本地预览视频 2. 使用回调函数获取视频帧
(1)在原聊天界面上增加“视频”按钮,点击按钮创建非模态子对话框,这样就可以实现文字聊天与视频的并行处理。
(2)视频聊天对话框 中,OnInitDialog()中利用capCreateCaptureWindow函数创建窗口,并且得到返回的窗口句柄。
BOOL m_bInit = FALSE;
CWnd *pWnd = GetDlgItem(IDC_VIDEO_STATIC);//得到预示窗口指针
CRect rect;
pWnd->GetWindowRect(&rect);
g_hWnd = capCreateCaptureWindow(NULL,
WS_CHILD|WS_VISIBLE|WS_EX_CLIENTEDGE|WS_EX_DLGMODALFRAME,
0,0,rect.Width(),rect.Width(),
pWnd->GetSafeHwnd(),0); // 设置预示窗口
这里g_hWnd是视频窗口句柄,会一直用到,这里设为了全局变量
(3)连接驱动器(win7下有时只能第一次连接上,建议使用循环不断连接驱动器),
while(!m_bInit)
{
m_bint = capDriverConnect(g_hwnd,0); //连接0号驱动器
}
//获得驱动器性能
CAPDRIVERCAPS m_CapDrvCap;
capDriverGetCaps(m_hWnd,sizeof(CAPDRIVERCAPS),&m_CapDrvCap);
(4)设置预览
if(m_CapDrvCap.fCaptureInitialized)
{
capSetUserData(m_hWndVideo,this); //指针绑定句柄
capGetStatus(m_hwnd, &m_CapStatus,sizeof(m_CapStatus));
capPreviewRate(m_hwnd,30);
capPreview(m_hwnd,TRUE); //预览视频
}
实现以上4步,就完成了视频预览的全过程,运行程序你就会欣喜地在对话框上看到摄像头的画面了,采集视频就是这么简单。
进一步,我们想将本地的视频实时数据获取出来并发送出去,可以利用VFW的回调机制。VFW有以下几种回调机制:
(1)BOOL capSetCallbackOnCapControl(hwnd,FrameCallbackProc); 此宏可以精确的控制视频采集的开始和结束时间
(2)BOOL capSetCallbackOnError(hwnd, FrameCallbackProc);此宏用于设置当视频采集过程中出现错误的时候反馈给程序处理的回调函数
(3)BOOL capSetCallbackOnFrame(hwnd, FrameCallbackProc); 此宏的作用是设置一个视频预览的回调函数,可以对视频数据进行特定的处理,与 capGrabFrameNoStop() 函数配合使用;
(4)BOOL capSetCallbackOnVideoStream(hwnd,FrameCallbackProc); 此宏用于获得视频流,在视频采集过程中与 capCaptureSequenceNoFile() 函数配合使用,进行视频缓冲,对采集到的数据流进行特定的处理
(5)BOOL capSetCallbackOnStatus(hwnd, FrameCallbackProc);此宏用于设置状态回调函数,用于检测捕捉状态的变化
什么是回调函数呢,简单的说就是通过宏绑定你自己写的函数,符合规定的参数和返回值类型,符合规定的调用约定,然后有相应的操作出发回调函数。如上,FrameCallbackProc() 就是回调函数的地址,然后我们自己去实现回调函数的内容,再用 capGrabFrameNoStop() 这样的函数触发回调机制。
这里我们想实时地获取视频数据,显然可用视频流回调机制
BOOL capSetCallbackOnVideoStream(hwnd,FrameCallbackProc) ,回调函数满足下列形式
LRESULT CALLBACK FrameCallbackProc(HWND hWnd, LPVIDEOHDR lpVHdr); 这里 LPVIDEOHDR lpVHdr 就是获取的视频数据结构,可在函数中对其操作。
然后用 capCaptureSequenceNoFile(g_hwnd) 函数触发回调机制。
(注:capCaptureSequenceNoFile 将数据进行缓冲,可能会导致对话框卡主,这时可使用回调函数(3))
在实现回调函数之前,我们要先知道摄像头采集的数据格式,常见的格式有RGB、YUYV、YUV420,因为接下来的FFmpeg编解码使用yuv420格式,所以用到格式转换。
进一步,在对话框上建立视频格式按钮、视频源按钮,实现如下功能
void CVideoDlg::OnBnClickedVidformatButton()
{
capDlgVideoFormat(g_hwnd); //视频格式
}
void CVideoDlg::OnBnClickedVidsourceButton()
{
capDlgVideoSource(g_hwnd); //视频源
}
这样的话,通过点击按钮,我们就能知道视频源、视频格式、分辨率。然后开始写回调函数,代码如下
typedef unsigned char uint8_t;
SDL_Event g_event ; //触发事件
extern CCriticalSection cs; //临界区,线程锁
extern list packetList;//公共列表,用于存储视频帧数据
int g_size; //YUV420图像大小
void CVideoDlg::OnInitDialog(){
//注册回调函数,触发回调函数
capSetCallbackOnVideoStream(g_hwnd,FrameCallbackProc);
capCaptureSequenceNoFile(g_hwnd);
BIMAPINFO m_InInfo; //像素信息
capGetVideoFormat(g_hwnd,&m_InInfo ,sizeof(BITMAPINFO));
g_width = m_InInfo.bmiHeader.biWidth; //像素宽
g_height = m_InInfo.bmiHeader.biHeight; //像素高
}
static LRESULT CALLBACK FrameCallbackProc(HWND hWnd, LPVIDEOHDR lpVHdr)
{
CVideoDlg* VFWObj = (CVideoDlg*) capGetUserData(hWnd);
//数据格式转换,YUYV ==> YUV420
YUY2YUV420(lpVHdr->lpData,picture_buf,width,height);
g_size = width*height*3/2;
cs.Lock();
packetList.push_back(temNode); //存入公共列表中
cs.Unlock();
g_event.type = SFM_REFRESH_EVENT;
SDL_PushEvent(&g_event); //SDL事件触发解码
//创建解码线程,只执行一次
if (0 == thread_exit)
{
thread_exit = 1;
CWinThread *m_CaptureThread;
m_CaptureThread = AfxBeginThread(CaptureThreadFunc,(void*) VFWObj);
}
}
这样,视频采集就完全结束了,这里将裸数据存入了公共列表中,接下来就是编码的工作了。因为编解码很占cpu,所以单独开线程完成编码工作。
视频采集的回调函数中,已经将采集的YUV420数据存到公共列表中,接下来就是实现编码线程。为什么要进行视频编解码呢,这是因为一帧视频裸数据很大,像一帧 640*480 的 YUV420 数据的大小为460800字节,编码过后只有几千字节,直接传裸数据非卡死不可,虽然编解码会损失一定的图像质量,但这是值得的。
在此我们选用FFmpeg编解码器,VFW自带的压缩器可以使用,但是FFmpeg更加灵活、功能更强大,适合学习使用。
FFmpeg 的音视频编解码功能真心强大,像暴风影音、QQ影音、格式工厂都使用 FFmpeg 作为内核,同时完美支持 windows 和 linux 系统,几乎囊括所有的音视频编码标准,只要做音视频开发,就会用到它。
在 windows下安装配置FFmpeg:
extern "C"
{
#include "libavutil\opt.h"
#include "libavcodec\avcodec.h"
#include "libavformat\avformat.h"
#include "libswscale\swscale.h"
#include "SDL2/SDL.h"
}
接下来就可以使用FFmpeg进行编解码了,在使用 FFmpeg 之前最好先了解一下几种基本的音视频数据格式、编解码协议、网络传输知识,如RGB/YUV像素数据处理、h264/h265码流、RTP/UDP网络协议。更多关于FFmpeg 的知识,请参考雷神的博客:
http://blog.csdn.net/leixiaohua1020/article/details/15811977/,绝对有收获。
在实现我们的编码线程之前,需要进行编码器初始化,在此我们想将 YUV420 数据编码成 h265 ( 或h264 ) 码流,
AVFormatContext* pFormatCtx; //视频格式结构体
AVOutputFormat* fmt; //输出格式
AVStream* video_st; //视频/音频流信息的结构体
AVCodecContext* pCodecCtx; //编码器结构体
AVCodec* pCodec; //编码器
AVFrame* pFrame; //帧结构体
AVPacket pkt; //码流包存储码流
uint8_t* picture_buf; //yuv420数据指针
bool CVideoDlg::InitEncoder()
{
av_register_all(); //注册所有编码器
const char* out_file = "test.hevc"; //用于确定码流格式
//初始化AVFromatContext *pFormatCtx;
pFormatCtx = avformat_alloc_context();
//输出格式
fmt = av_guess_format(NULL,out_file,NULL);
pFormatCtx->oformat = fmt;
//初始化AVStream* video_st;
video_st = avformat_new_stream(pFormatCtx,0);
video_st->time_base.num = 1;
video_st->time_base.den = 30;
pCodecCtx = video_st->codec;
pCodecCtx->codec_id = fmt->video_codec; //编码器ID
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO; //编码类型
pCodecCtx->pix_fmt = PIX_FMT_YUV420P; //像素格式
pCodecCtx->width = g_width;
pCodecCtx->height = g_height;
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 30; //帧率,即FPS
pCodecCtx->bit_rate = 400000; //码率
pCodecCtx->gop_size = 50; //I帧间隔
pCodecCtx->qmin = 10; //最小量化系数
pCodecCtx->qmax = 51; //最大量化系数
pCodecCtx->max_b_frames = 0; //间隔中B帧数
//输出格式信息
av_dump_format(pFormatCtx, 0, out_file, 1);
pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
if (!pCodec)
{
AfxMessageBox(_T("没找到合适的编码器!"));
return 0;
}
if (avcodec_open2(pCodecCtx, pCodec,¶m) < 0)
{
AfxMessageBox(_T("编码器打开失败!"));
return 0;
}
pFrame = avcodec_alloc_frame();
size = avpicture_get_size(pCodecCtx->pix_fmt, g_width,g_height);
picture_buf = (uint8_t *)av_malloc(size);
avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
av_new_packet(&pkt,g_width*g_height*3);
return TRUE;
}
编码器初始化后,接下来我们就实现编码线程。这里我们想实现一个新线程,从公共链表中获取裸数据,编码成码流,然后通过网络传输将码流发送出去。
视频采集中通过 AfxBeginThread(CaptureThreadFunc,(void *) VFWObj) 声明了线程函数 CaptureThreadFunc,现在完成这个函数。
static UINT CaptureThreadFunc(void *lpvoid)
{
CVideoDlg *pDlg = (CVideoDlg*) lpvoid; //从指针中获取对话框类
UINT Ret = 0;
pDlg->FFmpegEncode(); //编码函数
return Ret;
}
void CVideoDlg::FFmpegEncode()
{
while (1)
{
SDL_WaitEvent(&g_event);
if (g_event.type != SFM_REFRESH_EVENT)
{
break;
}
if (!packetList.empty())
{
cs.Lock();
picture_buf = packetList.front(); //公共列表中的数据
packetList.pop_front();
cs.Unlock();
pFrame->data[0] = picture_buf; //Y
pFrame->data[1] = picture_buf +g_size; //U
pFrame->data[2] = picture_buf +g_size*5/4; //V
EncodeVideoFrame(pFrame); //编码并发送
}
}
Printf("跳出循环,编码结束\n");
}
bool CVideoDlg::EncodeVideoFrame(AVFrame *Frame)
{
int got_picture = 0;
//编码成功返回0,got_pitcure变为1
int ret = avcodec_encode_video2(pCodecCtx, &pkt,Frame, ds&got_picture);
if(ret != 0 || got_picture != 1)
{
return 0;
}
Send(pkt.data) //编码成功将码流发送出去
return TRUE;
}
到此视频编码的工作就完成了,最终我们将视频帧数据编码到 AVPacket 容器 pkt 中,pkt.data就是得到的 和 h265 码流,最后将码流发送出去。
至于网络传输Socket部分,也是很关键的部分,需要使用到UDP媒体流传输协议,接下来会继续介绍,共同探讨。
用 [TOC]
来生成目录: