mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-27 15:30:31 +05:30
feat: smoother, frictionless transition between audio and video mode
This commit is contained in:
parent
b3e4a243a9
commit
caaa095faf
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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() }
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user