From d5920a381eb0210abc8a04dea7b37a0d4fd137ed Mon Sep 17 00:00:00 2001 From: mani Date: Mon, 5 Jan 2026 18:46:25 +0100 Subject: [PATCH] Fix WebSocket session flapping with ForceKeepAlive protocol Implements Jellyfin's ForceKeepAlive/KeepAlive mechanism to prevent constant reconnects every 2 minutes. WebSocketApp is now recreated for each connection attempt to avoid memory leaks. Removed problematic ping_timeout and reconnect parameters that caused instability. --- resources/lib/websocket_client.py | 41 +++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/resources/lib/websocket_client.py b/resources/lib/websocket_client.py index da2c1fb..44fd5fa 100644 --- a/resources/lib/websocket_client.py +++ b/resources/lib/websocket_client.py @@ -46,7 +46,12 @@ class WebSocketClient(threading.Thread): result = json.loads(message) message_type = result['MessageType'] - if message_type == 'Play': + if message_type == 'ForceKeepAlive': + timeout_seconds = result.get('Data', 60) + log.debug("Received ForceKeepAlive with timeout: {0}s".format(timeout_seconds)) + self._send_keep_alive() + + elif message_type == 'Play': data = result['Data'] self._play(data) @@ -237,6 +242,9 @@ class WebSocketClient(threading.Thread): def on_error(self, ws, error): log.debug("Error: {0}".format(error)) + def on_close(self, ws, close_status_code, close_msg): + log.debug("WebSocket closed. Code: {0}, Message: {1}".format(close_status_code, close_msg)) + def run(self): token = None @@ -259,17 +267,23 @@ class WebSocketClient(threading.Thread): ) log.debug("websocket url: {0}".format(websocket_url)) - self._client = websocket.WebSocketApp( - websocket_url, - on_open=lambda ws: self.on_open(ws), - on_message=lambda ws, message: self.on_message(ws, message), - on_error=lambda ws, error: self.on_error(ws, error)) - log.debug("Starting WebSocketClient") while not self.monitor.abortRequested(): - self._client.run_forever(ping_interval=5, reconnect=13, ping_timeout=2) + # Create a new WebSocketApp for each connection attempt to avoid + # memory leaks from reusing a potentially dirty connection object + self._client = websocket.WebSocketApp( + websocket_url, + on_open=lambda ws: self.on_open(ws), + on_message=lambda ws, message: self.on_message(ws, message), + on_error=lambda ws, error: self.on_error(ws, error), + on_close=lambda ws, status, msg: self.on_close(ws, status, msg)) + + # Use ping_interval without ping_timeout to keep connection alive + # without forcing disconnection. The server's ForceKeepAlive/KeepAlive + # mechanism handles the actual keepalive logic. + self._client.run_forever(ping_interval=10) if self._stop_websocket: break @@ -291,6 +305,17 @@ class WebSocketClient(threading.Thread): self._client.close() log.debug("Stopping WebSocket (stop_client called)") + def _send_keep_alive(self): + """Send a KeepAlive message to the server to maintain the connection.""" + try: + keep_alive_message = json.dumps({ + 'MessageType': 'KeepAlive' + }) + self._client.send(keep_alive_message) + log.debug("Sent KeepAlive message") + except Exception as error: + log.debug("Error sending KeepAlive: {0}".format(error)) + def post_capabilities(self): settings = xbmcaddon.Addon()