Initial commit
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user