diff --git a/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/FlutterCastFrameworkPlugin.kt b/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/FlutterCastFrameworkPlugin.kt index 48744bf..4ef6c4e 100644 --- a/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/FlutterCastFrameworkPlugin.kt +++ b/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/FlutterCastFrameworkPlugin.kt @@ -69,6 +69,10 @@ override fun onDetachedFromEngine( ) { channel.setMethodCallHandler(null) + if (::dialogLauncher.isInitialized) { + dialogLauncher.dispose() + } + if (::videoOptimizer.isInitialized) { videoOptimizer.dispose() } diff --git a/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastDialogLauncher.kt b/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastDialogLauncher.kt index 183bc84..5fbf69a 100644 --- a/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastDialogLauncher.kt +++ b/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastDialogLauncher.kt @@ -1,6 +1,7 @@ package com.aves.google_cast.cast import android.app.Activity +import android.util.Log import android.view.ViewGroup import androidx.mediarouter.app.MediaRouteButton import com.google.android.gms.cast.framework.CastButtonFactory @@ -10,22 +11,74 @@ class CastDialogLauncher( private val channel: MethodChannel ) { + companion object { + private const val TAG = "GCAST_NATIVE" + } + + private var currentButton: MediaRouteButton? = null + fun showCastDialog(activity: Activity) { activity.runOnUiThread { try { + removeCurrentButton() + val button = MediaRouteButton(activity) - CastButtonFactory.setUpMediaRouteButton(activity, button) - val root = activity.findViewById(android.R.id.content) - root?.addView(button, ViewGroup.LayoutParams(1, 1)) + CastButtonFactory.setUpMediaRouteButton( + activity, + button + ) - button.performClick() + val root = activity.findViewById( + android.R.id.content + ) + + root?.addView( + button, + ViewGroup.LayoutParams(1, 1) + ) + + currentButton = button + + val clicked = button.performClick() + + Log.d( + TAG, + "Cast dialog performClick=$clicked" + ) + + button.postDelayed( + { removeCurrentButton() }, + 1_000L + ) } catch (e: Exception) { + removeCurrentButton() + channel.invokeMethod( "onCastError", - mapOf("message" to (e.message ?: "unknown")) + mapOf( + "message" to + (e.message ?: "Unknown Cast dialog error") + ) ) } } } + + fun dispose() { + currentButton?.post { + removeCurrentButton() + } + } + + private fun removeCurrentButton() { + val button = currentButton ?: return + + try { + (button.parent as? ViewGroup)?.removeView(button) + } catch (_: Exception) { + } + + currentButton = null + } } diff --git a/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastMediaClient.kt b/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastMediaClient.kt index acd1d40..746553b 100644 --- a/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastMediaClient.kt +++ b/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastMediaClient.kt @@ -243,32 +243,68 @@ override fun onMediaError(mediaError: MediaError) { // ------------------------------------------------------------ // CLIENT ATTACH // ------------------------------------------------------------ - fun attachCurrentClientIfNeeded() { - val client = ctx.getClient() ?: run { - Log.d("GCAST_NATIVE", "attachCurrentClientIfNeeded(): ctx.getClient() == null") + fun attachClient(client: RemoteMediaClient?) { + if (client == null) { + Log.d("GCAST_NATIVE", "attachClient(): client == null") + detachCurrentClient() return } val identity = System.identityHashCode(client) - Log.d("GCAST_NATIVE", "attachCurrentClientIfNeeded(): attaching to client identity=$identity") + Log.d( + "GCAST_NATIVE", + "attachClient(): direct client identity=$identity", + ) - if (!mediaClientListenerAdded || lastClientIdentity != identity) { + if (mediaClientListenerAdded && lastClientIdentity == identity) { + Log.d( + "GCAST_NATIVE", + "attachClient(): client already attached", + ) + return + } + + try { + client.unregisterCallback(mediaClientCallback) + } catch (_: Exception) { + } + + client.registerCallback(mediaClientCallback) + + mediaClientListenerAdded = true + lastClientIdentity = identity + + lastSentIndex = null + lastHandledFinishedItemId = null + lastPlaybackState = null + lastVolume = null + lastMuted = null + } + + fun detachCurrentClient() { + val client = ctx.getClient() + + if (client != null) { try { client.unregisterCallback(mediaClientCallback) - } catch (_: Exception) {} - - client.registerCallback(mediaClientCallback) - - mediaClientListenerAdded = true - lastClientIdentity = identity - - lastSentIndex = null - lastHandledFinishedItemId = null - lastPlaybackState = null - lastVolume = null - lastMuted = null + } catch (_: Exception) { + } } + + resetClientTracking() + } + + fun attachCurrentClientIfNeeded() { + val client = ctx.getClient() ?: run { + Log.d( + "GCAST_NATIVE", + "attachCurrentClientIfNeeded(): ctx.getClient() == null", + ) + return + } + + attachClient(client) } fun resetClientTracking() { diff --git a/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastSessionHandler.kt b/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastSessionHandler.kt index 6d0bd63..343d519 100644 --- a/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastSessionHandler.kt +++ b/plugins/google_cast/android/src/main/kotlin/com/aves/google_cast/cast/CastSessionHandler.kt @@ -2,6 +2,7 @@ package com.aves.google_cast.cast import android.os.Handler import android.os.Looper +import android.os.SystemClock import android.util.Log import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.SessionManagerListener @@ -11,90 +12,219 @@ class CastSessionHandler( private val channel: MethodChannel ) : SessionManagerListener { + companion object { + private const val TAG = "GCAST_NATIVE" + } + private val mainHandler = Handler(Looper.getMainLooper()) + private var readySent = false private var currentSession: CastSession? = null + private var connectionStartedAtMs: Long = 0L - // ⭐ mediaClient viene assegnato DOPO la creazione private var mediaClient: CastMediaClient? = null fun setMediaClient(client: CastMediaClient) { mediaClient = client } + private fun beginConnectionTiming() { + if (connectionStartedAtMs == 0L) { + connectionStartedAtMs = SystemClock.elapsedRealtime() + } + } + + private fun elapsedMs(): Long { + if (connectionStartedAtMs == 0L) return 0L + return SystemClock.elapsedRealtime() - connectionStartedAtMs + } + override fun onSessionStarting(session: CastSession) { - Log.d("GCAST_NATIVE", "onSessionStarting") - currentSession = session + beginConnectionTiming() - mediaClient?.attachCurrentClientIfNeeded() // ⭐ PATCH - trySendReadyWhenClientExists() + Log.i( + TAG, + "CAST_TIMING onSessionStarting t=${elapsedMs()}ms" + ) + + currentSession = session + readySent = false + + session.remoteMediaClient?.let { + mediaClient?.attachClient(it) + } } - override fun onSessionStarted(session: CastSession, sessionId: String) { - Log.d("GCAST_NATIVE", "onSessionStarted sessionId=$sessionId") + override fun onSessionStarted( + session: CastSession, + sessionId: String + ) { + beginConnectionTiming() + + Log.i( + TAG, + "CAST_TIMING onSessionStarted " + + "sessionId=$sessionId t=${elapsedMs()}ms" + ) + currentSession = session - mediaClient?.attachCurrentClientIfNeeded() // ⭐ PATCH - trySendReadyWhenClientExists() + mediaClient?.attachClient(session.remoteMediaClient) + sendReadyIfPossible(session) } - override fun onSessionResuming(session: CastSession, sessionId: String) { - Log.d("GCAST_NATIVE", "onSessionResuming sessionId=$sessionId") + override fun onSessionResuming( + session: CastSession, + sessionId: String + ) { + beginConnectionTiming() + + Log.i( + TAG, + "CAST_TIMING onSessionResuming " + + "sessionId=$sessionId t=${elapsedMs()}ms" + ) + + currentSession = session + readySent = false + } + + override fun onSessionResumed( + session: CastSession, + wasSuspended: Boolean + ) { + beginConnectionTiming() + + Log.i( + TAG, + "CAST_TIMING onSessionResumed " + + "wasSuspended=$wasSuspended t=${elapsedMs()}ms" + ) + currentSession = session - mediaClient?.attachCurrentClientIfNeeded() // ⭐ PATCH - trySendReadyWhenClientExists() + mediaClient?.attachClient(session.remoteMediaClient) + sendReadyIfPossible(session) } - override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) { - Log.d("GCAST_NATIVE", "onSessionResumed wasSuspended=$wasSuspended") + override fun onSessionSuspended( + session: CastSession, + reason: Int + ) { + Log.i( + TAG, + "onSessionSuspended reason=$reason t=${elapsedMs()}ms" + ) + currentSession = session + readySent = false - mediaClient?.attachCurrentClientIfNeeded() // ⭐ PATCH - trySendReadyWhenClientExists() + mediaClient?.detachCurrentClient() } - override fun onSessionSuspended(session: CastSession, reason: Int) { - Log.d("GCAST_NATIVE", "onSessionSuspended reason=$reason") - currentSession = session - - mediaClient?.attachCurrentClientIfNeeded() // ⭐ PATCH - trySendReadyWhenClientExists() - } - - override fun onSessionEnded(session: CastSession, error: Int) { - Log.d("GCAST_NATIVE", "onSessionEnded error=$error") + override fun onSessionEnded( + session: CastSession, + error: Int + ) { + Log.i( + TAG, + "onSessionEnded error=$error t=${elapsedMs()}ms" + ) currentSession = null readySent = false + connectionStartedAtMs = 0L + + mediaClient?.detachCurrentClient() mainHandler.post { - Log.d("GCAST_NATIVE", "invokeMethod onCastStopped") + Log.d(TAG, "invokeMethod onCastStopped") channel.invokeMethod("onCastStopped", null) } } - override fun onSessionStartFailed(session: CastSession, error: Int) { - Log.d("GCAST_NATIVE", "onSessionStartFailed error=$error") + override fun onSessionStartFailed( + session: CastSession, + error: Int + ) { + Log.e( + TAG, + "onSessionStartFailed error=$error t=${elapsedMs()}ms" + ) + + currentSession = null + readySent = false + connectionStartedAtMs = 0L + + mediaClient?.detachCurrentClient() + + mainHandler.post { + channel.invokeMethod( + "onCastError", + mapOf( + "message" to "Cast session start failed", + "errorCode" to error + ) + ) + } } - override fun onSessionResumeFailed(session: CastSession, error: Int) { - Log.d("GCAST_NATIVE", "onSessionResumeFailed error=$error") + override fun onSessionResumeFailed( + session: CastSession, + error: Int + ) { + Log.e( + TAG, + "onSessionResumeFailed error=$error t=${elapsedMs()}ms" + ) + + currentSession = null + readySent = false + connectionStartedAtMs = 0L + + mediaClient?.detachCurrentClient() + + mainHandler.post { + channel.invokeMethod( + "onCastError", + mapOf( + "message" to "Cast session resume failed", + "errorCode" to error + ) + ) + } } override fun onSessionEnding(session: CastSession) { - Log.d("GCAST_NATIVE", "onSessionEnding") + Log.d(TAG, "onSessionEnding") } - private fun trySendReadyWhenClientExists() { - val session = currentSession ?: return - val client = session.remoteMediaClient ?: return + private fun sendReadyIfPossible(session: CastSession) { + val client = session.remoteMediaClient ?: run { + Log.d( + TAG, + "sendReadyIfPossible(): remoteMediaClient == null" + ) + return + } - if (readySent) return + if (readySent) { + Log.d( + TAG, + "sendReadyIfPossible(): ready already sent" + ) + return + } + + mediaClient?.attachClient(client) readySent = true mainHandler.post { - Log.d("GCAST_NATIVE", "invokeMethod onCastReady") + Log.i( + TAG, + "CAST_TIMING invokeMethod onCastReady " + + "t=${elapsedMs()}ms" + ) channel.invokeMethod("onCastReady", null) } }