在 Avalonia 如火如荼的現在,之前使用CPF實現的簡單IM,非常有必要基於 Avalonia 來實現了。Avalonia 在跨平臺上的表現非常出色,對信創國產作業系統(像銀河麒麟、統信UOS、Deepin等)也很不錯。
現在,我們就來使用 Avalonia 實現一個跨平臺的簡單IM,除了文字聊天外,還可以語音視訊通話。廢話不多說,我們開始吧!
下圖是這個簡單IM的Avalonia使用者端在國產統信UOS上的執行的截圖:
這個簡單的IM系統實現了以下功能:
(1)使用者端使用者上下線時,通知其他線上使用者。
(2)當用戶端與伺服器端網路斷開時,進行自動重連,當網路恢復後,重連成功。
(3)所有線上使用者之間可以進行文字聊天(支援表情,支援撤回訊息、刪除訊息)。
(4)檔案傳送。
(1)一方發起視訊對話請求,對方同意後,即可開始視訊對話。
(2)在對話的過程中,任何一方都可以結束通話,以終止對話。
(3)在對話的過程中,任何一方掉線,都會自動終止對話。
(4)雙擊視訊視窗,會全螢幕顯示視訊,按esc退出全螢幕。
(5)遠端桌面或遠端協助功能,也是跟視訊聊天同樣的流程,不再贅述。
Visual Studio 2022
.NET Core 3.1
C#
Avalonia UI 框架(版本:0.10.22)、ESFramework 通訊框架 (版本:7.2)
注:建議 Avalonia 使用0.10.*的版本,精簡而且很穩定,而最新的11.0的版本太龐大了。
下面我們講一下Demo中核心的程式碼實現,大家從文末下載原始碼並對照著原始碼看,會更清楚些。
若要實現上述功能列表中列出來的所有功能,我們先要定義相應的通訊訊息的訊息型別,如下所示:
public static class InformationTypes { /// <summary> /// 文字(表情)聊天資訊 /// </summary> public const int TextChat = 0; /// <summary> /// 文字(表情)聊天資訊 (由伺服器端轉發給訊息接收方) /// </summary> public const int TextChat4Transit = 1; /// <summary> /// 圖片聊天資訊 /// </summary> public const int ImageChat = 2; /// <summary> /// 收到訊息傳送者 撤回訊息請求 /// </summary> public const int RecallMsg = 3; /// <summary> /// 使用者端非同步呼叫伺服器端 /// </summary> public const int ClientSyncCallServer = 4; /// <summary> /// 視訊請求 5 /// </summary> public const int VideoRequest = 5; /// <summary> /// 回覆視訊請求的結果 6 /// </summary> public const int VideoResult = 6; /// <summary> /// 通知對方 結束通話 視訊連線 7 /// </summary> public const int CloseVideo = 7; /// <summary> /// 通知好友 網路原因,導致 視訊中斷 8 /// </summary> public const int NetReasonCloseVideo = 8; /// <summary> /// 通知對方(忙線中) 結束通話 視訊連線 9 /// </summary> public const int BusyLine = 9; /// <summary> /// 收到遠端協助請求 /// </summary> public const int AssistReceive = 10; /// <summary> /// 協助方拒絕遠端協助 /// </summary> public const int AssistGusetReject = 11; /// <summary> /// 協助方同意遠端協助 /// </summary> public const int AssistGusetAgree = 12; /// <summary> /// 請求方關閉遠端協助 /// </summary> public const int AssistOwnerClose = 13; /// <summary> /// 協助方關閉遠端協助 /// </summary> public const int AssistGusetClose = 14; }
在約定好訊息型別之後,我們就可以實現業務邏輯功能了。
資訊型別定義好後,我們接下來定義資訊協定。
對於聊天訊息(InformationTypes.EmotionTextChat),專門定義了一個協定類:ChatMessageRecord。
public class ChatMessageRecord { public string Guid { get; set; } public DateTime MessageTime { get; set; } public string SpeakerID { get; set; } public string ListenerID { get; set; } public ChatMessageType ChatMessageType { get; set; } public string ContentStr { get; set; } public byte[] ImgData { get; set; } public string FilePath { get; set; } }
對於同步呼叫(InformationTypes.ClientSyncCallServer),我們範例的是向伺服器請求加法運算的結果,協定類用的是MathModel。
使用者端的MainForm實現了ICustomizeHandler介面,其主要實現HandleInformation方法,來處理收到的聊天資訊和振動提醒。
void HandleInformation(string sourceUserID, int informationType, byte[] info);
伺服器端的CustomizeHandler實現了伺服器端的ICustomizeHandler介面,其主要實現HandleQuery方法來處理來自使用者端的同步呼叫(InformationTypes.ClientCallServer)。
byte[] HandleQuery(string sourceUserID, int informationType, byte[] info);
伺服器端的BasicHandler類實現IBasicHandler介面,以驗證登入使用者的賬號密碼。
public class BasicHandler : IBasicHandler { /// <summary> /// 此處驗證使用者的賬號和密碼。返回true表示通過驗證。 /// </summary> public bool VerifyUser(ClientType clientType, string systemToken, string userID, string password, out string failureCause) { failureCause = ""; return true; } public string HandleQueryBeforeLogin(AgileIPE clientAddr, int queryType, string query) { return ""; } }
本demo中,假設所有的驗證都通過,所以驗證方法直接返回true。
通過IRapidPassiveEngine的 CustomizeOutter 的 Send 方法來傳送文字表情聊天訊息。
在傳送文字聊天訊息時,有兩個傳送按鈕,「傳送1」和「傳送2」,分別演示了兩種傳送訊息給對方的方式:
(1)直接發給對方。(若P2P通道存在,則經由P2P通道傳送)
internal static void SendTextMsgToClient(ChatMessageRecord record) { try { string cont = JsonConvert.SerializeObject(record); byte[] recordInfo = Encoding.UTF8.GetBytes(cont); //使用Tag攜帶 接收者的ID App.PassiveEngine.CustomizeOutter.Send(record.ListenerID, InformationTypes.TextChat4Transit, recordInfo); } catch (Exception e) { logger.Log(e, "GlobalHelper.SendTextMsgToClient", ErrorLevel.Standard); } }
聊天訊息 ChatMessageRecord 物件先由JSON序列化成字串,然後在使用UTF-8轉成位元組陣列,然後通過通訊引擎的CustomizeOutter傳送出去。
(2)先發給伺服器,再由伺服器轉發給對方。
具體實現,大家去參看原始碼,這裡就不再贅述了。
語音視訊通話實際執行起來後的效果如下所示:
我們先簡單描述一下實現視訊對話流程的要點,更詳細的細節請查閱原始碼。
(1)發起方傳送InformationTypes.VideoRequest型別的資訊給對方,以請求視訊對話。
程式中是在 VideoChatWindow 視窗顯示的時候,來做這件事的:
protected override void OnInitialized() { base.OnInitialized(); this.SetWindowStats(); if (!this.IsWorking) { VideoController.Singleton.SendMessage(this.DestID, InformationTypes.VideoRequest, null); CommonHelper.AddSystemMsg(this.DestID, "向對方發起視訊通話邀請"); } }
(2)接收方收到請求後,介面提示使用者是同意還是拒絕,使用者選擇後,將傳送InformationTypes.VideoResult型別的資訊給請求方,資訊的內容是一個bool值,true表示同意,false表示拒絕。
(3)發起方收到回覆,如果回覆為拒絕,則介面給出對應的提示;如果回覆為同意,則進入(4)。
(4)先說接收方,如果同意視訊,則傳送回復後,立即呼叫DynamicCameraConnector和MicrophoneConnector的Connect方法,連線到對方的攝像頭、麥克風。
internal void BeginConnect() { UiSafeInvoker.ActionOnUI(() => { string tip = this.IsWorking ? "已同意對方的視訊通話" : "對方同意了你的視訊通話請求"; CommonHelper.AddSystemMsg(this.DestID, tip); this.IsWorking = true; this.NotifyOther = true; this.Title = this.title.Text = this.RepeatedCallTip(false); this.startTime = DateTime.Now; this.timer.Start(); this.otherCamera.Core.DisplayVideoParameters = true; this.otherCamera.Core.VideoDrawMode = VideoDrawMode.ScaleToFill; this.otherCamera.Core.ConnectEnded += DynamicCameraConnector_ConnectEnded; this.otherCamera.Core.Disconnected += DynamicCameraConnector_Disconnected; this.microphoneConnector.ConnectEnded += MicrophoneConnector_ConnectEnded; this.microphoneConnector.Disconnected += MicrophoneConnector_Disconnected; this.otherCamera.BeginConnect(this.DestID); this.microphoneConnector.BeginConnect(this.DestID); }); }
(5)對於發起方,當收到對方同意的回覆後,也立即呼叫DynamicCameraConnector和MicrophoneConnector的Connect方法,連線到接收方的攝像頭、麥克風。
(6)當一方點選結束通話的按鈕時,就會傳送InformationTypes.CloseVideo型別的資訊給對方,並呼叫DynamicCameraConnector和MicrophoneConnector的Disconnect方法斷開到對方裝置的連線。
(7)另一方接收到InformationTypes.CloseVideo型別的資訊時,也會呼叫DynamicCameraConnector和MicrophoneConnector的Disconnect方法以斷開連線。
protected override void OnClosing(CancelEventArgs e) { base.OnClosing(e); this.otherCamera?.Disconnect(); this.otherCamera?.Dispose(); this.microphoneConnector?.Disconnect(); this.microphoneConnector?.Dispose(); this.selfCamera?.Disconnect(); this.selfCamera?.Dispose(); }
(8)如果接收到自己掉線的事件或好友掉線的事件,也採用類似結束通話對話的處理。
Avalonia 版本即時通訊原始碼: IM_VideoChat.Avalonia.rar
該原始碼中包括如下專案:
(1)Oraycn.Demos.VideoChat.LinuxServer : 該Demo的Linux伺服器端(基於.NetCore)。
(2)Oraycn.Demos.VideoChat.ClientAvalonia : 該Demo的 Avalonia 使用者端。
注: Linux使用者端內建的是x86/x64非託管so庫,若需要其它架構的so,請聯絡我們免費獲取。