项目开发经验谈:即时语音视频通信·网络语音视频教学·语音视频会议室——作多了以后的技术沉淀分享

      最近这几年,我作过许多的网络语音视频类项目,包括远程监控、即时语音视频通信、网络语音视频教学、语音视频会议室等等。一开始作的时候,不少问题都须要费大量的周折去思考、去尝试。可是时至今日,不少通常性的东西,成为了本身的技术沉淀。一些思路和方案,我想在这里分享给你们。           网络

一.基础的抽象——音频视频聊天组

    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

二.情景与逻辑

1.远程监控

 

远程监控,就是监控方对其余加入组中的用户发起链接,从而监测到其音频视频。 code

2.在线教学

 

在线教学就是除了老师以外的其余组员,都对老师发起音视频链接。视频

3.视频会议

 

视频会议的链接关系看起来复杂,实际上就是每一个组员都对其余组员发起链接。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中,你们掌握起来也会更加容易。

音频聊天室

视频聊天室

相关文章
相关标签/搜索