From caaa095faf3b0054806979210e8270497a34be5b Mon Sep 17 00:00:00 2001 From: Bnyro Date: Mon, 31 Mar 2025 18:40:48 +0200 Subject: [PATCH] feat: smoother, frictionless transition between audio and video mode --- .../github/libretube/constants/IntentData.kt | 3 +- .../github/libretube/enums/PlayerCommand.kt | 3 +- .../libretube/helpers/BackgroundHelper.kt | 9 ++--- .../libretube/helpers/NavigationHelper.kt | 6 +++- .../services/AbstractPlayerService.kt | 25 +++++++++----- .../services/OfflinePlayerService.kt | 2 +- .../libretube/services/OnlinePlayerService.kt | 8 ++++- .../services/VideoOfflinePlayerService.kt | 2 +- .../services/VideoOnlinePlayerService.kt | 9 ----- .../libretube/ui/activities/MainActivity.kt | 7 +++- .../ui/activities/NoInternetActivity.kt | 2 +- .../ui/fragments/AudioPlayerFragment.kt | 19 +++++++---- .../libretube/ui/fragments/PlayerFragment.kt | 34 ++++++++----------- .../libretube/util/NowPlayingNotification.kt | 9 ++--- 14 files changed, 75 insertions(+), 63 deletions(-) delete mode 100644 app/src/main/java/com/github/libretube/services/VideoOnlinePlayerService.kt diff --git a/app/src/main/java/com/github/libretube/constants/IntentData.kt b/app/src/main/java/com/github/libretube/constants/IntentData.kt index c600654dc..1bd34b1b3 100644 --- a/app/src/main/java/com/github/libretube/constants/IntentData.kt +++ b/app/src/main/java/com/github/libretube/constants/IntentData.kt @@ -14,7 +14,7 @@ object IntentData { const val timeStamp = "timeStamp" const val playlistType = "playlistType" const val downloading = "downloading" - const val openAudioPlayer = "openAudioPlayer" + const val maximizePlayer = "openAudioPlayer" const val fragmentToOpen = "fragmentToOpen" const val comment = "comment" const val minimizeByDefault = "minimizeByDefault" @@ -60,4 +60,5 @@ object IntentData { const val alreadyStarted = "alreadyStarted" const val showUpcoming = "showUpcoming" const val customInstance = "customInstance" + const val audioOnly = "audioOnly" } diff --git a/app/src/main/java/com/github/libretube/enums/PlayerCommand.kt b/app/src/main/java/com/github/libretube/enums/PlayerCommand.kt index 04045581d..bad8d5d47 100644 --- a/app/src/main/java/com/github/libretube/enums/PlayerCommand.kt +++ b/app/src/main/java/com/github/libretube/enums/PlayerCommand.kt @@ -9,5 +9,6 @@ enum class PlayerCommand { SET_SUBTITLE, SET_SB_AUTO_SKIP_ENABLED, PLAY_VIDEO_BY_ID, - SET_AUTOPLAY_COUNTDOWN_ENABLED + SET_AUTOPLAY_COUNTDOWN_ENABLED, + TOGGLE_AUDIO_ONLY_MODE } \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/helpers/BackgroundHelper.kt b/app/src/main/java/com/github/libretube/helpers/BackgroundHelper.kt index d9c7c291e..2e1467191 100644 --- a/app/src/main/java/com/github/libretube/helpers/BackgroundHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/BackgroundHelper.kt @@ -18,7 +18,6 @@ import com.github.libretube.services.AbstractPlayerService import com.github.libretube.services.OfflinePlayerService import com.github.libretube.services.OnlinePlayerService import com.github.libretube.services.VideoOfflinePlayerService -import com.github.libretube.services.VideoOnlinePlayerService import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.NoInternetActivity import com.github.libretube.ui.fragments.DownloadTab @@ -56,7 +55,7 @@ object BackgroundHelper { startMediaService( context, OnlinePlayerService::class.java, - bundleOf(IntentData.playerData to playerData) + bundleOf(IntentData.playerData to playerData, IntentData.audioOnly to true) ) } @@ -67,8 +66,7 @@ object BackgroundHelper { arrayOf( OnlinePlayerService::class.java, OfflinePlayerService::class.java, - VideoOfflinePlayerService::class.java, - VideoOnlinePlayerService::class.java + VideoOfflinePlayerService::class.java ).forEach { val intent = Intent(context, it) context.stopService(intent) @@ -117,7 +115,6 @@ object BackgroundHelper { context: Context, serviceClass: Class<*>, arguments: Bundle = Bundle.EMPTY, - sendStartCommand: Boolean = true, onController: (MediaController) -> Unit = {} ) { val sessionToken = @@ -127,7 +124,7 @@ object BackgroundHelper { MediaController.Builder(context, sessionToken).buildAsync() controllerFuture.addListener({ val controller = controllerFuture.get() - if (sendStartCommand) controller.sendCustomCommand( + if (!arguments.isEmpty) controller.sendCustomCommand( AbstractPlayerService.startServiceCommand, arguments ) diff --git a/app/src/main/java/com/github/libretube/helpers/NavigationHelper.kt b/app/src/main/java/com/github/libretube/helpers/NavigationHelper.kt index 5d1cfbff5..b6835b144 100644 --- a/app/src/main/java/com/github/libretube/helpers/NavigationHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/NavigationHelper.kt @@ -59,6 +59,7 @@ object NavigationHelper { channelId: String? = null, keepQueue: Boolean = false, timestamp: Long = 0, + alreadyStarted: Boolean = false, forceVideo: Boolean = false ) { if (videoUrlOrId == null) return @@ -85,7 +86,10 @@ object NavigationHelper { val playerData = PlayerData(videoUrlOrId.toID(), playlistId, channelId, keepQueue, timestamp) - val bundle = bundleOf(IntentData.playerData to playerData) + val bundle = bundleOf( + IntentData.playerData to playerData, + IntentData.alreadyStarted to alreadyStarted + ) activity.supportFragmentManager.commitNow { replace(R.id.container, args = bundle) } diff --git a/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt b/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt index e070e969e..4f9af2947 100644 --- a/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt @@ -15,6 +15,7 @@ import androidx.media3.common.ForwardingPlayer import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackException import androidx.media3.common.Player +import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.trackselection.DefaultTrackSelector @@ -118,6 +119,13 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio onServiceCreated(args) notificationProvider?.intentActivity = getIntentActivity() + if (isAudioOnlyPlayer) { + trackSelector?.updateParameters { + setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) + } + } + + Log.e("custom start", "custom start") if (::videoId.isInitialized) startPlayback() } } @@ -179,6 +187,13 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio args.containsKey(PlayerCommand.PLAY_VIDEO_BY_ID.name) -> { navigateVideo(args.getString(PlayerCommand.PLAY_VIDEO_BY_ID.name) ?: return) } + + args.containsKey(PlayerCommand.TOGGLE_AUDIO_ONLY_MODE.name) -> { + isAudioOnlyPlayer = args.getBoolean(PlayerCommand.TOGGLE_AUDIO_ONLY_MODE.name) + trackSelector?.updateParameters { + setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, isAudioOnlyPlayer) + } + } } } @@ -233,7 +248,8 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio } abstract val isOfflinePlayer: Boolean - abstract val isAudioOnlyPlayer: Boolean + abstract var isAudioOnlyPlayer: Boolean + open val maximizePlayer: Boolean = true val watchPositionsEnabled get() = (PlayerHelper.watchPositionsAudio && isAudioOnlyPlayer) || (PlayerHelper.watchPositionsVideo && !isAudioOnlyPlayer) @@ -246,7 +262,6 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio notificationProvider = NowPlayingNotification( this, - backgroundOnly = isAudioOnlyPlayer, offlinePlayer = isOfflinePlayer, ) setMediaNotificationProvider(notificationProvider!!) @@ -293,12 +308,6 @@ abstract class AbstractPlayerService : MediaLibraryService(), MediaLibrarySessio val trackSelector = DefaultTrackSelector(this) this.trackSelector = trackSelector - if (isAudioOnlyPlayer) { - trackSelector.updateParameters { - setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) - } - } - val player = PlayerHelper.createPlayer(this, trackSelector, true) // prevent android from putting LibreTube to sleep when locked player.setWakeMode(if (isOfflinePlayer) C.WAKE_MODE_LOCAL else C.WAKE_MODE_NETWORK) diff --git a/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt b/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt index ecfe2fccc..78601d775 100644 --- a/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt @@ -33,7 +33,7 @@ import kotlin.io.path.exists @OptIn(UnstableApi::class) open class OfflinePlayerService : AbstractPlayerService() { override val isOfflinePlayer: Boolean = true - override val isAudioOnlyPlayer: Boolean = true + override var isAudioOnlyPlayer: Boolean = true private var noInternetService: Boolean = false private var downloadWithItems: DownloadWithItems? = null diff --git a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt index 7a0db3d5f..e5c78c8a8 100644 --- a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt @@ -8,6 +8,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.MimeTypes import androidx.media3.common.Player +import androidx.media3.common.util.Log import androidx.media3.datasource.DefaultDataSource import androidx.media3.exoplayer.hls.HlsMediaSource import com.github.libretube.R @@ -45,7 +46,7 @@ import java.io.IOException @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) open class OnlinePlayerService : AbstractPlayerService() { override val isOfflinePlayer: Boolean = false - override val isAudioOnlyPlayer: Boolean = true + override var isAudioOnlyPlayer: Boolean = false // PlaylistId/ChannelId for autoplay private var playlistId: String? = null @@ -100,6 +101,7 @@ open class OnlinePlayerService : AbstractPlayerService() { stopSelf() return } + isAudioOnlyPlayer = args.getBoolean(IntentData.audioOnly) // get the intent arguments setVideoId(playerData.videoId) @@ -115,6 +117,8 @@ open class OnlinePlayerService : AbstractPlayerService() { override suspend fun startPlayback() { super.startPlayback() + Log.e("start", "playback") + val timestampMs = startTimestampSeconds?.times(1000) ?: 0L startTimestampSeconds = null @@ -196,6 +200,8 @@ open class OnlinePlayerService : AbstractPlayerService() { this.streams = null this.sponsorBlockSegments = emptyList() + Log.e("play next", "play next") + scope.launch { startPlayback() } diff --git a/app/src/main/java/com/github/libretube/services/VideoOfflinePlayerService.kt b/app/src/main/java/com/github/libretube/services/VideoOfflinePlayerService.kt index bbadace10..deae198e6 100644 --- a/app/src/main/java/com/github/libretube/services/VideoOfflinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/VideoOfflinePlayerService.kt @@ -19,7 +19,7 @@ import kotlin.io.path.exists @OptIn(UnstableApi::class) class VideoOfflinePlayerService: OfflinePlayerService() { - override val isAudioOnlyPlayer = false + override var isAudioOnlyPlayer = false override fun setMediaItem(downloadWithItems: DownloadWithItems) { val downloadFiles = downloadWithItems.downloadItems.filter { it.path.exists() } diff --git a/app/src/main/java/com/github/libretube/services/VideoOnlinePlayerService.kt b/app/src/main/java/com/github/libretube/services/VideoOnlinePlayerService.kt deleted file mode 100644 index 1d6608af1..000000000 --- a/app/src/main/java/com/github/libretube/services/VideoOnlinePlayerService.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.libretube.services - -import androidx.annotation.OptIn -import androidx.media3.common.util.UnstableApi - -@OptIn(UnstableApi::class) -class VideoOnlinePlayerService : OnlinePlayerService() { - override val isAudioOnlyPlayer: Boolean = false -} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt index 00165c914..5d84eb748 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/MainActivity.kt @@ -433,7 +433,12 @@ class MainActivity : BaseActivity() { startActivity(nIntent) } - if (intent?.getBooleanExtra(IntentData.openAudioPlayer, false) == true) { + if (intent?.getBooleanExtra(IntentData.maximizePlayer, false) == true) { + // attempt to open the current player fragment first before creating a new one + // TODO: handle this differently + if (runOnPlayerFragment { binding.playerMotionLayout.transitionToStart(); true }) return + if (runOnAudioPlayerFragment { binding.playerMotionLayout.transitionToStart(); true }) return + val offlinePlayer = intent!!.getBooleanExtra(IntentData.offlinePlayer, false) NavigationHelper.openAudioPlayerFragment(this, offlinePlayer = offlinePlayer) return diff --git a/app/src/main/java/com/github/libretube/ui/activities/NoInternetActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/NoInternetActivity.kt index 90bc14754..98943f4c0 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/NoInternetActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/NoInternetActivity.kt @@ -18,7 +18,7 @@ class NoInternetActivity : BaseActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - if (intent.getBooleanExtra(IntentData.openAudioPlayer, false)) { + if (intent.getBooleanExtra(IntentData.maximizePlayer, false)) { NavigationHelper.openAudioPlayerFragment(this, offlinePlayer = true) } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/AudioPlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/AudioPlayerFragment.kt index d7f10b89a..e0fe53883 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/AudioPlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/AudioPlayerFragment.kt @@ -29,6 +29,7 @@ import com.github.libretube.api.JsonHelper import com.github.libretube.api.obj.ChapterSegment import com.github.libretube.constants.IntentData import com.github.libretube.databinding.FragmentAudioPlayerBinding +import com.github.libretube.enums.PlayerCommand import com.github.libretube.extensions.navigateVideo import com.github.libretube.extensions.normalize import com.github.libretube.extensions.seekBy @@ -174,13 +175,17 @@ class AudioPlayerFragment : Fragment(R.layout.fragment_audio_player), AudioPlaye } binding.openVideo.setOnClickListener { - killFragment() + playerController?.sendCustomCommand( + AbstractPlayerService.runPlayerActionCommand, + bundleOf(PlayerCommand.TOGGLE_AUDIO_ONLY_MODE.name to false) + ) + + killFragment(false) NavigationHelper.navigateVideo( context = requireContext(), videoUrlOrId = PlayingQueue.getCurrent()?.url, - timestamp = playerController?.currentPosition?.div(1000) ?: 0, - keepQueue = true, + alreadyStarted = true, forceVideo = true ) } @@ -209,7 +214,7 @@ class AudioPlayerFragment : Fragment(R.layout.fragment_audio_player), AudioPlaye } binding.miniPlayerClose.setOnClickListener { - killFragment() + killFragment(true) } val listener = AudioPlayerThumbnailListener(requireContext(), this) @@ -267,8 +272,8 @@ class AudioPlayerFragment : Fragment(R.layout.fragment_audio_player), AudioPlaye } } - private fun killFragment() { - playerController?.sendCustomCommand(AbstractPlayerService.stopServiceCommand, Bundle.EMPTY) + private fun killFragment(stopPlayer: Boolean) { + if (stopPlayer) playerController?.sendCustomCommand(AbstractPlayerService.stopServiceCommand, Bundle.EMPTY) playerController?.release() playerController = null @@ -439,6 +444,8 @@ class AudioPlayerFragment : Fragment(R.layout.fragment_audio_player), AudioPlaye _binding?.openChapters?.isVisible = !chapters.isNullOrEmpty() } }) + playerController?.mediaMetadata?.let { updateStreamInfo(it) } + initializeSeekBar() if (isOffline) { diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 0c3e1b016..93f38ce3e 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -87,7 +87,7 @@ import com.github.libretube.obj.ShareData import com.github.libretube.obj.VideoResolution import com.github.libretube.parcelable.PlayerData import com.github.libretube.services.AbstractPlayerService -import com.github.libretube.services.VideoOnlinePlayerService +import com.github.libretube.services.OnlinePlayerService import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.adapters.VideosAdapter import com.github.libretube.ui.base.BaseActivity @@ -503,9 +503,8 @@ class PlayerFragment : Fragment(R.layout.fragment_player), OnlinePlayerOptions { private fun attachToPlayerService(playerData: PlayerData, startNewSession: Boolean) { BackgroundHelper.startMediaService( requireContext(), - VideoOnlinePlayerService::class.java, - bundleOf(IntentData.playerData to playerData), - sendStartCommand = startNewSession + OnlinePlayerService::class.java, + if (startNewSession) bundleOf(IntentData.playerData to playerData, IntentData.audioOnly to false) else Bundle.EMPTY, ) { if (_binding == null) { playerController.sendCustomCommand( @@ -698,9 +697,6 @@ class PlayerFragment : Fragment(R.layout.fragment_player), OnlinePlayerOptions { } binding.relPlayerBackground.setOnClickListener { - // pause the current player - if (::playerController.isInitialized) playerController.pause() - // start the background mode playOnBackground() } @@ -782,19 +778,16 @@ class PlayerFragment : Fragment(R.layout.fragment_player), OnlinePlayerOptions { } private fun playOnBackground() { - val currentPosition = - if (::playerController.isInitialized) playerController.currentPosition else 0 - - BackgroundHelper.playOnBackground( - requireContext(), - videoId, - currentPosition, - playlistId, - channelId, - keepQueue = true, - keepVideoPlayerAlive = true + playerController.sendCustomCommand( + AbstractPlayerService.runPlayerActionCommand, + bundleOf(PlayerCommand.TOGGLE_AUDIO_ONLY_MODE.name to true) ) + + binding.player.player = null + + playerController.release() killPlayerFragment() + NavigationHelper.openAudioPlayerFragment(requireContext()) } @@ -942,7 +935,7 @@ class PlayerFragment : Fragment(R.layout.fragment_player), OnlinePlayerOptions { handler.removeCallbacksAndMessages(null) - if (::playerController.isInitialized) { + if (::playerController.isInitialized && playerController.isConnected) { playerController.removeListener(playerListener) playerController.pause() @@ -1303,7 +1296,8 @@ class PlayerFragment : Fragment(R.layout.fragment_player), OnlinePlayerOptions { subtitles.map { it.getDisplayName(requireContext()) }, preselectedItem = subtitles.firstOrNull { val roleFlags = PlayerHelper.getSubtitleRoleFlags(it) - val currentSubtitle = PlayerHelper.getCurrentPlayedCaptionFormat(playerController) + val currentSubtitle = + PlayerHelper.getCurrentPlayedCaptionFormat(playerController) it.code == currentSubtitle?.language && currentSubtitle?.roleFlags == roleFlags } ?.getDisplayName(requireContext()) ?: getString(R.string.none) diff --git a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt index a9a0afb27..8ca23217a 100644 --- a/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt +++ b/app/src/main/java/com/github/libretube/util/NowPlayingNotification.kt @@ -21,7 +21,6 @@ import com.google.common.collect.ImmutableList @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) class NowPlayingNotification( private val context: Context, - private val backgroundOnly: Boolean = false, private val offlinePlayer: Boolean = false ): MediaNotification.Provider { var intentActivity: Class<*> = MainActivity::class.java @@ -38,11 +37,9 @@ class NowPlayingNotification( // is set to "singleTop" in the AndroidManifest (important!!!) // that's the only way to launch back into the previous activity (e.g. the player view) val intent = Intent(context, intentActivity).apply { - if (backgroundOnly) { - putExtra(IntentData.openAudioPlayer, true) - putExtra(IntentData.offlinePlayer, offlinePlayer) - addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - } + putExtra(IntentData.maximizePlayer, true) + putExtra(IntentData.offlinePlayer, offlinePlayer) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } return PendingIntentCompat