前段時間作個項目,客戶須要將視頻對話的整個過程錄製下來,這樣,之後就能夠隨時觀看。想來錄製整個視頻聊天的過程這樣的功能應該是個比較常見的需求,好比,基於網絡語音視頻的1:1的英語口語輔導,若是能將輔導的整個過程錄製下來生成一個標準的MP4文件,就是一份可貴的資料,便於之後複習和分享。我將1:1的視頻對話錄製的功能實現爲了一個組件VideoChatRecorder,方便你們複用。而且,我在GG的最新版本4.3中使用了它,這樣GG也有了視頻聊天錄製的功能。html
(想要直接下載體驗的朋友請點擊:「下載中心」 )算法
若是你們已經作過相似錄製單我的的攝像頭和麥克風程序的話,那麼,錄製兩人視頻聊天就會遇到兩個新的難點:緩存
(1)如何將兩我的的視頻圖像整合成一個圖像?網絡
(2)如何將兩我的的聲音混成一路?ide
經過.NET提供的GDI+技術,咱們能夠將兩張圖片合成一張。在實現VideoChatRecorder組件時,我合成圖片所採用的規則是這樣的:佈局
(1)將對方的視頻做爲錄製的主體,而本身的視頻則覆蓋在對方視頻的右下角。this
(2)對方視頻的大小,就是其攝像頭的採集分辨率,依據(1),咱們知道這也是錄製生成的MP4文件播放時視頻的Size。spa
(3)合成後本身視頻圖像的寬和高,設定爲對方視頻寬和高的 1/3。線程
合成後的視頻的示意圖以下所示:code
咱們能夠手動將本身的聲音與對方的聲音混音成一路,網上能夠搜到不少混音算法(如直接相加法、平均法、歸一化算法、衰減因子法等),可是,混音算法的好壞直接關係到混音最終的質量。
還有一種更簡單的方案,就是直接使用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的最新版本中使用了上述的VideoChatRecorder類進行視頻聊天錄製以生成的MP4文件(默認是在運行目錄下名稱爲 VideoChat.mp4 的文件),用QQ影音播放器進行播放這個文件,其效果以下所示:
________________________________________________________________________
歡迎和我探討關於 GG 和 GGMeeting 的一切,個人QQ:2027224508,多多交流!
你們有什麼問題和建議,能夠留言,也能夠發送email到我郵箱:2027224508@qq.com。
若是你以爲還不錯,請粉我,順便再頂一下啊