diff --git a/Demo.Common/Events/AudioMuteStateChangedEvent.cs b/Demo.Common/Events/AudioMuteStateChangedEvent.cs new file mode 100644 index 0000000..73c40e5 --- /dev/null +++ b/Demo.Common/Events/AudioMuteStateChangedEvent.cs @@ -0,0 +1,28 @@ +using Agora.Rtc; +using Prism.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Demo.Common.Events +{ + public class AudioMuteStateChangedEvent : PubSubEvent + { + } + + public class AudioMuteInfo + { + public AudioMuteInfo(long uid, bool isMute, string channel) + { + Channel = channel; + UId = uid; + IsMute = isMute; + } + public string Channel { get; set; } + public long UId { get; set; } + public bool IsMute { get; set; } + + } +} diff --git a/Demo.Common/Events/VideoMuteStateChangedEvent.cs b/Demo.Common/Events/VideoMuteStateChangedEvent.cs new file mode 100644 index 0000000..edd8810 --- /dev/null +++ b/Demo.Common/Events/VideoMuteStateChangedEvent.cs @@ -0,0 +1,27 @@ +using Prism.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Demo.Common.Events +{ + public class VideoMuteStateChangedEvent:PubSubEvent + { + } + + public class VideoMuteInfo + { + public VideoMuteInfo(long uid, bool isMute, string channel) + { + Channel = channel; + UId = uid; + IsMute = isMute; + } + public string Channel { get; set; } + public long UId { get; set; } + public bool IsMute { get; set; } + + } +} diff --git a/Demo.Common/Extensions/ObservableCollectionExtension.cs b/Demo.Common/Extensions/ObservableCollectionExtension.cs new file mode 100644 index 0000000..85fddbe --- /dev/null +++ b/Demo.Common/Extensions/ObservableCollectionExtension.cs @@ -0,0 +1,56 @@ +using Demo.Common.Models; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Demo.Common.Extensions +{ + public static class ObservableCollectionExtension + { + public static void InitUserList(this ObservableCollection list, int count) where T : User + { + for (int i = 0; i < count; i++) + { + list.Add((T)User.GetInit()); + } + } + + public static int AddUser(this ObservableCollection list, T user) where T : User + { + for (int i = 0; i < list.Count; i++) + { + if (list[i].Id == 0) + { + list[i] = user; + return i; + } + } + return -1; + } + + public static int ClearUserById(this ObservableCollection list, long Id) where T : User + { + for (int i = 0; i < list.Count; i++) + { + if (list[i].Id == Id) + { + list[i] = (T)User.GetInit(); + return i; + } + } + + return -1; + } + + public static void ClearAllUser(this ObservableCollection list) where T : User + { + for (int i = 0; i < list.Count; i++) + { + list[i] = (T)User.GetInit(); + } + } + } +} diff --git a/Demo.Common/Helpers/AgoraHelper.cs b/Demo.Common/Helpers/AgoraHelper.cs index 54bc6af..1e91adf 100644 --- a/Demo.Common/Helpers/AgoraHelper.cs +++ b/Demo.Common/Helpers/AgoraHelper.cs @@ -9,12 +9,16 @@ using System.Threading.Tasks; namespace Demo.Common.Helpers { - public class AgoraHelper : IRtcEngineEventHandler + public class AgoraHelper { public static uint _localUId; public static IRtcEngine _RtcEngineInstance; public static int Init(string appId, uint localUid = 0) { + if (_RtcEngineInstance != null) + { + return 0; + } if (null == _RtcEngineInstance) { _RtcEngineInstance = RtcEngine.CreateAgoraRtcEngine(); diff --git a/Demo.Common/Helpers/AvatatHelper.cs b/Demo.Common/Helpers/AvatatHelper.cs new file mode 100644 index 0000000..a3b255d --- /dev/null +++ b/Demo.Common/Helpers/AvatatHelper.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Drawing.Drawing2D; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; +using System.Drawing.Imaging; +using System.IO; + +namespace Demo.Common.Helpers +{ + public class AvatatHelper + { + private static Image cameraCloseImage = null; + public static Image CameraCloseImage + { + get + { + if (cameraCloseImage != null) + { + return cameraCloseImage; + } + using (var fileStream = new FileStream($@"Assets/camera_close.png", FileMode.Open, FileAccess.Read)) + { + using var _imageStream = new MemoryStream(); + fileStream.CopyTo(_imageStream); + cameraCloseImage = Image.FromStream(_imageStream); + } + + return cameraCloseImage; + } + } + + + + /// + /// 获取姓名对应的圆形图片 + /// + /// + /// + /// + /// + public static Bitmap GetNickNameImage(string name, int width = 132, int height = 132) + { + string color = "#2B6EC0"; + if (string.IsNullOrEmpty(name)) + { + return null; + } + string firstName = GetFirstTwoChars(name); + Bitmap img = new Bitmap(width, height); + Graphics g = Graphics.FromImage(img); + + // 启用抗锯齿 + g.SmoothingMode = SmoothingMode.AntiAlias; + + // 创建圆形路径 + GraphicsPath path = new GraphicsPath(); + path.AddEllipse(0, 0, width, height); + + // 设置裁剪区域为圆形 + g.SetClip(path); + + // 填充背景色 + System.Drawing.Brush brush = new SolidBrush(ColorTranslator.FromHtml(color)); + g.FillEllipse(brush, 0, 0, width, height); + + // 填充文字 + Font font = new Font("微软雅黑", 50); + SizeF firstSize = g.MeasureString(firstName, font); + g.DrawString(firstName, font, System.Drawing.Brushes.White, + new PointF((img.Width - firstSize.Width) / 2, (img.Height - firstSize.Height) / 2)); + + g.Dispose(); + path.Dispose(); + brush.Dispose(); + font.Dispose(); + + return img; + } + + + public static string GetFirstTwoChars(string input) + { + if (string.IsNullOrEmpty(input)) + return string.Empty; + input = input.Trim(); + return input[..Math.Min(2, input.Length)]; + } + + + /// + /// 保存图片到磁盘 + /// + /// + /// + /// + /// + /// + public static Bitmap SaveNameImage(string name, string targetFile, int width = 132, int height = 132) + { + Bitmap img = GetNickNameImage(name, width, height); + img.Save(targetFile + ".png", ImageFormat.Png); + img.Dispose(); + return img; + } + } +} diff --git a/Demo.Common/Helpers/RtcEngineEventHandler.cs b/Demo.Common/Helpers/RtcEngineEventHandler.cs index 63856c7..4ff90a7 100644 --- a/Demo.Common/Helpers/RtcEngineEventHandler.cs +++ b/Demo.Common/Helpers/RtcEngineEventHandler.cs @@ -8,6 +8,8 @@ using Prism.Ioc; using Prism.Events; using Demo.Common.Events; using Masuit.Tools; +using System.Security.Cryptography; +using System.Threading.Channels; namespace Demo.Common.Helpers { @@ -19,10 +21,16 @@ namespace Demo.Common.Helpers aggregator = ContainerLocator.Container.Resolve(); } + /// + /// 用户加入频道回调 + /// + /// + /// + /// public override void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed) { - //base.OnUserJoined(connection, remoteUid, elapsed); - Console.WriteLine($@"新用户加入: channelId:{connection.channelId} localuid: {connection.localUid} remoteuid:{remoteUid} elapsed: {elapsed}"); + //base.OnUserJoined(connection, remoteUid, elapsed); + Console.WriteLine($@" {DateTime.Now.ToString("HH:mm:ss")}新用户加入: channelId:{connection.channelId} localuid: {connection.localUid} remoteuid:{remoteUid} elapsed: {elapsed}"); //var aggregator = ContainerLocator.Container.Resolve(); @@ -36,6 +44,12 @@ namespace Demo.Common.Helpers } + /// + /// 用户离开频道回调 + /// + /// + /// + /// public override void OnUserOffline(RtcConnection connection, uint remoteUid, USER_OFFLINE_REASON_TYPE reason) { //var aggregator = ContainerLocator.Container.Resolve(); @@ -48,6 +62,13 @@ namespace Demo.Common.Helpers }); } + /// + /// 音频音量提示回调 + /// + /// + /// + /// + /// public override void OnAudioVolumeIndication(RtcConnection connection, AudioVolumeInfo[] speakers, uint speakerNumber, int totalVolume) { if (speakers.Length <= 0) @@ -57,12 +78,134 @@ namespace Demo.Common.Helpers speakers.ForEach(sp => { sp.uid = sp.uid == 0 ? AgoraHelper._localUId : sp.uid; - - aggregator.GetEvent().Publish(sp); }); } + /// + /// 本地用户音频发布状态改变回调。 + /// + /// + /// + /// + /// + public override void OnAudioPublishStateChanged(string channel, STREAM_PUBLISH_STATE oldState, STREAM_PUBLISH_STATE newState, int elapseSinceLastState) + { + switch (newState) + { + case STREAM_PUBLISH_STATE.PUB_STATE_IDLE: + break; + case STREAM_PUBLISH_STATE.PUB_STATE_NO_PUBLISHED: + aggregator.GetEvent().Publish(new AudioMuteInfo(AgoraHelper._localUId, true, channel)); + break; + case STREAM_PUBLISH_STATE.PUB_STATE_PUBLISHING: + break; + case STREAM_PUBLISH_STATE.PUB_STATE_PUBLISHED: + aggregator.GetEvent().Publish(new AudioMuteInfo(AgoraHelper._localUId, false, channel)); + break; + } + } + + ///// + ///// 远端用户音频订阅状态发生改变回调。 + ///// + ///// + ///// + ///// + ///// + ///// + //public override void OnAudioSubscribeStateChanged(string channel, uint uid, STREAM_SUBSCRIBE_STATE oldState, STREAM_SUBSCRIBE_STATE newState, int elapseSinceLastState) + //{ + // //Console.WriteLine($@"OnAudioSubscribeStateChanged====================== channel {channel} uid {uid} {newState}"); + + // switch (newState) + // { + // case STREAM_SUBSCRIBE_STATE.SUB_STATE_IDLE: + // break; + // case STREAM_SUBSCRIBE_STATE.SUB_STATE_NO_SUBSCRIBED: + // aggregator.GetEvent().Publish(new AudioMuteInfo(uid, true, channel)); + // break; + // case STREAM_SUBSCRIBE_STATE.SUB_STATE_SUBSCRIBING: + // break; + // case STREAM_SUBSCRIBE_STATE.SUB_STATE_SUBSCRIBED: + // aggregator.GetEvent().Publish(new AudioMuteInfo(uid, false, channel)); + // break; + // } + //} + + /// + /// 远端用户音频订阅状态发生改变回调。 + /// + /// + /// + /// + public override void OnUserMuteAudio(RtcConnection connection, uint remoteUid, bool muted) + { + aggregator.GetEvent().Publish(new AudioMuteInfo(remoteUid, muted, connection.channelId)); + } + + /// + /// 远端用户视频订阅状态发生改变回调。 + /// + /// + /// + /// + public override void OnUserMuteVideo(RtcConnection connection, uint remoteUid, bool muted) + { + aggregator.GetEvent().Publish(new VideoMuteInfo(remoteUid, muted, connection.channelId)); + } + + /// + /// 本地用户视频发布状态改变回调。 + /// + /// + /// + /// + /// + /// + public override void OnVideoPublishStateChanged(VIDEO_SOURCE_TYPE source, string channel, STREAM_PUBLISH_STATE oldState, STREAM_PUBLISH_STATE newState, int elapseSinceLastState) + { + switch (newState) + { + case STREAM_PUBLISH_STATE.PUB_STATE_IDLE: + break; + case STREAM_PUBLISH_STATE.PUB_STATE_NO_PUBLISHED: + aggregator.GetEvent().Publish(new VideoMuteInfo(AgoraHelper._localUId, true, channel)); + break; + case STREAM_PUBLISH_STATE.PUB_STATE_PUBLISHING: + break; + case STREAM_PUBLISH_STATE.PUB_STATE_PUBLISHED: + aggregator.GetEvent().Publish(new VideoMuteInfo(AgoraHelper._localUId, false, channel)); + break; + } + } + + + /// + /// 远端用户视频订阅状态发生改变回调。 + /// + /// + /// + /// + /// + /// + public override void OnVideoSubscribeStateChanged(string channel, uint uid, STREAM_SUBSCRIBE_STATE oldState, STREAM_SUBSCRIBE_STATE newState, int elapseSinceLastState) + { + switch (newState) + { + case STREAM_SUBSCRIBE_STATE.SUB_STATE_IDLE: + break; + case STREAM_SUBSCRIBE_STATE.SUB_STATE_NO_SUBSCRIBED: + aggregator.GetEvent().Publish(new VideoMuteInfo(uid, true, channel)); + break; + case STREAM_SUBSCRIBE_STATE.SUB_STATE_SUBSCRIBING: + break; + case STREAM_SUBSCRIBE_STATE.SUB_STATE_SUBSCRIBED: + aggregator.GetEvent().Publish(new VideoMuteInfo(uid, false, channel)); + break; + } + + } } } diff --git a/Demo.Common/Models/AgoraDeviceInfo.cs b/Demo.Common/Models/AgoraDeviceInfo.cs new file mode 100644 index 0000000..dd3d3e4 --- /dev/null +++ b/Demo.Common/Models/AgoraDeviceInfo.cs @@ -0,0 +1,30 @@ +using Prism.Mvvm; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Demo.Common.Models +{ + public class AgoraDeviceInfo : BindableBase + { + public AgoraDeviceInfo(string deviceId, string deviceName, bool isSelected = false) + { + DeviceId = deviceId; + DeviceName = deviceName; + IsSelected = isSelected; + } + public string DeviceId { get; set; } + + public string DeviceName { get; set; } + + + private bool isSelected; + public bool IsSelected + { + get { return isSelected; } + set { SetProperty(ref isSelected, value); } + } + } +} diff --git a/Demo.Common/Models/User.cs b/Demo.Common/Models/User.cs index 620218c..3815799 100644 --- a/Demo.Common/Models/User.cs +++ b/Demo.Common/Models/User.cs @@ -1,10 +1,13 @@ -using Prism.Mvvm; +using Demo.Common.Helpers; +using Prism.Mvvm; using System; using System.Collections.Generic; using System.ComponentModel; +using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Windows; namespace Demo.Common.Models { @@ -21,8 +24,17 @@ namespace Demo.Common.Models public string UserName { get { return userName; } - set { SetProperty(ref userName, value); } + set + { + SetProperty(ref userName, value); + } } + + ///// + ///// 用户头像 + ///// + //public Bitmap Avatar => AvatatHelper.GetNickNameImage(userName); + private bool isManager; public bool IsManager { @@ -38,11 +50,73 @@ namespace Demo.Common.Models set { SetProperty(ref isLocal, value); } } - //private bool isMain; - //public bool IsMain + private bool isMuteAudio; + public bool IsMuteAudio + { + get { return isMuteAudio; } + set + { + SetProperty(ref isMuteAudio, value); + RaisePropertyChanged(nameof(MicoSlashLineVisibility)); + RaisePropertyChanged(nameof(MicStatusText)); + if (value) + { + // 静音,音量置为0 + MicoVolume = 0; + } + } + } + + private bool isMuteVideo; + public bool IsMuteVideo + { + get { return isMuteVideo; } + set + { + SetProperty(ref isMuteVideo, value); + RaisePropertyChanged(nameof(VideoSlashLineVisibility)); + RaisePropertyChanged(nameof(VideoStatusText)); + } + } + + /// + /// 麦克风音量 + /// + private uint micoVolume; + public uint MicoVolume + { + get { return micoVolume; } + set { SetProperty(ref micoVolume, value); } + } + + // 用于控制斜线显示的属性 + public Visibility MicoSlashLineVisibility => (IsMuteAudio ? Visibility.Visible : Visibility.Collapsed); + // 用于控制斜线显示的属性 + public Visibility VideoSlashLineVisibility => (IsMuteVideo ? Visibility.Visible : Visibility.Collapsed); + + //用于控制麦克风文本显示的属性 + public string MicStatusText => (IsMuteAudio ? "解除静音" : "静音"); + + //用于控制摄像头文本显示的属性 + public string VideoStatusText => (IsMuteVideo ? "开启视频" : "停止视频"); + + + + public static User GetInit() + { + return new User + { + Id = 0, + UserName = "待初始化" + }; + } + + //void IDisposable.Dispose() //{ - // get { return isMain; } - // set { SetProperty(ref isMain, value); } + // if (Avatar != null) + // { + // Avatar.Dispose(); + // } //} } } diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/App.xaml b/Meeting.V2.Demo/Meeting.V2.Demo/App.xaml index 6865043..3d81034 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/App.xaml +++ b/Meeting.V2.Demo/Meeting.V2.Demo/App.xaml @@ -8,8 +8,10 @@ + + diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Assets/Resource/MeetingRoomDictionary.xaml b/Meeting.V2.Demo/Meeting.V2.Demo/Assets/Resource/MeetingRoomDictionary.xaml new file mode 100644 index 0000000..40ed052 --- /dev/null +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Assets/Resource/MeetingRoomDictionary.xaml @@ -0,0 +1,151 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Assets/camera_close.svg b/Meeting.V2.Demo/Meeting.V2.Demo/Assets/camera_close.svg new file mode 100644 index 0000000..a9d532d --- /dev/null +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Assets/camera_close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Assets/mico_btn.svg b/Meeting.V2.Demo/Meeting.V2.Demo/Assets/mico_btn.svg new file mode 100644 index 0000000..6af55bc --- /dev/null +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Assets/mico_btn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Assets/video_btn.svg b/Meeting.V2.Demo/Meeting.V2.Demo/Assets/video_btn.svg new file mode 100644 index 0000000..ff4e4e3 --- /dev/null +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Assets/video_btn.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Meeting.V2.Demo.csproj b/Meeting.V2.Demo/Meeting.V2.Demo/Meeting.V2.Demo.csproj index cbfca6c..fba0eba 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/Meeting.V2.Demo.csproj +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Meeting.V2.Demo.csproj @@ -1,6 +1,6 @@  - Exe + WinExe net6.0-windows true True @@ -8,8 +8,11 @@ false - + + + + @@ -24,10 +27,20 @@ - + + PreserveNewest + + + + + PreserveNewest + + + PreserveNewest + \ No newline at end of file diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/MainWindowViewModel.cs b/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/MainWindowViewModel.cs index f0bc04c..745636b 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/MainWindowViewModel.cs +++ b/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/MainWindowViewModel.cs @@ -1,4 +1,5 @@ using Agora.Rtc; +using AngleSharp.Dom; using Demo.Common.Events; using Demo.Common.Helpers; using Demo.Common.Models; @@ -6,10 +7,12 @@ using Demo.Services.Interfaces; using HandyControl.Controls; using HandyControl.Data; using Masuit.Tools; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; using Prism.Commands; using Prism.Events; using Prism.Mvvm; using System; +using System.Collections.ObjectModel; using System.Windows; namespace Meeting.V2.Demo.ViewModels @@ -20,6 +23,15 @@ namespace Meeting.V2.Demo.ViewModels { this._aggregator = aggregator; this._agoraConfigService = agoraConfigService; + + Init(); + } + + private void Init() + { + AgoraHelper.Init("4a4f7be64fa1404ebda74784fe9ac381", (uint)UId); + InitMicoDevices(); + InitPlaybackDevices(); } private string _title = "Prism Application"; @@ -46,17 +58,46 @@ namespace Meeting.V2.Demo.ViewModels set { SetProperty(ref isJoin, value); } } + private ObservableCollection audioDevices = new(); + public ObservableCollection AudioDevices + { + get { return audioDevices; } + set { SetProperty(ref audioDevices, value); } + } + private ObservableCollection playbackDevices = new(); + public ObservableCollection PlaybackDevices + { + get { return playbackDevices; } + set { SetProperty(ref playbackDevices, value); } + } + + + + private User localUser; + public User LocalUser + { + get { return localUser; } + set + { + SetProperty(ref localUser, value); + } + } + + public DelegateCommand JoinChannelCommand => new DelegateCommand(JoinChannel); async void JoinChannel() - { - AgoraHelper.Init("4a4f7be64fa1404ebda74784fe9ac381", (uint)UId); + { + //var rrr = AgoraHelper._RtcEngineInstance.RegisterLocalUserAccount("4a4f7be64fa1404ebda74784fe9ac381", UId.ToString()); var rtcToken = await _agoraConfigService.GetRtcTokenAsync(); if (!string.IsNullOrWhiteSpace(rtcToken)) Application.Current.Properties["rtctoken"] = rtcToken; + //var result = AgoraHelper._RtcEngineInstance.EnableAudio(); + //Console.WriteLine($@"{nameof(JoinChannel)}-EnableAudio : {result}"); + var result = AgoraHelper._RtcEngineInstance.EnableVideo(); Console.WriteLine($@"{nameof(JoinChannel)}-EnableVideo : {result}"); @@ -76,28 +117,75 @@ namespace Meeting.V2.Demo.ViewModels options.autoSubscribeVideo.SetValue(true); AgoraHelper._RtcEngineInstance.JoinChannel(System.Windows.Application.Current.Properties["rtctoken"].ToString(), "999", (uint)UId, options); + //AgoraHelper._RtcEngineInstance.JoinChannelWithUserAccount(System.Windows.Application.Current.Properties["rtctoken"].ToString(), "999", UId.ToString(), options); ///启用用户音量提示。 - AgoraHelper._RtcEngineInstance.EnableAudioVolumeIndication(200, 5, false); + AgoraHelper._RtcEngineInstance.EnableAudioVolumeIndication(200, 3, false); - _aggregator.GetEvent().Publish(new User + LocalUser = new User { Id = UId, UserName = UId.ToString(), IsManager = true, - IsLocal = true - }); + IsLocal = true, + IsMuteVideo = false, + IsMuteAudio = false + }; + _aggregator.GetEvent().Publish(LocalUser); IsJoin = !IsJoin; Growl.Success("加入频道成功"); + UserInfo userInfo = new UserInfo(); + var rrra = AgoraHelper._RtcEngineInstance.GetUserInfoByUserAccount(UId.ToString(), ref userInfo); + + + //GetDevices(); + } + + public DelegateCommand JoinChannel4AudienceCommand => new DelegateCommand(JoinChannelForAudience); + private async void JoinChannelForAudience() + { + AgoraHelper.Init("4a4f7be64fa1404ebda74784fe9ac381", (uint)UId); + + var rtcToken = await _agoraConfigService.GetRtcTokenAsync(); + if (!string.IsNullOrWhiteSpace(rtcToken)) + Application.Current.Properties["rtctoken"] = rtcToken; + + var result = AgoraHelper._RtcEngineInstance.EnableVideo(); + AgoraHelper._RtcEngineInstance.EnableLocalAudio(false); + AgoraHelper._RtcEngineInstance.MuteLocalVideoStream(false); + AgoraHelper._RtcEngineInstance.MuteAllRemoteVideoStreams(true); + AgoraHelper._RtcEngineInstance.MuteAllRemoteAudioStreams(true); + + + ChannelMediaOptions options = new ChannelMediaOptions(); + options.channelProfile.SetValue(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING); + options.clientRoleType.SetValue(CLIENT_ROLE_TYPE.CLIENT_ROLE_AUDIENCE); + options.audienceLatencyLevel.SetValue(AUDIENCE_LATENCY_LEVEL_TYPE.AUDIENCE_LATENCY_LEVEL_LOW_LATENCY); + //// 发布麦克风采集的音频流 + //options.publishMicrophoneTrack.SetValue(true); + //// 发布摄像头采集的视频流 + //options.publishCameraTrack.SetValue(true); + // 自动订阅所有音频流 + options.autoSubscribeAudio.SetValue(true); + // 自动订阅所有视频流 + options.autoSubscribeVideo.SetValue(true); + + AgoraHelper._RtcEngineInstance.JoinChannel(System.Windows.Application.Current.Properties["rtctoken"].ToString(), "999", (uint)UId, options); + IsJoin = !IsJoin; + Growl.Success("加入频道成功"); + + + //GetDevices(); } public DelegateCommand LeavelChannelCommand => new DelegateCommand(LeavelChannel); void LeavelChannel() { + //AgoraHelper._RtcEngineInstance.DisableAudio(); AgoraHelper._RtcEngineInstance.LeaveChannel(); - AgoraHelper._RtcEngineInstance.StopPreview(); AgoraHelper._RtcEngineInstance.DisableVideo(); + AgoraHelper._RtcEngineInstance.StopPreview(); _aggregator.GetEvent().Publish(new User @@ -113,6 +201,115 @@ namespace Meeting.V2.Demo.ViewModels Growl.Success("离开频道成功"); } + public DelegateCommand OperMicoCommand => new DelegateCommand(ExecuteOperMicoCommand); + + void ExecuteOperMicoCommand() + { + if (LocalUser == null) + { + Growl.Error("请先加入频道!"); + return; + } + // 静音 + var result = AgoraHelper._RtcEngineInstance.MuteLocalAudioStream(!LocalUser.IsMuteAudio); + if (result != 0) + { + Growl.Error($@"开启音频错误code:{result}"); + return; + } + + } + + public DelegateCommand OperVideoCommand => new DelegateCommand(ExecuteOperVideoCommand); + + void ExecuteOperVideoCommand() + { + if (LocalUser == null) + { + Growl.Error("请先加入频道!"); + return; + } + if (!LocalUser.IsMuteVideo) + { + // 停止本地预览 + AgoraHelper._RtcEngineInstance.StopPreview(); + } + else + { + AgoraHelper._RtcEngineInstance.StartPreview(); + } + // 关闭摄像头 + var result = AgoraHelper._RtcEngineInstance.MuteLocalVideoStream(!LocalUser.IsMuteVideo); + if (result != 0) + { + Growl.Error($@"开启视频错误code:{result}"); + return; + } + + + } + + /// + /// 切换麦克风设备 + /// + public DelegateCommand ChangeAudioDeviceCommand => new DelegateCommand(ExecuteChangeAudioDeviceCommand); + void ExecuteChangeAudioDeviceCommand(string audioDeviceId) + { + var audioDeviceManager = AgoraHelper._RtcEngineInstance.GetAudioDeviceManager(); + audioDeviceManager.SetRecordingDevice(audioDeviceId); + } + + + /// + /// 切换扬声器设备 + /// + public DelegateCommand ChangePlaybackDeviceCommand => new DelegateCommand(ExecuteChangePlaybackDeviceCommand); + void ExecuteChangePlaybackDeviceCommand(string playbackDeviceId) + { + var audioDeviceManager = AgoraHelper._RtcEngineInstance.GetAudioDeviceManager(); + audioDeviceManager.SetPlaybackDevice(playbackDeviceId); + } + + + private void InitMicoDevices() + { + AudioDevices.Clear(); + var audioDeviceManager = AgoraHelper._RtcEngineInstance.GetAudioDeviceManager(); + var defaultAudioDeviceId = string.Empty; + var defaultAudioDeviceName = string.Empty; + audioDeviceManager.GetRecordingDefaultDevice(ref defaultAudioDeviceId, ref defaultAudioDeviceName); + defaultAudioDeviceName = $@"系统默认({defaultAudioDeviceName})"; + + AudioDevices.Add(new AgoraDeviceInfo(defaultAudioDeviceId, defaultAudioDeviceName, true)); + // 获取所有麦克风设备 + var deviceInfos = audioDeviceManager.EnumerateRecordingDevices(); + + foreach (var deviceInfo in deviceInfos) + { + AudioDevices.Add(new AgoraDeviceInfo(deviceInfo.deviceId, deviceInfo.deviceName)); + } + } + + private void InitPlaybackDevices() + { + PlaybackDevices.Clear(); + var audioDeviceManager = AgoraHelper._RtcEngineInstance.GetAudioDeviceManager(); + var defaultPlaybackDeviceId = string.Empty; + var defaultPlaybackDeviceName = string.Empty; + audioDeviceManager.GetPlaybackDefaultDevice(ref defaultPlaybackDeviceId, ref defaultPlaybackDeviceName); + defaultPlaybackDeviceName = $@"系统默认({defaultPlaybackDeviceName})"; + + PlaybackDevices.Add(new AgoraDeviceInfo(defaultPlaybackDeviceId, defaultPlaybackDeviceName, true)); + // 获取所有扬声器 + var deviceInfos = audioDeviceManager.EnumeratePlaybackDevices(); + + foreach (var deviceInfo in deviceInfos) + { + PlaybackDevices.Add(new AgoraDeviceInfo(deviceInfo.deviceId, deviceInfo.deviceName)); + } + } + + } } diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/VideoAreaViewModel.cs b/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/VideoAreaViewModel.cs index 6356239..4d60d25 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/VideoAreaViewModel.cs +++ b/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/VideoAreaViewModel.cs @@ -1,10 +1,14 @@ -using Demo.Common.Events; +using Agora.Rtc; +using Demo.Common.Events; +using Demo.Common.Extensions; +using Masuit.Tools; using Meeting.V2.Demo.Core.Mvvm; using Prism.Commands; using Prism.Events; using Prism.Regions; using System; using System.Collections.ObjectModel; +using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Threading; @@ -12,6 +16,7 @@ using User = Demo.Common.Models.User; namespace Meeting.V2.Demo.ViewModels { + public class VideoAreaViewModel : RegionViewModelBase { public VideoAreaViewModel(IRegionManager regionManager, @@ -21,8 +26,16 @@ namespace Meeting.V2.Demo.ViewModels aggregator.GetEvent().Subscribe((a) => Application.Current.Dispatcher.BeginInvoke(() => { - - UserInfos.Add(a); + // 不新增,避免重复创建销毁控件 + UserInfos.AddUser(a); + //for (var i = 0; i < UserInfos.Count; i++) + //{ + // if (UserInfos[i].Id == 0) + // { + // UserInfos[i] = a; + // return; + // } + //} })); aggregator.GetEvent().Subscribe((a) => { @@ -30,27 +43,30 @@ namespace Meeting.V2.Demo.ViewModels { if (a.IsLocal) { - UserInfos.Clear(); + //for (var i = 0; i < UserInfos.Count; i++) + //{ + // UserInfos[i] = new User() { Id = 0, UserName = "待初始化" }; + //} + UserInfos.ClearAllUser(); FocusUser = null; return; } - for (int i = 0; i < UserInfos.Count; i++) - { - if (UserInfos[i].Id == a.Id) - { - UserInfos.RemoveAt(i); - return; - } - } + + UserInfos.ClearUserById(a.Id); + //for (int i = 0; i < UserInfos.Count; i++) + //{ + // if (UserInfos[i].Id == a.Id) + // { + // UserInfos[i] = User.GetInit(); + // //UserInfos.RemoveAt(i); + // return; + // } + //} }); }); - //UserInfos.Add(new UserInfo() { Id = 1, UserName = "张三", IsManager = true }); - //UserInfos.Add(new UserInfo() { Id = 2, UserName = "李四", IsManager = false }); - //UserInfos.Add(new UserInfo() { Id = 3, UserName = "李四", IsManager = false }); - //UserInfos.Add(new UserInfo() { Id = 4, UserName = "李四", IsManager = false }); - //UserInfos.Add(new UserInfo() { Id = 4, UserName = "李四", IsManager = false }); - //UserInfos.Add(new UserInfo() { Id = 4, UserName = "李四", IsManager = false }); + + UserInfos.InitUserList(6); } private ObservableCollection userInfos = new(); diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/VideoViewModel.cs b/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/VideoViewModel.cs index 37814d4..b301285 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/VideoViewModel.cs +++ b/Meeting.V2.Demo/Meeting.V2.Demo/ViewModels/VideoViewModel.cs @@ -2,37 +2,62 @@ using Demo.Common.Events; using Demo.Common.Helpers; using Demo.Common.Models; +using DryIoc; +using HandyControl.Controls; using Masuit.Tools; using Meeting.V2.Demo.Core.Mvvm; using Meeting.V2.Demo.Views; using Prism.Commands; using Prism.Events; +using SixLabors.ImageSharp; using System; +using System.Drawing; using System.Windows; using System.Windows.Forms; +using System.Windows.Media.Imaging; +using System.Windows.Media; namespace Meeting.V2.Demo.ViewModels { public class VideoViewModel : ViewModelBase { - private VideoView _view; + private VideoView _this; public VideoViewModel(IEventAggregator aggregator) { - aggregator.GetEvent().Subscribe(SetUserVolume, + // 音频音量监听 + aggregator.GetEvent().Subscribe((arg => UserInfo.MicoVolume = arg.volume), arg => { - return UserInfo != null && UserInfo.Id > 0 && arg.uid > 0 && arg.uid == UserInfo.Id; + return UserInfo != null && UserInfo.Id > 0 && arg.uid > 0 && arg.uid == UserInfo.Id && !UserInfo.IsMuteAudio; }); + + // 语音状态监听 + aggregator.GetEvent().Subscribe((arg) => + { + UserInfo.IsMuteAudio = arg.IsMute; + }, arg => UserInfo != null && UserInfo.Id > 0 && arg.UId > 0 && arg.UId == UserInfo.Id); + + // 视频状态监听 + aggregator.GetEvent().Subscribe((arg) => + { + UserInfo.IsMuteVideo = arg.IsMute; + if (arg.IsMute) + { + RefreshPictureBox(); + //DisplayAvatar(); + } + }, arg => UserInfo != null && UserInfo.Id > 0 && arg.UId > 0 && arg.UId == UserInfo.Id); } + // 初始化控件时,将控件传入到ViewModel中 public DelegateCommand LoadedCommand => new DelegateCommand((u) => { if (u == null) return; Console.WriteLine($@"LoadedCoommand 被执行了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); - this._view = u; + this._this = u; RenderVideoView(); }); @@ -55,61 +80,67 @@ namespace Meeting.V2.Demo.ViewModels } } - private uint microphoneVolume; - /// - /// 用户麦克风音量 - /// - public uint MicrophoneVolume - { - get { return microphoneVolume; } - set { SetProperty(ref microphoneVolume, value); } - } - - private void SetUserVolume(AudioVolumeInfo volumeInfo) - { - MicrophoneVolume = volumeInfo.volume; - } - - - - - public DelegateCommand OperClickCommand => new DelegateCommand(() => { Console.WriteLine($@"id:{UserInfo.Id} userName:{UserInfo.UserName} isManager:{UserInfo.IsManager}"); - UserInfo.IsManager = !UserInfo.IsManager; - userInfo.UserName = DateTime.Now.ToString(); - Console.WriteLine($@"id:{UserInfo.Id} userName:{UserInfo.UserName} isManager:{UserInfo.IsManager}"); - //MessageBox.Show($@"id:{Id} userName:{UserName} isManager:{IsManager}"); + //UserInfo.IsMuteAudio = !UserInfo.IsMuteAudio; + //if (UserInfo.IsLocal) + //{ + // // 静音 + // var result = AgoraHelper._RtcEngineInstance.MuteLocalAudioStream(!UserInfo.IsMuteAudio); + // if (result != 0) + // { + // Growl.Error($@"开启音频错误code:{result}"); + // return; + // } + //} + + //if (UserInfo.Id > 0) + //{ + // UserInfo.Id = 0; + //} + }); - + /// + /// 渲染视频画面 + /// public void RenderVideoView() { - if (_view == null || UserInfo == null || UserInfo.Id <= 0) + if (_this == null || UserInfo == null) { - if (_view != null) - { - //_view.pic_frame.Invoke(() => - //{ - // _view.pic_frame.Refresh(); - //}); - - if (oldUserInfo != null) - { - AgoraHelper.ClearUserVideo((uint)(oldUserInfo.Id), (long)_view.pic_frame.Handle); - _view.pic_frame.Invoke(() => - { - _view.pic_frame.Refresh(); - }); - } - } return; } - AgoraHelper.SetupUserVideo((uint)UserInfo.Id, (long)_view.pic_frame.Handle, (uint)(oldUserInfo == null ? 0 : oldUserInfo.Id)); + if (_this != null && oldUserInfo != null && (UserInfo == null || UserInfo.Id == 0)) + { + AgoraHelper.ClearUserVideo((uint)(oldUserInfo.Id), (long)_this.pic_frame.Handle); + RefreshPictureBox(); + } + + AgoraHelper.SetupUserVideo((uint)UserInfo.Id, (long)_this.pic_frame.Handle, (uint)(oldUserInfo == null ? 0 : oldUserInfo.Id)); } + /// + /// 刷新PictureBox视图,解决最后一帧不刷新的问题 + /// + private void RefreshPictureBox() + { + _this.pic_frame.BeginInvoke(() => + { + _this.pic_frame.Refresh(); + }); + } + + private void DisplayAvatar() + { + _this.pic_frame.BeginInvoke(() => + { + //_this.pic_frame.Image = AvatatHelper.CameraCloseImage; + }); + } + + } } diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Views/MainWindow.xaml b/Meeting.V2.Demo/Meeting.V2.Demo/Views/MainWindow.xaml index 6692c73..06bab04 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/Views/MainWindow.xaml +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Views/MainWindow.xaml @@ -5,9 +5,11 @@ xmlns:cmn="clr-namespace:Demo.Common;assembly=Demo.Common" xmlns:cnt="clr-namespace:Demo.Common.Converters;assembly=Demo.Common" xmlns:hc="https://handyorg.github.io/handycontrol" + xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:intertop="clr-namespace:Microsoft.DwayneNeed.Interop;assembly=MahApps.Microsoft.DwayneNeed" xmlns:local="clr-namespace:Meeting.V2.Demo.Views" xmlns:prism="http://prismlibrary.com/" + xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" xmlns:wfc="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms" xmlns:wfh="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration" Width="962" @@ -15,16 +17,11 @@ MinWidth="1050" MinHeight="642" prism:ViewModelLocator.AutoWireViewModel="True" - Background="#252525" + Background="#FFFFFF" CloseButtonForeground="Red" - NonClientAreaBackground="Black" - NonClientAreaForeground="White" - OtherButtonForeground="White" + NonClientAreaForeground="#7B818F" + OtherButtonForeground="#7B818F" WindowStartupLocation="CenterScreen"> - - - - @@ -32,44 +29,193 @@ + - + + + + + + + + + + 仅主播可用 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --> diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Views/MainWindow.xaml.cs b/Meeting.V2.Demo/Meeting.V2.Demo/Views/MainWindow.xaml.cs index 8585407..d09746b 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/Views/MainWindow.xaml.cs +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Views/MainWindow.xaml.cs @@ -14,5 +14,10 @@ namespace Meeting.V2.Demo.Views { InitializeComponent(); } + + private void SplitButton_Click(object sender, RoutedEventArgs e) + { + + } } } diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoAreaView.xaml b/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoAreaView.xaml index 89ad0c7..057898a 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoAreaView.xaml +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoAreaView.xaml @@ -8,14 +8,16 @@ xmlns:local="clr-namespace:Meeting.V2.Demo.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:prism="http://prismlibrary.com/" + xmlns:sys="clr-namespace:System;assembly=mscorlib" x:Name="root" d:DesignHeight="530" d:DesignWidth="960" prism:ViewModelLocator.AutoWireViewModel="True" + Background="#434549" mc:Ignorable="d"> - + @@ -28,9 +30,16 @@ Grid.Row="0" Padding="3" ItemsSource="{Binding UserInfos}"> + + + + + + + - --> - + @@ -56,11 +65,18 @@ - - - + + + + @@ -70,16 +86,7 @@ Grid.Row="1" Grid.RowSpan="2" Background="#2D3033"> - - - - - + diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoView.xaml b/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoView.xaml index 3a70866..b148c2e 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoView.xaml +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoView.xaml @@ -16,8 +16,8 @@ x:Name="root" MinWidth="135" MinHeight="75" - d:DesignHeight="1080" - d:DesignWidth="1920" + d:DesignHeight="180" + d:DesignWidth="320" prism:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d"> @@ -26,104 +26,134 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - + Text="{Binding UserInfo.UserName}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoView.xaml.cs b/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoView.xaml.cs index ad98daa..90fc879 100644 --- a/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoView.xaml.cs +++ b/Meeting.V2.Demo/Meeting.V2.Demo/Views/VideoView.xaml.cs @@ -44,47 +44,41 @@ namespace Meeting.V2.Demo.Views #region 控制右上角操作按钮显示与消失 - /// - /// 控制操作按钮显示 - /// - private bool IsOperVisible = false; - + private bool isMouseInPicFrame = false; + private bool isMouseInBtnOper = false; + private void pic_frame_MouseEnter(object sender, EventArgs e) { - btn_oper.Visibility = Visibility.Visible; - + isMouseInPicFrame = true; + btn_oper.Visibility = Visibility.Visible; } - private async void pic_frame_MouseLeave(object sender, EventArgs e) + private void pic_frame_MouseLeave(object sender, EventArgs e) { - // 使用ConfigureAwait(false)可以避免不必要的线程上下文切换 - await Task.Delay(50).ConfigureAwait(false); - - // 如果需要操作UI,使用Dispatcher - await Application.Current.Dispatcher.InvokeAsync(() => + isMouseInPicFrame = false; + // 如果鼠标既不在大控件也不在小控件内,则隐藏小控件 + if (!isMouseInBtnOper) { - // UI相关操作 - if (!IsOperVisible) - { - btn_oper.Visibility = Visibility.Hidden; - } - }); - + btn_oper.Visibility = Visibility.Hidden; + } } private void btn_oper_MouseEnter(object sender, MouseEventArgs e) { - IsOperVisible = true; + isMouseInBtnOper = true; + + //IsOperVisible = true; } - private async void btn_oper_MouseLeave(object sender, MouseEventArgs e) + private void btn_oper_MouseLeave(object sender, MouseEventArgs e) { - //await Task.Delay(50).ConfigureAwait(false); - - - IsOperVisible = false; - + isMouseInBtnOper = false; + // 如果鼠标既不在大控件也不在小控件内,则隐藏小控件 + if (!isMouseInPicFrame) + { + btn_oper.Visibility = Visibility.Hidden; + } } #endregion