First working version

This commit is contained in:
Michael Hansen
2025-08-05 16:16:41 -05:00
parent 469ce9c3e8
commit 20419787cf
3 changed files with 64 additions and 152 deletions
+49 -132
View File
@@ -1,174 +1,91 @@
"""Media player using mpv in a subprocess."""
import subprocess
import logging
import socket
import os
import uuid
import threading
import json
import time
import tempfile
from collections.abc import Callable
from typing import Optional, Union, List, Any
from pathlib import Path
from threading import Lock
from mpv import MPV
_LOGGER = logging.getLogger(__name__)
class MpvMediaPlayer:
def __init__(
self,
device: Optional[str] = None,
socket_path: Optional[Union[str, Path]] = None,
) -> None:
self.device = device
def __init__(self, device: Optional[str] = None) -> None:
self.player = MPV()
if device:
self.player["audio-device"] = device
self.is_playing = False
if socket_path:
self._socket_path = str(Path(socket_path).resolve())
else:
base_path = (
Path(os.environ.get("XDG_RUNTIME_DIR") or "/dev/shm") / "mpv-sockets"
)
base_path.mkdir(parents=True, exist_ok=True)
self._socket_path = str(base_path / f"mpv-{uuid.uuid4().hex}.sock")
self._playlist_file = tempfile.NamedTemporaryFile(
"w+", suffix=".m3u8", encoding="utf-8"
)
cmd = [
"mpv",
f"--input-ipc-server={self._socket_path}",
"--no-video",
"--quiet",
"--term-playing-msg=",
"--msg-level=all=no",
"--idle=yes", # keep alive
]
if device:
cmd.append(f"--audio-device={device}")
_LOGGER.debug(cmd)
self._proc = subprocess.Popen(cmd)
self._socket_connected = False
self._playlist: List[str] = []
self._done_callback: Optional[Callable[[], None]] = None
self._done_callback_lock = Lock()
threading.Thread(target=self._read_socket, daemon=True).start()
self._duck_volume:int = 50
self._unduck_volume:int = 100
self.player.event_callback("end-file")(self._on_end_file)
def play(
self,
url: Union[str, List[str]],
done_callback: Optional[Callable[[], None]] = None,
) -> None:
self.stop()
if isinstance(url, str):
url = [url]
self._playlist = [url]
else:
self._playlist = url
self._done_callback = done_callback
self.is_playing = True
if len(url) == 1:
self.send_command(["loadfile", url[0], "replace"])
else:
self._playlist_file.seek(0)
for item in url:
print(item, file=self._playlist_file, flush=True)
self.send_command(["loadlist", self._playlist_file.name, "replace"])
def stop(self) -> None:
self.send_command(["stop"])
self.is_playing = False
self.player.play(self._playlist.pop(0))
def pause(self) -> None:
if not self.is_playing:
return
self.send_command(["set_property", "pause", True])
self.player.pause = True
self.is_playing = False
def resume(self) -> None:
if self.is_playing:
return
self.player.pause = False
if self._playlist:
self.is_playing = True
self.send_command(["set_property", "pause", False])
self.is_playing = True
def stop(self) -> None:
self.player.stop()
self._playlist.clear()
def mute(self) -> None:
self.send_command(["set_property", "mute", True])
def duck(self) -> None:
self.player.volume = self._duck_volume
def unmute(self) -> None:
self.send_command(["set_property", "mute", False])
def unduck(self) -> None:
self.player.volume = self._unduck_volume
def set_volume(self, volume: int) -> None:
self.send_command(["set_property", "volume", volume])
volume = max(0, min(100, volume))
self.player.volume = volume
def send_command(self, cmd: List[Any]):
if not self._socket_connected:
_LOGGER.warning("Socket not connected: %s", cmd)
self._unduck_volume = volume
self._duck_volume = volume // 2
def _on_end_file(self, event):
if self._playlist:
self.player.play(self._playlist.pop(0))
return
payload = json.dumps({"command": cmd}).encode("utf-8") + b"\n"
self.is_playing = False
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as mpv_socket:
mpv_socket.connect(self._socket_path)
mpv_socket.sendall(payload)
_LOGGER.debug("Sent command: %s", payload)
def _read_socket(self):
# Connect to socket
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as mpv_socket:
timeout = time.monotonic() + 5
while time.monotonic() < timeout:
try:
mpv_socket.connect(self._socket_path)
self._socket_connected = True
break
except (ConnectionRefusedError, FileNotFoundError, OSError):
pass
if not self._socket_connected:
_LOGGER.warning("Failed to connect to socket: %s", self._socket_path)
return
_LOGGER.debug("Connected to socket: %s", self._socket_path)
# Read from socket
mpv_buffer = bytes()
todo_callback: Optional[Callable[[], None]] = None
with self._done_callback_lock:
if self._done_callback:
todo_callback = self._done_callback
self._done_callback = None
if todo_callback:
try:
while True:
chunk = mpv_socket.recv(4096)
if not chunk:
break
mpv_buffer += chunk
while b"\n" in mpv_buffer:
line, mpv_buffer = mpv_buffer.split(b"\n", 1)
event = json.loads(line.decode())
_LOGGER.debug("Received event: %s", event)
event_type = event.get("event")
if event_type == "start-file":
self.is_playing = True
continue
if event_type != "end-file":
continue
self.is_playing = False
if not self._done_callback:
continue
try:
self._done_callback()
except:
_LOGGER.exception("Unexpected error in callback")
todo_callback()
except:
_LOGGER.exception("Unexpected error reading MPV socket")
pass