Turns out that this isn’t a feature that mpv wants to natively support:
- Feature request: single instance mode · Issue #4954 · mpv-player/mpv · GitHub
- mpv not opening file in same process · Issue #3811 · mpv-player/mpv · GitHub
And the provided solution is to use the umpv script, but there’s a caveat. It defaults to appending to the mpv playlist instead of replacing the currently playing file.
To get umpv to replace, you just need to change append-play to replace about half way down the script. Doing this the nix way with the correct ports, wrappers, and calling python correctly, etc. turned out to be not so trivial. Here’s what I came up with, but if you know of a better solution please share.
{ pkgs, ... }:
let
# Create our custom umpv with replace and correct socket path
umpv-custom = pkgs.writeScriptBin "umpv" ''
#!${pkgs.python3}/bin/python3
"""
This script emulates "unique application" functionality. When starting
playback with this script, it will try to reuse an already running instance of
mpv (but only if that was started with umpv). Other mpv instances (not started
by umpv) are ignored, and the script doesn't know about them.
This only takes filenames as arguments. Custom options can't be used; the script
interprets them as filenames. If mpv is already running, the files passed to
umpv are appended to mpv's internal playlist. If a file does not exist or is
otherwise not playable, mpv will skip the playlist entry when attempting to
play it (from the GUI perspective, it's silently ignored).
If mpv isn't running yet, this script will start mpv and let it control the
current terminal. It will not write output to stdout/stderr, because this
will typically just fill ~/.xsession-errors with garbage.
mpv will terminate if there are no more files to play, and running the umpv
script after that will start a new mpv instance.
Note: you can supply custom mpv path and options with the MPV environment
variable. The environment variable will be split on whitespace, and the
first item is used as path to mpv binary and the rest is passed as options
_if_ the script starts mpv. If mpv is not started by the script (i.e. mpv
is already running), this will be ignored.
"""
import os
import shlex
import socket
import string
import subprocess
import sys
from collections.abc import Iterable
from typing import BinaryIO
def is_url(filename: str) -> bool:
parts = filename.split("://", 1)
if len(parts) < 2:
return False
# protocol prefix has no special characters => it's an URL
allowed_symbols = string.ascii_letters + string.digits + "_"
prefix = parts[0]
return all(c in allowed_symbols for c in prefix)
def get_socket_path() -> str:
# Use the same socket path as configured in mpv
return "/tmp/mpvsocket"
def send_files_to_mpv(conn: socket.socket | BinaryIO, files: Iterable[str]) -> None:
try:
send = conn.send if isinstance(conn, socket.socket) else conn.write
for f in files:
f = f.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
send(f'raw loadfile "{f}" replace\n'.encode())
except Exception:
print("mpv is terminating or the connection was lost.", file=sys.stderr)
sys.exit(1)
def start_mpv(files: Iterable[str], socket_path: str) -> None:
mpv = "${pkgs.mpv}/bin/mpv" # Use full path to mpv
mpv_command = shlex.split(os.getenv("MPV", mpv))
mpv_command.extend([
"--profile=builtin-pseudo-gui",
f"--input-ipc-server={socket_path}",
"--",
])
mpv_command.extend(files)
subprocess.Popen(mpv_command, start_new_session=True)
def main() -> None:
files = list(os.path.abspath(f) if not is_url(f) else f for f in sys.argv[1:])
socket_path = get_socket_path()
try:
if os.name == "nt":
with open(socket_path, "r+b", buffering=0) as pipe:
send_files_to_mpv(pipe, files)
else:
with socket.socket(socket.AF_UNIX) as sock:
sock.connect(socket_path)
send_files_to_mpv(sock, files)
except (FileNotFoundError, ConnectionRefusedError):
start_mpv(files, socket_path)
if __name__ == "__main__":
main()
'';
# Create a custom mpv package that replaces umpv
mpv-with-custom-umpv = pkgs.symlinkJoin {
name = "mpv-with-custom-umpv";
paths = [ pkgs.mpv ];
postBuild = ''
rm $out/bin/umpv
ln -s ${umpv-custom}/bin/umpv $out/bin/umpv
'';
};
in
{
programs.mpv = {
enable = true;
package = mpv-with-custom-umpv;
config = {
input-ipc-server = "/tmp/mpvsocket";
force-window = "no";
audio-display = "no";
};
};
}