Skip to content

Latest commit

Β 

History

History
128 lines (87 loc) Β· 10.8 KB

File metadata and controls

128 lines (87 loc) Β· 10.8 KB

MPD protocol server

WaveFlow speaks the MPD protocol on TCP, so any existing MPD client can drive the player: MALP / MaximumMPD on a phone, mpc and ncmpcpp in a terminal, waybar / polybar status modules, Home Assistant, shell scripts.

This is an adapter, not a new UI. MPD has been the Linux remote-control lingua franca since 2003; speaking it hands us a large parc of already-written clients β€” in particular a phone remote without any mobile UI of our own (issue #471).

Ships disabled by default β€” enable it from Settings β†’ Connections β†’ Network sharing β†’ MPD server.

How it relates to the other control surfaces

What it does
media_controls (MPRIS / SMTC) OS media keys, same machine
DLNA MediaServer serves our files to a LAN receiver
MPD (this page) controls our player, from the LAN

Architecture

Same dedicated-worker shape as dlna: a sync MpdServer handle on AppState ferries Cmd::{Start, Stop, Status} over a crossbeam channel to the mpd-worker thread, which owns its own tokio runtime. Each accepted socket becomes a task.

AppState.mpd ─► Cmd channel ─► mpd-worker
                                β”œβ”€β–Ί TcpListener 0.0.0.0:<port> (default 6600; scans up to 10 ports forward, e.g. 6600–6609)
                                β”‚     └─► one task per client (connection.rs)
                                └─► Tauri event listeners ─► IdleBus
                                      player:state          ─► player
                                      player:track-changed  ─► player
                                      player:queue-changed  ─► playlist

Why this is cheap here

WaveFlow's audio engine lives in Rust. The dispatcher reads SharedPlayback atomics directly and drives playback through player_actions β€” no request/response bridge to the webview, no timeout, no degraded mode, and it keeps working with the window closed to tray, which is exactly when a remote matters.

For contrast: a player whose audio lives in the webview (an <audio> element) has to ask the UI for its own playback state over an event round-trip, and an MPD status β€” which clients poll about once a second β€” turns into several of those.

Shared player actions

next / previous / play <pos> go through player_actions, shared with the tray menu and the OS media controls. That sequence (advance the queue β†’ emit_track_changed β†’ emit_queue_changed β†’ hand the track to the decoder) used to be copy-pasted in lib.rs and media_controls.rs; MPD would have made it a third copy, each free to forget an emit and desync a surface. Any new non-frontend control surface should call into that module rather than re-deriving it.

play / playid with no argument, pause 0 and a bare pause go through it too. They used to send AudioCmd::Resume to the engine, which the decoder drops when no track is open, so mpc play did nothing after a launch or at the end of the queue (#609).

Configuration

Persisted in the global app_setting table β€” the listener is process-wide, not per-profile.

Key Default Note
mpd.enabled 0 Opt-in. Auto-started at boot when set. This flag is the security decision β€” see below.
mpd.port 6600 The MPD standard, which every client probes first. Scans up to 10 ports forward (e.g. 6600–6609) when taken.
mpd.password "" Empty = no authentication.

Bind address: 0.0.0.0

The server binds every interface, matching DLNA. Settled in #471 for two reasons.

It matches what we already ship. dlna/mod.rs binds 0.0.0.0 and the DLNA HTTP layer has no authentication at all (UPnP has no such concept). WaveFlow therefore already exposes an opt-in, default-off, LAN-bound, zero-auth service β€” one that exposes strictly more than MPD control does:

DLNA MPD
Enumerate the whole library βœ… ❌
Download the audio files βœ… ❌
See the current track βœ… βœ…
Control playback ❌ βœ…
Authentication none optional password

On loopback the feature loses its point. The phone remote is what justifies building this at all.

Accepted risk: unlike DLNA, MPD grants write access β€” anyone on the LAN can pause playback or change the volume. There's no arbitrary audio-file read or download (unlike DLNA, which serves the files themselves) and no command / shell execution β€” but MPD responses do expose the queued tracks' file paths and metadata (playlistinfo, currentsong, playlistid), so treat those as visible to anyone on the LAN. The damage ceiling is "my music stopped, and a peer can see what's queued and where the files live". On a shared network (cafΓ©, dorm, coworking) that is a real nuisance, and mpd.password is the answer β€” bearing in mind the protocol transmits it in cleartext, so it is a nuisance filter, not a security boundary.

Binding 0.0.0.0 triggers a firewall prompt on Windows/macOS the first time. DLNA already does this, so the behaviour is not new to users.

Supported commands

Control and queue inspection. Advertised through commands, so clients hide UI for the rest.

Group Commands
Connection ping Β· close Β· password Β· commands Β· notcommands Β· tagtypes Β· urlhandlers Β· decoders Β· outputs
State status Β· currentsong Β· stats
Queue read playlistinfo [range] Β· playlistid [id]
Transport play [pos] Β· playid Β· pause [0/1] Β· stop Β· next Β· previous Β· seek Β· seekid Β· seekcur
Mixer setvol Β· getvol Β· volume
Queue write clear Β· delete <range> Β· deleteid Β· move Β· moveid Β· shuffle
Options random Β· repeat Β· single

random is a boolean, and shuffle is not (#618). WaveFlow shuffles either tracks or whole albums; MPD's flag can only say on or off. So random 1 turns shuffle on using whichever grouping was last picked in the app rather than forcing tracks β€” a remote that cannot express the grouping should not quietly undo it β€” and random reads back as 1 for either grouping. player.shuffle remains the row both sides agree on. | Idle | idle [subsystems] Β· noidle |

Command lists (command_list_begin / command_list_ok_begin … command_list_end) are supported.

Not implemented

  • Library browsing β€” lsinfo, search, find, add, listplaylists. Deliberately deferred to keep the first cut reviewable. Unlike a streaming-only player we do have a local library to expose, and this is the piece that would make ncmpcpp genuinely useful (search an artist, queue it). Worth its own follow-up.
  • consume β€” WaveFlow has no consume mode. consume 0 is accepted, consume 1 ACKs rather than silently lying.
  • Stored-playlist mutation, stickers, partitions, multiple outputs.

Mapping notes

Song ids. MPD's Id must be stable per queue entry, not per track β€” the same file can sit in the queue twice and deleteid / moveid must tell them apart. queue_item.id is an INTEGER PRIMARY KEY that survives reordering, so it maps directly. This is why mpd/songs.rs has its own query instead of reusing queue::list_queue, which projects track.id.

Repeat. WaveFlow has a tri-state enum (off / all / one); MPD has two independent flags. one is repeat 1 + single 1. Both setters preserve the other flag so a client toggling one doesn't clobber the other β€” see the round-trip test in mpd/commands.rs. single oneshot (repeat the current track once, then auto-clear) has no durable equivalent, so it's rejected with an unsupported ACK rather than stored as a permanent single 1 that status would then misreport.

Web Radio. While a radio session owns the engine, current_track_id is a negative sentinel with no track row and no queue entry. status omits song / songid / duration and currentsong returns empty β€” same branch player_get_state takes. Without it a client would show the last library track as if it were playing.

idle. Backed by the Tauri events the frontend already listens to, bridged onto an IdleBus. Only subsystems we actually fire are advertised (player, playlist, mixer, options, output) β€” claiming database would leave a client waiting on it forever. A burst is coalesced into one wake-up, so a track change answers once carrying both player and playlist.

Trying it

# From the same machine
mpc -h 127.0.0.1 -p 6600 status
mpc -h 127.0.0.1 -p 6600 toggle

# From elsewhere on the LAN (address shown in Settings)
mpc -h 192.168.1.42 -p 6600 next
ncmpcpp -h 192.168.1.42

# With a password set
mpc -h 192.168.1.42 -P hunter2 status

On Android, point MALP at the address shown in Settings β†’ Connections β†’ Network sharing β†’ MPD server.