- 音视频
【免费下载链接】snapcast
Synchronous multiroom audio player
本篇技术指南以 doc/json_rpc_api/stream_plugin.md 为骨架,结合 Snapcast 服务器端源码(server/streamreader/、server/control_requests.cpp)与随安装包发布的示例插件(server/etc/plug-ins/),完整讲解 Stream Plugin 的协议定义、生命周期、全部请求/通知的 JSON 报文格式,以及如何为 MPD、Mopidy、librespot 等音源编写可用的控制插件。读完本文,你将掌握 Snapcast 服务器与插件之间通过 stdin/stdout 交换 NDJSON 的完整接口,并能自行实现或调试一个 Stream Plugin。
什么是 Stream Plugin
Stream Plugin 是 Snapcast 服务器为某个音频流启动的一个可执行二进制或脚本,它为流提供两类能力:
- 播放控制:播放、暂停、停止、上一曲/下一曲、跳转(seek)等;
- 状态与元数据上报:当前播放状态、音量、播放位置,以及当前曲目的艺术家、专辑、封面等元数据。
插件与 Snapcast 服务器的通信方式是通过 stdin/stdout 交换 NDJSON(newline delimited JSON 格式的 JSON-RPC-2.0 消息):
- Snapcast 服务器向插件的stdin写入 JSON-RPC 2.0请求(request);
- 插件向stdout输出响应(response)和通知(notification);
- 插件的 stderr 输出会被服务器逐行读取并映射为服务器日志(见下文
ScriptStreamControl::stderrReadLine)。
协议本身遵循 JSON-RPC 2.0 规范。官方文档同时说明,未来版本还可能支持共享库(shared library)形式的插件,目前实现的是可执行脚本/二进制。
一个流对应一个插件:controlscript 参数
插件通过流源(stream source)参数controlscript与某个流绑定。最典型的例子是 MPD 元数据插件:
[stream] source = pipe:///tmp/snapfifo?name=MPD&controlscript=meta_mpd.py规则与细节:
- 相对路径解析:如果
controlscript是相对路径,Snapserver 会在插件目录/usr/share/snapserver/plug-ins中查找脚本。该目录可通过配置项stream.plugin_dir修改(默认值定义在 server/server_settings.hpp,命令行参数--stream.plugin_dir在 server/snapserver.cpp 中注册)。 - 随服务器安装自带示例插件:
meta_mpd.py(MPD)和meta_mopidy.py(Mopidy),此外仓库的 server/etc/plug-ins/ 目录下还有example.py、meta_go-librespot.py、plex_bridge.py等可参考的脚本。 - 额外参数:通过
controlscriptparams传递,参数必须URL 编码(空格用%20),例如--mopidy-host=192.168.42.23%20--debug,参见 doc/configuration.md。 - 服务器总会附加的参数(由 ScriptStreamControl::doStart 实现):
--stream=<流的 id>:总是传入;- 当 HTTP 接口启用时,额外传入
--snapcast-port=<http.port>和--snapcast-host=<http.host>。
完整的流源 URI 语法(doc/configuration.md):
TYPE://host/path?name=<name>[&codec=<codec>][&sampleformat=<sampleformat>][&chunk_ms=<chunk ms>][&controlscript=<control script filename>[&controlscriptparams=<control script command line arguments>]]协议总览:三类请求与三类通知
一个 Stream Plugin必须支持并处理 Snapcast 服务器发送的以下三个请求(requests):
- Plugin.Stream.Player.Control
- Plugin.Stream.Player.SetProperty
- Plugin.Stream.Player.GetProperties
同时插件可以向服务器发送以下通知(notifications):
- Plugin.Stream.Player.Properties:属性变化通知;
- Plugin.Stream.Log:日志消息;
- Plugin.Stream.Ready:就绪通知,应尽早发送。
启动握手流程:插件启动 → 尽快发送Plugin.Stream.Ready→ 服务器收到后立即发起Plugin.Stream.Player.GetProperties查询流的属性。这一流程在 PcmStream::onControlNotification 中实现:收到Plugin.Stream.Ready通知后,服务器立刻构造一次Plugin.Stream.Player.GetProperties请求发给插件。
能力门控(capability gating)
插件上报的布尔属性决定了服务器会发送哪些控制命令:
canGoNext/canGoPrevious:是否支持next/previous;canPlay/canPause:是否支持play/pause/playPause;canSeek:是否支持seek/setPosition;canControl:总开关。若为false,服务器不会向插件发送任何控制命令。
在服务器端,这些门控由 PcmStream 的各个方法执行,例如play()在!properties_.can_play时直接返回can_play_is_false错误(pcm_stream.cpp),setPosition()/seek()则检查can_seek(pcm_stream.cpp)。
请求一:Plugin.Stream.Player.Control
用于控制播放器,要求canControl为true。
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.Control", "params": {"command": "<command>", "params": { "<param 1>": <value 1>, "<param 2>": <value 2>}}}支持的 command
| command | 前置能力要求 | params | 说明 |
|---|---|---|---|
play | canPlay | 无 | 开始播放 |
pause | canPause | 无 | 暂停播放 |
playPause | canPause | 无 | 在播放/暂停之间切换 |
stop | canControl | 无 | 停止播放 |
next | canGoNext | 无 | 跳转到下一曲 |
previous | canGoPrevious | 无 | 跳转到上一曲 |
seek | canSeek | offset:float,秒 | 相对当前进度前/后跳转(正数前进、负数后退) |
setPosition | canSeek | position:float,秒 | 将播放位置设置为指定秒数 |
示例
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.Control", "params": {"command": "setPosition", "params": { "position": 17.827 }}}预期响应
成功:
{"id": 1, "jsonrpc": "2.0", "result": "ok"}失败:返回任意符合 JSON-RPC 2.0 错误对象 规范的错误,错误消息应帮助用户诊断问题。
在服务器端,setPosition与seek的position/offset参数是必需的:Stream.Control处理逻辑中,缺少position或offset会抛出InvalidParamsException(control_requests.cpp),随后由PcmStream::sendRequest封装为对插件的Plugin.Stream.Player.Control请求(pcm_stream.cpp)。
请求二:Plugin.Stream.Player.SetProperty
当canControl为true时,Snapserver 会向插件发送SetProperty命令。
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.SetProperty", "params": {"<property>": <value>}}支持的 property
| property | 类型 | 说明 |
|---|---|---|
loopStatus | string | 重复播放状态,取值之一:none(播完列表后停止)、track(当前曲目播完后从头重播)、playlist(循环播放整个播放列表) |
shuffle | bool | 是否随机顺序播放播放列表 |
volume | int | 音量百分比,合法范围[0..100] |
mute | bool | 当前静音状态 |
rate | float | 当前播放速率,合法范围(0..) |
预期响应
成功:
{"id": 1, "jsonrpc": "2.0", "result": "ok"}失败:任意符合 JSON-RPC 2.0 规范的错误对象,错误消息应帮助用户诊断。
类型校验:服务器端Stream.SetProperty对值做了严格校验(control_requests.cpp):loopStatus必须是none/track/playlist之一;shuffle、mute必须是 bool;volume必须是整数;rate必须是 float,否则抛出InvalidParamsException。Properties::fromJson中也将这些属性归类为可读写属性(rw_props)与只读属性(ro_props),未知属性会输出 "Property not supported" 警告(properties.cpp)。
请求三:Plugin.Stream.Player.GetProperties
{"id": 1, "jsonrpc": "2.0", "method": "Plugin.Stream.Player.GetProperties"}支持的 property(响应 result 中返回)
除上述可写属性外,还包括:
| property | 类型 | 说明 |
|---|---|---|
playbackStatus | string | 当前播放状态:playing/paused/stopped |
loopStatus | string | 同 SetProperty:none/track/playlist |
shuffle | bool | 是否随机遍历播放列表 |
volume | int | 音量百分比[0..100] |
mute | bool | 当前静音状态 |
rate | float | 当前播放速率(0..) |
position | float | 当前播放位置(秒) |
canGoNext | bool | 是否可以调用next并期望切曲 |
canGoPrevious | bool | 是否可以调用previous并期望切曲 |
canPlay | bool | 是否可用play/playPause开始播放 |
canPause | bool | 是否可用pause/playPause暂停 |
canSeek | bool | 是否可用seek/setPosition控制位置 |
canControl | bool | 是否允许通过该接口控制播放器 |
metadata | json | 当前曲目元数据(字段见下) |
PlaybackStatus与LoopStatus在服务器端的字符串映射定义于 properties.hpp:playing/paused/stopped、none/track/playlist之外的取值会被解析为kUnknown。
metadata 支持的字段
metadata是一个 JSON 对象,全部字段均为可选,语义对齐 MPRIS 规范:
基础信息
trackId:string,曲目在 MPRIS 对象(如 tracklist)上下文中的唯一标识;file:string,当前歌曲文件;duration:float,歌曲时长(秒),可含小数;title:string,曲目标题;name:string,歌曲名称(非标题)。该标签含义不明确,常被标签损坏的网络电台用来同时塞入歌手与歌名;url:string uri,媒体文件位置;artUrl:string uri,代表曲目或专辑的图片地址。客户端不应假设播放器停止输出该 URL 后它仍存在;artData:json,Base64 编码的代表曲目/专辑的图片,含两个子字段:data(string,Base64 编码的图像)与extension(string,图片扩展名,如 "png"、"jpg"、"svg")。若未指定artUrl,Snapserver 会解码并缓存该图片,并通过artUrl对外发布。
艺术家与专辑
artist:list of strings,曲目艺术家;artistSort:list of strings,同artist,但用于排序(通常省略 "The" 等前缀);album:string,专辑名;albumSort:string,同album,用于排序;albumArtist:list of strings,专辑艺术家;albumArtistSort:list of strings,同albumArtist,用于排序;performer:string,演唱/演奏者;composer:list of strings,作曲者;conductor:string,指挥;lyrics/lyricist:list of strings,歌词/作词者。
日期与编号
date:string,发行日期(通常为 4 位年份);originalDate:string,原始发行日期;contentCreated:string,曲目创建时间(通常年份有用);firstUsed:string,首次播放时间;lastUsed:string,最近播放时间;discNumber:integer,唱片编号;trackNumber:string,专辑/唱片中的曲目编号(注意文档中该字段声明为 string,示例实现中以 int 输出)。
分类与备注
genre:list of strings,曲目流派;grouping:string,"声音属于更大类别"时的分组名(源自 IDv2.4.0 TIT1 描述);work:string,"可表达为一个或多个音频录音的独立智力或艺术创作";comment:list of strings,自由格式备注;label:string,厂牌或发行商名。
评分与计数
autoRating:float,自动生成的评分(依据播放频率等),范围0.0..1.0;userRating:float,用户评分,范围0.0..1.0;useCount:integer,曲目播放次数;bpm:integer,每分钟节拍数。
标识符(MusicBrainz / Spotify)
musicbrainzArtistId/musicbrainzAlbumId/musicbrainzAlbumArtistId/musicbrainzTrackId/musicbrainzReleaseTrackId/musicbrainzWorkId:string,对应 MusicBrainz 数据库中的各类 ID;spotifyArtistId/spotifyTrackId:string,Spotify Artist / Track ID。
服务器端Metadata类完整实现了上述字段的序列化/反序列化(metadata.cpp):toJson()输出全部标签;fromJson()仅接受supported_tags集合内的键,未知标签会记录警告;artData仅在同时包含data与extension时才被解析。
预期响应示例
不带元数据的最小响应:
{"id": 1, "jsonrpc": "2.0", "result": {"canControl":true,"canGoNext":true,"canGoPrevious":true,"canPause":true,"canPlay":true,"canSeek":false,"loopStatus":"none","playbackStatus":"playing","position":93.394,"shuffle":false,"volume":86,"mute":false}}带完整元数据的响应:
{"id": 1, "jsonrpc": "2.0", "result": {"canControl":true,"canGoNext":true,"canGoPrevious":true,"canPause":true,"canPlay":true,"canSeek":true,"loopStatus":"none","metadata":{"album":"Doldinger","albumArtist":["Klaus Doldinger's Passport"],"artUrl":"http://coverartarchive.org/release/0d4ff56b-2a2b-43b5-bf99-063cac1599e5/16940576164-250.jpg","artist":["Klaus Doldinger's Passport feat. Nils Landgren"],"contentCreated":"2016","duration":305.2929992675781,"genre":["Jazz"],"musicbrainzAlbumId":"0d4ff56b-2a2b-43b5-bf99-063cac1599e5","title":"Soul Town","trackId":"7","trackNumber":6,"url":"Klaus Doldinger's Passport - Doldinger (2016)/06 - Soul Town.mp3"},"playbackStatus":"playing","position":72.79499816894531,"shuffle":false,"volume":97,"mute":false}}通知一:Plugin.Stream.Player.Properties
格式与GetProperties的 result 相同:
{"jsonrpc": "2.0", "method": "Plugin.Stream.Player.Properties", "params": {"canControl":true,"canGoNext":true,"canGoPrevious":true,"canPause":true,"canPlay":true,"canSeek":false,"loopStatus":"none","playbackStatus":"playing","position":593.394,"shuffle":false,"volume":86,"mute":false}}增量更新语义:如果params中缺少metadata,服务器会沿用上一次已知的元数据。因此插件更新某个属性时不必重复发送完整的元数据。该逻辑在服务器端PcmStream::setProperties中实现:新属性缺少metadata时,用旧的properties_.metadata补齐(pcm_stream.cpp)。
通知二:Plugin.Stream.Log
发送一条日志消息:
{"jsonrpc": "2.0", "method": "Plugin.Stream.Log", "params": {"severity":"Info","message":"<Log message>"}}支持的 severity
| severity | 含义 |
|---|---|
trace | 冗长的调试级消息 |
debug | 调试级消息 |
info | 信息性消息 |
notice | 正常但值得注意的条件 |
warning | 警告条件 |
error | 错误条件 |
fatal | 致命条件 |
服务器收到该通知后,会在日志中打印Plugin log - severity: <severity>, message: <message>(pcm_stream.cpp)。此外,插件 stderr 的每一行也会被服务器按关键字(trace/debug/info/warning/error/fatal)推断日志级别后写入日志(PcmStream::onControlLog)。
通知三:Plugin.Stream.Ready
插件就绪、可以接收命令时发送此通知:
{"jsonrpc": "2.0", "method": "Plugin.Stream.Ready"}服务器收到后即查询流的属性(发送Plugin.Stream.Player.GetProperties)。参考实现中,meta_mpd.py在完成 MPD 连接与初始状态同步后发送 Ready(见 meta_mpd.py),meta_mopidy.py在 WebSocket 打开后发送 Ready(meta_mopidy.py)。
服务器侧转发:Stream.Control / Stream.SetProperty / Stream.OnProperties
stream_plugin.md的 Server 一节注明该部分内容归属 doc/json_rpc_api/control.md(原文标注TODO: this belongs to doc/json_rpc_api/control.md)。外部客户端通过 Snapcast 的 JSON-RPC 接口控制流的报文格式如下:
Stream.Control(等价于Plugin.Stream.Player.Control,但 method 为Stream.Control,且需携带流 id):
{"id": 1, "jsonrpc": "2.0", "method": "Stream.Control", "params": {"id": "Pipe", "command": command, "params": params}}Stream.SetProperty(等价于Plugin.Stream.Player.SetProperty):
{"id": 1, "jsonrpc": "2.0", "method": "Stream.SetProperty", "params": {"id": "Pipe", "property": property, "value": value}}Stream.OnProperties(服务器广播给客户端的属性通知,等价于Plugin.Stream.Player.Properties,但携带流 id):
{"jsonrpc": "2.0", "method": "Stream.OnProperties", "params": {"id": "Pipe", "properties": {}}}服务器端Stream.Control/Stream.SetProperty的实现细节见 control_requests.cpp,包括:checkParams强制要求必需参数、setPosition/seek的秒→毫秒换算(seconds * 1000)、各属性的类型校验,以及统一包装为"result": "ok"的响应。
安全限制:通过 RPC 接口动态添加流时(Stream.AddStream),streamUri中的controlscript必须位于[stream] plugin_dir(配置于snapserver.conf,默认/usr/share/snapserver/plug-ins),可以是绝对路径或相对路径(control.md)。服务器端通过utils::file::isInDirectory校验脚本必须位于插件目录内,否则抛出InvalidParamsException(control_requests.cpp)。
参考实现一:example.py(最小可运行插件)
example.py 是理解协议的最简入口,核心结构如下:
- 参数解析:用
getopt解析--snapcast-host、--snapcast-port、--stream、-d/--debug、-v/--version,默认值见defaults字典(localhost、1780、default); - 发送函数:
send()将 JSON 序列化后写到 stdout 并 flush,末尾带换行(NDJSON); - 属性初始化:
exampleControl.__init__填充playbackStatus="playing"、loopStatus="none"、shuffle=False、volume=100、mute=False、rate=1.0、position=0,以及全部can*能力位; - updateProperties:构造
metadata(含title、artist与artData——内置 Base64 的 Snapcast/Spotify SVG logo,extension="svg"),然后发送Plugin.Stream.Player.Properties通知; - 主循环:
for line in sys.stdin: example_ctrl.control(line)——逐行读取服务器命令,在control()中解析command(示例仅处理next/previous),更新属性后回{"jsonrpc": "2.0", "result": "ok", "id": id}; - 日志:通过
Plugin.Stream.Log通知把收到的命令发回服务器日志(severity: "Info")。
该示例同时演示了artData的用法:插件直接内嵌 Base64 图片数据,服务器端 PcmStream::setProperties 会将其解码后存入ImageCache,并自动生成artUrl形式的 HTTP 地址(/__image_cache?name=<md5>)对外发布。
参考实现二:meta_mpd.py(MPD 音源)
meta_mpd.py 是随安装包提供的生产级插件,用于 MPD 流源,依赖python-mpd2、musicbrainzngs、PyGObject、dbus-python。几个值得借鉴的设计:
- 状态映射:
status_mapping把 MPDstatus()的字段映射为 Snapcast 属性,例如state→playbackStatus(play/pause/stop→playing/paused/stopped)、repeat→loopStatus(0/1/2→none/track/playlist)、random→shuffle、volume、elapsed→position、duration、mute(meta_mpd.py); - 标签映射:
tag_mapping把 MPD 的标签(album、artist、musicbrainz_*等)映射为 Snapcast metadata 字段,含类型与是否为列表的声明(meta_mpd.py); - 能力位推导:
_get_properties中canSeek = 'duration' in snapstatus,即只有当前曲目已知时长时才允许 seek(meta_mpd.py); - 网络电台 hack:当只有
title、name、url而没有album/artist时,把title按" - "或" / "拆分为 artist 与 title(meta_mpd.py); - 封面获取:通过 MusicBrainz 查询专辑封面并缓存到
_album_art_map,仅在元数据不含artUrl时触发远程查询(get_albumart); - 命令处理:
control()按method的最后一个段分发——Control内的next/previous/play/pause/playPause/stop/setPosition/seek分别对应 MPD 命令,其中seek用带符号的偏移字符串("+10"/"-10")调用seekcur,setPosition用绝对秒数;SetProperty映射shuffle→random、loopStatus→repeat/single(仅在 MPD 支持single命令时使用)、volume→setvol;GetProperties直接回传_get_properties(self.status())(meta_mpd.py); - 事件驱动:基于 GLib 主循环,支持 MPD
idle命令,仅在player/mixer/options/playlist子系统变化时强制刷新属性并发送通知(_update_properties(force=True))。
参考实现三:meta_mopidy.py 与 meta_go-librespot.py
- meta_mopidy.py:通过 WebSocket(依赖
websocket-client >= 0.58.0)连接 Mopidy 的 JSON-RPC 接口,把 Snapcast 的 stdin 命令翻译为 Mopidy 的core.playback.*、core.tracklist.*、core.mixer.*调用。亮点包括:send_batch_request批量查询(一次拉取播放状态、重复/随机、音量、静音、时间位置等);seek先取当前时间位置再加偏移后调用core.playback.seek;在track_playback_started等事件通知时刷新属性;loopStatus由 Mopidy 的repeat+single两个标志组合推导(repeat && single→track,repeat→playlist,否则none)。 - meta_go-librespot.py(仓库 server/etc/plug-ins/ 中提供):用于 go-librespot 流源,配置示例见 doc/configuration.md:
controlscript=meta_go-librespot.py&controlscriptparams=--stream=<name>%20--librespot-host=127.0.0.1%20--librespot-port=24879,其中controlscriptparams需要 URL 编码。
服务端实现纵深:ScriptStreamControl 的进程与管道管理
理解插件协议的服务端实现,有助于调试插件行为:
StreamControl是基类,定义了command()(把请求投递到 asio strand 串行化,并按请求 id 保存响应回调request_callbacks_)与onReceive()(用 jsonrpcpp 解析插件 stdout 的一行 JSON,区分 notification / request / response 三种类型,stream_control.cpp);ScriptStreamControl用 Boost.Process 启动脚本,stdout/stderr 各接一个管道,用boost::asio::async_read_until(..., '\n')逐行异步读取;stdout 行进入onReceive(),stderr 行进入日志回调(stream_control.cpp);- 命令下发:
doCommand()把 JSON-RPC 请求序列化后追加\n写入插件的 stdin 并 flush(stream_control.cpp)——这就是文档所述 "newline delimited JSON-RPC-2.0" 的服务端实现; - 属性广播:
setProperties()在属性真正变化时通过pcmListeners_的onPropertiesChanged通知上层(pcm_stream.cpp),最终以Stream.OnProperties形式推送给连接的客户端。
编写你自己的 Stream Plugin:最小清单
结合协议与示例实现,编写一个插件需要满足:
- 逐行读取 stdin,每行是一个 JSON-RPC 2.0 请求,用请求中的
id回包; - 启动即发
Plugin.Stream.Ready,让服务器尽快发起GetProperties; - 实现
Plugin.Stream.Player.GetProperties,至少返回canControl与相关can*能力位(服务器据此决定是否下发控制命令); - 实现
Plugin.Stream.Player.Control/Plugin.Stream.Player.SetProperty(若canControl=true),成功回"result": "ok",失败回符合规范的错误对象(如{"code": -32601, "message": "Method not found", "id": ...},参考 example.py); - 状态变化时发
Plugin.Stream.Player.Properties通知,元数据未变化时省略metadata字段即可; - 处理 stderr 与
Plugin.Stream.Log,便于服务器侧排查问题; - 处理命令行参数:至少解析
--stream=<id>(始终传入),并在 HTTP 启用时解析--snapcast-port/--snapcast-host。
从 server/etc/plug-ins/example.py 复制骨架,是最快的起步方式。
相关文档
- 协议主文档:doc/json_rpc_api/stream_plugin.md
- 服务器 JSON-RPC 接口(含 Stream.Control / Stream.SetProperty / Stream.OnProperties / Stream.AddStream):doc/json_rpc_api/control.md
- 流源 URI 与
controlscript/controlscriptparams参数:doc/configuration.md - 服务端插件控制实现:server/streamreader/stream_control.hpp、server/streamreader/stream_control.cpp、server/streamreader/pcm_stream.cpp
- 属性与元数据结构:server/streamreader/properties.hpp、server/streamreader/properties.cpp、server/streamreader/metadata.cpp
- 示例插件:server/etc/plug-ins/example.py、server/etc/plug-ins/meta_mpd.py、server/etc/plug-ins/meta_mopidy.py、server/etc/plug-ins/meta_go-librespot.py
- 音视频
【免费下载链接】snapcast
Synchronous multiroom audio player
相关推荐
基于 JSON-RPC 2.0 编写 DBX Go 原生 Sidecar 插件:dbx-plugin-sdk 协议 v1 实战指南
基于 JSON RPC 2.0 编写 DBX Go 原生 Sidecar 插件:dbx plugin sdk 协议 v1 实战指南 DBX 原生 Sidecar
数据库开发者工具桌面应用CLIMCP 服务AI 应用Transmission RPC 协议规范深度解析:基于 JSON-RPC 2.0 的远程控制接口实战指南
Transmission RPC 协议规范深度解析:基于 JSON RPC 2.0 的远程控制接口实战指南 本文以 docs/rpc spec.md https
桌面应用后端CLI网络nullclaw 外部渠道插件协议实战指南:基于 stdio JSON-RPC 的 `channels.external` 插件开发与运维
nullclaw 外部渠道插件协议实战指南:基于 stdio JSON RPC 的 channels.external 插件开发与运维 nullclaw 通过
人工智能AI Agent大模型自主智能体工具调用RAGAgent 记忆MCP ClientsAgent 沙箱多智能体语音
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考