Skip to content

开发记录

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

拟声的开发框架

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

播放组件的选择

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

共享与控制的实现

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