mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 00:10:32 +05:30
refactor: don't recreate player on orientation change
This commit is contained in:
parent
206479ce31
commit
0a4ccae79a
@ -146,8 +146,10 @@ class OfflinePlayerService : LifecycleService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
nowPlayingNotification?.destroySelfAndPlayer()
|
||||
nowPlayingNotification?.destroySelf()
|
||||
|
||||
player?.stop()
|
||||
player?.release()
|
||||
player = null
|
||||
nowPlayingNotification = null
|
||||
|
||||
|
@ -387,7 +387,10 @@ class OnlinePlayerService : LifecycleService() {
|
||||
// reset the playing queue
|
||||
PlayingQueue.resetToDefaults()
|
||||
|
||||
if (this::nowPlayingNotification.isInitialized) nowPlayingNotification.destroySelfAndPlayer()
|
||||
if (this::nowPlayingNotification.isInitialized) nowPlayingNotification.destroySelf()
|
||||
|
||||
player?.stop()
|
||||
player?.release()
|
||||
|
||||
// called when the user pressed stop in the notification
|
||||
// stop the service from being in the foreground and remove the notification
|
||||
|
@ -50,10 +50,7 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.CronetHelper
|
||||
import com.github.libretube.api.JsonHelper
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.obj.ChapterSegment
|
||||
import com.github.libretube.api.obj.Message
|
||||
import com.github.libretube.api.obj.Segment
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.api.obj.Subtitle
|
||||
@ -82,7 +79,6 @@ import com.github.libretube.helpers.IntentHelper
|
||||
import com.github.libretube.helpers.NavBarHelper
|
||||
import com.github.libretube.helpers.NavigationHelper
|
||||
import com.github.libretube.helpers.PlayerHelper
|
||||
import com.github.libretube.helpers.PlayerHelper.SPONSOR_HIGHLIGHT_CATEGORY
|
||||
import com.github.libretube.helpers.PlayerHelper.checkForSegments
|
||||
import com.github.libretube.helpers.PlayerHelper.getVideoStats
|
||||
import com.github.libretube.helpers.PlayerHelper.isInSegment
|
||||
@ -116,14 +112,10 @@ import com.github.libretube.util.PlayingQueue
|
||||
import com.github.libretube.util.TextUtils
|
||||
import com.github.libretube.util.TextUtils.toTimeInSeconds
|
||||
import com.github.libretube.util.YoutubeHlsPlaylistParser
|
||||
import com.github.libretube.util.deArrow
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.abs
|
||||
@ -140,18 +132,16 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
private val viewModel: PlayerViewModel by activityViewModels()
|
||||
private val commentsViewModel: CommentsViewModel by activityViewModels()
|
||||
|
||||
/**
|
||||
* Video information passed by the intent
|
||||
*/
|
||||
// Video information passed by the intent
|
||||
private lateinit var videoId: String
|
||||
private var playlistId: String? = null
|
||||
private var channelId: String? = null
|
||||
private var keepQueue = false
|
||||
private var timeStamp = 0L
|
||||
|
||||
/**
|
||||
* Video information fetched at runtime
|
||||
*/
|
||||
// data and objects stored for the player
|
||||
private lateinit var exoPlayer: ExoPlayer
|
||||
private lateinit var trackSelector: DefaultTrackSelector
|
||||
private lateinit var streams: Streams
|
||||
|
||||
// progress state of the motion layout transition
|
||||
@ -159,11 +149,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
private var transitionEndId = 0
|
||||
private var isTransitioning = true
|
||||
|
||||
// data and objects stored for the player
|
||||
private lateinit var exoPlayer: ExoPlayer
|
||||
private lateinit var trackSelector: DefaultTrackSelector
|
||||
private var currentSubtitle = Subtitle(code = PlayerHelper.defaultSubtitleCode)
|
||||
|
||||
// if null, it's been set to automatic
|
||||
private var fullscreenResolution: Int? = null
|
||||
|
||||
@ -176,13 +161,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
Executors.newCachedThreadPool()
|
||||
)
|
||||
|
||||
// for the player notification
|
||||
private lateinit var nowPlayingNotification: NowPlayingNotification
|
||||
|
||||
// SponsorBlock
|
||||
private var segments = listOf<Segment>()
|
||||
private var sponsorBlockEnabled = PlayerHelper.sponsorBlockEnabled
|
||||
private var sponsorBlockConfig = PlayerHelper.getSponsorBlockCategories()
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
@ -311,9 +291,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
saveWatchPosition()
|
||||
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
}
|
||||
|
||||
// set the playback speed to one if having reached the end of a livestream
|
||||
if (playbackState == Player.STATE_BUFFERING && binding.player.isLive &&
|
||||
exoPlayer.duration - exoPlayer.currentPosition < 700
|
||||
@ -468,7 +445,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
if (currentId == transitionStartId) {
|
||||
viewModel.isMiniPlayerVisible.value = false
|
||||
// re-enable captions
|
||||
updateCurrentSubtitle(currentSubtitle)
|
||||
updateCurrentSubtitle(viewModel.currentSubtitle)
|
||||
binding.player.useController = true
|
||||
commentsViewModel.setCommentSheetExpand(true)
|
||||
mainMotionLayout.progress = 0F
|
||||
@ -608,7 +585,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
val hlsStream = withContext(Dispatchers.IO) {
|
||||
ProxyHelper.unwrapStreamUrl(streams.hls!!).toUri()
|
||||
}
|
||||
IntentHelper.openWithExternalPlayer(context, hlsStream, streams.title, streams.uploader)
|
||||
IntentHelper.openWithExternalPlayer(
|
||||
context,
|
||||
hlsStream,
|
||||
streams.title,
|
||||
streams.uploader
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -795,7 +777,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
|
||||
if (closedVideo) {
|
||||
closedVideo = false
|
||||
nowPlayingNotification.refreshNotification()
|
||||
viewModel.nowPlayingNotification?.refreshNotification()
|
||||
}
|
||||
|
||||
// re-enable and load video stream
|
||||
@ -810,7 +792,14 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
super.onDestroy()
|
||||
|
||||
if (this::exoPlayer.isInitialized) {
|
||||
if (viewModel.player == exoPlayer) viewModel.player = null
|
||||
exoPlayer.removeListener(playerListener)
|
||||
|
||||
// the player could also be a different instance because a new player fragment
|
||||
// got created in the meanwhile
|
||||
if (!viewModel.shouldUseExistingPlayer && viewModel.player == exoPlayer) {
|
||||
viewModel.player = null
|
||||
viewModel.trackSelector = null
|
||||
}
|
||||
|
||||
exoPlayer.pause()
|
||||
|
||||
@ -839,7 +828,13 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
try {
|
||||
saveWatchPosition()
|
||||
|
||||
nowPlayingNotification.destroySelfAndPlayer()
|
||||
viewModel.nowPlayingNotification?.destroySelf()
|
||||
viewModel.nowPlayingNotification = null
|
||||
|
||||
if (!viewModel.shouldUseExistingPlayer) {
|
||||
exoPlayer.stop()
|
||||
exoPlayer.release()
|
||||
}
|
||||
|
||||
(context as MainActivity).requestOrientationChange()
|
||||
} catch (e: Exception) {
|
||||
@ -867,9 +862,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
if (!exoPlayer.isPlaying || !PlayerHelper.sponsorBlockEnabled) return
|
||||
|
||||
handler.postDelayed(this::checkForSegments, 100)
|
||||
if (!sponsorBlockEnabled || segments.isEmpty()) return
|
||||
if (!sponsorBlockEnabled || viewModel.segments.isEmpty()) return
|
||||
|
||||
exoPlayer.checkForSegments(requireContext(), segments, sponsorBlockConfig)
|
||||
exoPlayer.checkForSegments(requireContext(), viewModel.segments, viewModel.sponsorBlockConfig)
|
||||
?.let { segment ->
|
||||
if (viewModel.isMiniPlayerVisible.value == true) return@let
|
||||
binding.sbSkipBtn.isVisible = true
|
||||
@ -879,7 +874,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!exoPlayer.isInSegment(segments)) binding.sbSkipBtn.isGone = true
|
||||
if (!exoPlayer.isInSegment(viewModel.segments)) binding.sbSkipBtn.isGone = true
|
||||
}
|
||||
|
||||
private fun playVideo() {
|
||||
@ -890,20 +885,14 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
// reset the comments to become reloaded later
|
||||
commentsViewModel.reset()
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
streams = try {
|
||||
RetrofitInstance.api.getStreams(videoId).apply {
|
||||
relatedStreams = relatedStreams.deArrow()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
viewModel.fetchVideoInfo(requireContext(), videoId).let { (streams, errorMessage) ->
|
||||
if (errorMessage != null) {
|
||||
context?.toastFromMainDispatcher(errorMessage, Toast.LENGTH_LONG)
|
||||
return@launch
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
context?.toastFromMainDispatcher(R.string.unknown_error, Toast.LENGTH_LONG)
|
||||
return@launch
|
||||
} catch (e: HttpException) {
|
||||
val errorMessage = e.response()?.errorBody()?.string()?.runCatching {
|
||||
JsonHelper.json.decodeFromString<Message>(this).message
|
||||
}?.getOrNull() ?: context?.getString(R.string.server_error).orEmpty()
|
||||
context?.toastFromMainDispatcher(errorMessage, Toast.LENGTH_LONG)
|
||||
return@launch
|
||||
|
||||
this@PlayerFragment.streams = streams!!
|
||||
}
|
||||
|
||||
val isFirstVideo = PlayingQueue.isEmpty()
|
||||
@ -920,103 +909,95 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
|
||||
val videoStream = streams.videoStreams.firstOrNull()
|
||||
val isShort = PlayingQueue.getCurrent()?.isShort == true ||
|
||||
(videoStream?.height ?: 0) > (videoStream?.width ?: 0)
|
||||
(videoStream?.height ?: 0) > (videoStream?.width ?: 0)
|
||||
|
||||
PlayingQueue.setOnQueueTapListener { streamItem ->
|
||||
streamItem.url?.toID()?.let { playNextVideo(it) }
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
// hide the button to skip SponsorBlock segments manually
|
||||
binding.sbSkipBtn.isGone = true
|
||||
// hide the button to skip SponsorBlock segments manually
|
||||
binding.sbSkipBtn.isGone = true
|
||||
|
||||
// set media sources for the player
|
||||
initStreamSources()
|
||||
// set media sources for the player
|
||||
if (!viewModel.shouldUseExistingPlayer) initStreamSources()
|
||||
|
||||
if (PreferenceHelper.getBoolean(PreferenceKeys.AUTO_FULLSCREEN_SHORTS, false) &&
|
||||
isShort && binding.playerMotionLayout.progress == 0f
|
||||
) {
|
||||
setFullscreen()
|
||||
playerBinding.fullscreen.isVisible = true
|
||||
} else {
|
||||
// disable the fullscreen button for auto fullscreen
|
||||
playerBinding.fullscreen.isVisible = !PlayerHelper.autoFullscreenEnabled
|
||||
}
|
||||
if (PreferenceHelper.getBoolean(PreferenceKeys.AUTO_FULLSCREEN_SHORTS, false) &&
|
||||
isShort && binding.playerMotionLayout.progress == 0f
|
||||
) {
|
||||
setFullscreen()
|
||||
playerBinding.fullscreen.isVisible = true
|
||||
} else {
|
||||
// disable the fullscreen button for auto fullscreen
|
||||
playerBinding.fullscreen.isVisible = !PlayerHelper.autoFullscreenEnabled
|
||||
}
|
||||
|
||||
binding.player.apply {
|
||||
useController = false
|
||||
player = exoPlayer
|
||||
}
|
||||
binding.player.apply {
|
||||
useController = false
|
||||
player = exoPlayer
|
||||
}
|
||||
|
||||
playerBinding.exoProgress.setPlayer(exoPlayer)
|
||||
playerBinding.exoProgress.setPlayer(exoPlayer)
|
||||
|
||||
initializePlayerView()
|
||||
initializePlayerView()
|
||||
|
||||
exoPlayer.playWhenReady = PlayerHelper.playAutomatically
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.playWhenReady = PlayerHelper.playAutomatically
|
||||
exoPlayer.prepare()
|
||||
|
||||
if (binding.playerMotionLayout.progress != 1.0f) {
|
||||
// show controllers when not in picture in picture mode
|
||||
val inPipMode = PlayerHelper.pipEnabled &&
|
||||
if (binding.playerMotionLayout.progress != 1.0f) {
|
||||
// show controllers when not in picture in picture mode
|
||||
val inPipMode = PlayerHelper.pipEnabled &&
|
||||
PictureInPictureCompat.isInPictureInPictureMode(requireActivity())
|
||||
if (!inPipMode) {
|
||||
binding.player.useController = true
|
||||
}
|
||||
}
|
||||
// show the player notification
|
||||
initializePlayerNotification()
|
||||
|
||||
// Since the highlight is also a chapter, we need to fetch the other segments
|
||||
// first
|
||||
fetchSponsorBlockSegments()
|
||||
|
||||
// enable the chapters dialog in the player
|
||||
playerBinding.chapterName.setOnClickListener {
|
||||
updateMaxSheetHeight()
|
||||
val sheet =
|
||||
chaptersBottomSheet ?: ChaptersBottomSheet().also {
|
||||
chaptersBottomSheet = it
|
||||
}
|
||||
if (sheet.isVisible) {
|
||||
sheet.dismiss()
|
||||
} else {
|
||||
sheet.show(childFragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentChapterName()
|
||||
|
||||
if (streams.category == Streams.categoryMusic) {
|
||||
exoPlayer.setPlaybackSpeed(1f)
|
||||
if (!inPipMode) {
|
||||
binding.player.useController = true
|
||||
}
|
||||
}
|
||||
// show the player notification
|
||||
initializePlayerNotification()
|
||||
|
||||
// enable the chapters dialog in the player
|
||||
playerBinding.chapterName.setOnClickListener {
|
||||
updateMaxSheetHeight()
|
||||
val sheet =
|
||||
chaptersBottomSheet ?: ChaptersBottomSheet().also {
|
||||
chaptersBottomSheet = it
|
||||
}
|
||||
if (sheet.isVisible) {
|
||||
sheet.dismiss()
|
||||
} else {
|
||||
sheet.show(childFragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentChapterName()
|
||||
|
||||
fetchSponsorBlockSegments()
|
||||
|
||||
if (streams.category == Streams.categoryMusic) {
|
||||
exoPlayer.setPlaybackSpeed(1f)
|
||||
}
|
||||
|
||||
viewModel.shouldUseExistingPlayer = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch the segments for SponsorBlock
|
||||
*/
|
||||
private fun fetchSponsorBlockSegments() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
if (sponsorBlockConfig.isEmpty()) return@launch
|
||||
segments =
|
||||
RetrofitInstance.api.getSegments(
|
||||
videoId,
|
||||
JsonHelper.json.encodeToString(sponsorBlockConfig.keys)
|
||||
).segments
|
||||
if (segments.isEmpty()) return@launch
|
||||
private suspend fun fetchSponsorBlockSegments() {
|
||||
viewModel.sponsorBlockConfig = PlayerHelper.getSponsorBlockCategories()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
playerBinding.exoProgress.setSegments(segments)
|
||||
playerBinding.sbToggle.isVisible = true
|
||||
updateDisplayedDuration()
|
||||
}
|
||||
segments.firstOrNull { it.category == SPONSOR_HIGHLIGHT_CATEGORY }?.let {
|
||||
initializeHighlight(it)
|
||||
}
|
||||
}
|
||||
// Since the highlight is also a chapter, we need to fetch the other segments
|
||||
// first
|
||||
viewModel.fetchSponsorBlockSegments(videoId)
|
||||
|
||||
if (viewModel.segments.isEmpty()) return
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
playerBinding.exoProgress.setSegments(viewModel.segments)
|
||||
playerBinding.sbToggle.isVisible = true
|
||||
updateDisplayedDuration()
|
||||
}
|
||||
viewModel.segments.firstOrNull { it.category == PlayerHelper.SPONSOR_HIGHLIGHT_CATEGORY }
|
||||
?.let {
|
||||
initializeHighlight(it)
|
||||
}
|
||||
}
|
||||
|
||||
// used for autoplay and skipping to next video
|
||||
@ -1151,10 +1132,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
* Update the displayed duration of the video
|
||||
*/
|
||||
private fun updateDisplayedDuration() {
|
||||
val duration = exoPlayer.duration / 1000
|
||||
if (duration < 0 || streams.livestream || _binding == null) return
|
||||
if (!this::streams.isInitialized || streams.livestream || _binding == null) return
|
||||
|
||||
val durationWithoutSegments = duration - segments.sumOf {
|
||||
val duration = exoPlayer.duration / 1000
|
||||
if (duration < 0) return
|
||||
|
||||
val durationWithoutSegments = duration - viewModel.segments.sumOf {
|
||||
val (start, end) = it.segmentStartAndEnd
|
||||
end.toDouble() - start.toDouble()
|
||||
}.toLong()
|
||||
@ -1276,7 +1259,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
|
||||
// set the default subtitle if available
|
||||
updateCurrentSubtitle(currentSubtitle)
|
||||
updateCurrentSubtitle(viewModel.currentSubtitle)
|
||||
|
||||
// set media source and resolution in the beginning
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
@ -1384,9 +1367,15 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
|
||||
private fun createExoPlayer() {
|
||||
// control for the track sources like subtitles and audio source
|
||||
trackSelector = DefaultTrackSelector(requireContext())
|
||||
viewModel.keepOrCreatePlayer(requireContext()).let { (player, trackSelector) ->
|
||||
this.exoPlayer = player
|
||||
this.trackSelector = trackSelector
|
||||
}
|
||||
|
||||
exoPlayer.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
exoPlayer.addListener(playerListener)
|
||||
|
||||
// control for the track sources like subtitles and audio source
|
||||
trackSelector.updateParameters {
|
||||
val enabledVideoCodecs = PlayerHelper.enabledVideoCodecs
|
||||
if (enabledVideoCodecs != "all") {
|
||||
@ -1399,21 +1388,15 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
this.setPreferredVideoMimeType(mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
PlayerHelper.applyPreferredAudioQuality(requireContext(), trackSelector)
|
||||
|
||||
exoPlayer = PlayerHelper.createPlayer(requireContext(), trackSelector, false)
|
||||
exoPlayer.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
exoPlayer.addListener(playerListener)
|
||||
viewModel.player = exoPlayer
|
||||
}
|
||||
|
||||
/**
|
||||
* show the [NowPlayingNotification] for the current video
|
||||
*/
|
||||
private fun initializePlayerNotification() {
|
||||
if (!this::nowPlayingNotification.isInitialized) {
|
||||
nowPlayingNotification = NowPlayingNotification(
|
||||
if (viewModel.nowPlayingNotification == null) {
|
||||
viewModel.nowPlayingNotification = NowPlayingNotification(
|
||||
requireContext(),
|
||||
exoPlayer,
|
||||
NowPlayingNotification.Companion.NowPlayingNotificationType.VIDEO_ONLINE
|
||||
@ -1424,7 +1407,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
streams.uploader,
|
||||
streams.thumbnailUrl
|
||||
)
|
||||
nowPlayingNotification.updatePlayerNotification(videoId, playerNotificationData)
|
||||
viewModel.nowPlayingNotification?.updatePlayerNotification(videoId, playerNotificationData)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1462,7 +1445,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
) { index ->
|
||||
val subtitle = subtitles.getOrNull(index) ?: return@setSimpleItems
|
||||
updateCurrentSubtitle(subtitle)
|
||||
this.currentSubtitle = subtitle
|
||||
viewModel.currentSubtitle = subtitle
|
||||
}
|
||||
.show(childFragmentManager)
|
||||
}
|
||||
@ -1571,11 +1554,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
// pause the video and keep the app alive
|
||||
if (lifecycle.currentState == Lifecycle.State.CREATED) {
|
||||
exoPlayer.pause()
|
||||
nowPlayingNotification.cancelNotification()
|
||||
viewModel.nowPlayingNotification?.cancelNotification()
|
||||
closedVideo = true
|
||||
}
|
||||
|
||||
updateCurrentSubtitle(currentSubtitle)
|
||||
updateCurrentSubtitle(viewModel.currentSubtitle)
|
||||
|
||||
// unset fullscreen if it's not been enabled before the start of PiP
|
||||
if (viewModel.isFullscreen.value != true) {
|
||||
@ -1670,10 +1653,13 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
|
||||
val orientation = resources.configuration.orientation
|
||||
if (viewModel.isFullscreen.value != true && orientation != playerLayoutOrientation) {
|
||||
// remember the current position before recreating the activity
|
||||
if (this::exoPlayer.isInitialized) {
|
||||
arguments?.putLong(IntentData.timeStamp, exoPlayer.currentPosition / 1000)
|
||||
}
|
||||
playerLayoutOrientation = orientation
|
||||
|
||||
viewModel.shouldUseExistingPlayer = true
|
||||
activity?.recreate()
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,46 @@
|
||||
|
||||
package com.github.libretube.ui.models
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.JsonHelper
|
||||
import com.github.libretube.api.RetrofitInstance
|
||||
import com.github.libretube.api.obj.ChapterSegment
|
||||
import com.github.libretube.api.obj.Message
|
||||
import com.github.libretube.api.obj.Segment
|
||||
import com.github.libretube.api.obj.Streams
|
||||
import com.github.libretube.api.obj.Subtitle
|
||||
import com.github.libretube.helpers.PlayerHelper
|
||||
import com.github.libretube.util.NowPlayingNotification
|
||||
import com.github.libretube.util.deArrow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class PlayerViewModel : ViewModel() {
|
||||
var player: ExoPlayer? = null
|
||||
var trackSelector: DefaultTrackSelector? = null
|
||||
|
||||
// data to remember for recovery on orientation change
|
||||
private var streamsInfo: Streams? = null
|
||||
var nowPlayingNotification: NowPlayingNotification? = null
|
||||
var segments = listOf<Segment>()
|
||||
var currentSubtitle = Subtitle(code = PlayerHelper.defaultSubtitleCode)
|
||||
var sponsorBlockConfig = PlayerHelper.getSponsorBlockCategories()
|
||||
|
||||
/**
|
||||
* Whether to continue using the current player
|
||||
* Set to true if the activity will be recreated due to an orientation change
|
||||
*/
|
||||
var shouldUseExistingPlayer = false
|
||||
|
||||
val isMiniPlayerVisible = MutableLiveData(false)
|
||||
val isFullscreen = MutableLiveData(false)
|
||||
@ -16,4 +50,49 @@ class PlayerViewModel : ViewModel() {
|
||||
val chaptersLiveData = MutableLiveData<List<ChapterSegment>>()
|
||||
|
||||
val chapters get() = chaptersLiveData.value.orEmpty()
|
||||
|
||||
/**
|
||||
* @return pair of the stream info and the error message if the request was not successful
|
||||
*/
|
||||
suspend fun fetchVideoInfo(context: Context, videoId: String): Pair<Streams?, String?> =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (shouldUseExistingPlayer && streamsInfo != null) return@withContext streamsInfo to null
|
||||
|
||||
streamsInfo = try {
|
||||
RetrofitInstance.api.getStreams(videoId).apply {
|
||||
relatedStreams = relatedStreams.deArrow()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return@withContext null to context.getString(R.string.unknown_error)
|
||||
} catch (e: HttpException) {
|
||||
val errorMessage = e.response()?.errorBody()?.string()?.runCatching {
|
||||
JsonHelper.json.decodeFromString<Message>(this).message
|
||||
}?.getOrNull() ?: context.getString(R.string.server_error)
|
||||
return@withContext null to errorMessage
|
||||
}
|
||||
|
||||
return@withContext streamsInfo to null
|
||||
}
|
||||
|
||||
suspend fun fetchSponsorBlockSegments(videoId: String) = withContext(Dispatchers.IO) {
|
||||
if (sponsorBlockConfig.isEmpty() || shouldUseExistingPlayer) return@withContext
|
||||
|
||||
runCatching {
|
||||
segments =
|
||||
RetrofitInstance.api.getSegments(
|
||||
videoId,
|
||||
JsonHelper.json.encodeToString(sponsorBlockConfig.keys)
|
||||
).segments
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun keepOrCreatePlayer(context: Context): Pair<ExoPlayer, DefaultTrackSelector> {
|
||||
if (!shouldUseExistingPlayer || player == null || trackSelector == null) {
|
||||
this.trackSelector = DefaultTrackSelector(context)
|
||||
this.player = PlayerHelper.createPlayer(context, trackSelector!!, false)
|
||||
}
|
||||
|
||||
return this.player!! to this.trackSelector!!
|
||||
}
|
||||
}
|
||||
|
@ -391,12 +391,9 @@ class NowPlayingNotification(
|
||||
/**
|
||||
* Destroy the [NowPlayingNotification]
|
||||
*/
|
||||
fun destroySelfAndPlayer() {
|
||||
fun destroySelf() {
|
||||
mediaSession.release()
|
||||
|
||||
player.stop()
|
||||
player.release()
|
||||
|
||||
runCatching {
|
||||
context.unregisterReceiver(notificationActionReceiver)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user