This commit is contained in:
youngq 2024-12-19 13:32:55 +08:00
parent ee2a75ba8d
commit 4e7b6a2f92
22 changed files with 1334 additions and 267 deletions

View File

@ -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<AudioMuteInfo>
{
}
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; }
}
}

View File

@ -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<VideoMuteInfo>
{
}
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; }
}
}

View File

@ -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<T>(this ObservableCollection<T> list, int count) where T : User
{
for (int i = 0; i < count; i++)
{
list.Add((T)User.GetInit());
}
}
public static int AddUser<T>(this ObservableCollection<T> 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<T>(this ObservableCollection<T> 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<T>(this ObservableCollection<T> list) where T : User
{
for (int i = 0; i < list.Count; i++)
{
list[i] = (T)User.GetInit();
}
}
}
}

View File

@ -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();

View File

@ -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;
}
}
/// <summary>
/// 获取姓名对应的圆形图片
/// </summary>
/// <param name="name"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
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)];
}
/// <summary>
/// 保存图片到磁盘
/// </summary>
/// <param name="name"></param>
/// <param name="targetFile"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
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;
}
}
}

View File

@ -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<IEventAggregator>();
}
/// <summary>
/// 用户加入频道回调
/// </summary>
/// <param name="connection"></param>
/// <param name="remoteUid"></param>
/// <param name="elapsed"></param>
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<IEventAggregator>();
@ -36,6 +44,12 @@ namespace Demo.Common.Helpers
}
/// <summary>
/// 用户离开频道回调
/// </summary>
/// <param name="connection"></param>
/// <param name="remoteUid"></param>
/// <param name="reason"></param>
public override void OnUserOffline(RtcConnection connection, uint remoteUid, USER_OFFLINE_REASON_TYPE reason)
{
//var aggregator = ContainerLocator.Container.Resolve<IEventAggregator>();
@ -48,6 +62,13 @@ namespace Demo.Common.Helpers
});
}
/// <summary>
/// 音频音量提示回调
/// </summary>
/// <param name="connection"></param>
/// <param name="speakers"></param>
/// <param name="speakerNumber"></param>
/// <param name="totalVolume"></param>
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<AudioVolumeIndicationEvent>().Publish(sp);
});
}
/// <summary>
/// 本地用户音频发布状态改变回调。
/// </summary>
/// <param name="channel"></param>
/// <param name="oldState"></param>
/// <param name="newState"></param>
/// <param name="elapseSinceLastState"></param>
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<AudioMuteStateChangedEvent>().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<AudioMuteStateChangedEvent>().Publish(new AudioMuteInfo(AgoraHelper._localUId, false, channel));
break;
}
}
///// <summary>
///// 远端用户音频订阅状态发生改变回调。
///// </summary>
///// <param name="channel"></param>
///// <param name="uid"></param>
///// <param name="oldState"></param>
///// <param name="newState"></param>
///// <param name="elapseSinceLastState"></param>
//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<AudioMuteStateChangedEvent>().Publish(new AudioMuteInfo(uid, true, channel));
// break;
// case STREAM_SUBSCRIBE_STATE.SUB_STATE_SUBSCRIBING:
// break;
// case STREAM_SUBSCRIBE_STATE.SUB_STATE_SUBSCRIBED:
// aggregator.GetEvent<AudioMuteStateChangedEvent>().Publish(new AudioMuteInfo(uid, false, channel));
// break;
// }
//}
/// <summary>
/// 远端用户音频订阅状态发生改变回调。
/// </summary>
/// <param name="connection"></param>
/// <param name="remoteUid"></param>
/// <param name="muted"></param>
public override void OnUserMuteAudio(RtcConnection connection, uint remoteUid, bool muted)
{
aggregator.GetEvent<AudioMuteStateChangedEvent>().Publish(new AudioMuteInfo(remoteUid, muted, connection.channelId));
}
/// <summary>
/// 远端用户视频订阅状态发生改变回调。
/// </summary>
/// <param name="connection"></param>
/// <param name="remoteUid"></param>
/// <param name="muted"></param>
public override void OnUserMuteVideo(RtcConnection connection, uint remoteUid, bool muted)
{
aggregator.GetEvent<VideoMuteStateChangedEvent>().Publish(new VideoMuteInfo(remoteUid, muted, connection.channelId));
}
/// <summary>
/// 本地用户视频发布状态改变回调。
/// </summary>
/// <param name="source"></param>
/// <param name="channel"></param>
/// <param name="oldState"></param>
/// <param name="newState"></param>
/// <param name="elapseSinceLastState"></param>
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<VideoMuteStateChangedEvent>().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<VideoMuteStateChangedEvent>().Publish(new VideoMuteInfo(AgoraHelper._localUId, false, channel));
break;
}
}
/// <summary>
/// 远端用户视频订阅状态发生改变回调。
/// </summary>
/// <param name="channel"></param>
/// <param name="uid"></param>
/// <param name="oldState"></param>
/// <param name="newState"></param>
/// <param name="elapseSinceLastState"></param>
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<VideoMuteStateChangedEvent>().Publish(new VideoMuteInfo(uid, true, channel));
break;
case STREAM_SUBSCRIBE_STATE.SUB_STATE_SUBSCRIBING:
break;
case STREAM_SUBSCRIBE_STATE.SUB_STATE_SUBSCRIBED:
aggregator.GetEvent<VideoMuteStateChangedEvent>().Publish(new VideoMuteInfo(uid, false, channel));
break;
}
}
}
}

View File

@ -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); }
}
}
}

View File

@ -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);
}
}
///// <summary>
///// 用户头像
///// </summary>
//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));
}
}
/// <summary>
/// 麦克风音量
/// </summary>
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();
// }
//}
}
}

View File

@ -8,8 +8,10 @@
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!--<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml" />-->
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
<ResourceDictionary Source="/Assets/Resource/MeetingRoomDictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

View File

@ -0,0 +1,151 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converter="clr-namespace:Demo.Common.Converters;assembly=Demo.Common"
xmlns:hc="https://handyorg.github.io/handycontrol">
<converter:BooleanInverseConverter x:Key="BoolInverseConverter" />
<converter:AspectRatioConverter x:Key="AspectRatioConverter" />
<!--<converter:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />-->
<converter:VolumeToScaleConverter x:Key="VolumeToScaleConverter" />
<Style
x:Key="WGS_ButtonDock"
BasedOn="{StaticResource ButtonBaseStyle}"
TargetType="{x:Type Button}">
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="45" />
<Setter Property="FontSize" Value="11" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="BorderBrush" Value="#FFFFFF" />
<Setter Property="Padding" Value="0" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#E6E8EB" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#c7c7c7" />
</Trigger>
</Style.Triggers>
</Style>
<Style
x:Key="WGS_SplitButtonDock"
BasedOn="{StaticResource SplitButtonBaseStyle}"
TargetType="hc:SplitButton">
<Setter Property="Width" Value="10" />
<Setter Property="Height" Value="45" />
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="hc:SplitButton">
<hc:SimplePanel x:Name="templateRoot">
<Border Background="{TemplateBinding Background}" CornerRadius="{Binding Path=(hc:BorderElement.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}" />
<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{Binding Path=(hc:BorderElement.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Orientation="Horizontal">
<Path
x:Name="PathMain"
Width="{TemplateBinding hc:IconElement.Width}"
Height="{TemplateBinding hc:IconElement.Height}"
Data="{TemplateBinding hc:IconElement.Geometry}"
Fill="{TemplateBinding Foreground}"
SnapsToDevicePixels="True"
Stretch="Uniform" />
<ContentPresenter
x:Name="ContentPresenterMain"
Margin="6,0,0,0"
VerticalAlignment="Center"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</StackPanel>
<ToggleButton
x:Name="PART_Arrow"
Grid.Column="1"
Width="Auto"
Height="Auto"
Padding="{Binding Padding, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource ThicknessSplitConverter}, ConverterParameter='0,1,1,1'}"
hc:IconElement.Geometry="{StaticResource UpGeometry}"
hc:IconElement.Width="9"
hc:IconSwitchElement.GeometrySelected="{StaticResource DownGeometry}"
Foreground="{TemplateBinding Foreground}"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource ToggleButtonIconTransparent}" />
</Grid>
</Border>
<Popup
Margin="1"
AllowsTransparency="true"
IsOpen="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
Placement="Top"
PopupAnimation="Fade"
StaysOpen="False">
<Border
x:Name="dropDownBorder"
MinWidth="{Binding ActualWidth, ElementName=templateRoot}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
Background="{DynamicResource RegionBrush}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="{Binding Path=(hc:BorderElement.CornerRadius), RelativeSource={RelativeSource TemplatedParent}}">
<ScrollViewer x:Name="DropDownScrollViewer" Margin="0,4">
<hc:SimplePanel RenderOptions.ClearTypeHint="Enabled">
<Canvas
Width="0"
Height="0"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Rectangle
x:Name="opaqueRect"
Width="{Binding ActualWidth, ElementName=dropDownBorder}"
Height="{Binding ActualHeight, ElementName=dropDownBorder}"
Fill="{Binding Background, ElementName=dropDownBorder}" />
</Canvas>
<ContentPresenter ContentSource="DropDownContent" />
</hc:SimplePanel>
</ScrollViewer>
</Border>
</Popup>
</hc:SimplePanel>
<ControlTemplate.Triggers>
<Trigger Property="Content" Value="{x:Null}">
<Setter TargetName="ContentPresenterMain" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="hc:IconElement.Geometry" Value="{x:Null}">
<Setter TargetName="PathMain" Property="Visibility" Value="Collapsed" />
<Setter TargetName="ContentPresenterMain" Property="Margin" Value="0" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HitMode" Value="Click" />
<Condition SourceName="PART_Arrow" Property="IsChecked" Value="True" />
</MultiTrigger.Conditions>
<Setter Property="IsDropDownOpen" Value="True" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource SecondaryRegionBrush}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{DynamicResource BorderBrush}" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1734572431893" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4287" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M318.513292 1001.73913a40.70559 40.70559 0 0 1-17.172671-4.452173 35.617391 35.617391 0 0 1-16.536646-21.624845 36.889441 36.889441 0 0 1 3.816149-26.077019l94.767702-164.730435a34.981366 34.981366 0 0 1 30.529193-17.17267 33.073292 33.073292 0 0 1 17.17267 4.452173 34.981366 34.981366 0 0 1 12.720497 47.701864l-94.767702 164.730435a34.981366 34.981366 0 0 1-30.529192 17.17267zM685.499627 1001.73913a33.709317 33.709317 0 0 1-29.893167-17.17267l-95.403727-164.730435a34.981366 34.981366 0 0 1 12.720497-47.701864 34.981366 34.981366 0 0 1 17.808695-4.452173 34.981366 34.981366 0 0 1 29.893168 17.17267l95.403727 164.730435a33.709317 33.709317 0 0 1 3.180124 26.077019 35.617391 35.617391 0 0 1-16.536646 21.624845 38.161491 38.161491 0 0 1-17.172671 4.452173z" fill="#8a8a8a" p-id="4288"></path><path d="M216.749317 1024a34.981366 34.981366 0 0 1-34.345342-34.981366 34.981366 34.981366 0 0 1 34.345342-34.981367h572.42236a34.981366 34.981366 0 0 1 34.981366 34.981367 34.981366 34.981366 0 0 1-34.981366 34.981366zM175.407702 669.734161a410.236025 410.236025 0 0 1-88.407454-254.409937A415.324224 415.324224 0 0 1 502.324472 0.636025a407.691925 407.691925 0 0 1 254.409938 87.771428l-45.157764 50.245963A365.714286 365.714286 0 0 0 502.324472 69.962733 345.997516 345.997516 0 0 0 156.962981 415.324224a338.365217 338.365217 0 0 0 71.870808 204.8l-48.337888 52.154037zM502.324472 829.376398a415.324224 415.324224 0 0 1-284.303106-112.576398l45.157764-49.609938a363.170186 363.170186 0 0 0 239.145342 92.859627 345.361491 345.361491 0 0 0 344.725466-345.36149 342.817391 342.817391 0 0 0-92.223603-234.693168l44.52174-48.973913a395.607453 395.607453 0 0 1 117.664596 283.667081 415.324224 415.324224 0 0 1-414.688199 414.688199z" fill="#8a8a8a" p-id="4289"></path><path d="M334.413913 509.455901a190.807453 190.807453 0 0 1-25.440994-94.767702 190.807453 190.807453 0 0 1 190.807454-190.807454 190.807453 190.807453 0 0 1 94.131677 26.077019l-45.793789 52.154037a183.175155 183.175155 0 0 0-48.337888-8.268323 127.204969 127.204969 0 0 0-124.024845 123.38882 127.204969 127.204969 0 0 0 8.904348 45.157764l-50.245963 53.426087zM502.324472 608.039752a190.807453 190.807453 0 0 1-127.204969-48.337889l44.521739-49.609938a141.83354 141.83354 0 0 0 82.047205 27.985094 123.38882 123.38882 0 0 0 123.38882-123.38882 123.38882 123.38882 0 0 0-28.621118-77.595031l45.157764-48.973913a177.450932 177.450932 0 0 1 52.790062 127.204969 190.807453 190.807453 0 0 1-192.079503 192.715528z" fill="#8a8a8a" p-id="4290"></path><path d="M108.625093 905.699379a34.981366 34.981366 0 0 1-24.168944-10.176398 33.709317 33.709317 0 0 1 0-48.973913L890.299627 40.069565a36.253416 36.253416 0 0 1 24.804969-10.176397 36.253416 36.253416 0 0 1 24.804969 10.176397 34.981366 34.981366 0 0 1 0 48.973913L133.430062 895.522981a34.981366 34.981366 0 0 1-24.804969 10.176398z" fill="#8a8a8a" p-id="4291"></path></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733909306083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13182" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M504.391111 734.862222h14.933333a200.675556 200.675556 0 0 0 200.533334-200.675555V200.675556A200.675556 200.675556 0 0 0 519.466667 0h-15.075556A200.675556 200.675556 0 0 0 304 200.675556v333.511111a200.675556 200.675556 0 0 0 200.391111 200.675555z" fill="#4E5461" p-id="13183"></path><path d="M843.022222 533.191111a34.986667 34.986667 0 0 0-69.973333 0 261.12 261.12 0 1 1-522.24 0 34.986667 34.986667 0 0 0-69.973333 0 331.662222 331.662222 0 0 0 296.106666 329.244445v91.448888h-121.457778a35.057778 35.057778 0 0 0 0 70.115556h312.888889a34.986667 34.986667 0 1 0 0-69.973333h-120.888889v-91.591111a331.662222 331.662222 0 0 0 295.537778-329.244445z" fill="#4E5461" p-id="13184"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1734420317677" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9974" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M860.16 322.56h-15.36L716.8 419.84v168.96c0 5.12 0 10.24 5.12 10.24l128 97.28c5.12 0 5.12 5.12 10.24 5.12h5.12c5.12 0 5.12-5.12 5.12-15.36V337.92c0-5.12-5.12-10.24-10.24-15.36zM599.04 291.84H215.04S153.6 291.84 153.6 353.28V665.6c0 61.44 61.44 61.44 61.44 61.44h384s61.44 0 61.44-61.44V358.4c5.12-66.56-61.44-66.56-61.44-66.56z" fill="#666666" p-id="9975"></path></svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>True</UseWindowsForms>
@ -8,8 +8,11 @@
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\manager.svg" />
<None Remove="Assets\camera_close.svg" />
<None Remove="Assets\huatong.png" />
<None Remove="Assets\mico_btn.svg" />
<None Remove="Assets\more.svg" />
<None Remove="Assets\video_btn.svg" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="agora_rtc_sdk" Version="4.2.1" />
@ -24,10 +27,20 @@
<ProjectReference Include="..\..\Demo.Services\Demo.Services.csproj" />
</ItemGroup>
<ItemGroup>
<Resource Include="Assets\manager.svg" />
<Resource Include="Assets\camera_close.svg" />
<Resource Include="Assets\mico_btn.svg" />
<Resource Include="Assets\more.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Assets\mico.svg" />
<Resource Include="Assets\video_btn.svg" />
</ItemGroup>
<ItemGroup>
<None Update="Assets\manager.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Assets\camera_close1.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -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<AgoraDeviceInfo> audioDevices = new();
public ObservableCollection<AgoraDeviceInfo> AudioDevices
{
get { return audioDevices; }
set { SetProperty(ref audioDevices, value); }
}
private ObservableCollection<AgoraDeviceInfo> playbackDevices = new();
public ObservableCollection<AgoraDeviceInfo> 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<UserJoinEvent>().Publish(new User
LocalUser = new User
{
Id = UId,
UserName = UId.ToString(),
IsManager = true,
IsLocal = true
});
IsLocal = true,
IsMuteVideo = false,
IsMuteAudio = false
};
_aggregator.GetEvent<UserJoinEvent>().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<UserLeaveEvent>().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;
}
}
/// <summary>
/// 切换麦克风设备
/// </summary>
public DelegateCommand<string> ChangeAudioDeviceCommand => new DelegateCommand<string>(ExecuteChangeAudioDeviceCommand);
void ExecuteChangeAudioDeviceCommand(string audioDeviceId)
{
var audioDeviceManager = AgoraHelper._RtcEngineInstance.GetAudioDeviceManager();
audioDeviceManager.SetRecordingDevice(audioDeviceId);
}
/// <summary>
/// 切换扬声器设备
/// </summary>
public DelegateCommand<string> ChangePlaybackDeviceCommand => new DelegateCommand<string>(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));
}
}
}
}

View File

@ -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<UserJoinEvent>().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<UserLeaveEvent>().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<User> userInfos = new();

View File

@ -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<AudioVolumeIndicationEvent>().Subscribe(SetUserVolume,
// 音频音量监听
aggregator.GetEvent<AudioVolumeIndicationEvent>().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<AudioMuteStateChangedEvent>().Subscribe((arg) =>
{
UserInfo.IsMuteAudio = arg.IsMute;
}, arg => UserInfo != null && UserInfo.Id > 0 && arg.UId > 0 && arg.UId == UserInfo.Id);
// 视频状态监听
aggregator.GetEvent<VideoMuteStateChangedEvent>().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<VideoView> LoadedCommand => new DelegateCommand<VideoView>((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;
/// <summary>
/// 用户麦克风音量
/// </summary>
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;
//}
});
/// <summary>
/// 渲染视频画面
/// </summary>
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));
}
/// <summary>
/// 刷新PictureBox视图解决最后一帧不刷新的问题
/// </summary>
private void RefreshPictureBox()
{
_this.pic_frame.BeginInvoke(() =>
{
_this.pic_frame.Refresh();
});
}
private void DisplayAvatar()
{
_this.pic_frame.BeginInvoke(() =>
{
//_this.pic_frame.Image = AvatatHelper.CameraCloseImage;
});
}
}
}

View File

@ -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">
<Window.Resources>
<cnt:BooleanInverseConverter x:Key="BoolInverseConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
@ -32,44 +29,193 @@
</Grid.RowDefinitions>
<ContentControl Grid.Row="0" prism:RegionManager.RegionName="{x:Static cmn:RegionNames.VideoRegion}" />
<!--<ScrollViewer
Grid.RowSpan="2"
HorizontalAlignment="Left"
VerticalScrollBarVisibility="Hidden">
<StackPanel Margin="0,10,10,10" VerticalAlignment="Top" />
</ScrollViewer>-->
<ScrollViewer
<DockPanel Grid.Row="1">
<StackPanel VerticalAlignment="Center" Orientation="Horizontal">
<hc:TextBox
x:Name="uid"
Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
hc:InfoElement.Placeholder="输入任意UID"
IsEnabled="False"
Text="{Binding UId}" />
<Button
Margin="5"
Command="{Binding JoinChannelCommand}"
IsEnabled="{Binding IsJoin, Converter={StaticResource BoolInverseConverter}}">
主播加入频道
</Button>
<Button Command="{Binding JoinChannel4AudienceCommand}" IsEnabled="{Binding IsJoin, Converter={StaticResource BoolInverseConverter}}">
观众加入频道
</Button>
<Button
Margin="5"
Command="{Binding LeavelChannelCommand}"
IsEnabled="{Binding IsJoin}"
Style="{StaticResource ButtonDanger}">
离开频道
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock>仅主播可用</TextBlock>
<!--#region 静音开关按钮-->
<Button Command="{Binding OperMicoCommand}" Style="{StaticResource WGS_ButtonDock}">
<StackPanel>
<Grid Width="50">
<svgc:SvgViewbox Height="19" Source="Assets/mico_btn.svg" />
<Rectangle
Width="8"
Height="14"
Margin="0,-5,0,1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="#36B282"
RadiusX="3"
RadiusY="3"
RenderTransformOrigin="0.5,1">
<Rectangle.RenderTransform>
<ScaleTransform>
<ScaleTransform.ScaleY>
<Binding
Converter="{StaticResource VolumeToScaleConverter}"
Mode="OneWay"
NotifyOnTargetUpdated="True"
Path="LocalUser.MicoVolume" />
</ScaleTransform.ScaleY>
</ScaleTransform>
</Rectangle.RenderTransform>
</Rectangle>
<Line
x:Name="SlashLine"
Stroke="#FA4E32"
StrokeThickness="3"
Visibility="{Binding LocalUser.MicoSlashLineVisibility}"
X1="35"
X2="15"
Y1="0"
Y2="17" />
</Grid>
<TextBlock
Margin="0,5,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="Black"
Text="{Binding LocalUser.MicStatusText}" />
</StackPanel>
</Button>
<!-- 扬声器、麦克风切换按钮 -->
<hc:SplitButton
x:Name="AudioSelector"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="#4e5461"
Style="{StaticResource WGS_SplitButtonDock}">
<hc:SplitButton.DropDownContent>
<StackPanel>
<TextBlock
Margin="10,10,10,0"
HorizontalAlignment="Stretch"
Style="{StaticResource TextBlockDefaultSecLight}"
Text="选择扬声器" />
<ItemsControl Margin="10,5,10,0" ItemsSource="{Binding PlaybackDevices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton
Margin="5,4,5,2"
HorizontalAlignment="Stretch"
Content="{Binding DeviceName}"
GroupName="PlaybackDeviceGroup"
IsChecked="{Binding IsSelected}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Checked">
<i:InvokeCommandAction Command="{Binding DataContext.ChangePlaybackDeviceCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding DeviceId}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<hc:Divider Margin="10,6,10,0" />
<TextBlock
Margin="10,10,10,0"
HorizontalAlignment="Stretch"
Style="{StaticResource TextBlockDefaultSecLight}"
Text="选择麦克风" />
<ItemsControl Margin="10,5,10,10" ItemsSource="{Binding AudioDevices}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton
Margin="5,4,5,2"
HorizontalAlignment="Stretch"
Content="{Binding DeviceName}"
GroupName="AudioDeviceGroup"
IsChecked="{Binding IsSelected}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Checked">
<i:InvokeCommandAction Command="{Binding DataContext.ChangeAudioDeviceCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}" CommandParameter="{Binding DeviceId}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</hc:SplitButton.DropDownContent>
</hc:SplitButton>
<Button
Margin="10,0,0,0"
Command="{Binding OperVideoCommand}"
Style="{StaticResource WGS_ButtonDock}">
<StackPanel>
<Grid Width="50" Margin="0,4,0,2">
<svgc:SvgViewbox Height="15" Source="Assets/video_btn.svg" />
<Line
Stroke="#FA4E32"
StrokeThickness="3"
Visibility="{Binding LocalUser.VideoSlashLineVisibility}"
X1="35"
X2="10"
Y1="0"
Y2="18" />
</Grid>
<TextBlock
Margin="0,5,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="Black"
Text="{Binding LocalUser.VideoStatusText}" />
</StackPanel>
</Button>
</StackPanel>
</DockPanel>
<!--<ScrollViewer
Grid.RowSpan="2"
HorizontalAlignment="Right"
VerticalScrollBarVisibility="Hidden">
<StackPanel Margin="0,10,10,10" VerticalAlignment="Top" />
</ScrollViewer>
<StackPanel
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="#242424"
Orientation="Horizontal">
<hc:TextBox
x:Name="uid"
HorizontalAlignment="Center"
VerticalAlignment="Center"
hc:InfoElement.Placeholder="输入任意UID"
IsEnabled="{Binding IsJoin, Converter={StaticResource BoolInverseConverter}}"
Text="{Binding UId}" />
<Button
Height="45"
Margin="10"
Command="{Binding JoinChannelCommand}"
IsEnabled="{Binding IsJoin, Converter={StaticResource BoolInverseConverter}}">
加入频道
</Button>
<Button
Height="45"
Margin="10"
Command="{Binding LeavelChannelCommand}"
IsEnabled="{Binding IsJoin}">
离开频道
</Button>
</StackPanel>
<!--<local:VideoView x:Name="videoVIew" />-->
</ScrollViewer>-->
</Grid>
</hc:Window>

View File

@ -14,5 +14,10 @@ namespace Meeting.V2.Demo.Views
{
InitializeComponent();
}
private void SplitButton_Click(object sender, RoutedEventArgs e)
{
}
}
}

View File

@ -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">
<UserControl.Resources>
<!--<UserControl.Resources>
<cnt:AspectRatioConverter x:Key="AspectRatioConverter" />
</UserControl.Resources>
</UserControl.Resources>-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" MaxHeight="155" />
@ -28,9 +30,16 @@
Grid.Row="0"
Padding="3"
ItemsSource="{Binding UserInfos}">
<ItemsControl.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#222426" />
<GradientStop Offset="1" Color="#525559" />
</LinearGradientBrush>
</ItemsControl.Background>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel
<VirtualizingStackPanel
x:Name="stack_users"
HorizontalAlignment="Center"
Background="#2D3033"
@ -40,13 +49,13 @@
<i:InvokeCommandAction Command="{Binding UserStackPanelSizeChangeCommand}" CommandParameter="{Binding ElementName=stack_users}" />
</i:EventTrigger>
</i:Interaction.Triggers>-->
</StackPanel>
</VirtualizingStackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:VideoView
Margin="3"
CacheMode="BitmapCache"
Command="{Binding DataContext.SelectedUserCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Border}}"
CommandParameter="{Binding DataContext, RelativeSource={RelativeSource AncestorType=ContentPresenter}}"
UserInfo="{Binding DataContext, RelativeSource={RelativeSource AncestorType=ContentPresenter}}">
@ -56,11 +65,18 @@
<Binding ElementName="user_vid" Path="ActualWidth" />
<!--<Binding Path="DataContext.UserPageSize" RelativeSource="{RelativeSource AncestorType=UserControl}" />
<Binding Path="Margin" RelativeSource="{RelativeSource Mode=Self}" />-->
</MultiBinding>
</local:VideoView.Width>
<!-- 添加数据触发器 -->
<local:VideoView.Style>
<Style TargetType="local:VideoView">
<Style.Triggers>
<DataTrigger Binding="{Binding DataContext.Id, RelativeSource={RelativeSource AncestorType=ContentPresenter}}" Value="0">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</local:VideoView.Style>
</local:VideoView>
</DataTemplate>
</ItemsControl.ItemTemplate>
@ -70,16 +86,7 @@
Grid.Row="1"
Grid.RowSpan="2"
Background="#2D3033">
<local:VideoView x:Name="main_video" UserInfo="{Binding DataContext.FocusUser, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Border}}">
<!--<local:VideoView.Width>
<MultiBinding Converter="{StaticResource AspectRatioConverter}">
<Binding ElementName="border_main" Path="ActualHeight" />
<Binding ElementName="border_main" Path="ActualWidth" />
</MultiBinding>
</local:VideoView.Width>-->
</local:VideoView>
<local:VideoView x:Name="main_video" UserInfo="{Binding DataContext.FocusUser, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Border}}" />
</Border>

View File

@ -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">
<i:Interaction.Triggers>
@ -26,104 +26,134 @@
</i:EventTrigger>
</i:Interaction.Triggers>
<UserControl.Resources>
<cnt:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<cnt:AspectRatioConverter x:Key="AspectRatioConverter" />
<cnt:VolumeToScaleConverter x:Key="VolumeToScaleConverter" />
</UserControl.Resources>
<Grid>
<!--#region 视频区域 + 右上角按钮-->
<intertop:AirspaceDecorator
AirspaceMode="Redirect"
IsInputRedirectionEnabled="True"
IsOutputRedirectionEnabled="True">
<WindowsFormsHost x:Name="gridHost">
<wfc:PictureBox
x:Name="pic_frame"
BackColor="#000000"
Dock="Fill"
MouseDoubleClick="pic_frame_MouseDoubleClick"
MouseEnter="pic_frame_MouseEnter"
MouseLeave="pic_frame_MouseLeave"
SizeMode="StretchImage" />
</WindowsFormsHost>
</intertop:AirspaceDecorator>
<Button
x:Name="btn_oper"
Margin="0,8,8,0"
Padding="5"
HorizontalAlignment="Right"
VerticalAlignment="Top"
hc:BorderElement.CornerRadius="4"
Background="#99727169"
BorderBrush="Transparent"
Command="{Binding OperClickCommand}"
MouseEnter="btn_oper_MouseEnter"
MouseLeave="btn_oper_MouseLeave"
Visibility="Collapsed">
<svgc:SvgViewbox Width="15" Source="Assets/more.svg" />
</Button>
<!--#endregion-->
<!--#region 左下角用户名-->
<StackPanel
Height="22"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Orientation="Horizontal">
<Border Background="#ff801a" Visibility="{Binding UserInfo.IsManager, Mode=TwoWay, Converter={StaticResource BoolToVisibilityConverter}}">
<svgc:SvgViewbox
x:Name="icon_manager"
Margin="3"
Source="Assets/manager.svg" />
</Border>
<Border Background="#8C000000" CornerRadius="0,8,0,0">
<DockPanel>
<Grid>
<!-- 原始的SVG图标 -->
<svgc:SvgViewbox
x:Name="icon_mic"
Margin="3"
Source="Assets/mico.svg" />
<!-- 音量进度条 -->
<Rectangle
Width="7"
Height="12"
Margin="0,-4,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="#36B282"
RadiusX="3.5"
RadiusY="3.5"
RenderTransformOrigin="0.5,1">
<Rectangle.RenderTransform>
<ScaleTransform>
<ScaleTransform.ScaleY>
<Binding
Converter="{StaticResource VolumeToScaleConverter}"
Mode="OneWay"
NotifyOnTargetUpdated="True"
Path="MicrophoneVolume" />
</ScaleTransform.ScaleY>
</ScaleTransform>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
<Border Padding="2" Background="#5E6166">
<Grid>
<!-- 添加蒙层 -->
<Grid
x:Name="MaskLayer"
Panel.ZIndex="1"
Visibility="{Binding UserInfo.IsMuteVideo, Converter={StaticResource Boolean2VisibilityConverter}}">
<Border
Padding="6"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="#2B6EC0"
CornerRadius="20">
<TextBlock
x:Name="txt_uname"
Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="12"
FontSize="25"
Foreground="White"
Text="{Binding UserInfo.UserName, Mode=TwoWay}" />
</DockPanel>
</Border>
</StackPanel>
<!--#endregion-->
</Grid>
Text="{Binding UserInfo.UserName}" />
</Border>
</Grid>
<!--#region 视频区域 + 右上角按钮-->
<intertop:AirspaceDecorator
AirspaceMode="Redirect"
IsInputRedirectionEnabled="True"
IsOutputRedirectionEnabled="True">
<WindowsFormsHost x:Name="gridHost">
<wfc:PictureBox
x:Name="pic_frame"
BackColor="#2D3033"
Dock="Fill"
MouseDoubleClick="pic_frame_MouseDoubleClick"
MouseEnter="pic_frame_MouseEnter"
MouseLeave="pic_frame_MouseLeave"
SizeMode="Zoom" />
</WindowsFormsHost>
</intertop:AirspaceDecorator>
<!-- 右上角操作按钮 -->
<Button
x:Name="btn_oper"
Margin="0,8,8,0"
Padding="5"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Panel.ZIndex="2"
hc:BorderElement.CornerRadius="4"
Background="#99727169"
BorderBrush="Transparent"
Command="{Binding OperClickCommand}"
MouseEnter="btn_oper_MouseEnter"
MouseLeave="btn_oper_MouseLeave"
Visibility="Collapsed">
<svgc:SvgViewbox Width="15" Source="Assets/more.svg" />
</Button>
<!--#endregion-->
<!--#region 左下角用户名-->
<StackPanel
Height="22"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Panel.ZIndex="2"
Orientation="Horizontal">
<!-- 管理员图标 -->
<Border Background="#ff801a" Visibility="{Binding UserInfo.IsManager, Mode=TwoWay, Converter={StaticResource Boolean2VisibilityConverter}}">
<svgc:SvgViewbox
x:Name="icon_manager"
Margin="3"
Source="Assets/manager.svg" />
</Border>
<Border Background="#8C000000" CornerRadius="0,8,0,0">
<DockPanel>
<Grid Width="18" Height="16">
<!-- 麦克风图标 -->
<svgc:SvgViewbox x:Name="icon_mic" Source="Assets/mico.svg" />
<!-- 音量进度条 -->
<Rectangle
Width="7"
Height="12"
Margin="-1,-4,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Fill="#36B282"
RadiusX="3.5"
RadiusY="3.5"
RenderTransformOrigin="0.5,1">
<Rectangle.RenderTransform>
<ScaleTransform>
<ScaleTransform.ScaleY>
<Binding
Converter="{StaticResource VolumeToScaleConverter}"
Mode="OneWay"
NotifyOnTargetUpdated="True"
Path="UserInfo.MicoVolume" />
</ScaleTransform.ScaleY>
</ScaleTransform>
</Rectangle.RenderTransform>
</Rectangle>
<Line
x:Name="SlashLine"
Stroke="#FA4E32"
StrokeThickness="2.5"
Visibility="{Binding UserInfo.MicoSlashLineVisibility}"
X1="15"
X2="1"
Y1="1"
Y2="16" />
</Grid>
<!-- 用户名 -->
<TextBlock
x:Name="txt_uname"
Margin="5"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="12"
Foreground="White"
Text="{Binding UserInfo.UserName, Mode=TwoWay}" />
</DockPanel>
</Border>
</StackPanel>
<!--#endregion-->
</Grid>
</Border>
</UserControl>

View File

@ -44,47 +44,41 @@ namespace Meeting.V2.Demo.Views
#region
/// <summary>
/// 控制操作按钮显示
/// </summary>
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