package com.suno.android.media import android.content.Context import androidx.annotation.OptIn import androidx.media3.cast.CastPlayer import androidx.media3.cast.DefaultMediaItemConverter import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.SessionManagerListener import com.suno.android.common_core_utils.SunoLogger import com.suno.android.media.player.SharedPlayer import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import javax.inject.Inject @OptIn(UnstableApi::class) interface CastManager { fun isCastSessionAvailable(): Boolean fun isCasting(): Boolean fun showCastDialog() fun dismissCastDialog() fun startCasting() fun stopCasting() fun castSessionStateFlow(): Flow fun getCastPlayer(): CastPlayer? fun setPlayerListener( listener: Player.Listener, ) fun isUsingCastPlayer(): Boolean fun cleanup() } @OptIn(UnstableApi::class) class DefaultCastManager @Inject constructor( loggerFactory: SunoLogger.Factory, @ApplicationContext context: Context, @CoreMediaPlayback sharedPlayer: SharedPlayer, ) : CastManager { private val logger = loggerFactory.create(this@DefaultCastManager) private var castContext: CastContext? = null private var castPlayer: CastPlayer? = null private var castSessionListener: SessionManagerListener? = null private val _castSessionStateFlow = MutableStateFlow(CastSessionState()) private var playerListener: Player.Listener? = null private val player by sharedPlayer init { initializeCastContext(context) } private fun initializeCastContext( context: Context, ) { try { CastContext.getSharedInstance(context) { it.run() } .addOnSuccessListener { castContextInstance -> logger.d { "Cast framework initialized successfully" } castContext = castContextInstance setupCastSessionListener() updateCastAvailability() } .addOnFailureListener { exception -> logger.e(exception) { "Cast framework unavailable" } _castSessionStateFlow.update { it.copy(isAvailable = false) } } } catch (exception: Exception) { logger.e(exception) _castSessionStateFlow.update { it.copy(isAvailable = false) } } } override fun setPlayerListener( listener: Player.Listener, ) { this.playerListener = listener castPlayer?.addListener(listener) } private fun setupCastSessionListener() { castContext?.let { context -> castSessionListener = object : SessionManagerListener { override fun onSessionStarted( session: CastSession, sessionId: String, ) { logger.d { "Cast session started: $sessionId" } onCastSessionConnected() } override fun onSessionEnded( session: CastSession, error: Int, ) { logger.d { "Cast session ended: $error" } onCastSessionDisconnected() } override fun onSessionSuspended( session: CastSession, reason: Int, ) { logger.d { "Cast session suspended" } updateCastSessionState() } override fun onSessionResumed( session: CastSession, wasSuspended: Boolean, ) { logger.d { "Cast session resumed" } updateCastSessionState() } override fun onSessionStarting( session: CastSession, ) { logger.d { "Cast session starting" } _castSessionStateFlow.update { it.copy(isConnecting = true) } } override fun onSessionStartFailed( session: CastSession, error: Int, ) { logger.d { "Cast session start failed: $error" } _castSessionStateFlow.update { it.copy(isConnecting = false) } } override fun onSessionEnding( session: CastSession, ) { logger.d { "Cast session ending" } } override fun onSessionResuming( session: CastSession, sessionId: String, ) { logger.d { "Cast session resuming" } _castSessionStateFlow.update { it.copy(isConnecting = true) } } override fun onSessionResumeFailed( session: CastSession, error: Int, ) { logger.d { "Cast session resume failed: $error" } _castSessionStateFlow.update { it.copy(isConnecting = false) } } } castSessionListener?.let { listener -> context.sessionManager.addSessionManagerListener(listener, CastSession::class.java) } } } private fun updateCastAvailability() { val isAvailable = castContext != null _castSessionStateFlow.update { it.copy(isAvailable = isAvailable) } } private fun onCastSessionConnected() { castContext?.let { context -> castPlayer = CastPlayer(context, DefaultMediaItemConverter()) playerListener?.let { castPlayer?.addListener(it) } transferPlaybackToCast(player) updateCastSessionState() } } private fun onCastSessionDisconnected() { transferPlaybackFromCast(player) playerListener?.let { castPlayer?.removeListener(it) } castPlayer = null updateCastSessionState() } private fun updateCastSessionState() { val session = castContext?.sessionManager?.currentCastSession val isConnected = session?.isConnected == true val deviceName = session?.castDevice?.friendlyName _castSessionStateFlow.update { state -> state.copy( isConnected = isConnected, isConnecting = false, deviceName = deviceName, showDialog = false, ) } } fun transferPlaybackToCast( localPlayer: Player, ) { try { val castPlayerInstance = castPlayer ?: return if (localPlayer.mediaItemCount > 0) { val mediaItems = mutableListOf() for (i in 0 until localPlayer.mediaItemCount) { mediaItems.add(localPlayer.getMediaItemAt(i)) } castPlayerInstance.setMediaItems( mediaItems, localPlayer.currentMediaItemIndex, localPlayer.currentPosition, ) castPlayerInstance.prepare() if (localPlayer.isPlaying) { castPlayerInstance.play() } localPlayer.pause() logger.d { "Transferred ${mediaItems.size} items to cast device" } } } catch (exception: Exception) { logger.e(exception) } } @OptIn(UnstableApi::class) fun transferPlaybackFromCast( localPlayer: Player, ) { try { val castPlayerInstance = castPlayer ?: return if (castPlayerInstance.mediaItemCount > 0) { val wasPlaying = castPlayerInstance.isPlaying val currentPosition = castPlayerInstance.currentPosition val currentIndex = castPlayerInstance.currentMediaItemIndex localPlayer.seekTo(currentIndex, currentPosition) if (wasPlaying) { localPlayer.play() } logger.d { "Transferred playback back from cast device" } } } catch (exception: Exception) { logger.e(exception) } } override fun isCastSessionAvailable(): Boolean = _castSessionStateFlow.value.isAvailable override fun isCasting(): Boolean = _castSessionStateFlow.value.isConnected override fun showCastDialog() { _castSessionStateFlow.update { it.copy(showDialog = true) } } override fun dismissCastDialog() { _castSessionStateFlow.update { it.copy(showDialog = false) } } override fun startCasting() { try { castContext?.sessionManager?.let { sessionManager -> val currentSession = sessionManager.currentCastSession if (currentSession == null || !currentSession.isConnected) { showCastDialog() } } } catch (exception: Exception) { logger.e(exception) } } override fun stopCasting() { try { castContext?.sessionManager?.endCurrentSession(true) } catch (exception: Exception) { logger.e(exception) } } override fun castSessionStateFlow(): Flow = _castSessionStateFlow.asStateFlow() override fun getCastPlayer(): CastPlayer? = castPlayer override fun isUsingCastPlayer(): Boolean = castPlayer != null && isCasting() override fun cleanup() { try { castSessionListener?.let { listener -> castContext?.sessionManager?.removeSessionManagerListener(listener, CastSession::class.java) } castPlayer = null castContext = null } catch (exception: Exception) { logger.e(exception) } } } data class CastSessionState( val isAvailable: Boolean = false, val isConnected: Boolean = false, val isConnecting: Boolean = false, val deviceName: String? = null, val showDialog: Boolean = false, )