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开发手册系列文章。
Q Q:168757008