开发记录
- 本页主要记录了一些开发时的技术选择、问题探讨和解决,让其他开发者少走弯路少踩坑,一起进步~
API接口
拟声的开发框架
- 后端:
c++/go;绝大部分是c++,实际上用c++开发并不舒服,仅仅是作者出于学习目的,一开始喜欢写c++才基于搜狗的workflow搓了一些库并拿它开发了后续的拟声服务端 - 客户端:
flutter;用flutter的目的就是跨平台,说实话开发体验还不错,dart和c系语言非常像,写起来很舒服。部分核心功能采用 c++ 实现,跨平台省心省事,整体性能和效果够不错 - 前端;拟声最初是用vue前端开发的,后来由于前端在手机上表现不佳,于是着力开发了客户端
播放组件的选择
- 拟声目前主要使用的是
media_kit,在此之前我们尝试过just_audio、audioplayer、assets_audio_player,他们体积很小,但都不尽人意,支持的格式并不丰富,而后刚好看到media_kit发布,试了下感觉着实不错,尽管它也有不少小问题,但基于libmpv和ffmpeg的播放组件能支持的格式和功能相当离谱
mediaxx
- 之前用了 ffmpeg_kit 实现读取音频信息、内嵌封面、歌词等,25年初它已经宣布不再更新了,另外 media_kit 的 libmpv 也依赖了 ffmpeg,相当于app里打包了两份 ffmpeg 的动态库,我们也尝试过合并 so 库使用,但发现他们的日志会互相冲突
- 25年底,我们开发的 流明 也很依赖 ffmpeg_kit 和 libmpv,由于编解码都需要,此时程序包体积很大,下定决心舍弃 ffmpeg_kit,开发了
mediaxx,仍是引用 ffmpeg 的能力,实现读取音频信息、封面、频谱分析,以及图片颜色分析。经过大量的摸索,mediaxx 依赖的 ffmpeg 实现与 libmpv 共用,也可以将 mediaxx、ffmpeg、libmpv 全部编译到一起,合并成一个动态库,并优化编译配置、添加了符号表限制导出符号,让链接器更好删除未使用的代码段,大幅缩减了安装包体积。 - 流明从 140MB 缩减到 50 MB,拟声win端从 60+MB 缩减到 40 MB,而且是引入的 libmpv、ffmpeg 添加了更多的 格式支持、硬件加速 的情况下大幅缩减体积,如果砍到跟之前一样的解码支持,应该还是再少 10 几mb
- https://github.com/coolight7/mediaxx
LRC歌词的读取和解码
- 因为LRC非标准格式很多,大部分库支持不完全,于是我们开发了尽力提高兼容性的 LRC编解码器,详见lyric_xx。
共享与控制的实现
- 本质是局域网内客户端之间的点对点通信,不少游戏也有类似的功能。
- 拟声并不是基于常见的
DLNA或Airplay实现,而是用基础通信协议实现了该功能。其实只是因为作者一开始不知道有这些通用协议,没办法就自己实现了私有协议呜呜T_T。 - 基于UDP的广播和组播实现客户端之间的互相发现,远程控制时建立TCP连接传输对端状态信息,并使用HTTP请求传输音视频文件和部分控制命令。有关UDP的细节可看这篇博客。
- 当然后面也会支持
DLNA和Airplay, 而重点发展会放在拟声自己的私有协议,因为私有协议从头到尾自己设计实现,能做的优化和功能更多. - 为什么同时使用了UDP的广播和组播呢?理论上使用组播是相当符合我们的需求的,一开始也是基于组播实现的,但我们发现如果网关是手机或电脑开的热点时,常常收不到组播消息,于是又尝试用广播实现,实测下来广播要好一些。因此你需要多测试一下不同网络环境。
win端的中文字体
- flutter在win端默认的中文字体超级丑,甚至应该说笔画有些都不对。可以指定为系统字体,比如
微软雅黑、黑体,但我用下来还是感觉不行,于是用了鸿蒙的免费字体,确实不错。
安卓系统级悬浮窗
- 可以使用插件
flutter_overlay_window创建系统级悬浮窗,在其他app上也能显示透明或不透明的内容。
win多窗口
- 尽管flutter官方还未支持桌面端多窗口,但有一个曲线救国的办法,启动多进程,每个进程就是一个窗口,然后用socket通信即可,实现简单不少,当然如此一来运行时占用肯定要高一些的,但架不住它简单省事呀。
- 那么如何启动多进程呢?
- 一个是开发时就是多个项目,到时候自然按多个程序启动即可;但重用代码会麻烦一些,可能需要独立一些库让他们能够共享使用。
- 另一个是同一个项目启动多次,桌面端程序不像移动端,是可以同时启动多个进程的:
dart
final progress = await io.Process.start(
// 当前项目的可执行程序文件名称
"musicxx.exe",
// 传递main函数参数
[
"cross-1", // 标记本次启动的进程是次级窗口-1,让它运行起来后知道自己的角色
server.port.toString(), // 传递通信端口,让对方启动后按这个端口和主窗口能建立socket连接
]
);- 接下来被启动的次级窗口在dart的main函数参数就能接收到刚刚传入的标记和通信端口,按照有没有这些参数就可以区分当前运行的进程的身份。