From fcfd9ec1b3b7dedb3015af633056acb7980201a2 Mon Sep 17 00:00:00 2001 From: sappho Date: Sat, 22 Aug 2026 14:09:15 -0400 Subject: [PATCH] Fix for chromecast staying wrongly "connected" after the chromecast ends the session Listen for SessionManager onSessionEnded so idle timeout and other receiver-side stops reach jellyfin-web. Treat non-connected CastSession objects as dead in initialize/requestSession, and clear the native session when setSession(null) is called. Should fix #1852 --- .../player/cast/ChromecastConnection.java | 24 +++++++++++++++---- .../mobile/player/cast/ChromecastSession.java | 1 + .../player/cast/ChromecastUtilities.java | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastConnection.java b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastConnection.java index 2e4e970b..5ec80081 100644 --- a/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastConnection.java +++ b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastConnection.java @@ -69,6 +69,17 @@ public class ChromecastConnection { @NonNull private String appId; + // Fires for every session end, whoever caused it - our own endSession() or the chromecast receiver (idle timeout, TV home, etc.) + // Status must be "stopped", the only value chrome.cast.js reports as dead, + // so jellyfin-web correctly drops the player instead of staying "connected" + private final SessionListener sessionListener = new SessionListener() { + @Override + public void onSessionEnded(@NonNull CastSession castSession, int error) { + chromecastSession.setSession(null); + listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, "stopped")); + } + }; + /** * Constructor. * @@ -90,6 +101,7 @@ public class ChromecastConnection { // CastContext and prep it for searching for a session to rejoin // Also adds the receiver update callback getContext().addCastStateListener(listener); + getSessionManager().addSessionManagerListener(sessionListener, CastSession.class); } /** @@ -140,10 +152,14 @@ public class ChromecastConnection { // Since we have a receiver we may also have an active session CastSession session = getSessionManager().getCurrentCastSession(); // If we do have a session - if (session != null) { + if (session != null && session.isConnected()) { // Let the client know chromecastSession.setSession(session); listener.onSessionRejoin(ChromecastUtilities.createSessionObject(session)); + } else { + // Session object can be stale (e.g. receiver-side timeout while backgrounded) + // Let the client know it's dead explicitly + listener.onSessionEnd(ChromecastUtilities.createSessionObject(session, "stopped")); } } } @@ -341,7 +357,7 @@ public class ChromecastConnection { public void requestSession(RequestSessionCallback callback) { activity.runOnUiThread(() -> { CastSession session = getSession(); - if (session == null) { + if (session == null || !session.isConnected()) { // show the "choose a connection" dialog // Add the connection listener callback @@ -496,15 +512,14 @@ public class ChromecastConnection { void endSession(boolean stopCasting, JavascriptCallback callback) { activity.runOnUiThread(new Runnable() { public void run() { + // sessionListener reports the end to JS, this one just resolves the caller getSessionManager().addSessionManagerListener(new SessionListener() { @Override public void onSessionEnded(@NonNull CastSession castSession, int error) { getSessionManager().removeSessionManagerListener(this, CastSession.class); - chromecastSession.setSession(null); if (callback != null) { callback.success(); } - listener.onSessionEnd(ChromecastUtilities.createSessionObject(castSession, stopCasting ? "stopped" : "disconnected")); } }, CastSession.class); @@ -515,6 +530,7 @@ public class ChromecastConnection { public void destroy() { getSessionManager().removeSessionManagerListener(newConnectionListener, CastSession.class); + getSessionManager().removeSessionManagerListener(sessionListener, CastSession.class); handler.removeCallbacksAndMessages(null); activity = null; } diff --git a/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastSession.java b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastSession.java index 9b233ff0..eafbe8e2 100644 --- a/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastSession.java +++ b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastSession.java @@ -81,6 +81,7 @@ public class ChromecastSession { public void setSession(CastSession castSession) { activity.runOnUiThread(() -> { if (castSession == null) { + session = null; client = null; return; } diff --git a/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastUtilities.java b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastUtilities.java index bbdf8a21..e558d0b4 100644 --- a/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastUtilities.java +++ b/app/src/proprietary/java/org/jellyfin/mobile/player/cast/ChromecastUtilities.java @@ -435,6 +435,7 @@ final class ChromecastUtilities { out.put("media", createMediaArray(session)); out.put("receiver", createReceiverObject(session)); out.put("sessionId", session.getSessionId()); + out.put("status", session.isConnected() ? "connected" : "disconnected"); } } catch (JSONException | NullPointerException | IllegalStateException ignored) { }