From af3e96de789631d02e1b540257766e9af50b0e2d Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 11 Aug 2026 16:27:24 +0200 Subject: [PATCH] fix(android): don't create a dummy queue --- .../src/main/java/dev/zoriya/omni/EventMap.kt | 19 ------- .../java/dev/zoriya/omni/NavigationPlayer.kt | 53 +++++++++++++++++++ .../zoriya/omni/OmniCastOptionsProvider.kt | 7 +++ .../dev/zoriya/omni/OmniMediaItemConverter.kt | 11 +++- .../main/java/dev/zoriya/omni/OmniPlayer.kt | 27 +++++----- 5 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 android/src/main/java/dev/zoriya/omni/NavigationPlayer.kt diff --git a/android/src/main/java/dev/zoriya/omni/EventMap.kt b/android/src/main/java/dev/zoriya/omni/EventMap.kt index 4756de6..a339606 100644 --- a/android/src/main/java/dev/zoriya/omni/EventMap.kt +++ b/android/src/main/java/dev/zoriya/omni/EventMap.kt @@ -5,12 +5,9 @@ import androidx.media3.common.C import androidx.media3.common.C.TRACK_TYPE_AUDIO import androidx.media3.common.C.TRACK_TYPE_TEXT import androidx.media3.common.C.TRACK_TYPE_VIDEO -import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player -import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_SEEK import androidx.media3.common.Player.STATE_BUFFERING import androidx.media3.common.Player.STATE_ENDED import androidx.media3.common.Player.STATE_IDLE @@ -59,7 +56,6 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl private val castStatusListeners = mutableSetOf<(CastStatus) -> Unit>() private val sourceListeners = mutableSetOf<(Source?) -> Unit>() - private var lastMediaItemIndex = 0 private var lastRendition: Rendition? = null private var lastRenditions: Array? = null private var lastIsAutoQuality: Boolean? = null @@ -73,7 +69,6 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl _player?.removeListener(this) _player = value value.addListener(this) - lastMediaItemIndex = value.currentMediaItemIndex lastRendition = null lastRenditions = null lastIsAutoQuality = null @@ -286,20 +281,6 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl playerStatusListeners.forEach { it(PlayerStatus.ERROR) } } - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - if (reason != MEDIA_ITEM_TRANSITION_REASON_AUTO && reason != MEDIA_ITEM_TRANSITION_REASON_SEEK) { - lastMediaItemIndex = player.currentMediaItemIndex - return - } - val newIndex = player.currentMediaItemIndex - if (newIndex < lastMediaItemIndex) { - onPrevListeners.forEach { it() } - } else if (newIndex > lastMediaItemIndex) { - onNextListeners.forEach { it() } - } - lastMediaItemIndex = newIndex - } - override fun onPositionDiscontinuity( oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, diff --git a/android/src/main/java/dev/zoriya/omni/NavigationPlayer.kt b/android/src/main/java/dev/zoriya/omni/NavigationPlayer.kt new file mode 100644 index 0000000..e6d0c8d --- /dev/null +++ b/android/src/main/java/dev/zoriya/omni/NavigationPlayer.kt @@ -0,0 +1,53 @@ +package dev.zoriya.omni + +import androidx.annotation.OptIn +import androidx.media3.common.ForwardingSimpleBasePlayer +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture + +@OptIn(UnstableApi::class) +class NavigationPlayer( + player: Player, + private val hasPrev: () -> Boolean, + private val hasNext: () -> Boolean, + private val onPrev: () -> Unit, + private val onNext: () -> Unit, +) : ForwardingSimpleBasePlayer(player) { + override fun getState(): State { + val state = super.getState() + return state.buildUpon() + .setAvailableCommands( + state.availableCommands.buildUpon() + .addIf(Player.COMMAND_SEEK_TO_PREVIOUS, hasPrev()) + .addIf(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPrev()) + .addIf(Player.COMMAND_SEEK_TO_NEXT, hasNext()) + .addIf(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNext()) + .build() + ) + .build() + } + + override fun handleSeek( + mediaItemIndex: Int, + positionMs: Long, + seekCommand: Int, + ): ListenableFuture<*> = when (seekCommand) { + Player.COMMAND_SEEK_TO_PREVIOUS, + Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM -> { + onPrev() + Futures.immediateVoidFuture() + } + + Player.COMMAND_SEEK_TO_NEXT, + Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM -> { + onNext() + Futures.immediateVoidFuture() + } + + else -> super.handleSeek(mediaItemIndex, positionMs, seekCommand) + } + + fun invalidateNavigation() = invalidateState() +} diff --git a/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt b/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt index 8a3b4a5..a29d095 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniCastOptionsProvider.kt @@ -3,6 +3,7 @@ package dev.zoriya.omni 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.OptionsProvider import com.google.android.gms.cast.framework.SessionProvider @@ -11,9 +12,15 @@ class OmniCastOptionsProvider : OptionsProvider { val appId = OmniPlayer.receiverApplicationId ?: CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID + val mediaOptions = CastMediaOptions.Builder() + .setMediaSessionEnabled(false) + .setNotificationOptions(null) + .build() + return CastOptions.Builder() .setReceiverApplicationId(appId) .setShowSystemOutputSwitcherOnCastIconClick(true) + .setCastMediaOptions(mediaOptions) .build() } diff --git a/android/src/main/java/dev/zoriya/omni/OmniMediaItemConverter.kt b/android/src/main/java/dev/zoriya/omni/OmniMediaItemConverter.kt index f703bea..8e458e6 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniMediaItemConverter.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniMediaItemConverter.kt @@ -24,8 +24,15 @@ class OmniMediaItemConverter : MediaItemConverter { builder.setMediaMetadata( androidx.media3.common.MediaMetadata.Builder() .setTitle(md.getString(CastMediaMetadata.KEY_TITLE)) - .setArtist(md.getString(CastMediaMetadata.KEY_ARTIST)) - .setAlbumTitle(md.getString(CastMediaMetadata.KEY_ALBUM_TITLE)) + .setArtist( + md.getString(CastMediaMetadata.KEY_ARTIST) + ?: md.getString(CastMediaMetadata.KEY_SUBTITLE) + ) + .setAlbumTitle( + md.getString(CastMediaMetadata.KEY_ALBUM_TITLE) + ?: md.getString(CastMediaMetadata.KEY_STUDIO) + ) + .setArtworkUri(md.images.firstOrNull()?.url) .build() ) } diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index c1b5590..d1f6f94 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -124,7 +124,13 @@ class OmniPlayer( .setRemotePlayer(remote) .build() } - active + NavigationPlayer( + active, + hasPrev = { source?.metadata?.hasPrev == true }, + hasNext = { source?.metadata?.hasNext == true }, + onPrev = { eventMap.emitPrev() }, + onNext = { eventMap.emitNext() }, + ) } override var showNotification: Boolean? = false @@ -140,19 +146,19 @@ class OmniPlayer( when { shouldShow && !serviceRunning -> { val otherIsPlaying = notificationPlayer?.let { other -> - other !== localPlayer && runOnMainThreadSync { other.isPlaying } + other !== player && runOnMainThreadSync { other.isPlaying } } == true if (otherIsPlaying) { throw Error("Two players can't display notifications at the same time.") } - notificationPlayer = localPlayer + notificationPlayer = player ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java)) serviceRunning = true } !shouldShow && serviceRunning -> { ctx.stopService(Intent(ctx, OmniPlayerService::class.java)) - if (notificationPlayer == localPlayer) notificationPlayer = null + if (notificationPlayer == player) notificationPlayer = null serviceRunning = false } } @@ -298,8 +304,8 @@ class OmniPlayer( } } - override val hasPrev: Boolean get() = player.hasPreviousMediaItem() - override val hasNext: Boolean get() = player.hasNextMediaItem() + override val hasPrev: Boolean get() = source?.metadata?.hasPrev == true + override val hasNext: Boolean get() = source?.metadata?.hasNext == true override val status by mainThreadProperty { when (player.playbackState) { Player.STATE_IDLE, @@ -394,6 +400,7 @@ class OmniPlayer( if (value == null) { runOnMainThreadSync { player.clearMediaItems() + player.invalidateNavigation() } syncNotificationService() return @@ -417,16 +424,12 @@ class OmniPlayer( value.castId, value.castData, ) - val mediaItems = mutableListOf() - if (value.metadata?.hasPrev == true) mediaItems.add(currentItem) - mediaItems.add(currentItem) - if (value.metadata?.hasNext == true) mediaItems.add(currentItem) runOnMainThreadSync { - val startIndex = if (value.metadata?.hasPrev == true) 1 else 0 val startPositionMs = ((value.startTime ?: 0.0).coerceAtLeast(0.0)) * 1000.0 - player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong()) + player.setMediaItems(listOf(currentItem), 0, startPositionMs.toLong()) player.prepare() + player.invalidateNavigation() } syncNotificationService() }