OMCS Demo -- 实现视频聊天室(多人视频聊天)

   多人视频聊天,或视频聊天室,也是即时通信应用中常见的功能之一,比如,QQ的群视频就是我们用得比较多的。

      本文将基于OMCS 实现一个简单的视频聊天室(PC端、手机端),让多个人可以进入同一个房间进行语音视频沟通。当然,在此之前,您必须对OMCS有所了解,并且已经阅读、理解了OMCS 开发手册(08) -- 多人语音/视频 这篇文章的内容。先看看PC端 Demo运行效果截图: 

        

      Demo运行时,会自动在大视窗显示正在发言人的视频,而非发言人的视频则在下方以小视窗显示。 

一. C/S结构

  很明显,我这个视频聊天室采用的是C/S结构,整个项目结构相对比较简单,如下所示:

      

  同语音聊天室一样,该项目的底层也是基于OMCS构建的。这样,服务端就基本没写代码,直接把OMCS服务端拿过来用;客户端就比较麻烦些,下面我就重点讲客户端的开发。

二. 客户端控件式开发

  客户端开发了多个自定义控件,然后将它们组装到一起,以完成视频聊天室的功能。为了便于讲解,我主界面的图做了标注,以指示出各个自定义控件。  

  现在我们分别介绍各个控件:

1.视频显示控件 VideoPanel

  VideoPanel用于表示聊天室中的一个成员,如上图中1所示。它显示了成员的ID,以及其麦克风的状态(启用、禁用)、摄像头的状态(不可用、正常、禁用)、成员的视频等。

  这个控件很重要,我将其源码贴出来:

    public partial class VideoPanel : UserControl
    {
        /// <summary>
        /// 该事件用于支持双击小视频窗口,全屏显示该成员的图像。
        /// </summary>
        public event CbGeneric<VideoPanel, IChatUnit> VideoDoubleClicked;
        public VideoPanel()
        {
            InitializeComponent();
        }
        public string MemberID
        {
            get
            {
                if (this.chatUnit == null)
                {
                    return null;
                }

                return this.chatUnit.MemberID;
            }
        }

    public DynamicCameraConnector DynamicCameraConnector
    {
        get { return this.chatUnit.DynamicCameraConnector;}
    }

    /// <summary>
    /// 初始化成员视频显示控件。
    /// </summary>    
    public void Initialize(IChatUnit unit, bool myself)
    {
        this.pictureBox_Camera.RenderSize = new Size(32, 32);
        this.pictureBox_Mic.RenderSize = new Size(24, 24);
        this.chatUnit = unit;
        this.isMySelf = myself;   
        this.toolStrip1.Content = chatUnit.MemberID; 
        //初始化麦克风连接器
        this.chatUnit.MicrophoneConnector.Mute = myself;
        this.chatUnit.MicrophoneConnector.SpringReceivedEventWhenMute = myself;
        this.chatUnit.MicrophoneConnector.ConnectEnded += new CbGeneric<string, ConnectResult>(MicrophoneConnector_ConnectEnded);
        this.chatUnit.MicrophoneConnector.OwnerOutputChanged += new CbGeneric(MicrophoneConnector_OwnerOutputChanged);
        this.chatUnit.MicrophoneConnector.BeginConnect(unit.MemberID);
            
        //初始化摄像头连接器
        this.chatUnit.DynamicCameraConnector.SetViewer(this.cameraPanel1);
        this.chatUnit.DynamicCameraConnector.VideoDrawMode = VideoDrawMode.Scale;
        this.chatUnit.DynamicCameraConnector.ConnectEnded += new CbGeneric<string, ConnectResult>(DynamicCameraConnector_ConnectEnded);
        this.chatUnit.DynamicCameraConnector.OwnerOutputChanged += new CbGeneric<string>(DynamicCameraConnector_OwnerOutputChanged);
        this.chatUnit.DynamicCameraConnector.Disconnected += new CbGeneric<string, ConnectorDisconnectedType>(DynamicCameraConnector_Disconnected);
        this.chatUnit.DynamicCameraConnector.BeginConnect(unit.MemberID);
    }

    void DynamicCameraConnector_Disconnected(string ownerID, ConnectorDisconnectedType disconnectedType)
    {
        if (!Dispatcher.CheckAccess())
        {
     Dispatcher.BeginInvoke(new CbGeneric<string, ConnectorDisconnectedType>(this.DynamicCameraConnector_Disconnected), ownerID, disconnectedType);
        }
        else
        {
            this.label_tip.Content = disconnectedType.ToString();
            this.label_tip.Visibility = Visibility.Visible;
        }
    }

    //好友启用或禁用摄像头
    void DynamicCameraConnector_OwnerOutputChanged(string ownerID)
    {
        if (!Dispatcher.CheckAccess())
        {
            Dispatcher.BeginInvoke(new CbGeneric<string>(this.DynamicCameraConnector_OwnerOutputChanged), ownerID);
        }
        else
        {
            this.ShowCameraState();
        }
    }

    private ConnectResult connectCameraResult;
    //摄像头连接器尝试连接的结果
    void DynamicCameraConnector_ConnectEnded(string ownerID, ConnectResult res)
    {
        if (!Dispatcher.CheckAccess())
        {
            Dispatcher.BeginInvoke(new CbGeneric<string, ConnectResult>(this.DynamicCameraConnector_ConnectEnded), ownerID, res);
        }
        else
        {
            if (res == ConnectResult.Succeed)
            {
                if (this.chatUnit.DynamicCameraConnector.OwnerMachineType == OMCS.Passive.Video.MachineType.Android ||
                    this.chatUnit.DynamicCameraConnector.OwnerMachineType == OMCS.Passive.Video.MachineType.IOS)
                {
                    this.chatUnit.DynamicCameraConnector.VideoDrawMode = VideoDrawMode.Scale;
                }
        }
        this.label_tip.Visibility = Visibility.Hidden;
        this.connectCameraResult = res;
        this.ShowCameraState();
        }
    }

    /// <summary>
    /// 综合显示摄像头的状态。
    /// </summary>
    private void ShowCameraState()
    {
        if (this.connectCameraResult != OMCS.Passive.ConnectResult.Succeed)
        {
            this.pictureBox_Camera.Source = null;
            this.pictureBox_Camera.Source = new BitmapImage(new Uri("../Resources/cameraFail.png", UriKind.Relative)); 
        }
        else
        {
            this.pictureBox_Camera.Visibility = this.chatUnit.DynamicCameraConnector.OwnerOutput ? Visibility.Hidden : Visibility.Visible;
            if (!this.chatUnit.DynamicCameraConnector.OwnerOutput)
            {
                this.pictureBox_Camera.Source = new BitmapImage(new Uri("../Resources/cameraDis.png", UriKind.Relative));
                return;
           }
        }
    }
   //好友启用或禁用麦克风
    void MicrophoneConnector_OwnerOutputChanged()
    {
        if (!Dispatcher.CheckAccess())
        {
            Dispatcher.BeginInvoke(new CbGeneric(this.MicrophoneConnector_OwnerOutputChanged));
        }
        else
        {
            this.ShowMicState();
        }
    }

    private ConnectResult connectMicResult;
    //麦克风连接器尝试连接的结果
    void MicrophoneConnector_ConnectEnded(string ownerID, ConnectResult res)
    {
        if (!Dispatcher.CheckAccess())
        {
            Dispatcher.BeginInvoke(new CbGeneric<string, ConnectResult>(this.MicrophoneConnector_ConnectEnded), ownerID, res);
        }
        else
        {
            this.connectMicResult = res;
            this.ShowMicState();
        }
    }

    /// <summary>
    /// 综合显示麦克风的状态。
    /// </summary>
    private void ShowMicState()
    {
        if (this.connectMicResult != OMCS.Passive.ConnectResult.Succeed)
        {
            this.pictureBox_Mic.Visibility = Visibility.Visible;
            MessageBox.Show(this.connectMicResult.ToString(), "ERROR");
        }
        else
        {
            this.pictureBox_Mic.Visibility = this.chatUnit.MicrophoneConnector.OwnerOutput ? Visibility.Hidden : Visibility.Visible;
            if (!this.chatUnit.MicrophoneConnector.OwnerOutput)
            {
                this.pictureBox_Mic.Source = new BitmapImage(new Uri("./Resources/micDis.png", UriKind.Relative)); 
                return;
            }

            this.pictureBox_Mic.Visibility = isMySelf ? Visibility.Hidden : Visibility.Visible;
            if (this.chatUnit.MicrophoneConnector.Mute)
            {
                this.pictureBox_Mic.Source = new BitmapImage(new Uri("./Resources/micDis.png", UriKind.Relative));
            }
            else
            {
                this.pictureBox_Mic.Visibility = Visibility.Hidden;
                this.pictureBox_Mic.Source = new BitmapImage(new Uri("./Resources/micNor.png", UriKind.Relative));
            }
        }
    }

    public void ResetViewer()
    {
        this.chatUnit.DynamicCameraConnector.SetViewer(this.cameraPanel1);
    }

    private void cameraPanel1_MouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
        if (this.VideoDoubleClicked != null)
        {
            this.VideoDoubleClicked(this, this.chatUnit);
        }
    }
}
 

(1)在代码中,IChatUnit就代表当前这个聊天室中的成员。我们使用其MicrophoneConnector连接到目标成员的麦克风、使用其DynamicCameraConnector连接到目标成员的摄像头。

(2)预定MicrophoneConnector的ConnectEnded和OwnerOutputChanged事件,根据其结果来显示VideoPanel控件上麦克风图标的状态(对应ShowMicState方法)。

(3)预定DynamicCameraConnector的ConnectEnded和OwnerOutputChanged事件,根据其结果来显示VideoPanel控件上摄像头图标的状态(对应ShowCameraState方法)。 

3. MultiVideoChatContainer 控件

  MultiAudioChatContainer对应上图中2标注的控件,它主要做了以下几件事情:

(1)在初始化时,加入聊天室:通过调用IMultimediaManager的ChatGroupEntrance属性的Join方法。

(2)使用FlowLayoutPanel将聊天室中每个成员对应的VideoPanel罗列出来。

(3)通过下图中的图标勾选按钮,可以选择大视窗是否自动显示发言人。两种情况如下:

        

        a. 默认勾选,当勾选上时,且有两个及以上成员加入视频聊天室时,会调用IMultimediaManager的MaxVoiceSpeakerNotified方法,每秒触发五次,每五次为一组检测说话声音最大且出现次数最多的成员,并将其视频图像显示到聊天室的大视窗如图中4(当只有一个成员时,默认将其视频放在聊天室的上方大屏)。

  private IChatUnit preSpeakerUnit;
  private int speakerCount = 0;
  private int count = 0;
  private IChatUnit speakerUnit;
  private int maxCount;
  private void MultimediaManager_MaxVoiceSpeakerNotified(string speakerID, int decibel)
  {
    //判断是否勾选
    if (!this.IsBigCheakBox)
    {
      return;
    }
    if (speakerID == null)
    {
      return;
    }
    IChatUnit unit = this.chatGroup.GetMember(speakerID);
    if (unit == null)
    {
      return;
    }
    speakerCount++;
    count++;
    //记录五次内声音最大发言人出现次数
    countManager.Add(speakerID,count);
    if (speakerCount != 5)
    {
      return;
    }
    speakerCount = 0;
    List<string> ids = new List<string>();
    //循环判断是否为出现次数最多 声音最大的发言人
    foreach (var item in countManager.GetKeyList())
    {
      int currentCount = countManager.Get(item);
      if (currentCount == maxCount)
      {
        ids.Add(item);
      }
      if (currentCount > maxCount)
      {  
        ids.Clear();
        maxCount = currentCount;
        ids.Add(item);
      }  
    }
    if(ids.Count > 1)
    {
      return;
    }
    speakerUnit = this.chatGroup.GetMember(ids[0]);
    maxCount = 0;
    count = 0;
    countManager.Clear();
    if (preSpeakerUnit != null && preSpeakerUnit.MemberID == speakerUnit.MemberID)
    {
      return;
    }
    VideoPanel panel = speakerUnit.Tag as VideoPanel;
           
    panel.cameraPanel1.ClearImage();
    speakerUnit.DynamicCameraConnector.SetViewer(this.cameraPanelSpeak);
    if (preSpeakerUnit != null && preSpeakerUnit.Tag != null)
    {
      VideoPanel rePanel = preSpeakerUnit.Tag as VideoPanel;
      rePanel.ResetViewer();
    }
    preSpeakerUnit = speakerUnit;
    if (!Dispatcher.CheckAccess())
    {
      Dispatcher.BeginInvoke(new CbGeneric<string>(this.SetSpeakerId),speakerUnit.MemberID);
    }            
  }

        b. 点击取消勾选时,如下图所示,弹窗提示。当勾选上时,且有两个及以上成员加入视频聊天室时,双击当前在小视窗显示视频的任意一个成员的VideoPanel时,会触发双击事件,将其视频显示到大视窗。

    

void panel_VideoDoubleClicked(VideoPanel videoPanel, IChatUnit chatUnit)
{
  // 判断是否勾选
  if ( !this.IsBigCheakBox)
  {
    VideoPanel rePanel = preSpeakerUnit.Tag as VideoPanel;
    videoPanel.cameraPanel1.ClearImage();
    if(preSpeakerUnit.MemberID == chatUnit.MemberID || preSpeakerUnit == null)
    {  
      return;
    }
    rePanel.ResetViewer();
    chatUnit.DynamicCameraConnector.SetViewer(
this.cameraPanelSpeak);     preSpeakerUnit = chatUnit;     if (!Dispatcher.CheckAccess())     {       Dispatcher.BeginInvoke(new CbGeneric<string>(this.SetSpeakerId), chatUnit.MemberID);     }   } }

(4)当有成员加入或退出聊天室时(对应ChatGroup的SomeoneJoin和SomeoneExit事件),动态添加或移除对应的VideoPanel实例。

(5)通过图中3中的图标按钮,我们可以启用或禁用我们自己的摄像头、麦克风或扬声器。

(6)注意,其提供了Close方法,这意味着,在关闭包含了该控件的宿主窗体时,要调用其Close方法以释放其内部持有的麦克风连接器、摄像头连接器等资源。

  在完成MultiAudioChatContainer后,我们这个聊天室的核心就差不多了。接下来就是弄个主窗体,然后把MultiVideoChatContainer拖上去,初始化IMultimediaManager,并传递给MultiVideoChatContainer就大功告成了。

三. 源码下载

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

服务端+PC 端: VideoChatRoom.rar  (该Demo的 PC端有WPF版本和Winform版本,任选其一即可。)       

   Android 端: VideoChatRoom_Android.rar

         Web 端:  VideoChatRoom_Web.rar       

        对于本Demo 的 Web端,如果尚未安装OMCS视频服务Web插件,网页会自动提示下载安装。完成安装后,刷新网页,会提示启动插件,点击同意后,再次刷新网页即可。

 

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

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

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

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

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

  

下载免费版本的OMCS以及 demo源码

阅读 更多OMCS开发手册系列文章

Q Q:168757008

官网:www.oraycn.com

导航

首页

官方网站

联系我们

立即咨询 

站内搜索

OrayTalk 企业即时通讯系统

傲瑞通官网

详细说明

客户端下载

OrayMeeting 视频会议系统

详细说明

客户端下载

ESFramework 通信框架

详细说明

SDK与Demo下载

ESFramework FAQ

版本变更记录

OMCS 语音视频框架

详细说明

SDK与Demo下载

OMCS FAQ

版本变更记录

OVCS 视频会议Demo

详细说明

源码下载

傲瑞实用组件

SDK下载

H5Media 纯网页音视频交互

NPusher 推流组件

MCapture 语音视频采集组件

MFile 语音视频录制组件

MPlayer 语音视频播放组件

OAUS 自动升级系统

StriveEngine 轻量级的通信引擎

傲瑞组件 FAQ

授权

授权流程

产品选购指南

授权方案说明

授权SDK使用说明

其它

支持信创国产化

SDK使用技巧

联系我们