开发记录
- 本页主要记录了一些开发时的技术选择、问题探讨和解决,让其他开发者少走弯路少踩坑,一起进步~
拟声的开发框架
- 后端:
c++/go
;绝大部分是c++,实际上用c++开发并不舒服,仅仅是作者出于学习目的,一开始喜欢写c++才基于搜狗的workflow
搓了一些库并拿它开发了后续的拟声服务端。 - 客户端:
flutter
;用flutter的目的就是跨平台,说实话开发体验还不错,dart和c系语言非常像,写起来很舒服。 - 前端;拟声最初是用vue前端开发的,后来由于前端在手机上表现不佳,于是着力开发了客户端。
播放组件的选择
- 拟声目前主要使用的是
media_kit
,在此之前我们尝试过just_audio
、audioplayer
、assets_audio_player
,他们体积很小,但都不尽人意,支持的格式并不丰富,而后刚好看到media_kit
发布,试了下感觉着实不错,尽管它也有不少小问题,但基于libmpv
和ffmpeg
的播放组件能支持的格式和功能相当离谱。 - 另外对于安卓端,拟声还用
just_audio
大改并适配media3
接入。
LRC歌词的读取和解码
- 拟声是用ffmpeg读取内嵌歌词的,插件包
ffmpeg_kit_fultter
,但它不支持windows,因此我们用ffmpeg_helper
,并修了些bug、增加功能。 - ffmpeg读取后可以得到音频的一些元数据,往往是{key, value}形式,因此返回值应该是个map,但读取内嵌歌词标签时需要注意应判断是否存在标签名包含
lyric
(忽略大小写),而不应该直接map["lyric"]取值,因为不少歌曲的内嵌歌词标签名不是标准名称,有的多了下划线,有的大小写不一样。 - 读取时还要注意编码,一般是
UTF-8
,如果编码抛异常可以优先自动尝试GBK
。 - 拟声的LRC解码是自己写的,因为LRC非标准格式很多,大部分库支持不完全,于是自己搞,详见my_lyric。
共享与控制的实现
- 本质是局域网内客户端之间的点对点通信,不少游戏也有类似的功能。
- 拟声并不是基于常见的
DLNA
或Airplay
实现,而是用基础通信协议实现了该功能。其实只是因为作者一开始不知道有这些通用协议,没办法就自己实现了私有协议呜呜T_T。 - 基于UDP的广播和组播实现客户端之间的互相发现,远程控制时建立TCP连接传输对端状态信息,并使用HTTP请求传输音视频文件和部分控制命令。有关UDP的细节可看这篇博客。
- 当然后面也会支持
DLNA
和Airplay
, 而重点发展会放在拟声自己的私有协议,因为私有协议从头到尾自己设计实现,能做的优化和功能更多. - 为什么同时使用了UDP的广播和组播呢?理论上使用组播是相当符合我们的需求的,一开始也是基于组播实现的,但我们发现如果网关是手机或电脑开的热点时,常常收不到组播消息,于是又尝试用广播实现,实测下来广播要好一些。因此你需要多测试一下不同网络环境。
拟声的UI
- 拟声是按
新拟物
风格设计开发的,但没有用UI库,因为没找到拟物相关好用的UI库,没办法就自己实现。当然后来也似乎逐渐偏离新拟物了,在逐步调整的过程中加入了大量自己的想法。 - 作者并不了解设计相关,这实际上是按着百度上的图开发,然后按自己的想法一版一版改的,一开始其实也不好看,然后几乎每一版更新都在改,改着改着就感觉还行了哈哈。也许有个设计师朋友才是正解,但架不住我没有这样的朋友,拟声一开始也只是作者为了自己的需求才做的,特地花大钱找设计也不现实。有些东西并没有想象的那么复杂,也许倾注时间也是一种解决办法。
win端的中文字体
- flutter在win端默认的中文字体超级丑,甚至应该说笔画有些都不对。可以指定为系统字体,比如
微软雅黑
、黑体
,但我用下来还是感觉不行,于是用了鸿蒙的免费字体,确实不错。
安卓系统级悬浮窗
- 可以使用插件
flutter_overlay_window
创建系统级悬浮窗,在其他app上也能显示透明或不透明的内容。
win多窗口
- 尽管flutter官方还未支持桌面端多窗口,但有一个曲线救国的办法,启动多进程,每个进程就是一个窗口,然后用socket通信即可,实现简单不少,当然如此一来运行时占用肯定要高一些的,但架不住它简单省事呀。
- 那么如何启动多进程呢?
- 一个是开发时就是多个项目,到时候自然按多个程序启动即可;但重用代码会麻烦一些,可能需要独立一些库让他们能够共享使用。
- 另一个是同一个项目启动多次,桌面端程序不像移动端,是可以同时启动多个进程的:
dart
final progress = await io.Process.start(
// 当前项目的可执行程序文件名称
"mymusic.exe",
// 传递main函数参数
[
"cross-1", // 标记本次启动的进程是次级窗口-1,让它运行起来后知道自己的角色
server.port.toString(), // 传递通信端口,让对方启动后按这个端口和主窗口能建立socket连接
]
);
- 接下来被启动的次级窗口在dart的main函数参数就能接收到刚刚传入的标记和通信端口,按照有没有这些参数就可以区分当前运行的进程的身份。