項目開發經驗談:即時語音視頻通訊·網絡語音視頻教學·語音視頻會議室——做多了之后的技術沉淀分享


      最近這幾年,我做過許多的網絡語音視頻類項目,包括遠程監控、即時語音視頻通訊、網絡語音視頻教學、語音視頻會議室等等。一開始做的時候,很多問題都需要費大量的周折去思考、去嘗試。但是時至今日,很多一般性的東西,成為了自己的技術沉淀。一些思路和方案,我想在這里分享給大家。           

一.基礎的抽象——音頻視頻聊天組

    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分為兩種:

    public enum ChatType
    {
        /// <summary>
        /// 語音聊天組。
        /// </summary>
        Audio = 0,
        /// <summary>
        /// 視頻聊天組。
        /// </summary>
        Video
    }

      所以<chatType , chatGroupID>這樣一個元組,標志了某一個特定的聊天組。加入一個聊天組,就以為這大家進入到一個聊天室,可以相互的對話,濟濟一堂。調用IChatGroupEntrance 的Join方法加入某個聊天組,方法會返回一個IChatGroup引用,它代表了目標聊天組。

    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();        
    }

      加入到這樣一個組之后,不僅可以完成聊天室的功能,而且可以獲知其他組友的上下線情況。而且可以獲知到其他組友的相關信息。

      下面是核心的調用語句。

二.情景與邏輯

1.遠程監控

 

遠程監控,就是監控方對其他加入組中的用戶發起連接,從而監測到其音頻視頻。 

2.在線教學

 

在線教學就是除了老師之外的其他組員,都對老師發起音視頻連接。

3.視頻會議

 

視頻會議的連接關系看起來復雜,實際上就是每個組員都對其他組員發起連接。

三.動態組的概念

 以上所說到的組的概念不同於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中,大家掌握起來也會更加容易。

音頻聊天室

視頻聊天室

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM