信創是現階段國家發展的重要戰略之一,面對這一趨勢,所有的軟體應用只有支援信創國產化的基礎軟硬體設施,在未來才不會被淘汰。那麼,如何可以使用C#來實現支援信創環境的視訊會議系統嗎?答案是肯定的。
本文講述如何使用C#來實現視訊會議系統的Linux服務端與Linux客戶端,並讓其支援國產作業系統(如銀河麒麟,統信UOS)和國產CPU(如鯤鵬、龍芯、海光、兆芯、飛騰等)。
先看看該Demo在統信UOS上的執行效果:
一.功能介紹
1.基本功能
(1)主持人:當進入同一房間的第一個使用者預設成為主持人,預設開啟麥克風。
(2)當進入會議房間的每個人,都能自由選擇是否開啟攝像頭、揚聲器和麥克風。
(3)當同一房間內無人開啟桌面共享時,所有使用者均可開啟桌面共享,供其他使用者觀看其桌面,同一時間內只允許一個使用者開啟桌面共享。
(4)當使用者為主持人時,可以選擇是否開啟電子白板;當主持人開啟電子白板後,所有使用者均可自由切換電子白板和會議影片。
(5)每個使用者的影片視窗上方均顯示聲音分貝條,根據聲音大小自動渲染。
(6)當使用者關閉攝像頭或者使用者數量超過9個,不顯示影片。
(7)所有使用者均可收發文字訊息,包括帶表情的文字訊息。
2.功能演示
在銀河麒麟上執行:
3.佈局風格
(1)當只有一個人開啟影片時,採用大視窗顯示
(2)當2~4人開啟影片時,使用2x2佈局
(3)當超過4人開啟影片時,使用3x3佈局
二.開發環境
1.開發工具:
Visual Studio 2022
2. 開發框架:
.NET Core 3.1,.NET 6,.NET 7
3.開發語言:
C#
4.其它框架:
CPF.net UI 框架、OMCS 語音影片框架
三.具體實現
1. 新使用者進入會議房間
(1)影片顯示視窗控制元件VideoPanel
預定SomeoneJoin事件,當新的使用者加入房間時,將觸發該事件:
this.chatGroup.SomeoneJoin += new CbGeneric<IChatUnit>(chatGroup_SomeoneJoin);
void chatGroup_SomeoneJoin(IChatUnit unit)
{
if (!Dispatcher.CheckAccess())
{
Dispatcher.BeginInvoke(new CbGeneric<IChatUnit>(this.chatGroup_SomeoneJoin), unit);
}
else
{
VideoPanel panel = new VideoPanel();
panel.Initialize(unit, false);
VideoHidePanel videoHidePanel = new VideoHidePanel();
videoHidePanel.Initialize(false, unit,false);
VideoAllPanel videoAllPanel;
videoAllPanel.videoPanel = panel;
videoAllPanel.videoHidePanel = videoHidePanel;
if (panel.ConnectCameraResult != ConnectResult.Succeed)
{
this.flowLayoutPanel2.Children.Insert(0, videoHidePanel);
videoHidePanel.cameraVisibilityChange(false);
}
else
{
if (this.isVideoShowBeyond)
{
this.flowLayoutPanel2.Children.Insert(0, videoHidePanel);
videoHidePanel.cameraVisibilityChange(true);
}
else
{
this.cameraViewbox.FlowLayoutPanel.Children.Insert(this.videoShowCount, panel);
}
}
this.TotalNumChange(this.chatGroup.GetOtherMembers().Count + 1);
panel.ChangeState += Panel_ChangeState;
panel.CameraStateChange += Panel_CameraStateChange;
panel.VideoByCount += Panel_VideoByCount;
panel.VideoConnect += Panel_VideoConnect;
unit.Tag = videoAllPanel;
this.VideoNumBeyond();
this.VideoSizeChange(new System.Windows.Size(this.cameraViewbox.FlowLayoutPanel.Width, this.cameraViewbox.FlowLayoutPanel.Height));
}
}
其中 VideoPanel 是影片視窗顯示控制元件,初始化如下:
public void Initialize(IChatUnit unit, bool myself)
{
this.pictureBox_Mic.RenderSize = new System.Windows.Size(24, 24);
this.chatUnit = unit;
this.isMySelf = myself;
this.toolStrip1.Text = chatUnit.MemberID;
//初始化麥克風聯結器
this.chatUnit.MicrophoneConnector.ConnectEnded += new CbGeneric<string, ConnectResult>(MicrophoneConnector_ConnectEnded);
this.chatUnit.MicrophoneConnector.OwnerOutputChanged += new CbGeneric<string>(MicrophoneConnector_OwnerOutputChanged);
if (!this.isMySelf)
{
this.chatUnit.MicrophoneConnector.BeginConnect(unit.MemberID);
}
if (this.isMySelf)
{
this.videoSizeShow.Visibility = Visibility.Collapsed;
}
//初始化攝像頭聯結器
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.OwnerVideoSizeChanged += DynamicCameraConnector_OwnerVideoSizeChanged;
this.chatUnit.DynamicCameraConnector.BeginConnect(unit.MemberID);
}
當新使用者進入房間時,房間內其他使用者透過MicrophoneConnector麥克風聯結器和DynamicCameraConnector攝像頭聯結器連線到該使用者的麥克風和攝像頭
(2)開啟或關閉攝像頭、麥克風、揚聲器
以開啟或關閉攝像頭為例:
private void skinCheckBox_camera_MouseDown(object sender, MouseButtonEventArgs e)
{
this.isMyselfVideo = !this.isMyselfVideo;
System.Windows.Controls.Image imageVideo = sender as System.Windows.Controls.Image;
imageVideo.Source = this.isMyselfVideo ? ResourceManager.Singleton.CameraOpenImage : ResourceManager.Singleton.CameraCloseImage;
imageVideo.ToolTip = this.isMyselfVideo ? "攝像頭:開" : "攝像頭:關";
VideoPanel myPanel = ((VideoAllPanel)this.chatGroup.GetMember(this.loginId).Tag).videoPanel;
VideoHidePanel myHidePanel = ((VideoAllPanel)this.chatGroup.GetMember(this.loginId).Tag).videoHidePanel;
this.VideoNumBeyond();
if (this.isVideoShowBeyond)
{
myHidePanel.cameraVisibilityChange(this.isMyselfVideo);
}
else
{
if (!isMyselfVideo)
{
this.cameraViewbox.FlowLayoutPanel.Children.Remove(myPanel);
myPanel.cameraPanel1.ClearImage();
this.flowLayoutPanel2.Children.Insert(0, myHidePanel);
myHidePanel.cameraVisibilityChange(false);
}
else
{
this.flowLayoutPanel2.Children.Remove(myHidePanel);
this.cameraViewbox.FlowLayoutPanel.Children.Insert(this.videoShowCount, myPanel);
}
}
if (IsInitialized)
{
this.multimediaManager.OutputVideo = this.isMyselfVideo;
this.VideoSizeChange(new System.Windows.Size(this.cameraViewbox.FlowLayoutPanel.Width, this.cameraViewbox.FlowLayoutPanel.Height));
}
}
其中透過多媒體管理器multimediaManager的OutputVideo屬性,設定是否將採集到的影片輸出,進而控制攝像頭的開啟或關閉。
2. 佈局切換
(1)根據開啟影片的使用者數量可分為 1x1、2x2、3x3三種佈局格式
(2)根據不同的佈局格式以及外部控制元件容器的寬高,手動計算影片控制元件的寬高。
private void VideoSizeChange(Size size)
{
if (this.cameraViewbox.FlowLayoutPanel.Children.Count > 4)
{
foreach (VideoPanel panel in this.cameraViewbox.FlowLayoutPanel.Children)
{
panel.Height = (size.Height - 6) / 3;
panel.Width = (size.Width - 12) / 3;
}
}
else if (this.cameraViewbox.FlowLayoutPanel.Children.Count <= 4 && this.cameraViewbox.FlowLayoutPanel.Children.Count > 1)
{
foreach (VideoPanel panel in this.cameraViewbox.FlowLayoutPanel.Children)
{
panel.Height = (size.Height - 4) / 2;
panel.Width = (size.Width - 8) / 2;
}
}
else if (this.cameraViewbox.FlowLayoutPanel.Children.Count == 1)
{
foreach (VideoPanel panel in this.cameraViewbox.FlowLayoutPanel.Children)
{
panel.Height = size.Height - 2;
panel.Width = size.Width - 4;
}
}
}
透過流式佈局控制元件的特性:超過流式控制元件的寬度,子控制元件將自動換行,修改影片控制元件的寬高;
外部容器實際容納所有影片控制元件的寬高為:外部容器的寬高減去所有影片控制元件的外邊距;
當只有一個使用者開啟影片,即將使用1x1佈局時,影片控制元件寬高即為外部容器實際容納所有影片控制元件的寬高;
當2~4人開啟影片,即將使用2x2佈局時,影片控制元件寬高即為外部容器實際容納所有影片控制元件的寬高的1/2,此時每個影片控制元件將佔外部控制元件的1/4;
當超過4人開啟影片,即將使用3x3佈局時,影片控制元件寬高即為外部容器實際容納所有影片控制元件的寬高的1/3,此時每個影片控制元件將佔外部控制元件的1/9
3.自定義訊息型別
public static class InformationTypes
{
#region 廣播訊息
/// <summary>
/// 廣播聊天資訊
/// </summary>
public const int BroadcastChat = 0;
/// <summary>
/// 廣播共享桌面
/// </summary>
public const int BroadcastShareDesk = 1;
/// <summary>
/// 廣播白板
/// </summary>
public const int BroadcastWhiteBoard = 2;
#endregion
#region 給新加入成員傳送訊息
/// <summary>
/// 共享桌面
/// </summary>
public const int ShareDesk = 53;
/// <summary>
/// 電子白板
/// </summary>
public const int WhiteBoard = 54;
#endregion
/// <summary>
/// 獲取服務端 組擴充套件資訊
/// </summary>
public const int GetGroupExtension = 101;
}
(1)當使用者傳送聊天訊息時,將透過BroadcastChat向所有線上使用者廣播聊天訊息;當使用者開啟桌面共享時,將透過BroadcastShareDesk向所有線上使用者廣播桌面共享訊息;當主持人開啟電子白板時,將透過BroadcastWhiteBoard
向所有線上使用者廣播電子白板訊息。
(2)當使用者上線時,如果有使用者開啟桌面共享,就將透過ShareDesk 向新使用者傳送桌面共享訊息;如果主持人開啟電子白板,就將透過WhiteBoard向新使用者傳送電子白板訊息。
(3)使用者將透過GetGroupExtension向服務端獲取組擴充套件資訊。
4.組擴充套件資訊
public class GroupExtension
{
/// <summary>
/// 主持人ID
/// </summary>
public string ModeratorID { get; set; }
/// <summary>
/// 正在共享遠端桌面的使用者ID
/// </summary>
public string DesktopSharedUserID { get; set; }
/// <summary>
/// 主持人是否開啟白板
/// </summary>
public bool IsModeratorWhiteBoardNow { get; set; }
}
(1)ModeratorID 表示當前房間主持人的ID;
(2)DesktopSharedUserID 正在桌面共享的使用者ID;若值為null,表示當前房間內無人開啟桌面共享,客戶端透過該值判斷當前是否有使用者開啟桌面共享;當使用者開啟或關閉桌面共享時,都將手動修改該值;
(3)IsModeratorWhiteBoardNow表示當前主持人是否開啟電子白板;當主持人開啟或關閉電子白板時,都將手動修改該值。
四.原始碼下載
1. 原始碼專案說明
(1)OVCS.ServerLinux :視訊會議 Linux 服務端
(2)OVCS.ClientLinux :視訊會議 Linux 客戶端
注: Linux客戶端內建的是x86/x64非託管so庫,若需要其它架構的so,請聯絡QQ:2027224508 獲取。
(3)另附上Android客戶端的原始碼:Android端 。
2. 部署執行說明
在部署之前,需要在linux服務端和客戶端上分別安裝 .Net core 3.1版本,命令列安裝命令如下:
yum install dotnet-sdk-3.1
檢查版本安裝情況
dotnet --version
執行:
(1)在CentOS上啟動OVCS.ServerLinux服務端:複製OVCS.ServerLinux專案下的Debug資料夾,到CentOS作業系統上,開啟Debug -> netcoreapp3.1目錄 ,在目錄下開啟終端,執行以下命令啟動服務端
dotnet OVCS.ServerLinux.dll
(2)在麒麟或統信UOS、Ubuntu上執行OVCS.ClientLinux客戶端:複製OVCS.ClientLinux專案下的Debug資料夾,到麒麟或統信UOS、Ubuntu作業系統上,開啟Debug -> netcoreapp3.1目錄 ,在目錄下開啟終端,執行以下命令啟動客戶端
dotnet OVCS.ClientLinux.dll