☰
Snapcast Stream Plugin 开发指南:基于 JSON-RPC 2.0 的播放控制与元数据插件协议
2026/9/27 8:06:25 网站建设 项目流程
  • 音视频

【免费下载链接】snapcast

Synchronous multiroom audio player

项目地址:https://gitcode.com/gh_mirrors/sn/snapcast
点击查看免费下载

本篇技术指南以 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 服务器为某个音频流启动的一个可执行二进制或脚本,它为流提供两类能力:

  1. 播放控制:播放、暂停、停止、上一曲/下一曲、跳转(seek)等;
  2. 状态与元数据上报:当前播放状态、音量、播放位置,以及当前曲目的艺术家、专辑、封面等元数据。

插件与 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说明
playcanPlay无开始播放
pausecanPause无暂停播放
playPausecanPause无在播放/暂停之间切换
stopcanControl无停止播放
nextcanGoNext无跳转到下一曲
previouscanGoPrevious无跳转到上一曲
seekcanSeekoffset:float,秒相对当前进度前/后跳转(正数前进、负数后退)
setPositioncanSeekposition: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类型说明
loopStatusstring重复播放状态,取值之一:none(播完列表后停止)、track(当前曲目播完后从头重播)、playlist(循环播放整个播放列表)
shufflebool是否随机顺序播放播放列表
volumeint音量百分比,合法范围[0..100]
mutebool当前静音状态
ratefloat当前播放速率,合法范围(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类型说明
playbackStatusstring当前播放状态:playing/paused/stopped
loopStatusstring同 SetProperty:none/track/playlist
shufflebool是否随机遍历播放列表
volumeint音量百分比[0..100]
mutebool当前静音状态
ratefloat当前播放速率(0..)
positionfloat当前播放位置(秒)
canGoNextbool是否可以调用next并期望切曲
canGoPreviousbool是否可以调用previous并期望切曲
canPlaybool是否可用play/playPause开始播放
canPausebool是否可用pause/playPause暂停
canSeekbool是否可用seek/setPosition控制位置
canControlbool是否允许通过该接口控制播放器
metadatajson当前曲目元数据(字段见下)

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 是理解协议的最简入口,核心结构如下:

  1. 参数解析:用getopt解析--snapcast-host、--snapcast-port、--stream、-d/--debug、-v/--version,默认值见defaults字典(localhost、1780、default);
  2. 发送函数:send()将 JSON 序列化后写到 stdout 并 flush,末尾带换行(NDJSON);
  3. 属性初始化:exampleControl.__init__填充playbackStatus="playing"、loopStatus="none"、shuffle=False、volume=100、mute=False、rate=1.0、position=0,以及全部can*能力位;
  4. updateProperties:构造metadata(含title、artist与artData——内置 Base64 的 Snapcast/Spotify SVG logo,extension="svg"),然后发送Plugin.Stream.Player.Properties通知;
  5. 主循环:for line in sys.stdin: example_ctrl.control(line)——逐行读取服务器命令,在control()中解析command(示例仅处理next/previous),更新属性后回{"jsonrpc": "2.0", "result": "ok", "id": id};
  6. 日志:通过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 主循环,支持 MPDidle命令,仅在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:最小清单

结合协议与示例实现,编写一个插件需要满足:

  1. 逐行读取 stdin,每行是一个 JSON-RPC 2.0 请求,用请求中的id回包;
  2. 启动即发Plugin.Stream.Ready,让服务器尽快发起GetProperties;
  3. 实现Plugin.Stream.Player.GetProperties,至少返回canControl与相关can*能力位(服务器据此决定是否下发控制命令);
  4. 实现Plugin.Stream.Player.Control/Plugin.Stream.Player.SetProperty(若canControl=true),成功回"result": "ok",失败回符合规范的错误对象(如{"code": -32601, "message": "Method not found", "id": ...},参考 example.py);
  5. 状态变化时发Plugin.Stream.Player.Properties通知,元数据未变化时省略metadata字段即可;
  6. 处理 stderr 与Plugin.Stream.Log,便于服务器侧排查问题;
  7. 处理命令行参数:至少解析--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

项目地址:https://gitcode.com/gh_mirrors/sn/snapcast
点击查看免费下载

相关推荐

上一篇:DeepSeek-V3推理性能倍增:MTP模块与Speculative Decoding实战优化指南
下一篇:从显存危机到效率革命:DeepSeek-V3 FP8量化技术如何解锁超大规模模型训练

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询