175 lines
5.2 KiB
Python
175 lines
5.2 KiB
Python
"""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
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class MpvMediaPlayer:
|
|
def __init__(
|
|
self,
|
|
device: Optional[str] = None,
|
|
socket_path: Optional[Union[str, Path]] = None,
|
|
) -> None:
|
|
self.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._done_callback: Optional[Callable[[], None]] = None
|
|
|
|
threading.Thread(target=self._read_socket, daemon=True).start()
|
|
|
|
def play(
|
|
self,
|
|
url: Union[str, List[str]],
|
|
done_callback: Optional[Callable[[], None]] = None,
|
|
) -> None:
|
|
if isinstance(url, str):
|
|
url = [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
|
|
|
|
def pause(self) -> None:
|
|
if not self.is_playing:
|
|
return
|
|
|
|
self.send_command(["set_property", "pause", True])
|
|
self.is_playing = False
|
|
|
|
def resume(self) -> None:
|
|
if self.is_playing:
|
|
return
|
|
|
|
self.send_command(["set_property", "pause", False])
|
|
self.is_playing = True
|
|
|
|
def mute(self) -> None:
|
|
self.send_command(["set_property", "mute", True])
|
|
|
|
def unmute(self) -> None:
|
|
self.send_command(["set_property", "mute", False])
|
|
|
|
def set_volume(self, volume: int) -> None:
|
|
self.send_command(["set_property", "volume", volume])
|
|
|
|
def send_command(self, cmd: List[Any]):
|
|
if not self._socket_connected:
|
|
_LOGGER.warning("Socket not connected: %s", cmd)
|
|
return
|
|
|
|
payload = json.dumps({"command": cmd}).encode("utf-8") + b"\n"
|
|
|
|
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()
|
|
|
|
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")
|
|
except:
|
|
_LOGGER.exception("Unexpected error reading MPV socket")
|