diff options
| author | Christopher Baines <mail@cbaines.net> | 2026-08-09 10:20:02 +0100 |
|---|---|---|
| committer | Christopher Baines <mail@cbaines.net> | 2026-08-22 20:03:01 +0100 |
| commit | 9ceab34e9bd42c878bfeab15c71687ab754e4cf4 (patch) | |
| tree | 5eb50180c5a7b577f461b2c89d777213308d2c2e /gnu/services | |
| parent | 239327961888f109b0a66bda099a5ac23ce9f9ef (diff) | |
gnu: services: audio: Add snapcast snapserver service.
This is just the server component, another service definition is needed for
the snapclient.
* gnu/services/audio.scm: (snapserver-stream, snapserver-stream?,
snapserver-stream-name, snapserver-stream-type, snapserver-stream-location,
snapserver-stream-codec, snapserver-stream-sampleformat,
snapserver-stream-chunk-ms, snapserver-stream-parameters,
snapserver-configuration, snapserver-configuration?,
snapserver-configuration-package, snapserver-configuration-user,
snapserver-configuration-group, snapserver-configuration-shepherd-requirement,
snapserver-configuration-log-file, snapserver-configuration-log-filter,
snapserver-configuration-data-directory, snapserver-configuration-mdns?,
snapserver-configuration-http?, snapserver-configuration-http-port,
snapserver-configuration-http-bind-address, snapserver-configuration-doc-root,
snapserver-configuration-tcp-control?,
snapserver-configuration-tcp-control-port,
snapserver-configuration-tcp-streaming?,
snapserver-configuration-tcp-streaming-port, snapserver-configuration-streams,
snapserver-configuration-codec, snapserver-configuration-sampleformat,
snapserver-configuration-buffer-ms, snapserver-configuration-chunk-ms,
snapserver-configuration-send-to-muted?,
snapserver-configuration-extra-content): New procedures.
(snapserver-service-type): New variable.
* doc/guix.texi (Audio Services): Document it.
Change-Id: I5dd2c15ff8c1b560abd481147312a8d6fc844776
Diffstat (limited to 'gnu/services')
| -rw-r--r-- | gnu/services/audio.scm | 361 |
1 files changed, 360 insertions, 1 deletions
diff --git a/gnu/services/audio.scm b/gnu/services/audio.scm index 518f8f2c0fa..2981b89ed8c 100644 --- a/gnu/services/audio.scm +++ b/gnu/services/audio.scm | |||
| @@ -32,6 +32,7 @@ | |||
| 32 | #:use-module (gnu services admin) | 32 | #:use-module (gnu services admin) |
| 33 | #:use-module (gnu system shadow) | 33 | #:use-module (gnu system shadow) |
| 34 | #:use-module (gnu packages admin) | 34 | #:use-module (gnu packages admin) |
| 35 | #:use-module ((gnu packages audio) #:select (snapcast)) | ||
| 35 | #:use-module (gnu packages mpd) | 36 | #:use-module (gnu packages mpd) |
| 36 | #:use-module (guix records) | 37 | #:use-module (guix records) |
| 37 | #:use-module (ice-9 format) | 38 | #:use-module (ice-9 format) |
| @@ -123,7 +124,44 @@ | |||
| 123 | mympd-ip-acl | 124 | mympd-ip-acl |
| 124 | mympd-ip-acl? | 125 | mympd-ip-acl? |
| 125 | mympd-ip-acl-allow | 126 | mympd-ip-acl-allow |
| 126 | mympd-ip-acl-deny)) | 127 | mympd-ip-acl-deny |
| 128 | |||
| 129 | snapserver-stream | ||
| 130 | snapserver-stream? | ||
| 131 | snapserver-stream-name | ||
| 132 | snapserver-stream-type | ||
| 133 | snapserver-stream-location | ||
| 134 | snapserver-stream-codec | ||
| 135 | snapserver-stream-sampleformat | ||
| 136 | snapserver-stream-chunk-ms | ||
| 137 | snapserver-stream-parameters | ||
| 138 | |||
| 139 | snapserver-configuration | ||
| 140 | snapserver-configuration? | ||
| 141 | snapserver-configuration-package | ||
| 142 | snapserver-configuration-user | ||
| 143 | snapserver-configuration-group | ||
| 144 | snapserver-configuration-shepherd-requirement | ||
| 145 | snapserver-configuration-log-file | ||
| 146 | snapserver-configuration-log-filter | ||
| 147 | snapserver-configuration-data-directory | ||
| 148 | snapserver-configuration-mdns? | ||
| 149 | snapserver-configuration-http? | ||
| 150 | snapserver-configuration-http-port | ||
| 151 | snapserver-configuration-http-bind-address | ||
| 152 | snapserver-configuration-doc-root | ||
| 153 | snapserver-configuration-tcp-control? | ||
| 154 | snapserver-configuration-tcp-control-port | ||
| 155 | snapserver-configuration-tcp-streaming? | ||
| 156 | snapserver-configuration-tcp-streaming-port | ||
| 157 | snapserver-configuration-streams | ||
| 158 | snapserver-configuration-codec | ||
| 159 | snapserver-configuration-sampleformat | ||
| 160 | snapserver-configuration-buffer-ms | ||
| 161 | snapserver-configuration-chunk-ms | ||
| 162 | snapserver-configuration-send-to-muted? | ||
| 163 | snapserver-configuration-extra-content | ||
| 164 | snapserver-service-type)) | ||
| 127 | 165 | ||
| 128 | ;;; Commentary: | 166 | ;;; Commentary: |
| 129 | ;;; | 167 | ;;; |
| @@ -945,3 +983,324 @@ prompting a pin from the user.") | |||
| 945 | mympd-serialize-configuration))) | 983 | mympd-serialize-configuration))) |
| 946 | (description "Run myMPD, a frontend for MPD (Music Player Daemon).") | 984 | (description "Run myMPD, a frontend for MPD (Music Player Daemon).") |
| 947 | (default-value (mympd-configuration)))) | 985 | (default-value (mympd-configuration)))) |
| 986 | |||
| 987 | |||
| 988 | ;;; | ||
| 989 | ;;; Snapcast | ||
| 990 | ;;; | ||
| 991 | |||
| 992 | (define %snapserver-user | ||
| 993 | (user-account | ||
| 994 | (name "snapserver") | ||
| 995 | (group %lazy-group) | ||
| 996 | (system? #t) | ||
| 997 | (comment "Snapcast server user") | ||
| 998 | (home-directory "/var/lib/snapserver") | ||
| 999 | (shell (file-append shadow "/sbin/nologin")))) | ||
| 1000 | |||
| 1001 | (define %snapserver-group | ||
| 1002 | (user-group | ||
| 1003 | (name "snapserver") | ||
| 1004 | (system? #t))) | ||
| 1005 | |||
| 1006 | (define (snapserver-stream-type? value) | ||
| 1007 | (memq value '(pipe file process librespot airplay tcp alsa meta))) | ||
| 1008 | |||
| 1009 | (define-maybe/no-serialization file-like) | ||
| 1010 | |||
| 1011 | (define-configuration/no-serialization snapserver-stream | ||
| 1012 | (name | ||
| 1013 | (string "default") | ||
| 1014 | "Name of the stream, as shown by Snapcast clients.") | ||
| 1015 | |||
| 1016 | (type | ||
| 1017 | (snapserver-stream-type 'pipe) | ||
| 1018 | "Type of the stream source: one of @code{pipe}, @code{file}, | ||
| 1019 | @code{process}, @code{librespot}, @code{airplay}, @code{tcp}, @code{alsa} or | ||
| 1020 | @code{meta}.") | ||
| 1021 | |||
| 1022 | (location | ||
| 1023 | (string "/run/snapserver/snapfifo") | ||
| 1024 | "Location of the stream source: a file name for @code{pipe}, @code{file} | ||
| 1025 | and @code{process} streams, a host name for @code{tcp} streams, a device name | ||
| 1026 | for @code{alsa} streams, and a slash-separated list of stream names for | ||
| 1027 | @code{meta} streams.") | ||
| 1028 | |||
| 1029 | (codec | ||
| 1030 | maybe-string | ||
| 1031 | "Codec to use for this stream, overriding the @code{codec} field of | ||
| 1032 | @code{snapserver-configuration}.") | ||
| 1033 | |||
| 1034 | (sampleformat | ||
| 1035 | maybe-string | ||
| 1036 | "Sample format of this stream, given as | ||
| 1037 | @code{@var{rate}:@var{bits}:@var{channels}}, overriding the | ||
| 1038 | @code{sampleformat} field of @code{snapserver-configuration}.") | ||
| 1039 | |||
| 1040 | (chunk-ms | ||
| 1041 | maybe-integer | ||
| 1042 | "Duration, in milliseconds, of the audio chunks read from this source, | ||
| 1043 | overriding the @code{chunk-ms} field of @code{snapserver-configuration}.") | ||
| 1044 | |||
| 1045 | (parameters | ||
| 1046 | (alist '()) | ||
| 1047 | "Additional source parameters, given as an association list, for example | ||
| 1048 | @code{'((\"controlscript\" . \"meta_mpd.py\"))}.")) | ||
| 1049 | |||
| 1050 | (define (snapserver-stream->uri stream) | ||
| 1051 | (match-record stream <snapserver-stream> | ||
| 1052 | (name type location codec sampleformat chunk-ms parameters) | ||
| 1053 | (let ((query `(("name" . ,name) | ||
| 1054 | ,@(if (maybe-value-set? codec) | ||
| 1055 | `(("codec" . ,codec)) | ||
| 1056 | '()) | ||
| 1057 | ,@(if (maybe-value-set? sampleformat) | ||
| 1058 | `(("sampleformat" . ,sampleformat)) | ||
| 1059 | '()) | ||
| 1060 | ,@(if (maybe-value-set? chunk-ms) | ||
| 1061 | `(("chunk_ms" . ,chunk-ms)) | ||
| 1062 | '()) | ||
| 1063 | ,@parameters))) | ||
| 1064 | (simple-format #f "~A://~A?~A" type location | ||
| 1065 | (string-join | ||
| 1066 | (map (match-lambda | ||
| 1067 | ((key . value) | ||
| 1068 | (simple-format #f "~A=~A" key value))) | ||
| 1069 | query) | ||
| 1070 | "&"))))) | ||
| 1071 | |||
| 1072 | (define list-of-snapserver-stream? | ||
| 1073 | (list-of snapserver-stream?)) | ||
| 1074 | |||
| 1075 | (define-configuration/no-serialization snapserver-configuration | ||
| 1076 | (package | ||
| 1077 | (file-like snapcast) | ||
| 1078 | "The Snapcast package to use.") | ||
| 1079 | |||
| 1080 | (user | ||
| 1081 | (user-account %snapserver-user) | ||
| 1082 | "Owner of the @command{snapserver} process.") | ||
| 1083 | |||
| 1084 | (group | ||
| 1085 | (user-group %snapserver-group) | ||
| 1086 | "Owner group of the @command{snapserver} process.") | ||
| 1087 | |||
| 1088 | (shepherd-requirement | ||
| 1089 | (list-of-symbols '()) | ||
| 1090 | "This is a list of symbols naming Shepherd services that this service | ||
| 1091 | will depend on.") | ||
| 1092 | |||
| 1093 | (log-file | ||
| 1094 | (string "/var/log/snapserver.log") | ||
| 1095 | "File where @command{snapserver} writes its log.") | ||
| 1096 | |||
| 1097 | (log-filter | ||
| 1098 | (string "*:info") | ||
| 1099 | "Log filter, given as @code{@var{tag}:@var{level}} pairs separated by | ||
| 1100 | commas, where @var{tag} is @code{*} or a log tag and @var{level} is one of | ||
| 1101 | @code{trace}, @code{debug}, @code{info}, @code{notice}, @code{warning}, | ||
| 1102 | @code{error} or @code{fatal}.") | ||
| 1103 | |||
| 1104 | (data-directory | ||
| 1105 | (string "/var/lib/snapserver") | ||
| 1106 | "Directory where @command{snapserver} stores persistent data, such as | ||
| 1107 | client settings.") | ||
| 1108 | |||
| 1109 | (mdns? | ||
| 1110 | (boolean #t) | ||
| 1111 | "Whether to publish the Snapcast services over mDNS. This requires | ||
| 1112 | @code{avahi-service-type} (@pxref{Networking Services}).") | ||
| 1113 | |||
| 1114 | (http? | ||
| 1115 | (boolean #t) | ||
| 1116 | "Whether to enable the HTTP JSON-RPC interface, which also serves the | ||
| 1117 | Snapweb web interface.") | ||
| 1118 | |||
| 1119 | (http-port | ||
| 1120 | (integer 1780) | ||
| 1121 | "Port the HTTP server listens on.") | ||
| 1122 | |||
| 1123 | (http-bind-address | ||
| 1124 | (string "::") | ||
| 1125 | "Address the HTTP server listens on.") | ||
| 1126 | |||
| 1127 | (doc-root | ||
| 1128 | maybe-file-like | ||
| 1129 | "Directory served by the HTTP server. It defaults to the Snapweb web | ||
| 1130 | interface bundled with the @code{package} field.") | ||
| 1131 | |||
| 1132 | (tcp-control? | ||
| 1133 | (boolean #t) | ||
| 1134 | "Whether to enable the TCP JSON-RPC control interface.") | ||
| 1135 | |||
| 1136 | (tcp-control-port | ||
| 1137 | (integer 1705) | ||
| 1138 | "Port the TCP control interface listens on.") | ||
| 1139 | |||
| 1140 | (tcp-streaming? | ||
| 1141 | (boolean #t) | ||
| 1142 | "Whether to enable TCP streaming, which is what Snapcast clients connect | ||
| 1143 | to.") | ||
| 1144 | |||
| 1145 | (tcp-streaming-port | ||
| 1146 | (integer 1704) | ||
| 1147 | "Port the TCP streaming interface listens on.") | ||
| 1148 | |||
| 1149 | (streams | ||
| 1150 | (list-of-snapserver-stream (list (snapserver-stream))) | ||
| 1151 | "List of @code{snapserver-stream} records describing the audio sources | ||
| 1152 | @command{snapserver} reads from.") | ||
| 1153 | |||
| 1154 | (codec | ||
| 1155 | (string "flac") | ||
| 1156 | "Default codec used to send audio to clients, given as | ||
| 1157 | @code{@var{codec}[:@var{options}]} where @var{codec} is one of @code{flac}, | ||
| 1158 | @code{ogg}, @code{opus} or @code{pcm}.") | ||
| 1159 | |||
| 1160 | (sampleformat | ||
| 1161 | (string "48000:16:2") | ||
| 1162 | "Default sample format of the streams, given as | ||
| 1163 | @code{@var{rate}:@var{bits}:@var{channels}}.") | ||
| 1164 | |||
| 1165 | (buffer-ms | ||
| 1166 | (integer 1000) | ||
| 1167 | "End-to-end latency, in milliseconds.") | ||
| 1168 | |||
| 1169 | (chunk-ms | ||
| 1170 | (integer 20) | ||
| 1171 | "Default duration, in milliseconds, of the audio chunks read from the | ||
| 1172 | sources.") | ||
| 1173 | |||
| 1174 | (send-to-muted? | ||
| 1175 | (boolean #f) | ||
| 1176 | "Whether to send audio to muted clients.") | ||
| 1177 | |||
| 1178 | (extra-content | ||
| 1179 | maybe-string | ||
| 1180 | "Content appended verbatim to @file{snapserver.conf}, for options that are | ||
| 1181 | not covered by the fields above, such as the @code{[ssl]} and | ||
| 1182 | @code{[authorization]} sections.")) | ||
| 1183 | |||
| 1184 | (define (snapserver-config-file config) | ||
| 1185 | (define (boolean->string value) | ||
| 1186 | (if value "1" "0")) | ||
| 1187 | |||
| 1188 | (match-record config <snapserver-configuration> | ||
| 1189 | (package log-filter data-directory mdns? | ||
| 1190 | http? http-port http-bind-address doc-root | ||
| 1191 | tcp-control? tcp-control-port tcp-streaming? tcp-streaming-port | ||
| 1192 | streams codec sampleformat buffer-ms chunk-ms send-to-muted? | ||
| 1193 | extra-content) | ||
| 1194 | (apply mixed-text-file "snapserver.conf" | ||
| 1195 | `("[server]\n" | ||
| 1196 | "datadir = " ,data-directory "\n" | ||
| 1197 | "mdns_enabled = " ,(boolean->string mdns?) "\n" | ||
| 1198 | |||
| 1199 | "\n[http]\n" | ||
| 1200 | "enabled = " ,(boolean->string http?) "\n" | ||
| 1201 | "port = " ,(number->string http-port) "\n" | ||
| 1202 | "bind_to_address = " ,http-bind-address "\n" | ||
| 1203 | "doc_root = " ,(if (maybe-value-set? doc-root) | ||
| 1204 | doc-root | ||
| 1205 | (file-append package | ||
| 1206 | "/share/snapserver/snapweb")) | ||
| 1207 | "\n" | ||
| 1208 | |||
| 1209 | "\n[tcp-control]\n" | ||
| 1210 | "enabled = " ,(boolean->string tcp-control?) "\n" | ||
| 1211 | "port = " ,(number->string tcp-control-port) "\n" | ||
| 1212 | |||
| 1213 | "\n[tcp-streaming]\n" | ||
| 1214 | "enabled = " ,(boolean->string tcp-streaming?) "\n" | ||
| 1215 | "port = " ,(number->string tcp-streaming-port) "\n" | ||
| 1216 | |||
| 1217 | "\n[stream]\n" | ||
| 1218 | ;; The upstream defaults for these two point into /usr/share. | ||
| 1219 | "plugin_dir = " ,(file-append package | ||
| 1220 | "/share/snapserver/plug-ins") "\n" | ||
| 1221 | "sandbox_dir = " ,(file-append package | ||
| 1222 | "/share/snapserver/sandbox") "\n" | ||
| 1223 | ,@(map (lambda (stream) | ||
| 1224 | (string-append "source = " | ||
| 1225 | (snapserver-stream->uri stream) "\n")) | ||
| 1226 | streams) | ||
| 1227 | "codec = " ,codec "\n" | ||
| 1228 | "sampleformat = " ,sampleformat "\n" | ||
| 1229 | "buffer = " ,(number->string buffer-ms) "\n" | ||
| 1230 | "chunk_ms = " ,(number->string chunk-ms) "\n" | ||
| 1231 | "send_to_muted = " ,(boolean->string send-to-muted?) "\n" | ||
| 1232 | |||
| 1233 | "\n[logging]\n" | ||
| 1234 | ;; Log to standard error, which the Shepherd redirects to the | ||
| 1235 | ;; 'log-file' of the service. | ||
| 1236 | "sink = stderr\n" | ||
| 1237 | "filter = " ,log-filter "\n" | ||
| 1238 | ,@(if (maybe-value-set? extra-content) | ||
| 1239 | `("\n" ,extra-content "\n") | ||
| 1240 | '()))))) | ||
| 1241 | |||
| 1242 | (define (snapserver-shepherd-service config) | ||
| 1243 | (match-record config <snapserver-configuration> | ||
| 1244 | (package user group shepherd-requirement log-file data-directory | ||
| 1245 | mdns? streams) | ||
| 1246 | (let ((config-file (snapserver-config-file config)) | ||
| 1247 | (username (user-account-name user)) | ||
| 1248 | (groupname (user-group-name group)) | ||
| 1249 | ;; 'snapserver' creates the named pipes of its 'pipe' sources, but | ||
| 1250 | ;; not the directories holding them. | ||
| 1251 | (directories | ||
| 1252 | (delete-duplicates | ||
| 1253 | (cons* data-directory (dirname log-file) | ||
| 1254 | (filter-map (lambda (stream) | ||
| 1255 | (and (eq? 'pipe | ||
| 1256 | (snapserver-stream-type | ||
| 1257 | stream)) | ||
| 1258 | (dirname | ||
| 1259 | (snapserver-stream-location | ||
| 1260 | stream)))) | ||
| 1261 | streams))))) | ||
| 1262 | (shepherd-service | ||
| 1263 | (documentation "Run the Snapcast server.") | ||
| 1264 | (requirement `(user-processes loopback | ||
| 1265 | ,@(if mdns? '(avahi-daemon) '()) | ||
| 1266 | ,@shepherd-requirement)) | ||
| 1267 | (provision '(snapserver)) | ||
| 1268 | (start | ||
| 1269 | (with-imported-modules (source-module-closure | ||
| 1270 | '((gnu build activation))) | ||
| 1271 | #~(begin | ||
| 1272 | (use-modules (gnu build activation)) | ||
| 1273 | |||
| 1274 | (let ((user (getpw #$username))) | ||
| 1275 | (for-each (lambda (directory) | ||
| 1276 | (unless (file-exists? directory) | ||
| 1277 | (mkdir-p/perms directory user #o755))) | ||
| 1278 | '#$directories)) | ||
| 1279 | |||
| 1280 | (make-forkexec-constructor | ||
| 1281 | (list #$(file-append package "/bin/snapserver") | ||
| 1282 | "--config" #$config-file) | ||
| 1283 | #:user #$username | ||
| 1284 | #:group #$groupname | ||
| 1285 | #:log-file #$log-file)))) | ||
| 1286 | (stop #~(make-kill-destructor)) | ||
| 1287 | (actions (list (shepherd-configuration-action config-file))))))) | ||
| 1288 | |||
| 1289 | (define (snapserver-accounts config) | ||
| 1290 | (match-record config <snapserver-configuration> (user group) | ||
| 1291 | (list (if (eq? (user-account-group user) %lazy-group) | ||
| 1292 | (set-user-group user group) | ||
| 1293 | user) | ||
| 1294 | group))) | ||
| 1295 | |||
| 1296 | (define snapserver-service-type | ||
| 1297 | (service-type | ||
| 1298 | (name 'snapserver) | ||
| 1299 | (extensions | ||
| 1300 | (list (service-extension shepherd-root-service-type | ||
| 1301 | (compose list snapserver-shepherd-service)) | ||
| 1302 | (service-extension account-service-type | ||
| 1303 | snapserver-accounts))) | ||
| 1304 | (description "Run the Snapcast server, @command{snapserver}, which streams | ||
| 1305 | audio to Snapcast clients.") | ||
| 1306 | (default-value (snapserver-configuration)))) | ||
