实现一个简单的视频聊天室(源码)

简介: 在 《实现一个简单的语音聊天室》一文发布后,很多朋友建议我也实现一个视频聊天室给他们参考一下,其实,视频聊天室与语音聊天室的原理是差不多的,由于加入了摄像头、视频的处理,逻辑会繁杂一些,本文就实现一个简单的多人视频聊天系统,让多个人可以进入同一个房间进行语音视频沟通。

       在 《实现一个简单的语音聊天室》一文发布后,很多朋友建议我也实现一个视频聊天室给他们参考一下,其实,视频聊天室与语音聊天室的原理是差不多的,由于加入了摄像头、视频的处理,逻辑会繁杂一些,本文就实现一个简单的多人视频聊天系统,让多个人可以进入同一个房间进行语音视频沟通。先看看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就大功告成了。

三. 源码下载

  上面只是讲了实现多人视频聊天室中的几个重点,并不全面,大家下载下面的源码可以更深入的研究。

  VideoChatRoom.rar  

  最后,跟大家说说部署的步骤:

(1)将服务端部署在一台机器上,启动服务端。

(2)修改客户端配置文件中的ServerIP为刚才服务器的IP。

(3)在多台机器上运行客户端,以不同的帐号登录到同一个房间(如默认的R1000)。

(4)如此,多个用户就处于同一个聊天室进行视频聊天了。

 

目录
相关文章
makefile include 使用介绍
makefile include 使用介绍
346 0
|
存储 运维 关系型数据库
Cloudreve 自建云盘实践,我说了没人能限得了我的容量和速度!
一、前言 二、Cloudreve 介绍 🔉 功能 ✨ 特性 📌 资料 三、环境准备 四、宝塔配置 1. 获取用户名和密码 2. 8888 端口授权 3. 登录宝塔后台 五、服务安装 1. 在宝塔终端查看服务内核 2. 下载和安装 3. 开放端口 5212 4. 登录服务 六、进程守护 1. Supervisor 配置 2. Supervisor 启动 七、配置域名 1. 解析域名 2. 反向代理 八、数据库切换 九、总结 十、系列推荐
2880 0
Cloudreve 自建云盘实践,我说了没人能限得了我的容量和速度!
|
安全 关系型数据库 MySQL
Linux 实用小脚本系列(2)----mysql安全初始化脚本的免交互执行--mysql_secure_installation
Linux 实用小脚本系列(2)----mysql安全初始化脚本的免交互执行--mysql_secure_installation
422 0
|
C# UED SEO
C# 异步方法async / await任务超时处理
通过使用 `Task.WhenAny`和 `Task.Delay`方法,您可以在C#中有效地实现异步任务的超时处理机制。这种方法允许您在指定时间内等待任务完成,并在任务超时时采取适当的措施,如抛出异常或执行备用操作。希望本文提供的详细解释和代码示例能帮助您在实际项目中更好地处理异步任务超时问题,提升应用程序的可靠性和用户体验。
525 3
|
Java 关系型数据库 MySQL
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第4天】本文分析了Spring Boot应用在连接数据库时可能遇到的问题及其解决方案。主要从四个方面探讨:配置文件格式错误、依赖缺失或版本不兼容、数据库服务问题、配置属性未正确注入。针对这些问题,提供了详细的检查方法和调试技巧,如检查YAML格式、验证依赖版本、确认数据库服务状态及用户权限,并通过日志和断点调试定位问题。
1307 6
|
负载均衡 网络协议 算法
|
数据采集 存储 数据挖掘
如何利用Python进行网页数据抓取
本文将详细介绍如何使用Python进行网页数据抓取。首先,我们将了解什么是网络爬虫以及其基本原理。然后,逐步讲解如何使用Python的requests库和BeautifulSoup库来抓取和解析网页数据。最后,通过实例展示如何实际应用这些技术来获取所需的数据并进行简单的数据处理。希望通过这篇文章,读者能够掌握基本的网页数据抓取技巧,并能在实际应用中灵活运用。
1027 1
|
运维 负载均衡 算法
“分布式基础概念”全面解析,让你秒懂分布式系统!【一】
该博客文章全面解析了分布式系统的基础概念,包括微服务架构、集群与分布式的区别、节点定义、远程调用、负载均衡、服务注册与发现、配置中心、服务熔断与降级以及API网关,帮助读者快速理解分布式系统的关键组成部分和工作原理。
“分布式基础概念”全面解析,让你秒懂分布式系统!【一】
|
C++
VS、ReSharper 设置修改代码颜色、提高代码辨识度!附VS超实用快捷!
ReSharper 配置代码颜色 本文提供全流程,中文翻译。 Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 —— 高分辨率用户请根据需求调整网页缩放比例) ...
5153 0
strstr(str1,str2) 函数与sscanf()函数功能详解
strstr(str1,str2) 函数与sscanf()函数功能详解
412 0