上次介绍了视频聊天软件的界面、文字聊天、文件传输部分,这此介绍视频聊天功能,这算是音视频领域一个很广的应用。首先视频聊天的双方需要有一个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. 预览视频

(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步,就完成了视频预览的全过程,运行程序你就会欣喜地在对话框上看到摄像头的画面了,采集视频就是这么简单。

2. 回调函数

进一步,我们想将本地的视频实时数据获取出来并发送出去,可以利用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<uint8_t> 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 ==> YUV420YUY2YUV420(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更加灵活、功能更强大,适合学习使用。

1. FFmpeg的安装

FFmpeg 的音视频编解码功能真心强大,像暴风影音、QQ影音、格式工厂都使用 FFmpeg 作为内核,同时完美支持 windows 和 linux 系统,几乎囊括所有的音视频编码标准,只要做音视频开发,就会用到它。
在 windows下安装配置FFmpeg:

  1. FFmpeg的官方网站是:http://ffmpeg.org,windows版本在 http://ffmpeg.zeranoe.com/builds/网站中,里面有Static,Shared,Dev这3个版本
  2. 下载Dev版本,里面包含了ffmpeg的xxx.h头文件以及xxx.lib库文件。
  3. 下载Shared版本,里面包含了ffmpeg的dll文件。
  4. 将这两部分文件拷贝到VC工程下面,添加到附加包含目录、附加库目录中
  5. 头文件加入以下代码
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/,绝对有收获。

2. 编码初始化

在实现我们的编码线程之前,需要进行编码器初始化,在此我们想将 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;       //编码器IDpCodecCtx->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,&param) < 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;
}

3. 编码线程

编码器初始化后,接下来我们就实现编码线程。这里我们想实现一个新线程,从公共链表中获取裸数据,编码成码流,然后通过网络传输将码流发送出去。
视频采集中通过 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;             //YpFrame->data[1] = picture_buf +g_size;     //UpFrame->data[2] = picture_buf +g_size*5/4; //VEncodeVideoFrame(pFrame);          //编码并发送}}Printf("跳出循环,编码结束\n");
}bool CVideoDlg::EncodeVideoFrame(AVFrame *Frame)
{int got_picture = 0;//编码成功返回0,got_pitcure变为1int 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]来生成目录:

  • 视频采集

    • 预览视频
    • 回调函数
  • 视频编码
    • FFmpeg的安装
    • 编码初始化
    • 编码线程
  • 目录

VC++实现视频聊天:VFW视频采集+FFmpeg编码相关推荐

  1. Android 音视频入门之音频采集、编码、播放

    今天我们学习音频的采集.编码.生成文件.转码等操作,我们生成三种格式的文件格式,pcm.wav.aac 三种格式,并且我们用 AudioStack 来播放音频,最后我们播放这个音频. 本篇文章你将学到 ...

  2. 基于WebRtc在H5视频聊天、视频教学、视频会议、视频直播、白板互动低延时方案

    随移动互联应用加快,4G,5G网络上马,低延时网络视频应改越来越走近生活,在教学,会议,在线医疗,招聘交友及时视频要求高等场景需求越来越大,传统基于rtmp直播应用已经大量应用在各个方向,由于rtmp ...

  3. 智能会议系统(10)---WebRtc在H5视频聊天

    基于WebRtc在H5视频聊天.视频教学.视频会议.视频直播.白板互动低延时方案 随移动互联应用加快,4G,5G网络上马,低延时网络视频应改越来越走近生活,在教学,会议,在线医疗,招聘交友及时视频要求 ...

  4. 如何选择视频聊天程序搭建视频聊天网站

    搭建1个视频聊天网站,特别是带有精彩视频的聊天网站,无疑是一种非常快捷的网络创业方式.可是自己没有开发能力怎么办?那么你就需要选购一些视频聊天程序或则视频聊天源码.那么如何选择最适合自己的视频聊天程序 ...

  5. VC++实现视频聊天:FFmpeg解码+SDL播放视频

    经过网络传输接收到的码流,已经存放在公共链表 PacketNode_t 中,码流经过解码成YUV或RGB后才能播放,接下来就介绍FFmpeg解码过程和 SDL 播放视频. FFmpeg 解码 码流解码 ...

  6. .VC++关于的VFW视频采集方案【转 360doc】

    2.2  VFW视频采集方案 VFW是Microsoft于1992年推出的数字视频软件包,它不依赖于专用的硬件设备,提供了通用的数字视频开发方案.VFW主要由AVICap.dll.MSVideo.dl ...

  7. Window 下 VFW 视频采集与显示

    引言 经过几天的努力终于将VFW视频采集与显示功能完整实现了,不得不说网上对这方面完整的详细讲解文章是在太少了.所以就要本人来好好总结一下让后来者不再像我一样折腾好久.在本文中我将详细讲解VFW视频采 ...

  8. 【190319】VC++ C/S结构视频聊天软件源码源代码

    源码下载简介 C/S结构的VC++视频聊天程序源码,源码可直接在VC6下编译,客户端截图如上示,设置IP和端口,先建立连接,还需要摄像头等设备的配合,关于摄像头方面的实现请参阅其它资料. 源码下载地址 ...

  9. VFW视频采集方案(Captureparms参数详细)

    2.2 VFW视频采集方案 VFW是Microsoft于1992年推出的数字视频软件包,它不依赖于专用的硬件设备,提供了通用的数字视频开发方 案.VFW主要由AVICap.dll.MSVideo.dl ...

最新文章

  1. python学到什么程度可以写爬虫-刚开始学习 Python 到可以写出一个爬虫大约需要多长时间...
  2. Spring cloud技术栈
  3. apache添加支持php的模块,配置Apache支持PHP5 apache php 套件 apache添加php模块 apache部署php项...
  4. java中钩子函数回调函数_钩子函数 和回调函数
  5. 【数据湖存储】数据湖的终极奥秘,无招胜有招
  6. Linux rm 删除指定文件外的其他文件 方法汇总
  7. 本地如何安装运行多个vue.js项目?
  8. 基于Python-Flask实现的网站例子
  9. 如何加声调口诀_李变美:美容院老板小白如何快速打造自己的引流型文案系统!...
  10. [转]ASP.NET面试题
  11. java流程图什么代表活动_举例分析流程图与活动图的区别与联系
  12. zTree入门实例(一眼就看会)
  13. 【浙江大学PAT真题练习乙级】1009 说反话 (20分)真题解析
  14. y7000 安装linux双系统,联想拯救者Y7000安装双系统:win10 + ubuntu16.04 (GTX1060显卡)...
  15. 耿建超英语语法---陈述句(1)
  16. 公有云迁移,需要考虑的问题
  17. mysql ignore用法_MySQL中的insert ignore into, replace into等的一些用法总结
  18. iOS symbol(s) not found for architecture armv7
  19. 注销linux用户的方法,Linux下注销登录用户的方法
  20. 上面两点下面一个三角形_解三角形的新视野——定角对定边~

热门文章

  1. (已解决)Windows使用transmac制作macos启动U盘重启按option不能识别的问题
  2. 个人支付接口开通(教程)
  3. U盘插入电脑后有图标但读不出来
  4. 云计算的那些事之存储虚拟化
  5. ClaudiaIDE无法修改设置或背景图片的解决方法
  6. SAP中采购协议中未清交货需要处理吗?
  7. 部署企业级RAC+DG架构
  8. 2022年监理工程师基本理论与相关法规考试每日一练及答案
  9. 如何绘制炫酷的韦恩图(薇恩图)Venn diagram
  10. RS485应用电路图