From 0545e06a04bcfb7075954a4518e6be54855dbf40 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 14 Aug 2026 21:03:29 +0200 Subject: [PATCH] fix(cast): keep cast running after app close --- .../zoriya/omni/OmniCastOptionsProvider.kt | 24 +++++++++++++++++-- .../main/java/dev/zoriya/omni/OmniPlayer.kt | 23 ++++++++++-------- example/webpack.config.js | 1 + 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt b/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt index a29d095..e2d1d7e 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt @@ -4,6 +4,8 @@ import android.content.Context import com.google.android.gms.cast.CastMediaControlIntent import com.google.android.gms.cast.framework.CastOptions import com.google.android.gms.cast.framework.media.CastMediaOptions +import com.google.android.gms.cast.framework.media.MediaIntentReceiver +import com.google.android.gms.cast.framework.media.NotificationOptions import com.google.android.gms.cast.framework.OptionsProvider import com.google.android.gms.cast.framework.SessionProvider @@ -12,9 +14,27 @@ class OmniCastOptionsProvider : OptionsProvider { val appId = OmniPlayer.receiverApplicationId ?: CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID + val notificationOptions = NotificationOptions.Builder() + .setActions( + listOf( + MediaIntentReceiver.ACTION_SKIP_PREV, + MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK, + MediaIntentReceiver.ACTION_SKIP_NEXT, + ), + intArrayOf(0, 1, 2), + ) + .setSmallIconDrawableResId( + androidx.media3.session.R.drawable.media3_notification_small_icon + ) + .apply { + context.packageManager.getLaunchIntentForPackage(context.packageName) + ?.component?.className + ?.let { setTargetActivityClassName(it) } + } + .build() + val mediaOptions = CastMediaOptions.Builder() - .setMediaSessionEnabled(false) - .setNotificationOptions(null) + .setNotificationOptions(notificationOptions) .build() return CastOptions.Builder() diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index 4dfdf19..339c013 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -42,7 +42,6 @@ import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.TrackSelectionOverride import androidx.media3.cast.CastPlayer import androidx.media3.cast.RemoteCastPlayer -import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import androidx.mediarouter.app.MediaRouteChooserDialog @@ -105,6 +104,7 @@ class OmniPlayer( private val castStateListener = CastStateListener { eventMap.emitCastStatus(computeCastStatus()) listenToReceiver() + syncNotificationService() } // receivers have no queue to skip in (a queue would make them play things on @@ -166,7 +166,8 @@ class OmniPlayer( private var serviceRunning = false private fun syncNotificationService() { - val shouldShow = showNotification == true && source != null + val shouldShow = showNotification == true && source != null && + !runOnMainThreadSync { isCasting } when { shouldShow && !serviceRunning -> { val otherIsPlaying = notificationPlayer?.let { other -> @@ -232,7 +233,7 @@ class OmniPlayer( runOnMainThread { eventMap.dispose() castContext?.removeCastStateListener(castStateListener) - player.release() + if (!isCasting) player.release() } } @@ -681,10 +682,6 @@ class OmniPlayerService : MediaSessionService() { } .build() - setMediaNotificationProvider(DefaultMediaNotificationProvider.Builder(this).build().apply { - setSmallIcon(applicationInfo.icon.takeIf { it != 0 } - ?: android.R.drawable.ic_media_play) - }) addSession(mediaSession) setShowNotificationForIdlePlayer(SHOW_NOTIFICATION_FOR_IDLE_PLAYER_ALWAYS) @@ -725,8 +722,7 @@ class OmniPlayerService : MediaSessionService() { ) return NotificationCompat.Builder(this, "omni_playback") - .setSmallIcon(applicationInfo.icon.takeIf { it != 0 } - ?: android.R.drawable.ic_media_play) + .setSmallIcon(androidx.media3.session.R.drawable.media3_notification_small_icon) .setContentTitle("Omni Player") .setContentText("Preparing playback...") .setContentIntent(pendingIntent) @@ -737,7 +733,14 @@ class OmniPlayerService : MediaSessionService() { override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession override fun onTaskRemoved(rootIntent: Intent?) { - pauseAllPlayersAndStopSelf() + val casting = try { + CastContext.getSharedInstance(this) + .sessionManager.currentCastSession?.isConnected == true + } catch (_: Throwable) { + false + } + // keep service open if casting + if (!casting) pauseAllPlayersAndStopSelf() } override fun onDestroy() { diff --git a/example/webpack.config.js b/example/webpack.config.js index de5f60d..26a453f 100644 --- a/example/webpack.config.js +++ b/example/webpack.config.js @@ -66,6 +66,7 @@ module.exports = { port: 3000, hot: true, historyApiFallback: true, + allowedHosts: "all", // jassub's wasm is multithreaded (pthreads), which needs SharedArrayBuffer // and therefore a cross-origin-isolated page. `credentialless` keeps // cross-origin video/subtitle/font requests working without CORP headers.