C#錄製視頻聊天

 前段時間做個項目,客戶需要將視頻對話的整個過程錄製下來,這樣,以後就可以隨時觀看。想來錄製整個視頻聊天的過程這樣的功能應該是個比較常見的需求,比如,基於網絡語音視頻的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]。  

如果你覺得還不錯,請粉我,順便再頂一下啊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章