First working version
This commit is contained in:
+49
-132
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user