Skip to content

开发记录

  • 本页主要记录了一些开发时的技术选择、问题探讨和解决,让其他开发者少走弯路少踩坑,一起进步~

API接口

拟声的开发框架

  • 后端c++/go;绝大部分是c++,实际上用c++开发并不舒服,仅仅是作者出于学习目的,一开始喜欢写c++才基于搜狗的workflow搓了一些库并拿它开发了后续的拟声服务端
  • 客户端flutter;用flutter的目的就是跨平台,说实话开发体验还不错,dart和c系语言非常像,写起来很舒服。部分核心功能采用 c++ 实现,跨平台省心省事,整体性能和效果够不错
  • 前端;拟声最初是用vue前端开发的,后来由于前端在手机上表现不佳,于是着力开发了客户端

播放组件的选择

  • 拟声目前主要使用的是media_kit,在此之前我们尝试过just_audioaudioplayerassets_audio_player,他们体积很小,但都不尽人意,支持的格式并不丰富,而后刚好看到media_kit发布,试了下感觉着实不错,尽管它也有不少小问题,但基于libmpvffmpeg的播放组件能支持的格式和功能相当离谱

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

共享与控制的实现

  • 本质是局域网内客户端之间的点对点通信,不少游戏也有类似的功能。
  • 拟声并不是基于常见的DLNAAirplay实现,而是用基础通信协议实现了该功能。其实只是因为作者一开始不知道有这些通用协议,没办法就自己实现了私有协议呜呜T_T。
  • 基于UDP的广播和组播实现客户端之间的互相发现,远程控制时建立TCP连接传输对端状态信息,并使用HTTP请求传输音视频文件和部分控制命令。有关UDP的细节可看这篇博客
  • 当然后面也会支持DLNAAirplay, 而重点发展会放在拟声自己的私有协议,因为私有协议从头到尾自己设计实现,能做的优化和功能更多.
  • 为什么同时使用了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函数参数就能接收到刚刚传入的标记和通信端口,按照有没有这些参数就可以区分当前运行的进程的身份。