Improved handling of disconnection for mpv
All checks were successful
Check / mypy (push) Successful in 2m49s
Check / ruff (push) Successful in 6s

This commit is contained in:
Christoph Stahl 2025-05-18 22:58:56 +02:00
parent e2acc4f41e
commit 806d26767b
3 changed files with 68 additions and 20 deletions

View file

@ -14,6 +14,7 @@ be one of:
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from functools import partial
import logging import logging
import os import os
import asyncio import asyncio
@ -46,6 +47,29 @@ from .sources import configure_sources, Source
from .log import logger from .log import logger
class ConnectionState:
__is_connected__ = False
__mpv_running__ = False
def is_connected(self) -> bool:
return self.__is_connected__
def is_mpv_running(self) -> bool:
return self.__mpv_running__
def set_disconnected(self) -> None:
self.__is_connected__ = False
def set_connected(self) -> None:
self.__is_connected__ = True
def set_mpv_running(self) -> None:
self.__mpv_running__ = True
def set_mpv_terminated(self) -> None:
self.__mpv_running__ = False
def default_config() -> dict[str, Optional[int | str]]: def default_config() -> dict[str, Optional[int | str]]:
""" """
Return a default configuration for the client. Return a default configuration for the client.
@ -134,8 +158,7 @@ class Client:
def __init__(self, config: dict[str, Any]): def __init__(self, config: dict[str, Any]):
config["config"] = default_config() | config["config"] config["config"] = default_config() | config["config"]
self.is_running = False self.connection_state = ConnectionState()
self.is_quitting = False
self.set_log_level(config["config"]["log_level"]) self.set_log_level(config["config"]["log_level"])
self.sio = socketio.AsyncClient(json=jsonencoder) self.sio = socketio.AsyncClient(json=jsonencoder)
self.loop: Optional[asyncio.AbstractEventLoop] = None self.loop: Optional[asyncio.AbstractEventLoop] = None
@ -150,6 +173,8 @@ class Client:
QRPosition.from_string(config["config"]["qr_position"]), QRPosition.from_string(config["config"]["qr_position"]),
self.quit_callback, self.quit_callback,
) )
self.connection_state.set_mpv_running()
logger.info(f"MPV: {self.connection_state.is_mpv_running()} ")
self.register_handlers() self.register_handlers()
self.queue_callbacks: list[Callable[[list[Entry]], None]] = [] self.queue_callbacks: list[Callable[[list[Entry]], None]] = []
@ -183,7 +208,18 @@ class Client:
self.sio.on("disconnect", self.handle_disconnect) self.sio.on("disconnect", self.handle_disconnect)
async def handle_disconnect(self) -> None: async def handle_disconnect(self) -> None:
logger.info("Disconnected from server") self.connection_state.set_disconnected()
await self.ensure_disconnect()
async def ensure_disconnect(self) -> None:
logger.info("Disconnecting from server")
logger.info(f"Connection: {self.connection_state.is_connected()}")
logger.info(f"MPV: {self.connection_state.is_mpv_running()}")
if self.connection_state.is_connected():
await self.sio.disconnect()
if self.connection_state.is_mpv_running():
if self.player.mpv is not None:
self.player.mpv.terminate()
async def handle_msg(self, data: dict[str, Any]) -> None: async def handle_msg(self, data: dict[str, Any]) -> None:
""" """
@ -262,7 +298,6 @@ class Client:
""" """
self.state.queue.clear() self.state.queue.clear()
self.state.queue.extend([Entry(**entry) for entry in data["queue"]]) self.state.queue.extend([Entry(**entry) for entry in data["queue"]])
# self.state.queue = [Entry(**entry) for entry in data["queue"]]
self.state.waiting_room = [Entry(**entry) for entry in data["waiting_room"]] self.state.waiting_room = [Entry(**entry) for entry in data["waiting_room"]]
self.state.recent = [Entry(**entry) for entry in data["recent"]] self.state.recent = [Entry(**entry) for entry in data["recent"]]
@ -521,25 +556,40 @@ class Client:
elif updated_config is not None: elif updated_config is not None:
await self.sio.emit("config", {"source": data["source"], "config": updated_config}) await self.sio.emit("config", {"source": data["source"], "config": updated_config})
def signal_handler(self) -> None: def signal_handler(self, loop: asyncio.AbstractEventLoop) -> None:
""" """
Signal handler for the client. Signal handler for the client.
This function is called when the client receives a signal to terminate. It This function is called when the client receives a signal to terminate. It
will disconnect from the server and kill the current player. will disconnect from the server and kill the current player.
:param loop: The asyncio event loop
:type loop: asyncio.AbstractEventLoop
:rtype: None :rtype: None
""" """
engineio.async_client.async_signal_handler() engineio.async_client.async_signal_handler()
if self.player.mpv is not None: asyncio.ensure_future(self.ensure_disconnect(), loop=loop)
self.player.mpv.terminate()
def quit_callback(self) -> None: def quit_callback(self) -> None:
if self.is_quitting: """
return Callback function for the player, terminating the player and disconnecting
self.is_quitting = True
:rtype: None
"""
self.connection_state.set_mpv_terminated()
if self.loop is not None: if self.loop is not None:
asyncio.run_coroutine_threadsafe(self.sio.disconnect(), self.loop) asyncio.run_coroutine_threadsafe(self.ensure_disconnect(), self.loop)
asyncio.run_coroutine_threadsafe(self.kill_mpv(), self.loop)
async def kill_mpv(self) -> None:
"""
Kill the mpv process. Needs to be called in a thread, because of mpv...
See https://github.com/jaseg/python-mpv/issues/114#issuecomment-1214305952
:rtype: None
"""
if self.player.mpv is not None:
self.player.mpv.terminate()
async def start_client(self, config: dict[str, Any]) -> None: async def start_client(self, config: dict[str, Any]) -> None:
""" """
@ -576,18 +626,17 @@ class Client:
# this is not supported under windows # this is not supported under windows
if os.name != "nt": if os.name != "nt":
asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self.signal_handler) loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, partial(self.signal_handler, loop))
self.is_running = True self.connection_state.set_connected()
await self.sio.wait() await self.sio.wait()
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
except ConnectionError: except ConnectionError:
logger.critical("Could not connect to server") logger.critical("Could not connect to server")
finally: finally:
self.is_running = False await self.ensure_disconnect()
if self.player.mpv is not None:
self.player.mpv.terminate()
def create_async_and_start_client( def create_async_and_start_client(

View file

@ -824,7 +824,7 @@ class SyngGui(QMainWindow):
self.timer.stop() self.timer.stop()
return return
if not self.client.is_running: if not self.client.connection_state.is_connected():
self.client = None self.client = None
self.set_client_button_start() self.set_client_button_start()
else: else:
@ -837,7 +837,7 @@ class SyngGui(QMainWindow):
self.startbutton.setText("Connect") self.startbutton.setText("Connect")
def start_syng_client(self) -> None: def start_syng_client(self) -> None:
if self.client is None or not self.client.is_running: if self.client is None or not self.client.connection_state.is_connected():
logger.debug("Starting client") logger.debug("Starting client")
self.save_config() self.save_config()
config = self.gather_config() config = self.gather_config()

View file

@ -61,7 +61,7 @@ class Player:
def start(self) -> None: def start(self) -> None:
self.mpv = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True, osc=True) self.mpv = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True, osc=True)
self.mpv.title = "Syng - Player" self.mpv.title = "Syng.Rocks! - Player"
self.mpv.keep_open = "yes" self.mpv.keep_open = "yes"
self.mpv.play( self.mpv.play(
f"{self.base_dir}/background.png", f"{self.base_dir}/background.png",
@ -199,4 +199,3 @@ class Player:
self.mpv.play( self.mpv.play(
f"{self.base_dir}/background.png", f"{self.base_dir}/background.png",
) )
# self.mpv.playlist_next()