feeluown / FeelUOwn

trying to be a robust, user-friendly and hackable music player
http://feeluown.readthedocs.io
GNU General Public License v3.0
3.53k stars 598 forks source link

一种支持 macOS nowplaying 的方法:支持前后端分离 #538

Closed cosven closed 1 year ago

cosven commented 2 years ago

简介与背景

Nowplaying 功能在 Linux 系统上可以比较容易实现。Linux 的进程间大都通过 dbus 来进行通信,FeelUOwn 可以通过它将当前播放的信息传输给系统,并且系统可以通过这个通信来控制音乐播放器。通信接口是 mpris2。

而 macOS 上没有统一的进程间通信方式,并且系统也没有给 nowplaying 定义 RPC 接口。但官方有提供一些 swift/objc 的一些代码示例。因此,在 macOS 上实现 nowplaying 功能有两种可行的方式:

  1. 通过 pyobjc 调用 macOS SDK 来实现 nowplaying。
  2. 使用 swift/objc 编写一个 client,该 client 通过 RPC 与 fuo 进程通信。
    fuo RPC server <--- client ---> MPNowPlayingInfoCenter

    考虑到性能、可调试性和 native app 带来的更多可能性,我倾向选择第 2 种方案。

方案概述

方案 2 要实现,关键是实现这个 client。而 client 有几个关键问题

  1. 基于 swift/objc 实现这个 client,它能控制 MPNowPlayingInfoCenter。
  2. client 需要能监听到 duration/media/media_metadata 等信息的变化。

问题 1 目前已经攻克,主要难点在于看懂 macOS 文档,以及编写一个 swift 程序。

对于问题 2,则需要 FeelUOwn 有一个能够通过 RPC 向外 push 信息的方式。dbus + mpris2 本质就是一种 push 的方式。pull 方式不适用于这个场景,因为播放器的进度等信息都是实时的,而使用 push,能极大的较少 poll 带来的性能开销,因为 push 只需要在进度发生突变的时候进行通知即可。FeelUOwn 现有的 pubsub 功能是实现 push 较好的方式。

pubsub 的方案选择

调研了解到有这些技术可以实现 pubsub 实现,有

  1. websocket
  2. redis pubsub
  3. dbus
  4. xxx-mq

2,3,4 要么依赖太重,要么不跨平台。1 是一个不错的侯选项。就像 RPC 一样,之前也有考虑基于 HTTP 协议的 RPC,比如 JSONRPC 等。使用这些技术的主要好处是不需要自己造轮子,坏处有两个:json 可读性不强;http 偏重。

当前,FeelUOwn 有一个 pubsub 的实现,但它比较简单:一个 TCP 连接只能 pub 一个 topic,并且传输的是字节流,没有消息的概念。这让 sub 端能做的事情比较有限,需要支持:在一个连接中订阅多个 topic。

pubsub 的细节

客户端发送

use version 2.0
sub player.*,app.*

客户端应该收到

OK pubsub 1.0
ACK ok {length}   // 注:这个是 use version 2.0 的 response
{body}
ACK ok {length}  // 注:这个是 sub player.*,app.* 的 response
{body}

消息1
消息2

消息结构

和 fuo RPC 响应的结构相似,这里的消息也分为两部分:报头(header)和正文(body)。报头是一行,以 \r\n 结束。报头里面会包含正文的大小等信息。

MSG {topic_name} {body_length}  #: json
[0, null]                       

之后可能在报头添加更多字段,比如添加正文格式:

MSG {topic_name} {body_length} {body_type}
MSG {topic_name} {body_length} #: {body_type}

为了保证向前兼容,客户端解析正文长度的时候应该使用类似如下代码

header = parse_header()
body_length = header.split(' ')[2]

其它

RPC 与 pubsub 的解析可以编写一个 Python SDK。其它 swift 可以参考规范或者 Python SDK 的代码来实现。

cosven commented 1 year ago

nowplaying 已经在 #674 中支持