Fix mpv pause state when loading new media
Fixes #107 This fixes an issue where loading a new media URL while the player was paused would keep playback paused, causing Music Assistant to show the player as playing while no audio was audible. The mpv pause state is now always set explicitly when starting playback, ensuring deterministic behavior
This commit is contained in:
1 parent
7a92d69864
commit
d42abaa2c9
6 files changed
+205
-68
No files matched your search
@@ -1,32 +1,18 @@
|
||||
"""Media player using mpv in a subprocess."""
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from threading import Lock
|
||||
from typing import List, Optional, Union
|
||||
from typing import Union, List, Callable, Optional
|
||||
|
||||
from mpv import MPV
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .player.libmpv import LibMpvPlayer
|
||||
|
||||
|
||||
class MpvMediaPlayer:
|
||||
def __init__(self, device: Optional[str] = None) -> None:
|
||||
self.player = MPV()
|
||||
"""
|
||||
LVA MediaPlayer implementation using libmpv.
|
||||
"""
|
||||
|
||||
if device:
|
||||
self.player["audio-device"] = device
|
||||
|
||||
self.is_playing = False
|
||||
|
||||
self._playlist: List[str] = []
|
||||
def __init__(self, device: str | None = None):
|
||||
self._log = logging.getLogger(self.__class__.__name__)
|
||||
self._player = LibMpvPlayer(device=device)
|
||||
self._done_callback: Optional[Callable[[], None]] = None
|
||||
self._done_callback_lock = Lock()
|
||||
|
||||
self._duck_volume: int = 50
|
||||
self._unduck_volume: int = 100
|
||||
|
||||
self.player.event_callback("end-file")(self._on_end_file)
|
||||
|
||||
def play(
|
||||
self,
|
||||
@@ -34,61 +20,31 @@ class MpvMediaPlayer:
|
||||
done_callback: Optional[Callable[[], None]] = None,
|
||||
stop_first: bool = True,
|
||||
) -> None:
|
||||
self.stop()
|
||||
self._log.info("Playing %s", url)
|
||||
|
||||
if isinstance(url, str):
|
||||
self._playlist = [url]
|
||||
else:
|
||||
self._playlist = url
|
||||
|
||||
next_url = self._playlist.pop(0)
|
||||
_LOGGER.debug("Playing %s", next_url)
|
||||
if isinstance(url, list):
|
||||
url = url[0]
|
||||
|
||||
self._done_callback = done_callback
|
||||
self.is_playing = True
|
||||
self.player.play(next_url)
|
||||
self._player.play(url, paused=False)
|
||||
|
||||
def pause(self) -> None:
|
||||
self.player.pause = True
|
||||
self.is_playing = False
|
||||
self._player.pause()
|
||||
|
||||
def resume(self) -> None:
|
||||
self.player.pause = False
|
||||
if self._playlist:
|
||||
self.is_playing = True
|
||||
self._player.resume()
|
||||
|
||||
def stop(self) -> None:
|
||||
self.player.stop()
|
||||
self._playlist.clear()
|
||||
self._player.stop()
|
||||
if self._done_callback:
|
||||
self._done_callback()
|
||||
self._done_callback = None
|
||||
|
||||
def duck(self) -> None:
|
||||
self.player.volume = self._duck_volume
|
||||
def set_volume(self, volume: float) -> None:
|
||||
self._player.set_volume(volume)
|
||||
|
||||
def duck(self, factor: float = 0.5) -> None:
|
||||
self._player.duck(factor)
|
||||
|
||||
def unduck(self) -> None:
|
||||
self.player.volume = self._unduck_volume
|
||||
|
||||
def set_volume(self, volume: int) -> None:
|
||||
volume = max(0, min(100, volume))
|
||||
self.player.volume = volume
|
||||
|
||||
self._unduck_volume = volume
|
||||
self._duck_volume = volume // 2
|
||||
|
||||
def _on_end_file(self, event) -> None:
|
||||
if self._playlist:
|
||||
self.player.play(self._playlist.pop(0))
|
||||
return
|
||||
|
||||
self.is_playing = False
|
||||
|
||||
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:
|
||||
todo_callback()
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error running done callback")
|
||||
self._player.unduck()
|
||||
@@ -0,0 +1,25 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from .state import PlayerState
|
||||
|
||||
|
||||
class AudioPlayer(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def play(self, url: str) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pause(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def resume(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def state(self) -> PlayerState:
|
||||
pass
|
||||
@@ -0,0 +1,86 @@
|
||||
import threading
|
||||
import logging
|
||||
from .base import AudioPlayer
|
||||
from .state import PlayerState
|
||||
import mpv
|
||||
|
||||
|
||||
class LibMpvPlayer(AudioPlayer):
|
||||
|
||||
def __init__(self, device: str | None = None):
|
||||
self._log = logging.getLogger(self.__class__.__name__)
|
||||
self._state = PlayerState.IDLE
|
||||
self._state_lock = threading.Lock()
|
||||
|
||||
self._last_volume: float = 100.0 # mpv Volume 0-100
|
||||
|
||||
self._mpv = mpv.MPV(
|
||||
audio_display=False,
|
||||
log_handler=self._on_mpv_log,
|
||||
loglevel="error",
|
||||
)
|
||||
if device:
|
||||
self._log.info("Using audio device: %s", device)
|
||||
self._mpv["audio-device"] = device
|
||||
|
||||
# -------- Core Methods --------
|
||||
|
||||
def play(self, url: str, paused: bool = False) -> None:
|
||||
with self._state_lock:
|
||||
self._set_state(PlayerState.LOADING)
|
||||
|
||||
self._mpv.pause = paused
|
||||
|
||||
self._log.info("Loading media: %s (paused=%s)", url, paused)
|
||||
self._mpv.play(url)
|
||||
|
||||
def pause(self):
|
||||
with self._state_lock:
|
||||
self._mpv.pause = True
|
||||
self._set_state(PlayerState.PAUSED)
|
||||
|
||||
def resume(self):
|
||||
with self._state_lock:
|
||||
self._mpv.pause = False
|
||||
self._set_state(PlayerState.PLAYING)
|
||||
|
||||
def stop(self):
|
||||
with self._state_lock:
|
||||
self._mpv.stop()
|
||||
self._set_state(PlayerState.IDLE)
|
||||
|
||||
def state(self):
|
||||
with self._state_lock:
|
||||
return self._state
|
||||
|
||||
# -------- Volume / Ducking --------
|
||||
|
||||
def set_volume(self, volume: float):
|
||||
"""
|
||||
volume: float 0.0-1.0
|
||||
"""
|
||||
with self._state_lock:
|
||||
self._last_volume = volume * 100
|
||||
self._mpv.volume = self._last_volume
|
||||
|
||||
def duck(self, factor: float = 0.5):
|
||||
with self._state_lock:
|
||||
self._last_volume = getattr(self, "_last_volume", self._mpv.volume)
|
||||
self._mpv.volume = self._last_volume * factor
|
||||
|
||||
def unduck(self):
|
||||
with self._state_lock:
|
||||
self._mpv.volume = getattr(self, "_last_volume", self._mpv.volume)
|
||||
|
||||
# -------- Internal Helpers --------
|
||||
|
||||
def _on_mpv_log(self, level, prefix, text):
|
||||
if level in ("error", "fatal"):
|
||||
self._log.error("[mpv] %s", text.strip())
|
||||
with self._state_lock:
|
||||
self._set_state(PlayerState.ERROR)
|
||||
|
||||
def _set_state(self, new_state: PlayerState):
|
||||
if self._state != new_state:
|
||||
self._log.debug("State %s → %s", self._state.name, new_state.name)
|
||||
self._state = new_state
|
||||
@@ -0,0 +1,10 @@
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class PlayerState(Enum):
|
||||
IDLE = auto()
|
||||
LOADING = auto()
|
||||
PLAYING = auto()
|
||||
PAUSED = auto()
|
||||
STOPPING = auto()
|
||||
ERROR = auto()
|
||||
@@ -0,0 +1,37 @@
|
||||
import time
|
||||
import mpv
|
||||
|
||||
"""
|
||||
mpv_smoke_test.py - test playback of media
|
||||
"""
|
||||
|
||||
def handle_event(event):
|
||||
print(f"EVENT: {event['event_id']}")
|
||||
|
||||
|
||||
player = mpv.MPV(
|
||||
audio_display=False,
|
||||
log_handler=print,
|
||||
loglevel="info",
|
||||
)
|
||||
|
||||
player.event_callback = handle_event
|
||||
|
||||
print("Loading media...")
|
||||
player.play("https://icecast.radiofrance.fr/fip-midfi.mp3")
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
print("Pausing...")
|
||||
player.pause = True
|
||||
time.sleep(2)
|
||||
|
||||
print("Resuming...")
|
||||
player.pause = False
|
||||
time.sleep(5)
|
||||
|
||||
print("Stopping...")
|
||||
player.stop()
|
||||
|
||||
time.sleep(2)
|
||||
print("Done.")
|
||||
@@ -0,0 +1,23 @@
|
||||
import time
|
||||
import logging
|
||||
|
||||
from linux_voice_assistant.player.libmpv import LibMpvPlayer
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
player = LibMpvPlayer()
|
||||
|
||||
player.play("https://icecast.radiofrance.fr/fip-midfi.mp3")
|
||||
time.sleep(5)
|
||||
|
||||
player.pause()
|
||||
time.sleep(2)
|
||||
|
||||
player.resume()
|
||||
time.sleep(5)
|
||||
|
||||
player.stop()
|
||||
time.sleep(2)
|
||||
|
||||
print("Final state:", player.state())
|
||||
Reference in new issue
Block a user