前段时间做个项目,客户需要将视频对话的整个过程录制下来,这样,以后就可以随时观看。想来录制整个视频聊天的过程这样的功能应该是个比较常见的需求,比如,基于网络语音视频的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 / 3, this.videoSize.Height / 3); 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, 10, AudioCodecType.AAC, 16000, 1, 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(100); } } /// <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是可在广域网部署运行的QQ高仿版,2013.8.7发布V1.0版本,至今最新是4.3版本,关于GG更详细的介绍,可以查看 可在广域网部署运行的QQ高仿版 -- GG2013概要。
在GG的最新版本中使用了上述的VideoChatRecorder类进行视频聊天录制以生成的MP4文件(默认是在运行目录下名称为 VideoChat.mp4 的文件),用QQ影音播放器进行播放这个文件,其效果如下所示:
GG 4.3 下载:GG-V4.3.rar
________________________________________________________________________
欢迎和我探讨关于 GG 和 GGMeeting 的一切,我的QQ:2027224508,多多交流!
大家有什么问题和建议,可以留言,也可以发送email到我邮箱:[email protected]。
如果你觉得还不错,请粉我,顺便再顶一下啊