From ab0ba3db76d1accec1ca944e62b325c8d92592a6 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 3 Oct 2026 13:44:47 +0200 Subject: Cast: receiver position and pause/play on both clients The player's remote mode reads where the television is instead of the local playhead, which drifts. Co-Authored-By: Claude Opus 5.5 --- .../kotlin/org/meshbay/client/cast/CastChannels.kt | 2 ++ .../kotlin/org/meshbay/client/cast/CastControl.kt | 30 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) (limited to 'packages/meshbay-android/app/src/main/kotlin/org/meshbay') diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastChannels.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastChannels.kt index ecad8ec..74a086b 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastChannels.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastChannels.kt @@ -56,6 +56,8 @@ class CastChannels( val o = a.optJSONObject(0) ?: JSONObject() reported { control.reload(relayUrl(o), subtitleOf(o)) } } + "cast:chromecast:pause" -> control.pause() + "cast:chromecast:play" -> control.play() "cast:chromecast:disconnect" -> control.disconnect() else -> throw Refused("Refused: no such channel") } diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastControl.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastControl.kt index 9f47f42..f574c89 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastControl.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/cast/CastControl.kt @@ -283,6 +283,28 @@ class CastControl(private val context: Context) { return onMain { stateName(session.remoteMediaClient?.playerState ?: MediaStatus.PLAYER_STATE_UNKNOWN) } } + /** The player as a remote: pause and play the receiver, answered once it has. */ + fun pause(): JSONObject = command { it.pause() } + fun play(): JSONObject = command { it.play() } + + private fun command(send: (com.google.android.gms.cast.framework.media.RemoteMediaClient) -> + com.google.android.gms.common.api.PendingResult): JSONObject { + need() + val done = CountDownLatch(1) + var ok = false + onMain { + val c = CastContext.getSharedInstance(context).sessionManager.currentCastSession?.remoteMediaClient + ?: throw Refused("Not connected") + send(c).setResultCallback { r -> ok = r.status.isSuccess; done.countDown() } + } + if (!done.await(10, TimeUnit.SECONDS)) throw IllegalStateException("The receiver did not answer") + if (!ok) throw IllegalStateException("The receiver refused the command") + return JSONObject().put("playerState", onMain { + stateName(CastContext.getSharedInstance(context).sessionManager.currentCastSession?.remoteMediaClient?.playerState + ?: MediaStatus.PLAYER_STATE_UNKNOWN) + }) + } + fun disconnect(): Boolean { if (!available()) return true onMain { CastContext.getSharedInstance(context).sessionManager.endCurrentSession(true) } @@ -295,7 +317,15 @@ class CastControl(private val context: Context) { else onMain { val s = CastContext.getSharedInstance(context).sessionManager.currentCastSession val on = s != null && s.isConnected + val c = if (on) s!!.remoteMediaClient else null JSONObject().put("connected", on).put("deviceName", if (on) (deviceName ?: s!!.castDevice?.friendlyName) else JSONObject.NULL) + // Where the television is, on the stream's timeline (zero at + // the relay's start): the page adds that start. Not the local + // playhead, which started earlier and drifts. + .put("playerState", c?.let { stateName(it.playerState) } ?: JSONObject.NULL) + .put("idleReason", c?.takeIf { it.playerState == MediaStatus.PLAYER_STATE_IDLE } + ?.let { idleName(it.idleReason) } ?: JSONObject.NULL) + .put("position", c?.let { it.approximateStreamPosition / 1000.0 } ?: JSONObject.NULL) } } catch (e: Exception) { Log.w("MeshBay", "cast status", e) -- cgit v1.2.3