如何实现:录制视频聊天的全过程? 【低调赠送:QQ高仿版GG 4.3 最新源码】
前段时间做个项目,客户需要将视频对话的整个过程录制下来,这样,以后就可以随时观看。想来录制整个视频聊天的过程这样的功能应该是个比较常见的需求,比如,基于网络语音视频的1:1的英语口语辅导,如果能将辅导的整个过程录制下来生成一个标准的MP4文件,就是一份难得的资料,便于以后复习和分享。我将1:1的视频对话录制的功能实现为了一个组件VideoChatRecorder,方便大家复用。并且,我在GG的最新版本4.3中使用了它,这样GG也有了视频聊天录制的功能。
(想要直接下载体验的朋友请点击:“下载中心” )
如果大家已经做过类似录制单个人的摄像头和麦克风程序的话,那么,录制两人视频聊天就会遇到两个新的难点:
(1)如何将两个人的视频图像整合成一个图像?
(2)如何将两个人的声音混成一路?
一.实现原理
1.视频合成
通过.NET提供的GDI+技术,我们可以将两张图片合成一张。在实现VideoChatRecorder组件时,我合成图片所采用的规则是这样的:
(1)将对方的视频作为录制的主体,而自己的视频则覆盖在对方视频的右下角。
(2)对方视频的大小,就是其摄像头的采集分辨率,依据(1),我们知道这也是录制生成的MP4文件播放时视频的Size。
(3)合成后自己视频图像的宽和高,设定为对方视频宽和高的 1/3。
合成后的视频的示意图如下所示:

2.音频合成
我们可以手动将自己的声音与对方的声音混音成一路,网上可以搜到很多混音算法(如直接相加法、平均法、归一化算法、衰减因子法等),但是,混音算法的好坏直接关系到混音最终的质量。
还有一种更简单的方案,就是直接使用OMCS提供的AudioInOutMixer组件,它可以将麦克风采集的声音(也就是自己的声音)和扬声器播放的声音(也就是对方的声音)混音成一路,并通过 AudioMixed 事件暴露混音后的数据。
二.实现具体步骤
解决了视频合成和音频合成两个关键难点后,我们就可以将实现的整个流程串起来了。
(1)使用一个摄像头连接器实例连接到对方的摄像头,然后调用其GetCurrentImage方法,就可以获取对方的视频图像。
(2)使用另一个摄像头连接器实例连接到自己的摄像头,然后调用其GetCurrentImage方法,就可以获取自己的视频图像。
(3)使用一个MFile提供的VideoFileMaker来将语音、视频录制成标准的MP4文件。
(4)使用一个AudioInOutMixer实例,来进行混音。预定其AudioMixed 事件,以获取混音后的语音数据,并将其提交给VideoFileMaker进行录制声音。
(5)使用一个后台线程,每隔100ms(即对应帧频为10fps)就调用前面两个连接器的GetCurrentImage方法,并将返回的两个图片进行合成变成一张,并将其提交给VideoFileMaker进行录制图像。
这里的关键,是使用GDI+进行图像合成的过程,其代码比较简单,如下所示:
Bitmap bmFriend = this.dynamicCameraConnector2Friend.GetCurrentImage();
if (bmFriend != null)
{
Bitmap bmMyself = this.cameraConnector2Myself.GetCurrentImage();
//合成图像
if (bmMyself != null)
{
Graphics g = Graphics.FromImage(bmFriend);
g.DrawImage(bmMyself ,this.myVideoRect);
g.Dispose();
} //录制图像
this.videoFileMaker.AddVideoFrame(bmFriend);
}
注:如果不想将自己的视频图像叠加在对方的图像之上,那么,上述的代码稍作修改即可。可以new一个新的Bitmap,然后在上面的不同区域分别绘制对方的图像和自己的图像就可以了。当然,新的Bitmap的Size,以及对方和自己图像在新的Bitmap中的布局位置要设置正确。
(6)当停止录制时,就停止用于合成图像的后台线程,并关闭VideoFileMaker。
注意:在某些配置比较差的机器上,可能生产的速度大于录制(也就是消费)的速度,这样,在关闭VideoFileMaker时,就会阻塞一段时间,直至所有的缓存中的所有视频帧都写入了录制文件中,才会返回。
在有了上面的整体思路之后,再来看VideoChatRecorder的完整代码,就很容易理解了。
/// <summary>
/// 视频聊天录制器。将视频聊天的完整过程录制成标准的MP4文件。
/// </summary>
class VideoChatRecorder : IDisposable
{
private DynamicCameraConnector dynamicCameraConnector2Friend ; //连接到好友摄像头的连接器。
private CameraConnector cameraConnector2Myself; //连接到自己摄像头的连接器。
private IMultimediaManager multimediaManager;
private VideoFileMaker videoFileMaker;
private Size videoSize;
private Rectangle myVideoRect;
private volatile bool isRecording = false;
private AudioInOutMixer audioInOutMixer; public VideoChatRecorder(IMultimediaManager mgr ,DynamicCameraConnector friend, CameraConnector myself)
{
this.multimediaManager = mgr;
this.dynamicCameraConnector2Friend = friend;
this.cameraConnector2Myself = myself;
this.dynamicCameraConnector2Friend.Disconnected += new ESBasic.CbGeneric<ConnectorDisconnectedType>(dynamicCameraConnector2Friend_Disconnected); //混音器。将自己和对方的声音混成一路。
this.audioInOutMixer = new AudioInOutMixer();
this.audioInOutMixer.AudioMixed += new CbGeneric<byte[]>(audioInOutMixer_AudioMixed);
} //得到混音数据,将其录制到文件。
void audioInOutMixer_AudioMixed(byte[] data)
{
if (this.isRecording)
{
this.videoFileMaker.AddAudioFrame(data);
}
} //摄像头连接器断开时,就停止录制。
void dynamicCameraConnector2Friend_Disconnected(ConnectorDisconnectedType obj)
{
if (!this.isRecording)
{
return;
} this.Dispose();
} //初始化录像设备,并开始录制。
public void Initialize(string filePath)
{
if (!this.dynamicCameraConnector2Friend.Connected)
{
throw new Exception("连接器尚未连接到对方的摄像头!");
}
this.videoSize = this.dynamicCameraConnector2Friend.VideoSize;
Size myVideoSize = new Size(this.videoSize.Width / , this.videoSize.Height / );
this.myVideoRect = new Rectangle(this.videoSize.Width - myVideoSize.Width, this.videoSize.Height - myVideoSize.Height, myVideoSize.Width, myVideoSize.Height); this.videoFileMaker = new VideoFileMaker();
this.videoFileMaker.AutoDisposeVideoFrame = true;
this.videoFileMaker.Initialize(filePath, VideoCodecType.H264, this.videoSize.Width, this.videoSize.Height, , AudioCodecType.AAC, , , true); this.audioInOutMixer.Initialize(this.multimediaManager);
this.isRecording = true; CbGeneric cb = new CbGeneric(this.RecordThread);
cb.BeginInvoke(null, null);
} //录制线程。每隔100ms(对应VideoFileMaker的帧频为10fps)就合成一张图片,并录制它。
private void RecordThread()
{
while (this.isRecording)
{
Bitmap bmFriend = this.dynamicCameraConnector2Friend.GetCurrentImage();
if (bmFriend != null)
{
Bitmap bmMyself = this.cameraConnector2Myself.GetCurrentImage();
//合成图像
if (bmMyself != null)
{
Graphics g = Graphics.FromImage(bmFriend);
g.DrawImage(bmMyself ,this.myVideoRect);
g.Dispose();
} //录制图像
this.videoFileMaker.AddVideoFrame(bmFriend);
} System.Threading.Thread.Sleep();
} } /// <summary>
/// 停止录制,并释放录制设备。
/// </summary>
public void Dispose()
{
this.dynamicCameraConnector2Friend.Disconnected -= new ESBasic.CbGeneric<ConnectorDisconnectedType>(dynamicCameraConnector2Friend_Disconnected);
this.audioInOutMixer.AudioMixed -= new CbGeneric<byte[]>(audioInOutMixer_AudioMixed);
this.audioInOutMixer.Dispose(); if (!this.isRecording)
{
return;
} this.isRecording = false;
this.videoFileMaker.Close(true);
}
}
三.GG V4.3 源码
下载最新版本,请转到这里。
在GG的最新版本中使用了上述的VideoChatRecorder类进行视频聊天录制以生成的MP4文件(默认是在运行目录下名称为 VideoChat.mp4 的文件),用QQ影音播放器进行播放这个文件,其效果如下所示:

________________________________________________________________________
欢迎和我探讨关于 GG 和 GGMeeting 的一切,我的QQ:2027224508,多多交流!
大家有什么问题和建议,可以留言,也可以发送email到我邮箱:2027224508@qq.com。
如果你觉得还不错,请粉我,顺便再顶一下啊
如何实现:录制视频聊天的全过程? 【低调赠送:QQ高仿版GG 4.3 最新源码】的更多相关文章
- 即时通信系统中如何实现:聊天消息加密,让通信更安全? 【低调赠送:QQ高仿版GG 4.5 最新源码】
加密重要的通信消息,是一个常见的需求.在一些政府部门的即时通信软件中(如税务系统),对聊天消息进行加密是非常重要的一个功能,因为谈话中可能会涉及到机密的数据.我在最新的GG 4.5中,增加了对聊天消息 ...
- QQ揭秘:如何实现托盘闪动消息提醒?【低调赠送:QQ高仿版GG 4.1 最新源码】
当QQ收到好友的消息时,托盘的图标会变成好友的头像,并闪动起来,点击托盘,就会弹出与好友的聊天框,随即,托盘恢复成QQ的图标,不再闪动.当然,如果还有其它的好友的消息没有提取,托盘的图标会变成另一个好 ...
- 如何做到在虚拟数据库和真实数据库之间自由切换?【低调赠送:QQ高仿版GG 4.4 最新源码】
记得以前在公司上班时,有时候白天的活没干完,我就会把工作带回家晚上加班继续做.但是,我们开发用的数据库是部署在公司局网内部的一台服务器上的,在家里是肯定连不上这台机器的.在家里没有数据库,服务端就跑不 ...
- QQ揭秘:如何实现窗体靠边隐藏?【低调赠送:QQ高仿版GG 4.2 最新源码】
QQ有个靠边隐藏的功能,使用起来很方便:在屏幕上拖动QQ的主窗体,当窗体的上边沿与屏幕的上边沿对齐时,主窗体就会duang~~地隐藏起来,当将鼠标移到屏幕上边沿的对应区域时,主窗体又会duang~~显 ...
- 即时通信系统中如何实现:全局系统通知,并与Web后台集成?【低调赠送:QQ高仿版GGTalk 5.1 最新源码】
像QQ这样的即时通信软件,时不时就会从桌面的右下角弹出一个小窗口,或是显示一个广告.或是一个新闻.或是一个公告等.在这里,我们将其统称为“全局系统通知”.很多使用GGTalk的朋友都建议我加上一个类似 ...
- 可在广域网部署运行的QQ高仿版 -- GG叽叽V3.7,优化视频聊天、控制更多相关细节
在广域网中,由于网络的结构纷繁复杂.而且其实时状况又是千变万化的,所以,要使广域网中的视频聊天达到一个令人满意的效果,存在诸多挑战.这次发布的GG 3.7版本尝试在这一方向上做一些努力,据我自己测试, ...
- 即时通信系统中实现聊天消息加密,让通信更安全【低调赠送:C#开源即时通讯系统(支持广域网)——GGTalk4.5 最新源码】
在即时通讯系统(IM)中,加密重要的通信消息,是一个常见的需求.尤其在一些政府部门的即时通信软件中(如税务系统),对即时聊天消息进行加密是非常重要的一个功能,因为谈话中可能会涉及到机密的数据.我在最新 ...
- 仿爱奇艺视频,腾讯视频,搜狐视频首页推荐位轮播图(二)之SuperIndicator源码分析
转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼:http://blog.csdn.net/hejjunlin/article/details/52510431 背景:仿爱奇艺视频,腾讯视频 ...
- Android 音视频深入 十七 FFmpeg 获取RTMP流保存为flv (附源码下载)
项目地址https://github.com/979451341/RtmpSave 这个项目主要代码我是从雷神那弄过来的,不愧是雷神,我就配个环境搞个界面就可以用代码了. 这一次说的是将RTMP流媒体 ...
随机推荐
- Matlab(3) -- 编写M文件(函数)
转自:http://blog.csdn.net/misskissc/article/details/8178089 matlab的命令编辑窗口(Command Window)界面主要是用来调用系统命令 ...
- 即时通讯 TCP UDP
TCP协议与UDP协议的区别 首先咱们弄清楚,TCP协议和UCP协议与TCP/IP协议的联系,很多人犯糊涂了,一直都是说TCP/IP协议与UDP协议的区别,我觉得这是没有从本质上弄清楚网络通信! ...
- 怎么计算Oracle的表一条记录占用空间的大小
如何计算Oracle的表一条记录占用空间的大小? 如何计算Oracle的表记录占用空间的大小? 是把所有字段的大小都加起来吗?varchar(256),char,number算几个字节? ------ ...
- HTML5+CSS3学习笔记(一)
HTML5+CSS3概述 HTML5和CSS3不仅仅是两项新的Web技术标准,更代表了下一代HTML和CSS技术.虽然HTML5的标准规范还没有正式发布,但是未来的发展前景已经可以预见,那就是HTML ...
- Linux三剑客之awk
awk awk是linux下的一个命令,他对其他命令的输出,对文件的处理都十分强大,其实他更像一门编程语言,他可以自定义变量,有条件语句,有循环,有数组,有正则,有函数等.他读取输出,或者文件的方式是 ...
- input输入内容时放大问题
最近做的微信网站有一个关于input输入框页面放大的问题.比如登录页面刚打开时正常,但用户输入信息登录时,页面就会放大.解决这个问题,首先需要在头部加一个 <meta name="vi ...
- Java程序员从笨鸟到菜鸟之(一百零二)sql注入攻击详解(三)sql注入解决办法
sql注入攻击详解(二)sql注入过程详解 sql注入攻击详解(一)sql注入原理详解 我们了解了sql注入原理和sql注入过程,今天我们就来了解一下sql注入的解决办法.怎么来解决和防范sql注入, ...
- AOP实现原理
Spring 为解耦而生,其中AOP(面向切面编程)是很浓重的一笔. 本文来探讨一下AOP实现的原理. 一. 概述 代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负 ...
- ERDAS文件格式:IGE、IMG、RRD、AUX
ERDAS如果需要打开大于2GB的文件,ERDAS需要把文件转换成IMG格式.这时候,ERDAS自动生成三个文件,分别是IMG.IGE和RRD文件,其中:1.IGE:是数据文件,实际用来存储栅格数据: ...
- myBatis+SpringMVC+Maven整合
一.先创建表结构 二.使用generator通过表结构自动生成model和dao.mapper 使用步骤: 1.解压generator.rar文件 2.文件中的generator.xml文件需要进行修 ...