在 《實現一個簡單的語音聊天室》一文發布后,很多朋友建議我也實現一個視頻聊天室給他們參考一下,其實,視頻聊天室與語音聊天室的原理是差不多的,由于加入了攝像頭、視頻的處理,邏輯會繁雜一些,本文就實現一個簡單的多人視頻聊天系統,讓多個人可以進入同一個房間進行語音視頻溝通。先看看3個人進行視頻聊天的運行效果截圖:
上面兩張截圖分別是:登錄界面、標注了各個控件的視頻聊天室的主界面。
一. C/S結構
很明顯,我這個語音聊天室采用的是C/S結構,整個項目結構相對比較簡單,如下所示:
同語音聊天室一樣,該項目的底層也是基于OMCS構建的。這樣,服務端就基本沒寫代碼,直接把OMCS服務端拿過來用;客戶端就比較麻煩些,下面我就重點講客戶端的開發。
二. 客戶端控件式開發
客戶端開發了多個自定義控件,然后將它們組裝到一起,以完成視頻聊天室的功能。為了便于講解,我主界面的圖做了標注,以指示出各個自定義控件。
現在我們分別介紹各個控件:
1. 分貝顯示器
分貝顯示器用于顯示聲音的大小,比如麥克風采集到的聲音的大小,或揚聲器播放的聲音的大小。如上圖中2標注的。
(1)傅立葉變換
將聲音數據轉換成分貝強度使用的是傅立葉變換。其對應的是客戶端項目中的FourierTransformer靜態類。源碼比較簡單,就不貼出來了,大家自己去看。
(2)聲音強度顯示控件 DecibelDisplayer
DecibelDisplayer 使用的是PrograssBar來顯示聲音強度的大小。
每當有聲音數據交給DecibelDisplayer顯示時,首先,DecibelDisplayer會調用上面的傅立葉變換將其轉換為分貝,然后,將其映射為PrograssBar的對應的Value。
2.視頻顯示控件 VideoPanel
VideoPanel用于表示聊天室中的一個成員,如上圖中1所示。它顯示了成員的ID,成員的聲音的強度(使用DecibelDisplayer控件),以及其麥克風的狀態(啟用、禁用)、攝像頭的狀態(不可用、正常、禁用)、成員的視頻等。
這個控件很重要,我將其源碼貼出來:
public partial class VideoPanel : UserControl { private IChatUnit chatUnit; private bool isMySelf = false; public VideoPanel() { InitializeComponent(); } /// <summary> /// 初始化成員視頻顯示控件。 /// </summary> public void Initialize(IChatUnit unit ,bool myself) { this.chatUnit = unit; this.isMySelf = myself; this.toolStripLabel_displayName.Text = unit.MemberID; this.decibelDisplayer1.Visible = !myself; //初始化麥克風連接器 this.chatUnit.MicrophoneConnector.Mute = myself; this.chatUnit.MicrophoneConnector.SpringReceivedEventWhenMute = myself; this.chatUnit.MicrophoneConnector.ConnectEnded += new CbGeneric<ConnectResult>(MicrophoneConnector_ConnectEnded); this.chatUnit.MicrophoneConnector.OwnerOutputChanged += new CbGeneric(MicrophoneConnector_OwnerOutputChanged); this.chatUnit.MicrophoneConnector.AudioDataReceived += new CbGeneric<byte[]>(MicrophoneConnector_AudioDataReceived); this.chatUnit.MicrophoneConnector.BeginConnect(unit.MemberID); //初始化攝像頭連接器 this.chatUnit.DynamicCameraConnector.SetViewer(this.skinPanel1); this.chatUnit.DynamicCameraConnector.ConnectEnded += new CbGeneric<ConnectResult>(DynamicCameraConnector_ConnectEnded); this.chatUnit.DynamicCameraConnector.OwnerOutputChanged += new CbGeneric(DynamicCameraConnector_OwnerOutputChanged); this.chatUnit.DynamicCameraConnector.BeginConnect(unit.MemberID); } //好友啟用或禁用攝像頭 void DynamicCameraConnector_OwnerOutputChanged() { if (this.InvokeRequired) { this.BeginInvoke(new CbGeneric(this.DynamicCameraConnector_OwnerOutputChanged)); } else { this.ShowCameraState(); } } private ConnectResult connectCameraResult; //攝像頭連接器嘗試連接的結果 void DynamicCameraConnector_ConnectEnded(ConnectResult res) { if (this.InvokeRequired) { this.BeginInvoke(new CbGeneric<ConnectResult>(this.DynamicCameraConnector_ConnectEnded), res); } else { this.label_tip.Visible = false; this.connectCameraResult = res; this.ShowCameraState(); } } /// <summary> /// 綜合顯示攝像頭的狀態。 /// </summary> private void ShowCameraState() { if (this.connectCameraResult != OMCS.Passive.ConnectResult.Succeed) { this.pictureBox_Camera.BackgroundImage = null; this.pictureBox_Camera.BackgroundImage = this.imageList2.Images[2]; this.pictureBox_Camera.Visible = true; this.toolTip1.SetToolTip(this.pictureBox_Camera, this.connectCameraResult.ToString()); } else { this.pictureBox_Camera.Visible = !this.chatUnit.DynamicCameraConnector.OwnerOutput; if (!this.chatUnit.DynamicCameraConnector.OwnerOutput) { this.pictureBox_Camera.BackgroundImage = this.imageList2.Images[1]; this.toolTip1.SetToolTip(this.pictureBox_Camera, "攝像頭被主人禁用!"); return; } } } //將接收到的聲音數據交給分貝顯示器顯示 void MicrophoneConnector_AudioDataReceived(byte[] data) { this.decibelDisplayer1.DisplayAudioData(data); } //好友啟用或禁用麥克風 void MicrophoneConnector_OwnerOutputChanged() { if (this.InvokeRequired) { this.BeginInvoke(new CbGeneric(this.MicrophoneConnector_OwnerOutputChanged)); } else { this.ShowMicState(); } } private ConnectResult connectMicResult; //麥克風連接器嘗試連接的結果 void MicrophoneConnector_ConnectEnded(ConnectResult res) { if (this.InvokeRequired) { this.BeginInvoke(new CbGeneric<ConnectResult>(this.MicrophoneConnector_ConnectEnded), res); } else { this.connectMicResult = res; this.ShowMicState(); } } /// <summary> /// 綜合顯示麥克風的狀態。 /// </summary> private void ShowMicState() { if (this.connectMicResult != OMCS.Passive.ConnectResult.Succeed) { this.pictureBox_Mic.Visible = true; this.toolTip1.SetToolTip(this.pictureBox_Mic, this.connectMicResult.ToString()); } else { this.decibelDisplayer1.Working = false; this.pictureBox_Mic.Visible = !this.chatUnit.MicrophoneConnector.OwnerOutput; this.decibelDisplayer1.Visible = this.chatUnit.MicrophoneConnector.OwnerOutput && !this.isMySelf; if (!this.chatUnit.MicrophoneConnector.OwnerOutput) { this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[1]; this.toolTip1.SetToolTip(this.pictureBox_Mic, "麥克風被主人禁用!"); return; } this.pictureBox_Mic.Visible = !isMySelf; if (this.chatUnit.MicrophoneConnector.Mute) { this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[1]; this.toolTip1.SetToolTip(this.pictureBox_Mic, "靜音"); } else { this.pictureBox_Mic.Visible = false; this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[0]; this.toolTip1.SetToolTip(this.pictureBox_Mic, "正常"); this.decibelDisplayer1.Working = true; } } } /// <summary> /// 展開或收起視頻面板。 /// </summary> private void toolStripButton1_Click(object sender, EventArgs e) { try { if (this.Height > this.toolStrip1.Height) { this.toolStripButton1.Text = "展開"; this.toolStripButton1.Image = Resources.Hor; this.chatUnit.DynamicCameraConnector.SetViewer(null); this.Height = this.toolStrip1.Height; } else { this.toolStripButton1.Text = "收起"; this.toolStripButton1.Image = Resources.Ver;
this.chatUnit.DynamicCameraConnector.SetViewer(this.skinPanel1); } } catch (Exception ee) { MessageBox.Show(ee.Message); } } }
(1)在代碼中,IChatUnit就代表當前這個聊天室中的成員。我們使用其MicrophoneConnector連接到目標成員的麥克風、使用其DynamicCameraConnector連接到目標成員的攝像頭。
(2)預定MicrophoneConnector的AudioDataReceived事件,當收到語音數據時,將其交給DecibelDisplayer去顯示聲音的大小。
(3)預定MicrophoneConnector的ConnectEnded和OwnerOutputChanged事件,根據其結果來顯示VideoPanel控件上麥克風圖標的狀態(對應ShowMicState方法)。
(4)預定DynamicCameraConnector的ConnectEnded和OwnerOutputChanged事件,根據其結果來顯示VideoPanel控件上攝像頭圖標的狀態(對應ShowCameraState方法)。
3. MultiVideoChatContainer 控件
MultiAudioChatContainer對應上圖中3標注的控件,它主要做了以下幾件事情:
(1)在初始化時,加入聊天室:通過調用IMultimediaManager的ChatGroupEntrance屬性的Join方法。
(2)使用FlowLayoutPanel將聊天室中每個成員對應的VideoPanel羅列出來。
(3)當有成員加入或退出聊天室時(對應ChatGroup的SomeoneJoin和SomeoneExit事件),動態添加或移除對應的VideoPanel實例。
(4)通過CheckBox將自己設備(攝像頭、麥克風、揚聲器)的控制權暴露出來。我們可以啟用或禁用我們自己的麥克風或揚聲器。
(5)注意,其提供了Close方法,這意味著,在關閉包含了該控件的宿主窗體時,要調用其Close方法以釋放其內部持有的麥克風連接器、攝像頭連接器等資源。
在完成MultiAudioChatContainer后,我們這個聊天室的核心就差不多了。接下來就是弄個主窗體,然后把MultiVideoChatContainer拖上去,初始化IMultimediaManager,并傳遞給MultiVideoChatContainer就大功告成了。
三. 源碼下載
上面只是講了實現多人視頻聊天室中的幾個重點,并不全面,大家下載下面的源碼可以更深入的研究。
最后,跟大家說說部署的步驟:
(1)將服務端部署在一臺機器上,啟動服務端。
(2)修改客戶端配置文件中的ServerIP為剛才服務器的IP。
(3)在多臺機器上運行客戶端,以不同的帳號登錄到同一個房間(如默認的R1000)。
(4)如此,多個用戶就處于同一個聊天室進行視頻聊天了。
文章列表