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


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

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

        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中,大家掌握起来也会更加容易。

    音频聊天室

    视频聊天室

  • 相关阅读:
    会员管理软件
    正则表达式查找未记录的异常
    网络通信 数据压缩后发送
    SQL 工具系列一
    P5443 [APIO2019]桥梁 [分块+并查集]
    #6499. 「雅礼集训 2018 Day2」颜色 [分块,倍增,bitset]
    CF594D REQ [离线+树状数组,欧拉函数]
    雅礼集训板刷合集
    [HNOI2016]网络 [树链剖分,可删除堆]
    「BZOJ3065」带插入区间K小值 [分块]
  • 原文地址:https://www.cnblogs.com/woyipiaolingjiu/p/6703111.html
Copyright © 2020-2023  润新知