最近這幾年,我作過許多的網絡語音視頻類項目,包括遠程監控、即時語音視頻通信、網絡語音視頻教學、語音視頻會議室等等。一開始作的時候,不少問題都須要費大量的周折去思考、去嘗試。可是時至今日,不少通常性的東西,成爲了本身的技術沉澱。一些思路和方案,我想在這裏分享給你們。 網絡
public interface IChatGroupEntrance { /// <summary> /// 加入某個聊天組。若是目標組不存在,將自動建立目標組。 /// </summary> /// <param name="chatType">聊天組的類型。</param> /// <param name="chatGroupID">目標組ID。</param> IChatGroup Join(ChatType chatType ,string chatGroupID); /// <summary> /// 離開聊天組。若是掉線,也會自動從聊天組中退出。 /// </summary> /// <param name="chatType">聊天組的類型。</param> /// <param name="chatGroupID">目標組ID。</param> void Exit(ChatType chatType, string chatGroupID); }
這個接口給予了多人音視頻的通常性支持。ChatType分爲兩種:ide
public enum ChatType { /// <summary> /// 語音聊天組。 /// </summary> Audio = 0, /// <summary> /// 視頻聊天組。 /// </summary> Video }
因此<chatType , chatGroupID>這樣一個元組,標誌了某一個特定的聊天組。加入一個聊天組,就覺得這你們進入到一個聊天室,能夠相互的對話,濟濟一堂。調用IChatGroupEntrance 的Join方法加入某個聊天組,方法會返回一個IChatGroup引用,它表明了目標聊天組。ui
public interface IChatGroup { /// <summary> /// 當有新成員加入聊天組時,將觸發此事件。 /// </summary> event CbGeneric<IChatUnit> SomeoneJoin; /// <summary> /// 當某成員掉線或離開聊天組時,觸發此事件。 /// </summary> event CbGeneric<string> SomeoneExit; /// <summary> /// 聊天組的ID。 /// </summary> string GroupID { get; } /// <summary> /// 聊天組的類型。若是爲語音聊天,則DynamicCameraConnector爲null。 /// </summary> ChatType ChatType { get; } /// <summary> /// 獲取組成員的信息。 /// </summary> IChatUnit GetMember(string memberID); /// <summary> /// 獲取組內除本身以外的其它成員的信息。 /// </summary> List<IChatUnit> GetOtherMembers(); }
加入到這樣一個組以後,不只能夠完成聊天室的功能,並且能夠獲知其餘組友的上下線狀況。並且能夠獲知到其餘組友的相關信息。this
下面是核心的調用語句。spa
遠程監控,就是監控方對其餘加入組中的用戶發起鏈接,從而監測到其音頻視頻。 code
在線教學就是除了老師以外的其餘組員,都對老師發起音視頻鏈接。視頻
視頻會議的鏈接關係看起來複雜,實際上就是每一個組員都對其餘組員發起鏈接。blog
以上所說到的組的概念不一樣於QQ這種,而是相似於臨時進入的聊天房間。概括起來,有兩點:接口
1.是否持久化到外存,直觀的判別依據就是服務端重啓後原先的關係是否還在。事件
2.是按需工做,仍是事先準備好,具體到組關係來講,按需工做一般意味着掉線時退組、全部組員退組後銷燬該組等邏輯
如下是我封裝的一個語音聊天控件,是一個核心的控件,裏面封裝了以上所說的這些邏輯。
public partial class SpeakerPanel : UserControl ,IDisposable { private ChatUnit chatUnit; public SpeakerPanel() { InitializeComponent(); this.SetStyle(ControlStyles.ResizeRedraw, true);//調整大小時重繪 this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);// 雙緩衝 this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);// 禁止擦除背景. this.SetStyle(ControlStyles.UserPaint, true);//自行繪製 this.UpdateStyles(); } public string MemberID { get { if (this.chatUnit == null) { return null; } return this.chatUnit.MemberID; } } public void Initialize(ChatUnit unit) { this.chatUnit = unit; this.skinLabel_name.Text = unit.MemberID; this.chatUnit.MicrophoneConnector.ConnectEnded += new CbGeneric<OMCS.Passive.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); } public void Initialize(string curUserID) { this.skinLabel_name.Text = curUserID; this.skinLabel_name.ForeColor = Color.Red; this.pictureBox_Mic.Visible = false; this.decibelDisplayer1.Visible = false; } 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 connectResult; void MicrophoneConnector_ConnectEnded(ConnectResult res) { if (this.InvokeRequired) { this.BeginInvoke(new CbGeneric<ConnectResult>(this.MicrophoneConnector_ConnectEnded), res); } else { this.connectResult = res; this.ShowMicState(); } } public void Dispose() { this.chatUnit.Close(); } private void ShowMicState() { if (this.connectResult != OMCS.Passive.ConnectResult.Succeed) { this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[2]; this.toolTip1.SetToolTip(this.pictureBox_Mic, this.connectResult.ToString()); } else { this.decibelDisplayer1.Working = false; if (!this.chatUnit.MicrophoneConnector.OwnerOutput) { this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[1]; this.toolTip1.SetToolTip(this.pictureBox_Mic, "好友禁用了麥克風"); return; } if (this.chatUnit.MicrophoneConnector.Mute) { this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[1]; this.toolTip1.SetToolTip(this.pictureBox_Mic, "靜音"); } else { this.pictureBox_Mic.BackgroundImage = this.imageList1.Images[0]; this.toolTip1.SetToolTip(this.pictureBox_Mic, "正常"); this.decibelDisplayer1.Working = true; } } } private void pictureBox_Mic_Click(object sender, EventArgs e) { if (!this.chatUnit.MicrophoneConnector.OwnerOutput) { return; } this.chatUnit.MicrophoneConnector.Mute = !this.chatUnit.MicrophoneConnector.Mute; this.ShowMicState(); } }
(1)在代碼中,ChatUnit就表明當前這個聊天室中的成員。咱們使用其MicrophoneConnector鏈接到目標成員的麥克風。
(2)預約MicrophoneConnector的AudioDataReceived事件,當收到語音數據時,將其交給DecibelDisplayer去顯示聲音的大小。
(3)預約MicrophoneConnector的ConnectEnded和OwnerOutputChanged事件,根據其結果來顯示SpeakerPanel空間上麥克風圖標的狀態(對應ShowMicState方法)。
以上的這些介紹不免掛一漏萬,想要深刻了解的朋友能夠下載源碼進行研究。
我把基礎的內容都濃縮到了Demo中,你們掌握起來也會更加容易。