mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 08:20: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() {
|
override fun onDestroy() {
|
||||||
nowPlayingNotification?.destroySelfAndPlayer()
|
nowPlayingNotification?.destroySelf()
|
||||||
|
|
||||||
|
player?.stop()
|
||||||
|
player?.release()
|
||||||
player = null
|
player = null
|
||||||
nowPlayingNotification = null
|
nowPlayingNotification = null
|
||||||
|
|
||||||
|
@ -387,7 +387,10 @@ class OnlinePlayerService : LifecycleService() {
|
|||||||
// reset the playing queue
|
// reset the playing queue
|
||||||
PlayingQueue.resetToDefaults()
|
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
|
// called when the user pressed stop in the notification
|
||||||
// stop the service from being in the foreground and remove 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 androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.CronetHelper
|
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.ChapterSegment
|
||||||
import com.github.libretube.api.obj.Message
|
|
||||||
import com.github.libretube.api.obj.Segment
|
import com.github.libretube.api.obj.Segment
|
||||||
import com.github.libretube.api.obj.Streams
|
import com.github.libretube.api.obj.Streams
|
||||||
import com.github.libretube.api.obj.Subtitle
|
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.NavBarHelper
|
||||||
import com.github.libretube.helpers.NavigationHelper
|
import com.github.libretube.helpers.NavigationHelper
|
||||||
import com.github.libretube.helpers.PlayerHelper
|
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.checkForSegments
|
||||||
import com.github.libretube.helpers.PlayerHelper.getVideoStats
|
import com.github.libretube.helpers.PlayerHelper.getVideoStats
|
||||||
import com.github.libretube.helpers.PlayerHelper.isInSegment
|
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
|
||||||
import com.github.libretube.util.TextUtils.toTimeInSeconds
|
import com.github.libretube.util.TextUtils.toTimeInSeconds
|
||||||
import com.github.libretube.util.YoutubeHlsPlaylistParser
|
import com.github.libretube.util.YoutubeHlsPlaylistParser
|
||||||
import com.github.libretube.util.deArrow
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@ -140,18 +132,16 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
private val viewModel: PlayerViewModel by activityViewModels()
|
private val viewModel: PlayerViewModel by activityViewModels()
|
||||||
private val commentsViewModel: CommentsViewModel 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 lateinit var videoId: String
|
||||||
private var playlistId: String? = null
|
private var playlistId: String? = null
|
||||||
private var channelId: String? = null
|
private var channelId: String? = null
|
||||||
private var keepQueue = false
|
private var keepQueue = false
|
||||||
private var timeStamp = 0L
|
private var timeStamp = 0L
|
||||||
|
|
||||||
/**
|
// data and objects stored for the player
|
||||||
* Video information fetched at runtime
|
private lateinit var exoPlayer: ExoPlayer
|
||||||
*/
|
private lateinit var trackSelector: DefaultTrackSelector
|
||||||
private lateinit var streams: Streams
|
private lateinit var streams: Streams
|
||||||
|
|
||||||
// progress state of the motion layout transition
|
// progress state of the motion layout transition
|
||||||
@ -159,11 +149,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
private var transitionEndId = 0
|
private var transitionEndId = 0
|
||||||
private var isTransitioning = true
|
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
|
// if null, it's been set to automatic
|
||||||
private var fullscreenResolution: Int? = null
|
private var fullscreenResolution: Int? = null
|
||||||
|
|
||||||
@ -176,13 +161,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
Executors.newCachedThreadPool()
|
Executors.newCachedThreadPool()
|
||||||
)
|
)
|
||||||
|
|
||||||
// for the player notification
|
|
||||||
private lateinit var nowPlayingNotification: NowPlayingNotification
|
|
||||||
|
|
||||||
// SponsorBlock
|
// SponsorBlock
|
||||||
private var segments = listOf<Segment>()
|
|
||||||
private var sponsorBlockEnabled = PlayerHelper.sponsorBlockEnabled
|
private var sponsorBlockEnabled = PlayerHelper.sponsorBlockEnabled
|
||||||
private var sponsorBlockConfig = PlayerHelper.getSponsorBlockCategories()
|
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
@ -311,9 +291,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
saveWatchPosition()
|
saveWatchPosition()
|
||||||
|
|
||||||
if (playbackState == Player.STATE_READY) {
|
|
||||||
}
|
|
||||||
|
|
||||||
// set the playback speed to one if having reached the end of a livestream
|
// set the playback speed to one if having reached the end of a livestream
|
||||||
if (playbackState == Player.STATE_BUFFERING && binding.player.isLive &&
|
if (playbackState == Player.STATE_BUFFERING && binding.player.isLive &&
|
||||||
exoPlayer.duration - exoPlayer.currentPosition < 700
|
exoPlayer.duration - exoPlayer.currentPosition < 700
|
||||||
@ -468,7 +445,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
if (currentId == transitionStartId) {
|
if (currentId == transitionStartId) {
|
||||||
viewModel.isMiniPlayerVisible.value = false
|
viewModel.isMiniPlayerVisible.value = false
|
||||||
// re-enable captions
|
// re-enable captions
|
||||||
updateCurrentSubtitle(currentSubtitle)
|
updateCurrentSubtitle(viewModel.currentSubtitle)
|
||||||
binding.player.useController = true
|
binding.player.useController = true
|
||||||
commentsViewModel.setCommentSheetExpand(true)
|
commentsViewModel.setCommentSheetExpand(true)
|
||||||
mainMotionLayout.progress = 0F
|
mainMotionLayout.progress = 0F
|
||||||
@ -608,7 +585,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
val hlsStream = withContext(Dispatchers.IO) {
|
val hlsStream = withContext(Dispatchers.IO) {
|
||||||
ProxyHelper.unwrapStreamUrl(streams.hls!!).toUri()
|
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) {
|
if (closedVideo) {
|
||||||
closedVideo = false
|
closedVideo = false
|
||||||
nowPlayingNotification.refreshNotification()
|
viewModel.nowPlayingNotification?.refreshNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-enable and load video stream
|
// re-enable and load video stream
|
||||||
@ -810,7 +792,14 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
if (this::exoPlayer.isInitialized) {
|
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()
|
exoPlayer.pause()
|
||||||
|
|
||||||
@ -839,7 +828,13 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
try {
|
try {
|
||||||
saveWatchPosition()
|
saveWatchPosition()
|
||||||
|
|
||||||
nowPlayingNotification.destroySelfAndPlayer()
|
viewModel.nowPlayingNotification?.destroySelf()
|
||||||
|
viewModel.nowPlayingNotification = null
|
||||||
|
|
||||||
|
if (!viewModel.shouldUseExistingPlayer) {
|
||||||
|
exoPlayer.stop()
|
||||||
|
exoPlayer.release()
|
||||||
|
}
|
||||||
|
|
||||||
(context as MainActivity).requestOrientationChange()
|
(context as MainActivity).requestOrientationChange()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@ -867,9 +862,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
if (!exoPlayer.isPlaying || !PlayerHelper.sponsorBlockEnabled) return
|
if (!exoPlayer.isPlaying || !PlayerHelper.sponsorBlockEnabled) return
|
||||||
|
|
||||||
handler.postDelayed(this::checkForSegments, 100)
|
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 ->
|
?.let { segment ->
|
||||||
if (viewModel.isMiniPlayerVisible.value == true) return@let
|
if (viewModel.isMiniPlayerVisible.value == true) return@let
|
||||||
binding.sbSkipBtn.isVisible = true
|
binding.sbSkipBtn.isVisible = true
|
||||||
@ -879,7 +874,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!exoPlayer.isInSegment(segments)) binding.sbSkipBtn.isGone = true
|
if (!exoPlayer.isInSegment(viewModel.segments)) binding.sbSkipBtn.isGone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playVideo() {
|
private fun playVideo() {
|
||||||
@ -890,22 +885,16 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
// reset the comments to become reloaded later
|
// reset the comments to become reloaded later
|
||||||
commentsViewModel.reset()
|
commentsViewModel.reset()
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
streams = try {
|
viewModel.fetchVideoInfo(requireContext(), videoId).let { (streams, errorMessage) ->
|
||||||
RetrofitInstance.api.getStreams(videoId).apply {
|
if (errorMessage != null) {
|
||||||
relatedStreams = relatedStreams.deArrow()
|
|
||||||
}
|
|
||||||
} 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)
|
context?.toastFromMainDispatcher(errorMessage, Toast.LENGTH_LONG)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this@PlayerFragment.streams = streams!!
|
||||||
|
}
|
||||||
|
|
||||||
val isFirstVideo = PlayingQueue.isEmpty()
|
val isFirstVideo = PlayingQueue.isEmpty()
|
||||||
if (isFirstVideo) {
|
if (isFirstVideo) {
|
||||||
PlayingQueue.updateQueue(streams.toStreamItem(videoId), playlistId, channelId)
|
PlayingQueue.updateQueue(streams.toStreamItem(videoId), playlistId, channelId)
|
||||||
@ -926,12 +915,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
streamItem.url?.toID()?.let { playNextVideo(it) }
|
streamItem.url?.toID()?.let { playNextVideo(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
// hide the button to skip SponsorBlock segments manually
|
// hide the button to skip SponsorBlock segments manually
|
||||||
binding.sbSkipBtn.isGone = true
|
binding.sbSkipBtn.isGone = true
|
||||||
|
|
||||||
// set media sources for the player
|
// set media sources for the player
|
||||||
initStreamSources()
|
if (!viewModel.shouldUseExistingPlayer) initStreamSources()
|
||||||
|
|
||||||
if (PreferenceHelper.getBoolean(PreferenceKeys.AUTO_FULLSCREEN_SHORTS, false) &&
|
if (PreferenceHelper.getBoolean(PreferenceKeys.AUTO_FULLSCREEN_SHORTS, false) &&
|
||||||
isShort && binding.playerMotionLayout.progress == 0f
|
isShort && binding.playerMotionLayout.progress == 0f
|
||||||
@ -966,10 +954,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
// show the player notification
|
// show the player notification
|
||||||
initializePlayerNotification()
|
initializePlayerNotification()
|
||||||
|
|
||||||
// Since the highlight is also a chapter, we need to fetch the other segments
|
|
||||||
// first
|
|
||||||
fetchSponsorBlockSegments()
|
|
||||||
|
|
||||||
// enable the chapters dialog in the player
|
// enable the chapters dialog in the player
|
||||||
playerBinding.chapterName.setOnClickListener {
|
playerBinding.chapterName.setOnClickListener {
|
||||||
updateMaxSheetHeight()
|
updateMaxSheetHeight()
|
||||||
@ -986,38 +970,35 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
|
|
||||||
setCurrentChapterName()
|
setCurrentChapterName()
|
||||||
|
|
||||||
|
fetchSponsorBlockSegments()
|
||||||
|
|
||||||
if (streams.category == Streams.categoryMusic) {
|
if (streams.category == Streams.categoryMusic) {
|
||||||
exoPlayer.setPlaybackSpeed(1f)
|
exoPlayer.setPlaybackSpeed(1f)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
viewModel.shouldUseExistingPlayer = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private suspend fun fetchSponsorBlockSegments() {
|
||||||
* fetch the segments for SponsorBlock
|
viewModel.sponsorBlockConfig = PlayerHelper.getSponsorBlockCategories()
|
||||||
*/
|
|
||||||
private fun fetchSponsorBlockSegments() {
|
// Since the highlight is also a chapter, we need to fetch the other segments
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
// first
|
||||||
runCatching {
|
viewModel.fetchSponsorBlockSegments(videoId)
|
||||||
if (sponsorBlockConfig.isEmpty()) return@launch
|
|
||||||
segments =
|
if (viewModel.segments.isEmpty()) return
|
||||||
RetrofitInstance.api.getSegments(
|
|
||||||
videoId,
|
|
||||||
JsonHelper.json.encodeToString(sponsorBlockConfig.keys)
|
|
||||||
).segments
|
|
||||||
if (segments.isEmpty()) return@launch
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
playerBinding.exoProgress.setSegments(segments)
|
playerBinding.exoProgress.setSegments(viewModel.segments)
|
||||||
playerBinding.sbToggle.isVisible = true
|
playerBinding.sbToggle.isVisible = true
|
||||||
updateDisplayedDuration()
|
updateDisplayedDuration()
|
||||||
}
|
}
|
||||||
segments.firstOrNull { it.category == SPONSOR_HIGHLIGHT_CATEGORY }?.let {
|
viewModel.segments.firstOrNull { it.category == PlayerHelper.SPONSOR_HIGHLIGHT_CATEGORY }
|
||||||
|
?.let {
|
||||||
initializeHighlight(it)
|
initializeHighlight(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// used for autoplay and skipping to next video
|
// used for autoplay and skipping to next video
|
||||||
private fun playNextVideo(nextId: String? = null) {
|
private fun playNextVideo(nextId: String? = null) {
|
||||||
@ -1151,10 +1132,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
* Update the displayed duration of the video
|
* Update the displayed duration of the video
|
||||||
*/
|
*/
|
||||||
private fun updateDisplayedDuration() {
|
private fun updateDisplayedDuration() {
|
||||||
val duration = exoPlayer.duration / 1000
|
if (!this::streams.isInitialized || streams.livestream || _binding == null) return
|
||||||
if (duration < 0 || 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
|
val (start, end) = it.segmentStartAndEnd
|
||||||
end.toDouble() - start.toDouble()
|
end.toDouble() - start.toDouble()
|
||||||
}.toLong()
|
}.toLong()
|
||||||
@ -1276,7 +1259,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the default subtitle if available
|
// set the default subtitle if available
|
||||||
updateCurrentSubtitle(currentSubtitle)
|
updateCurrentSubtitle(viewModel.currentSubtitle)
|
||||||
|
|
||||||
// set media source and resolution in the beginning
|
// set media source and resolution in the beginning
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
@ -1384,9 +1367,15 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createExoPlayer() {
|
private fun createExoPlayer() {
|
||||||
// control for the track sources like subtitles and audio source
|
viewModel.keepOrCreatePlayer(requireContext()).let { (player, trackSelector) ->
|
||||||
trackSelector = DefaultTrackSelector(requireContext())
|
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 {
|
trackSelector.updateParameters {
|
||||||
val enabledVideoCodecs = PlayerHelper.enabledVideoCodecs
|
val enabledVideoCodecs = PlayerHelper.enabledVideoCodecs
|
||||||
if (enabledVideoCodecs != "all") {
|
if (enabledVideoCodecs != "all") {
|
||||||
@ -1399,21 +1388,15 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
this.setPreferredVideoMimeType(mimeType)
|
this.setPreferredVideoMimeType(mimeType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerHelper.applyPreferredAudioQuality(requireContext(), trackSelector)
|
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
|
* show the [NowPlayingNotification] for the current video
|
||||||
*/
|
*/
|
||||||
private fun initializePlayerNotification() {
|
private fun initializePlayerNotification() {
|
||||||
if (!this::nowPlayingNotification.isInitialized) {
|
if (viewModel.nowPlayingNotification == null) {
|
||||||
nowPlayingNotification = NowPlayingNotification(
|
viewModel.nowPlayingNotification = NowPlayingNotification(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
exoPlayer,
|
exoPlayer,
|
||||||
NowPlayingNotification.Companion.NowPlayingNotificationType.VIDEO_ONLINE
|
NowPlayingNotification.Companion.NowPlayingNotificationType.VIDEO_ONLINE
|
||||||
@ -1424,7 +1407,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
streams.uploader,
|
streams.uploader,
|
||||||
streams.thumbnailUrl
|
streams.thumbnailUrl
|
||||||
)
|
)
|
||||||
nowPlayingNotification.updatePlayerNotification(videoId, playerNotificationData)
|
viewModel.nowPlayingNotification?.updatePlayerNotification(videoId, playerNotificationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1462,7 +1445,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
) { index ->
|
) { index ->
|
||||||
val subtitle = subtitles.getOrNull(index) ?: return@setSimpleItems
|
val subtitle = subtitles.getOrNull(index) ?: return@setSimpleItems
|
||||||
updateCurrentSubtitle(subtitle)
|
updateCurrentSubtitle(subtitle)
|
||||||
this.currentSubtitle = subtitle
|
viewModel.currentSubtitle = subtitle
|
||||||
}
|
}
|
||||||
.show(childFragmentManager)
|
.show(childFragmentManager)
|
||||||
}
|
}
|
||||||
@ -1571,11 +1554,11 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
// pause the video and keep the app alive
|
// pause the video and keep the app alive
|
||||||
if (lifecycle.currentState == Lifecycle.State.CREATED) {
|
if (lifecycle.currentState == Lifecycle.State.CREATED) {
|
||||||
exoPlayer.pause()
|
exoPlayer.pause()
|
||||||
nowPlayingNotification.cancelNotification()
|
viewModel.nowPlayingNotification?.cancelNotification()
|
||||||
closedVideo = true
|
closedVideo = true
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentSubtitle(currentSubtitle)
|
updateCurrentSubtitle(viewModel.currentSubtitle)
|
||||||
|
|
||||||
// unset fullscreen if it's not been enabled before the start of PiP
|
// unset fullscreen if it's not been enabled before the start of PiP
|
||||||
if (viewModel.isFullscreen.value != true) {
|
if (viewModel.isFullscreen.value != true) {
|
||||||
@ -1670,10 +1653,13 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
|||||||
|
|
||||||
val orientation = resources.configuration.orientation
|
val orientation = resources.configuration.orientation
|
||||||
if (viewModel.isFullscreen.value != true && orientation != playerLayoutOrientation) {
|
if (viewModel.isFullscreen.value != true && orientation != playerLayoutOrientation) {
|
||||||
|
// remember the current position before recreating the activity
|
||||||
if (this::exoPlayer.isInitialized) {
|
if (this::exoPlayer.isInitialized) {
|
||||||
arguments?.putLong(IntentData.timeStamp, exoPlayer.currentPosition / 1000)
|
arguments?.putLong(IntentData.timeStamp, exoPlayer.currentPosition / 1000)
|
||||||
}
|
}
|
||||||
playerLayoutOrientation = orientation
|
playerLayoutOrientation = orientation
|
||||||
|
|
||||||
|
viewModel.shouldUseExistingPlayer = true
|
||||||
activity?.recreate()
|
activity?.recreate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,46 @@
|
|||||||
|
|
||||||
package com.github.libretube.ui.models
|
package com.github.libretube.ui.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
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.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() {
|
class PlayerViewModel : ViewModel() {
|
||||||
var player: ExoPlayer? = null
|
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 isMiniPlayerVisible = MutableLiveData(false)
|
||||||
val isFullscreen = MutableLiveData(false)
|
val isFullscreen = MutableLiveData(false)
|
||||||
@ -16,4 +50,49 @@ class PlayerViewModel : ViewModel() {
|
|||||||
val chaptersLiveData = MutableLiveData<List<ChapterSegment>>()
|
val chaptersLiveData = MutableLiveData<List<ChapterSegment>>()
|
||||||
|
|
||||||
val chapters get() = chaptersLiveData.value.orEmpty()
|
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]
|
* Destroy the [NowPlayingNotification]
|
||||||
*/
|
*/
|
||||||
fun destroySelfAndPlayer() {
|
fun destroySelf() {
|
||||||
mediaSession.release()
|
mediaSession.release()
|
||||||
|
|
||||||
player.stop()
|
|
||||||
player.release()
|
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
context.unregisterReceiver(notificationActionReceiver)
|
context.unregisterReceiver(notificationActionReceiver)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user