feat: smoother, frictionless transition between audio and video mode

This commit is contained in:
Bnyro 2025-03-31 18:40:48 +02:00
parent b3e4a243a9
commit caaa095faf
No known key found for this signature in database
14 changed files with 75 additions and 63 deletions

View File

@ -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"
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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<PlayerFragment>(R.id.container, args = bundle)
}

View File

@ -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)

View File

@ -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

View File

@ -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()
}

View File

@ -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() }

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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)

View File

@ -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