前段时间做个项目,客户需要将视频对话的整个过程录制下来,这样,以后就可以随时观看。想来录制整个视频聊天的过程这样的功能应该是个比较常见的需求,比如,基于网络语音视频的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 最新源码】的更多相关文章

  1. 即时通信系统中如何实现:聊天消息加密,让通信更安全? 【低调赠送:QQ高仿版GG 4.5 最新源码】

    加密重要的通信消息,是一个常见的需求.在一些政府部门的即时通信软件中(如税务系统),对聊天消息进行加密是非常重要的一个功能,因为谈话中可能会涉及到机密的数据.我在最新的GG 4.5中,增加了对聊天消息 ...

  2. QQ揭秘:如何实现托盘闪动消息提醒?【低调赠送:QQ高仿版GG 4.1 最新源码】

    当QQ收到好友的消息时,托盘的图标会变成好友的头像,并闪动起来,点击托盘,就会弹出与好友的聊天框,随即,托盘恢复成QQ的图标,不再闪动.当然,如果还有其它的好友的消息没有提取,托盘的图标会变成另一个好 ...

  3. 如何做到在虚拟数据库和真实数据库之间自由切换?【低调赠送:QQ高仿版GG 4.4 最新源码】

    记得以前在公司上班时,有时候白天的活没干完,我就会把工作带回家晚上加班继续做.但是,我们开发用的数据库是部署在公司局网内部的一台服务器上的,在家里是肯定连不上这台机器的.在家里没有数据库,服务端就跑不 ...

  4. QQ揭秘:如何实现窗体靠边隐藏?【低调赠送:QQ高仿版GG 4.2 最新源码】

    QQ有个靠边隐藏的功能,使用起来很方便:在屏幕上拖动QQ的主窗体,当窗体的上边沿与屏幕的上边沿对齐时,主窗体就会duang~~地隐藏起来,当将鼠标移到屏幕上边沿的对应区域时,主窗体又会duang~~显 ...

  5. 即时通信系统中如何实现:全局系统通知,并与Web后台集成?【低调赠送:QQ高仿版GGTalk 5.1 最新源码】

    像QQ这样的即时通信软件,时不时就会从桌面的右下角弹出一个小窗口,或是显示一个广告.或是一个新闻.或是一个公告等.在这里,我们将其统称为“全局系统通知”.很多使用GGTalk的朋友都建议我加上一个类似 ...

  6. 可在广域网部署运行的QQ高仿版 -- GG叽叽V3.7,优化视频聊天、控制更多相关细节

    在广域网中,由于网络的结构纷繁复杂.而且其实时状况又是千变万化的,所以,要使广域网中的视频聊天达到一个令人满意的效果,存在诸多挑战.这次发布的GG 3.7版本尝试在这一方向上做一些努力,据我自己测试, ...

  7. 即时通信系统中实现聊天消息加密,让通信更安全【低调赠送:C#开源即时通讯系统(支持广域网)——GGTalk4.5 最新源码】

    在即时通讯系统(IM)中,加密重要的通信消息,是一个常见的需求.尤其在一些政府部门的即时通信软件中(如税务系统),对即时聊天消息进行加密是非常重要的一个功能,因为谈话中可能会涉及到机密的数据.我在最新 ...

  8. 仿爱奇艺视频,腾讯视频,搜狐视频首页推荐位轮播图(二)之SuperIndicator源码分析

    转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼:http://blog.csdn.net/hejjunlin/article/details/52510431 背景:仿爱奇艺视频,腾讯视频 ...

  9. Android 音视频深入 十七 FFmpeg 获取RTMP流保存为flv (附源码下载)

    项目地址https://github.com/979451341/RtmpSave 这个项目主要代码我是从雷神那弄过来的,不愧是雷神,我就配个环境搞个界面就可以用代码了. 这一次说的是将RTMP流媒体 ...

随机推荐

  1. Qt 程序访问 sqlite 权限错误

    在Linux桌面上开发应用,想要拥有root权限,可是又需要弹窗申请.所以尽量避免这种情况发生. 另外:gksu,pkexec可以提供gui的root权限索取功能. 因为db文件是安装的时候放到etc ...

  2. [linux] 默认权限修改(umask)

    1 文件默认权限 对于目录,默认权限=777-umask 对于文件,默认权限=666-umask(文件默认无执行权限) 默认权限修改: vim /etc/bashrc 71行是普通用户的更改,73是超 ...

  3. survey on Time Series Analysis Lib

    (1)I spent my 4th year Computing project on implementing time series forecasting for Java heap usage ...

  4. jsp中,个别乱码进行转码操作

    来自大神 if(xh!=null && xh!=""){ xhmc =new String(xh.getBytes("ISO-8859-1"), ...

  5. C#图片保存到本地

    /// <summary> /// 上传微信头像到服务器 /// </summary> /// <param name="imgUrl">< ...

  6. HTML字体及颜色设置

    字体(FONT)标记(TAGS) 标题字体(Header) <h#> ... </h#> #=1, 2, 3, 4, 5, 6<h1>今天天气真好!</h1& ...

  7. js定义多行字符串

    js本身没有提供类似的定义方式,但是可以通过多行注释(/* */),已经借助function的方式来达到多行字符的定义,例如代码: var jstr = function() { var fun = ...

  8. 深入研究C语言 第三篇

    本篇研究TC2.0下其他几个工具.同时看看TC由源代码到exe程序的过程. 1. 用TCC将下面的程序编为.obj文件 我们知道,TCC在默认的编译连接一个C语言的源程序a.c的时候分为以下两步: ( ...

  9. 开启梦幻般的webrtc之旅

    废话不多说,直接上demo <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...

  10. Linux echo, sort, sed 等一些命令总结

    linux echo, sort, sed是初学linux shell script 的一些常用的命令.基本上来说,如果能够掌握了这些命令,我们就能写出一些不错的linux脚本.以下是我遇到的以下常用 ...