2024年7月

〇、写在前面

本应用基于开源UI框架PyDracula进行开发,除去最基本的UI框架外,所有功能的前后端实现都由我个人开发完成,但也有部分UI(如开关控件和进度条)是参考其他大佬的分享。

这个应用是我的本科毕业设计,但因为个人能力不足,姑且只能使用Python+PySide6开发。

开发这个应用的启发是,曾经我作为深度学习的萌新,在初出训练模型时费了不少力气去学习,一个人摸索了很长时间,直到现在我可以熟练掌握模型的训练。回过头来看我曾经踩过的坑,我把自己在模型训练过程中经常使用的图像处理脚本工具集成在了这个应用中,并实现了一站式的模型选择可视化训练。

但我也知道,真正训练一个好模型远比这复杂的多,因此本应用可以当作新手朋友们的入门体验。

希望本应用能对大家有所帮助。

一、应用简介

本应用主要分为三大功能:

1. 单张图片预处理

本部分功能主要实现仅对一张图片的处理,该功能分为预处理工具和图像增强两部分。

1.1 预处理工具

(1) 更改后缀

在这部分功能中实现了对图片后缀的修改,可以支持jpg、jpeg、png和bmp四种常见数据集图像格式图片的后缀更改,用户通过点击相应的按钮,执行对应的功能。

  1. 修改为.jpg后缀功能:

    将图片统一转为 *.jpg 后缀。支持转换 *.JPG、*.png、*.PNG、*.jpeg、*.JPEG、*.bmp、*.BMP后缀格式。

  2. 修改为.jpeg后缀功能:

    将图片转为 *.jpeg 后缀。支持转换 *.jpg、*.JPG、*.png、*.PNG、*.JPEG、*.bmp、*.BMP后缀格式。

  3. 修改为.png后缀功能:

    将图片转为 *.png 后缀。支持转换 *.jpg、*.JPG、*.PNG、*.jpeg、*.JPEG、*.bmp、*.BMP后缀格式。

  4. 修改为.bmp后缀功能:

    将图片转为 *.bmp 后缀。支持转换 *.jpg、*.JPG、*.png、*.PNG、*.jpeg、*.JPEG、*.BMP后缀格式。

(2) 更改大小

该部分功能实现了直接将图片像素大小进行修改。插值方式说明如下:

插值方式 说明
INTER_LINEAR 双线性插值。
INTER_NEAREAST 最邻近插值。
INTER_CUBIC 三次样条插值。
INTER_AREA 区域插值。(使用像素面积关系进行重采样)
INTER_LANCZOS4 一种Lanczos插值方法(超过8×放大时效果最好)。
INTER_LINEAR_EXACT 位精确双线性插值。

(3) 图片方形化

该部分功能实现了将不规则矩形图片统一为方形图片。

该功能模块所支持的边框拓充方式有:

  1. BORDER_CONSTANT:用纯色背景(常数)填充,需指定拓充的RGB颜色。
  2. BORDER_REFLECT:用边界元素的镜面反射拓充。
  3. BORDER_REPLICATE:使用最边界的像素值代替。
  4. BORDER_WRAP:进行上下边缘调换的外包复制操作。

1.2 图像增强

注意:本部分所有功能可以
按顺序同时叠加使用

(1) 图像旋转

该功能可使图像进行任意角度旋转,并同时缩放处理。

(2) 水平翻转

(3) 垂直翻转

(4) 模糊

该功能可对图像进行模糊处理,支持四种滤波方式:

  1. 均值滤波
  2. 方框滤波
  3. 高斯滤波:滤波核必须为奇数。
  4. 中值滤波:滤波核必须为大于1的奇数。

(5) 噪声

该功能可对图像增加噪声,支持两种噪声方式:

  1. 高斯噪声
  2. 椒盐噪声

这两种方式可以单独使用,也可以同时使用。

(6) 亮度

(7) 对比度

(8) 所有功能同时启用演示

2. 数据集预处理

本部分功能主要实现对整个数据集图片的处理,该功能分为预处理工具和图像增强两部分。

2.1 预处理工具

(1) 统一后缀

该功能和单张图片预处理的
更改后缀
功能类似,故不再过多赘述。

(2) 统一命名

在该模块下,用户可以对数据集进行统一命名,支持的统一命名格式为:

  1. 分类_i
  2. 分类i
  3. 分类-i

(3) 统一大小

该功能和单张图片预处理的
更改大小
功能类似,故不再过多赘述。

(4) 图片方形化

该功能和单张图片预处理的
图片方形化
功能类似,故不再过多赘述。

(5) 数据集分割

该功能用于将整个数据集按照比例分割成训练集(Train)、验证集(Val)、测试集(Test)。

2.2 图像增强

该模块的图像增强功能实现了图像旋转、水平翻转、垂直翻转、模糊、噪声、亮度和对比度的功能,功能的实现同单张图像预处理模块的图像增强功能相似,故不再重复说明。

不同之处在于,由于该模块下的图像增强功能是对整个数据集进行处理,因此该功能引入了
概率随机处理
,以及
参数在一个区间内随机取值

同样:本部分所有功能可以
按顺序同时叠加使用

3. 可视化训练器

在该模块中,可以使用预集成在应用内的36神经网络模型进行训练,包含如下模型:

  1. AlexNet
  2. VGG11
  3. VGG13
  4. VGG16
  5. VGG19
  6. GoogLeNet
  7. ResNet18
  8. ResNet34
  9. RsNet50
  10. ResNet101
  11. ResNet152
  12. ResNeXt50(32×4d)
  13. ResNeXt101(32×8d)
  14. MobileNetV2
  15. MobileNetV3(large)
  16. MobileNetV3(small)
  17. ShuffleNetV2(×0.5)
  18. ShuffleNetV2(×1.0)
  19. ShuffleNetV2(×1.5)
  20. ShuffleNetV2(×2.0)
  21. EfficientNet(B0)
  22. EfficientNet(B1)
  23. EfficientNet(B2)
  24. EfficientNet(B3)
  25. EfficientNet(B4)
  26. EfficientNet(B5)
  27. EfficientNet(B6)
  28. EfficientNet(B7)
  29. EfficientNetV2(S)
  30. EfficientNetV2(M)
  31. EfficientNetV2(L)
  32. VisionTransformer(b16)
  33. VisionTransformer(b32)
  34. SwinTransformer(t)
  35. SwinTransformer(s)
  36. SwinTransformer(b)

可以直接使用以上模型进行训练,如果不开启迁移学习则是重新从头开始训练自己的数据集,如果需要使用迁移学习,则需要自行下载官方的.pth权重文件,具体操作事项在后续的
说明
中陈述。

本功能除了实现
训练过程可视化
,还可以在训练完成后查看
混淆矩阵
,以及
log输出

注意:
本功能暂未实现继续训练的功能,因此建议使用者在无法确定何时停止训练的情况下,将Epoch数值设置到一个较大的值,在通过可视化确认差不多收敛时点击终止按钮停止训练。

3.1 训练过程可视化

3.2 混淆矩阵

3.3 log输出

二、环境要求

本程序在使用时需要安装以下Python环境:

  1. pyside6
  2. pyqt5
  3. matplotlib
  4. opencv
  5. pytorch

三、使用说明

  1. 本应用的图像处理功能由OpenCV实现,相关参数请参考OpenCV的参数说明。

  2. 数据集预处理功能中,传入的数据集文件夹格式必须严格按照如下方式:
    root文件夹/分类1..n文件夹/图片1..n

  3. 可视化训练器部分中,模型的训练是基于PyTorch深度学习框架实现的,因此最终训练后保存的权重文件是以.pth后缀结尾的形式,如果需要其它形式,可以自行进行权重文件格式的转换。

  4. 如果需要使用迁移学习,请自行下载官方权重文件放在
    algorithms/trainer/imagenet
    目录下,并将下载后的权重文件重命名为“xxx.pth”,且命名必须为如下命名之一:

    alexnet.pth、efficientnet_b0.pth、efficientnet_b1.pth、efficientnet_b2.pth、efficientnet_b3.pth、efficientnet_b4.pth、efficientnet_b5.pth、efficientnet_b6.pth、efficientnet_b7.pth、efficientnet_v2_l.pth、efficientnet_v2_m.pth、efficientnet_v2_s.pth、googlenet.pth、mobilenet_v2.pth、mobilenet_v3_large.pth、mobilenet_v3_small.pth、resnet101.pth、resnet152.pth、resnet18.pth、resnet34.pth、resnet50.pth、resnext101_32x8d.pth、resnext50_32x4d.pth、shufflenetv2_x0_5.pth、shufflenetv2_x1_0.pth、shufflenetv2_x1_5.pth、shufflenetv2_x2_0.pth、swin_b.pth、swin_s.pth、swin_t.pth、vgg11.pth、vgg13.pth、vgg16.pth、vgg19.pth、vit_b_16.pth、vit_b_32.pth

四、代码获取

完整应用程序已免费开源在我的GitHub中:
https://github.com/CorianderSaint/TrainerGUI

请需要者在GitHub中点一颗免费的Star⭐,十分感谢!

导航


0、前言

Linux最优秀的地方之一,就在于他的多人多工环境。而为了让各个使用者具有较保密的档案资料,因此档案的权限管理就变的很重要了。
Linux一般将档案可存取的身份分为三个类别,分别是 owner/group/others,且三种身份各有 read/write/execute 等权限。
image

故对于"静态"的档案来说,其中的权限属性即确定了“
哪些身份的人拥有什么样的权限可以去做什么动作
”,如上图所示。

而对于"动态"的进程来说,操作系统又为进程分配了它们的用户身份,即有效用户身份euid、有效群组身份egid、群组身份groups、还有继承uid、继承gid。【注:不管进程是否有SUID/SGID加持,进程都将拥有这5个身份,只不过无差异时 id 命令默认不显示euid/egid而已,默认euid=uid、egid=gid】

注:以下实验中涉及的 id、cat、touch 命令均是通过
cp $(which id) /tmp
从系统命令拷贝而来,通过对 id 命令赋予特权以观察同样被赋予特权的cat、touch 命令在此情景之下进程内部发生的变化,以及实际会产生什么样的效果。

1、权限匹配流程

于是,当一个进程想要操作某个档案文件时,操作系统便会根据
进程拥有的身份

档案拥有的权限标记
去做判断。判断流程如下(示例以读权限 r 举例):

  1. 如果进程的 euid 等于档案的 owner-id,则继续开始判断档案
    拥有者
    对应的权限位中是否包含 r 权限,若包含则文件被进程顺利读取,若不包含则提示进程无权限,此时不管 r 权限包含与否判断流程都将不再继续;如果进程的 euid 不等于档案的 owner-id,则开始步骤 2 的判断。
  2. 如果进程的 groups 包含档案的 group-id,则继续开始判断档案
    所属群组
    对应的权限位中是否包含 r 权限,若包含则文件被进程顺利读取,若不包含则提示进程无权限,此时不管 r 权限包含与否判断流程都将不再继续;如果进程的 groups 不包含档案的 group-id,则开始步骤 3 的判断。
  3. 此时直接开始判断档案
    其它人
    对应的权限位中是否包含 r 权限,若包含则文件被进程顺利读取,若不包含则提示进程无权限。至此,流程不再递归判断,直接结束。

image

image

image

2、五种身份变化

当一个二进制命令被授予 SUID/SGID 特权时,命令进程中 5 种身份的变化。

  • 当无特殊权限时,uid=继承shell的uid,gid=继承shell的gid,euid=uid,egid=gid,groups=uid所加入的群组。
  • 当授予suid特殊权限时,uid=继承shell的uid,gid=继承shell的gid,euid=suid的值,egid=gid,groups=uid所加入的群组。
  • 当授予sgid特殊权限时,uid=继承shell的uid,gid=继承shell的gid,euid=uid,egid=sgid的值,groups=uid所加入的群组+sgid。
  • 当授予suid和sgid特殊权限时,uid=继承shell的uid,gid=继承shell的gid,euid=suid的值,egid=sgid的值,groups=uid所加入的组+sgid。

image

image

image

image

3、有效用户/组

不管实验2中进程的5种身份如何变化,当进程产生新档案时,档案的拥有者和所属群组都是以euid和egid的值去赋予的。

image

4、特权对 Shell 脚本无效

特殊权限 SUID/SGID 对于 shell 脚本不起作用,授予和不授予的状态一样。

image

5、Sudo 与 SUID/SGID 的优先级

当 Sudo 和 SUID/SGID 同时作用二进制命令时,优先以SUID/SGID的权限为主,这其实就相当于在root shell下执行特殊授权的命令一样,命令进程的5种身份依旧按照小节(2)描述的过程一样,root也不例外。

image

6、SUID、SGID、Sticky 各自的功能。

  • SUID:只作用于二进制文件,当命令被执行时,命令会以命令拥有者的身份走完进程的整个生命周期,而非以当前 shell 的用户身份运行。
  • SGID:当作用于二进制文件时,效果与 SUID 类似,只是在命令进程的整个整个生命周期中又多了一个群组的援助;当作用于目录时,使用者进入此目录下时他的有效群组将会变成该目录的群组,此时新建的任何文件目录,他们的 群组id 都将和该目录的 群组id 一样。
  • Sticky:只作用于目录,使用者在该目录下新建的任何文件目录,都将只有自己与 root 才有权力删除。如 /tmp 目录。

继上篇《
GGTalk 开源即时通讯系统源码剖析之:客户端全局缓存及本地存储
》GGTalk客户端的全局缓存以及客户端的本地持久化存储。接下来我们将介绍GGTalk的聊天消息防错漏机制。

GGTalk V8.0
对消息的可靠性,即消息的不丢失和不重复做了一系列优化处理,以保证不会错漏消息。

这篇文章将会详细的介绍GGTalk聊天消息防错漏机制。还没有GGTalk源码的朋友,可以到
GGTalk源码下载中心
下载。

一.  客户端聊天消息存储

GGTalk 在客户端本地缓存了所有与自己相关的聊天消息,包括1对1的聊天消息、和自己所属群的群聊消息。

消息缓存在本地的Sqlite数据库中,使用
SqliteChatRecordPersister
类向Sqlite中插入和查询聊天消息。

关于这部分的代码位于

GGTalk/GGTalk/TalkBase.Client/Core/ClientHandler.cs

GGTalk/TalkBase/Core/IChatRecordPersister.cs

当客户端收到来自其它用户发送的消息时,会触发ClientHandler类中的rapidPassiveEngine_MessageReceived方法

void rapidPassiveEngine_MessageReceived(string sourceUserID, ClientTypeclientType, int informationType, byte[] info, stringtag)
{
//收到文字消息if (informationType == this.resourceCenter.TalkBaseInfoTypes.Chat)
{
   //逻辑处理
}
}

之后再通过
SqliteChatRecordPersister
的父类DefaultChatRecordPersister中的InsertChatMessageRecord方法存储聊天消息:

/// <summary>
///
插入一条聊天记录。/// </summary> public intInsertChatMessageRecord(ChatMessageRecord record)
{
if (this.transactionScopeFactory == null)
{
return 0;
}
object identity = 0;using (TransactionScope scope = this.transactionScopeFactory.NewTransactionScope())
{
IOrmAccesser
<ChatMessageRecord> accesser = scope.NewOrmAccesser<ChatMessageRecord>();
accesser.Insert(record);
scope.Commit();
}
return (int)record.AutoID;
}

当需要查看历史聊天记录时,GGTalk客户端会首先查询本地Sqlite数据库,这样就大大地减轻了服务器和数据库的压力,而且也减少了服务器的带宽占用。

接下来的问题是,离线消息要如何处理了?比如A用户不在线时,有好友发了消息给他,那么,当A上线时,是如何不错漏的获取到这些消息的?

二. GGTalk 是如何处理离线消息的?

回想一下,《GGTalk 开源即时通讯系统源码剖析之:数据库设计》中,我们介绍了OfflineMessage 表,即离线消息表,当目标用户不在线时,发送给他的消息存在该表中。

客户端在登录成功时,会向服务器请求离线消息。服务端收到该请求后,则从OfflineMessage 表中提取与它相关的离线消息推送给他。

关于这部分的代码位于

客户端:

GGTalk/GGTalk/TalkBase.Client/Core/ClientOutter.cs

GGTalk/GGTalk/TalkBase.Client/Core/ClientHandler.cs

服务端:

GGTalk/TalkBase/Server/Core/ServerHandler.cs

GGTalk/GGTalk.Server/DBPersister.cs

GGTalk/TalkBase/Server/Application/OfflineMemoryCache.cs

1. 客户端离线消息处理

每次登录或断线重连成功后,都会通过ClientOutter类中的RequestOfflineMessage方法向服务端请求离线消息,

/// <summary>
///请求离线消息/// </summary>
public voidRequestOfflineMessage()
{
this.rapidPassiveEngine.CustomizeOutter.Send(this.talkBaseInfoTypes.GetOfflineMessage, null);this.rapidPassiveEngine.CustomizeOutter.Send(this.talkBaseInfoTypes.GetGroupOfflineMessage, null);
}

在ClientHandler类中的HandleInformation方法收到服务端返回的离线消息后,进行相应的处理

if (informationType == this.resourceCenter.TalkBaseInfoTypes.OfflineMessage)
{//逻辑处理
}

2.服务端离线消息处理

在ServerHandler类中的rapidServerEngine_MessageReceived方法收到需要转发给其他用户的消息时,会先判断接收方是否在线,如果不在线的话,会通过IDBPersister接口中的StoreOfflineMessage方法将消息存储起来。当收到客户端请求离线消息时,则会调用PickupOfflineMessage将提取的目标用户的所有离线消息发送给对方。目前服务端中有三个类实现了此接口,分别是DBPersister(真实数据库)、DBPersister_SqlSugar(数据库是Oracle时使用)和OfflineMemoryCache(虚拟数据库)

/// <summary>
///存储离线消息。/// </summary>       
/// <param name="msg">要存储的离线消息</param> 
voidStoreOfflineMessage(OfflineMessage msg);/// <summary>
///提取目标用户的所有离线消息,并从DB中删除。/// </summary>       
/// <param name="destUserID">接收离线消息用户的ID</param>
/// <returns>属于目标用户的离线消息列表,按时间升序排列</returns>
List<OfflineMessage> PickupOfflineMessage(string destUserID);

在离线消息的问题解决之后,还剩下一个与消息可靠性相关的难题,那就是当同一个账号同时登录到多个设备时(比如PC和手机),消息是如何在多端之间自动同步的了?

三. 聊天消息是如何在多端之间自动同步的?

这个问题可以拆解为两部分:

(1)作为发送方:我在某一设备上发送给好友的消息,如何同步到我登录的其它设备上?

(2)作为接收方:好友发给我的消息,如何发送给我登录的多个设备?

关于这部分的代码位于

客户端:

GGTalk/GGTalk/TalkBase.Client/Core/ClientHandler.cs

服务端:

GGTalk/TalkBase/Server/Core/ServerHandler.cs

1. 发送方的消息同步

客户端在ClientHandler中预定IRapidPassiveEngine.
EchoMessageReceived
事件,当(当前用户在其它客户端设备上发送了消息)时,就会触发此事件。

/// <summary>
///初始化客户端消息处理器。/// </summary>
/// <param name="center">资源中心</param>
/// <param name="icon">支持闪动的托盘。允许为null</param>
public void Initialize(ResourceCenter<TUser, TGroup>center, TwinkleNotifyIcon icon)
{
this.resourceCenter =center;this.twinkleNotifyIcon =icon;this.brige4ClientOutter = (IBrige4ClientOutter)this.resourceCenter.ClientOutter;this.resourceCenter.RapidPassiveEngine.MessageReceived += new CbGeneric<string, ClientType,int, byte[], string>(rapidPassiveEngine_MessageReceived);this.resourceCenter.RapidPassiveEngine.EchoMessageReceived += new CbGeneric<ClientType, string, int, byte[], string>(RapidPassiveEngine_EchoMessageReceived);this.resourceCenter.RapidPassiveEngine.ContactsOutter.BroadcastReceived += new CbGeneric<string,ClientType, string, int, byte[] ,string>(ContactsOutter_BroadcastReceived);
}
//clientType - destUserID - informationType - message - tag 。 void RapidPassiveEngine_EchoMessageReceived(ClientType clientType, string destUserID, int informationType, byte[] info, stringtag)
{
}

2.接收方的消息同步

服务端在ServerHandler类的rapidServerEngine_MessageReceived方法收到需要转发给其他用户的消息时,会先判断接收方是否在线,如果在线的话,会调用IRapidServerEngine.
SendMessage
方法将消息发送给对方的所有设备,来保证同一账号不同设备之间消息的同步。

void rapidServerEngine_MessageReceived(string sourceUserID, ClientType sourceType, int informationType, byte[] info, stringtag)
{
if (informationType == this.talkBaseInfoTypes.Chat)
{
string destID =tag;if (this.rapidServerEngine.UserManager.IsUserOnLine(destID))
{
this.rapidServerEngine.SendMessage(sourceType, destID, informationType, info, sourceUserID);
}
}
}

四. 结语

以上就是关于GGTalk聊天消息防错漏机制设计与实现的核心了。聊天消息防错漏机制在保障信息准确性、完整性和安全性方面发挥着重要作用,所以,作为一款即时通讯软件,实现该机制是绝对必要的。

如果你觉得还不错,请点赞支持啊!下篇再见!

当你使用Edge等浏览器或系统软件播放媒体时,Windows控制中心就会出现相应的媒体信息以及控制播放的功能,如图。

SMTC (SystemMediaTransportControls) 是一个Windows App SDK (旧为UWP) 中提供的一个API,用于与系统媒体交互。接入SMTC的好处在于,将媒体控制和媒体信息共享给系统,使用通用的特性(例如接受键盘快捷键的播放暂停、接受蓝牙设备的控制),便于与其它支持SMTC的应用交互等。

在UWP App中使用它很简单,只需要调用SystemMediaTransportControls.GetForCurrentView()方法即可,但是该方法仅限在有效的UWP App中调用,否则将抛出“Invalid window handle”异常。实际上,在官方文档中提到所有XXXForCurrentView方法均不适用于UWP App以外的程序调用。

这些 XxxForCurrentView 方法对 ApplicationView 类型具有隐式依赖关系,桌面应用不支持该类型。由于桌面应用不支持 ApplicationView,因此也不支持任何 XxxForCurrentView 方法。

此外官方文档还给出一个可替代的接口ISystemMediaTransportControlsInterop,然而这个接口在给的SDK中有保护性,无法访问。

至此,直接创建SMTC的方法走不通。但是我发现一个奇怪的地方,UWP提供的在Windows.Media.Playback命名空间下的MediaPlayer可以和SMTC自动集成,并且可以通过SystemMediaTransportControls属性直接拿到SMTC对象。MediaPlayer内部通过某种COM组件直接创建了该NativeObject,而没有走API提供的GetForCurrentView或FromAbi方法。也就是说,SMTC组件其实不需要使用合法的UWP Window句柄来创建,只不过可能为了某些特性而加上了该限制(后文将提到)。幸运的是,MediaPlayer帮我们绕过了这点。

下文讲解手动与SMTC交互而不是直接使用MediaPlayer进行播放,你的项目可能已经有了其它的解码器(如WPF版本的MediaPlayer、Bass.Net解码器、NAudio等),则只需要将交互部分接入SMTC而不更换解码器。

文末提供了我封装好的SMTCCreator和SMTCListener,可以直接使用。

一、引用WinRT API到项目

最便捷的方法是直接修改目标框架到win10,这样就能自动引入WinRT API:

<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>

或者一些其他的方法,可以参考这篇博客:
如何在WPF中调用Windows 10/11 API(UWP/WinRT) - zhaotianff - 博客园 (cnblogs.com)

二、通过MediaPlayer获取SMTC对象

usingWindows.Media;usingWindows.Storage.Streams;
...
private readonly Windows.Media.Playback.MediaPlayer _player = new();private readonlySystemMediaTransportControls _smtc;
...
//先禁用系统播放器的命令 _player.CommandManager.IsEnabled = false;//直接创建SystemMediaTransportControls对象被平台限制,神奇的是MediaPlayer对象可以创建该NativeObject _smtc =_player.SystemMediaTransportControls;//启用smtc以进行自定义 _smtc.IsEnabled = true;

拿到SMTC对象之后的操作与UWP中无异,这里简单看一下:

1.设置可交互性

_smtc.IsPlayEnabled = true;
_smtc.IsPauseEnabled
= true;
_smtc.IsNextEnabled
= true;
_smtc.IsPreviousEnabled
= true;

2.设置媒体信息

1 var updater =_smtc.DisplayUpdater;2 updater.AppMediaId = "xxx"; //用于区分不同来源的媒体
3 updater.Type = MediaPlaybackType.Music; //必须指定媒体类型否则抛异常
4 updater.MusicProperties.Title = “Title”;//标题
5 /*...省略相同的字段设置...*/
6 updater.Thumbnail = RandomAccessStreamReference.CreateFromUri(new Uri(ImgUrl));//设置封面图
7 updater.Update();//最后调用以生效

播放状态需要单独设置:

_smtc.PlaybackStatus = MediaPlaybackStatus.Playing; //Paused \ Stopped//直接设置无需更新

3.响应SMTC交互

1 _smtc.ButtonPressed +=_smtc_ButtonPressed;2 ...3  private void_smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)4 {5             switch(args.Button)6 {7                 caseSystemMediaTransportControlsButton.Play:8                     //Play
9                     break;10                 caseSystemMediaTransportControlsButton.Pause:11                     //Pause
12                     break;13                 caseSystemMediaTransportControlsButton.Next:14                     //Next
15                     break;16                 caseSystemMediaTransportControlsButton.Previous:17                     //Previous
18                     break;19 }20         }

注意,文中所有SMTC的事件均由系统触发,意味着非同一线程,需要用Dispatcher来操作UI

三、获取和控制系统媒体

好消息是,负责这部分的模块GlobalSystemMediaTransportControlsSession公开可以任意使用,不受UWP平台限制。

1.获取媒体信息

1 var gsmtcsm = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync();//获取SMTC会话管理器
2 gsmtcsm.CurrentSessionChanged += xxx; //当前会话改变或退出时发生,微软对CurrentSession的解释是用户可能最希望控制的媒体会话,实测为Windows控制中心顶部显示的媒体,当有多个媒体时用户可以在此选择切换
3 ...4 var session =gsmtcsm.GetCurrentSession();5 if(session == null)6     return; //当前没有注册的SMTC会话7 
8 //接下来操作session即可,下面仅提供参考信息9 
10 //媒体信息改变时发生,奇怪的是这些事件提供的参数e并没有任何信息
11 session.MediaPropertiesChanged += async (sender, e)=>{12     //触发事件时主动拉取信息
13     var info = await_globalSMTCSession.TryGetMediaPropertiesAsync();14 };15 //播放状态改变时发生
16 session.PlaybackInfoChanged +=(sender,e)=>{17     var status =globalSMTCSession.GetPlaybackInfo().PlaybackStatus;18 };

2.控制媒体播放

直接调用即可

awaitsession.TryPauseAsync();awaitsession.TryPlayAsync();awaitsession.TrySkipPreviousAsync();await session.TrySkipNextAsync();

四、一些奇怪的地方

1.无法显示媒体来源,并且不会清空上一个来源的信息

可能是因为没有提供合法的UWP句柄,Windows虽然能确定是哪个exe调用的SMTC,但是拒绝直接显示exe的信息。逻辑上来说这个来源信息会被空覆盖掉,但是并没有。

2.信息更新不一致和延时

系统显示的会话以及提供GlobalSMTCSessionMng.获取的信息有时会不一致,二者都有可能和应用真实在播放的不一致,后者获取的封面图有时也会不一致。此外,MusicProperty的更新有时并不会实时反馈到GlobalSMTCSession的Changed事件,我测试的时候当系统内存爆满(98% 我开了一堆浏览器标签页和4个vs)的时候,更新丢失的概率在70%左右,而资源充足时可以做到几乎即时更新。

3.暂未实现点击跳转到App

正统UWP App的SMTC会话是可以点击跳转到App播放界面的,但是我并没有找到相关的事件。

4.奇怪的MediaId

Windows系统似乎通过这个来区分不同的媒体来源(明明可以获得调用者- -),神奇的是如果你为两个应用设置了同样的MediaId,那么只有两个都关闭时,SMTC会话才会释放。此外通过GlobalSMTCSession.SourceAppUserModelId并不能获得你设置的MediaId,而是调用者的文件名"xxx.exe"。

五、使用我封装的库

Demo和库已经开源:
TwilightLemon/MediaTest: .NET 8 WPF using SMTC (github.com)

简单地将现有的解码器接入SMTC:

SMTCCreator? _smtcCreator = null;
...
_smtcCreator
??= new SMTCCreator("MediaTest");//修改播放状态 _smtcCreator.SetMediaStatus(SMTCMediaStatus.Playing);//设置媒体信息 _smtcCreator.Info.SetAlbumTitle("AlbumTitle")
.SetArtist(
"Taylor Swift")
.SetTitle(
"Dancing With Our Hands Tied")
.SetThumbnail(
"https://y.qq.com/music/photo_new/T002R300x300M000003OK4yP2MBOip_1.jpg?max_age=2592000")
.Update();
//注册交互响应 _smtcCreator.PlayOrPause +=_smtcCreator_PlayOrPause;
_smtcCreator.Previous
+=_smtcCreator_Previous;
_smtcCreator.Next
+= _smtcCreator_Next;

//合适的时候调用释放资源
_smtcCreator.Dispose();

简单地控制系统媒体:

SMTCListener _smtcListener = null;
...
_smtcListener
= awaitSMTCListener.CreateInstance();
_smtcListener.MediaPropertiesChanged
+=_smtcListener_MediaPropertiesChanged;
_smtcListener.PlaybackInfoChanged
+=_smtcListener_PlaybackInfoChanged;
_smtcListener.SessionExited
+=_smtcListener_SessionExited;
...
//媒体退出 private void _smtcListener_SessionExited(object?sender, EventArgs e) { }//播放状态改变 private void _smtcListener_PlaybackInfoChanged(object?sender, EventArgs e)
{
Dispatcher.Invoke(()
=>{var info =_smtcListener.GetPlaybackStatus();if (info == null) return;
StatusTb.Text
=info.ToString();
});
}
//媒体信息改变 private void _smtcListener_MediaPropertiesChanged(object?sender, EventArgs e)
{
Dispatcher.Invoke(
async () =>{var info = await_smtcListener.GetMediaInfoAsync();if (info == null) return;
TitleTb.Text
=info.Title;
ArtistTb.Text
=info.Artist;
AlbumTitleTb.Text
=info.AlbumTitle;//获取封面图的方法 if (info.Thumbnail != null)
{
var img = newBitmapImage();
img.BeginInit();
img.StreamSource
= (awaitinfo.Thumbnail.OpenReadAsync()).AsStream();
img.EndInit();
ThumbnailImg.Source
=img;
}
});
}
...
//控制播放 await_smtcListener.Previous();await_smtcListener.Next();await_smtcListener.Pause();await _smtcListener.Play();

六、写在最后

参考资料:

1)
SystemMediaTransportControls 类 (Windows.Media) - Windows UWP applications | Microsoft Learn

2)
桌面应用中不支持 Windows 运行时 API - Windows 应用 |Microsoft学习 --- Windows Runtime APIs not supported in desktop apps - Windows apps | Microsoft Learn

3)
GlobalSystemMediaTransportControlsSessionManager Class (Windows.Media.Control) - Windows UWP applications | Microsoft Learn

打个小广告,我的顶部栏项目正在开发中,现已集成SMTC和众多小功能,欢迎支持:
TwilightLemon/MyToolBar: 为Surface Pro而生的顶部工具栏 支持触控和笔快捷方式 (github.com)

全局媒体播放控制:

未来将支持更多插件:


本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。欢迎转载、使用、重新发布,但务必保留文章署名TwilightLemon,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

【进阶篇】Java 项目中对使用递归的理解分享

前言

笔者在最近的项目开发中,遇到了两个父子关系紧密相关的场景:评论树结构、部门树结构。具体的需求如:找出某条评论下的所有子评论id集合,找出某个部门下所有的子部门id集合。

在之前的项目开发经验中,递归使用得是较少的,但作为一个在数据结构操作中遍历树节点的解决方案,我还是拿出来作为技术积累进行记录以及分享。

一、什么是递归

1.1基本概念

这里就有必要简单介绍一下关于递归的基本概念了。

在 Java 中,递归是指在方法的定义中调用自身的过程,递归是基于方法调用栈的原理实现的:当一个方法被调用时,会在调用栈中创建一个对应的栈帧,包含方法的参数、局部变量和返回地址等信息。
在递归中,方法会在自身的定义中调用自身,这会导致多个相同方法的栈帧依次入栈。当满足终止条件时,递归开始回溯,栈帧依次出栈,方法得以执行完毕。

递归的关键是定义好递归的终止条件和递归调用的条件。如果没有适当的终止条件或递归调用的条件不满足,递归可能会陷入无限循环,导致栈内存溢出。

1.2优缺点

优点:

  1. 简化问题:递归能够将复杂问题分解成更小规模的子问题,简化了问题的解决过程;
  2. 实现高效算法:递归在某些算法中能够实现高效的解决方法,如数据结构操作中遍历树节点等。

缺点:

  1. 栈溢出风险:递归可能导致方法调用栈过深,造成栈内存溢出;
  2. 性能损耗:递归调用需要创建多个栈帧,对系统资源有一定的消耗;
  3. 可读性不高:递归的使用需要谨慎,不合理地使用可能造成代码难以理解和调试。

1.3与迭代的区别

  • 迭代(Iteration)

    迭代常见于 for 循环中:比如有一个集合 A,对 A 进行 foreach,在内部设置条件,符合条件后将集合中某个元素的值替换成别的值。

迭代示例简图
    @Test
    public void iterationTest(){
        ArrayList<String> list = new ArrayList<>();
        list.add("计算机技术");
        list.add("土木工程");
        list.add("市场营销");
        list.forEach(val -> {
            if (val.contains("计算机")){
                log.info("迭代前的的专业名称:{}", val);
                String str = val.replace(val, "计算机科学与技术");
                log.info("迭代后的的专业名称:{}", str);
            }
        });
    }

结果为:

迭代结果简图
  • 递归(Recursion)


递归的例子会在下一小节详细给出。


二、实际案例

下面笔者以递归获取某个评论id下面所有的子级评论id为例子,向大家介绍这个递归的过程。

首先,这里给出一个简单的数据库评论表的 demo,id 是主键id 也是评论唯一 id,parent_id 是该条评论的父评论 id,status 为1表示审核通过的状态。

其中,我们可以简单发现:这里21为第一层,28和29为第二层、31和32为第三层,草图如下所示:

评论id简单层级示意图

那么,我们如何将21、28、29、31、32都放进一个集合里返回呢?下面的代码示例可以给你一个参考。

但是,在看代码之前,有个问题请你思考一下:

从21开始后,遍历的路线是21-28-29?还是21-28-31?还是21-29-32?或者是21-28-31-29-32?

下面是经过脱敏处理后的参看代码示例,注释都写得比较清楚了:

    /**
     * 这里可以看作是外部接口的调用,会得到递归的结果
     * @param id
     */
    private List<Integer> getIdListMethod(Integer id){
        ArrayList<Integer> idList = new ArrayList<>();
        this.getAllIdByRecursion(id, idList);
        log.info("递归后得到的id集合:{}", idList);
        return idList;
    }

    /**
     * 这里是递归的过程
     * @param id
     * @param idList
     */
    private void getAllIdByRecursion(Integer id, List<Integer> idList){
        LambdaQueryWrapper<Comment> wrapper = new LambdaQueryWrapper<>();
        //先把该id下所有的第一级子id找到
        wrapper.eq(Comment::getParentId, id).eq(Comment::getStatus, NumberUtils.INTEGER_ONE);
        List<Comment> commentList = this.list(wrapper);
        for (Comment children : commentList){
            this.getAllIdByRecursion(children.getId(), idList);
        }
        log.info("放入集合的id为:{}", id);
        idList.add(id);
    }

上面问题的答案是:
递归后得到的id集合:[21,28,31,29,32],原因就是:迭代会从一棵树开始遍历到底,没有元素了再从头开始遍历,依次迭代,类似于深度优先遍历。

比如:21下面有两个子id:28和29,那么会先走21-28-31这棵树,到底了后接着按照29-32遍历。


三、改进方案

我根据自己的开发经验,可以从控制递归层数和改用 Stream 这两种办法来对递归进行改进。

3.1控制递归层数

JVM 默认控制的递归最大深度限制在 1000 层,可以通过设置 JVM 参数来控制其深度,如:

java -Xss5m #表示将每个线程的栈内存大小设置为5MB,已经是比较大了

或者在代码层面对递归的层数进行控制:

        int depth = 0;
        //递归方法调用
        for (int i = 0; i < 20; i++) {
            depth++;
        }
        if (depth > 100){
            //其它操作
        }

3.1用 Stream 遍历

核心思路是:先数据库全量查询(10万条以内),内存中使用 Stream 流操作、Lambda 表达式、Java 地址引用进行筛选。

适用于数据总量不多的情况,如:部门树,部门数量一般情况是比较固定的,一个组织或者公司最多也就几百上千个部门。

详情可以看我这篇文章:
https://www.cnblogs.com/CodeBlogMan/p/17965824


四、文章小结

笔者确实不推荐在项目中过度使用递归,但是合理使用的话也能成为解决特定问题的一个利器,至于怎么拿捏这个度,那就要看大家的具体情况了。

Java 项目中对使用递归的理解分享到这里就结束了,文章如有不足和错误,或者你有更好的解决思路,欢迎大家的指正和交流!