mpv web remote

2023-08-11 #rust#opensource

Web remote control for the mpv media player

Source code/binaries Github


Remote control for video players

I recently switched from VLC to mpv as my main video player, as the former was producing tearing artefacts that resisted getting fixed.

One nice feature of VLC was the HTTP control interface that could be used to turn a smartphone into a remote control, either via a browser or with a native app such as VLC Remote Lite.

Unfortunately, it seems like mpv does not not have a similar embedded web interface.

It does however expose a control interface over a unix socket, using JSON to encode messages.

Existing solutions

A quick search will reveal two “mpv remote” Android apps using the aforementioned mpv control interface. Naturally, they require an additional component to run on the host machine to expose the API to the app.


Unwilling to install too much Python or Node code on my machine, nor mobile apps on my phone, I implemented a simple Rust application that replicates the web remote feature from VLC.

A binary on the host machine binds to the mpv socket, and exposes a simplistic control interface (play, pause, seek, rewind 10 seconds, full screen) in a web server. Any mobile device can then access this interface as a web app.

The mpv interface even allows to retrieve screenshots
of the media to display in the app.

The code and binaries can be found on https://github.com/cpg314/mpv-web-remote

For now, only the following basic functions are supported:

Implementation details

There are two components:

The web server is straightforward and is implemented with axum. For page interactions, we simply use jQuery.

The mpv interface takes the form

impl Mpv {
    pub fn connect(socket: impl AsRef<Path>) -> Result<Self, Error>;
    pub fn send(&mut self, mut request: Request) -> Result<Response, Error>;
    pub fn wait_event(&self, filter: impl Fn(&Event) -> bool);

The connect constructor connects to the socket and creates a thread responsible for reading replies and events from the servers into a buffer. In particular, we are robust against changes to this implementation detail:

“Currently, the mpv-side IPC implementation does not service the socket while a command is executed and the reply is written. It is for example not possible that other events, that happened during the execution of the command, are written to the socket before the reply is written.

This might change in the future. The only guarantee is that replies to IPC messages are sent in sequence.“

The send method writes a request to the socket, serialized using serde_json. It then blocks until the server responds to that request.

A selection of commands is implemented:

impl Request {
    pub fn playback_time() -> Self;
    pub fn get_property(property: &str) -> Self;
    pub fn seek(target: f32, flags: &str) -> Self;
    pub fn set_property<T: Into<serde_json::Value>>(property: &str, value: T) -> Self;
    pub fn show_text(text: &str) -> Self;
    pub fn observe_property(id: i64, property: &str) -> Self;
    pub fn screenshot<P: AsRef<Path> + ?Sized>(filename: &P) -> Self;

A generic response takes the form

#[derive(Deserialize, Debug)]
pub struct Response {
    pub request_id: i64,
    pub data: Option<serde_json::Value>,
    pub error: String,

and can be downcast to the expected data type

impl Response {
    pub fn into_inner<T: DeserializeOwned>(self) -> Result<T, Error>;

This could also be encapsulated into higher-level methods (e.g. get_playback_time() -> f64).

Finally, the wait_event method simply blocks until an event occurs. This is for example useful to only trigger a screenshot at the new position after a seek has finished.

Note that the mpvipc crate provides a similar and more complete interface, but the above was an opportunity to experiment with a different implementation, under a more permissive license.