mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 22:30:30 +05:30
commit
d9c6a751fb
@ -16,4 +16,7 @@ object Globals {
|
||||
|
||||
// for playlists
|
||||
var SELECTED_PLAYLIST_ID: String? = null
|
||||
|
||||
// history of played videos in the current lifecycle
|
||||
val playingQueue = mutableListOf<String>()
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
package com.github.libretube.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.PLAYER_NOTIFICATION_ID
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.preferences.PreferenceHelper
|
||||
import com.github.libretube.util.BackgroundHelper
|
||||
@ -27,12 +31,22 @@ class VideoOptionsDialog(
|
||||
/**
|
||||
* List that stores the different menu options. In the future could be add more options here.
|
||||
*/
|
||||
val optionsList = listOf(
|
||||
val optionsList = mutableListOf(
|
||||
context?.getString(R.string.playOnBackground),
|
||||
context?.getString(R.string.addToPlaylist),
|
||||
context?.getString(R.string.share)
|
||||
)
|
||||
|
||||
/**
|
||||
* Check whether the player is running by observing the notification
|
||||
*/
|
||||
val notificationManager = context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.activeNotifications.forEach {
|
||||
if (it.id == PLAYER_NOTIFICATION_ID) {
|
||||
optionsList += context?.getString(R.string.add_to_queue)
|
||||
}
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setAdapter(
|
||||
@ -68,6 +82,9 @@ class VideoOptionsDialog(
|
||||
// using parentFragmentManager is important here
|
||||
shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
|
||||
}
|
||||
context?.getString(R.string.add_to_queue) -> {
|
||||
Globals.playingQueue += videoId
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
@ -27,7 +27,6 @@ import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
@ -176,11 +175,6 @@ class PlayerFragment : BaseFragment() {
|
||||
*/
|
||||
private lateinit var nowPlayingNotification: NowPlayingNotification
|
||||
|
||||
/**
|
||||
* history of played videos in the current lifecycle
|
||||
*/
|
||||
val videoIds = mutableListOf<String>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
@ -207,6 +201,9 @@ class PlayerFragment : BaseFragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.hideKeyboard(view)
|
||||
|
||||
// clear the playing queue
|
||||
Globals.playingQueue.clear()
|
||||
|
||||
setUserPrefs()
|
||||
|
||||
if (autoplayEnabled) playerBinding.autoplayIV.setImageResource(R.drawable.ic_toggle_on)
|
||||
@ -719,68 +716,49 @@ class PlayerFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
private fun playVideo() {
|
||||
fun run() {
|
||||
lifecycleScope.launchWhenCreated {
|
||||
streams = try {
|
||||
RetrofitInstance.api.getStreams(videoId!!)
|
||||
} catch (e: IOException) {
|
||||
println(e)
|
||||
Log.e(TAG, "IOException, you might not have internet connection")
|
||||
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
|
||||
return@launchWhenCreated
|
||||
} catch (e: HttpException) {
|
||||
Log.e(TAG, "HttpException, unexpected response")
|
||||
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
|
||||
return@launchWhenCreated
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
// set media sources for the player
|
||||
setResolutionAndSubtitles(streams)
|
||||
prepareExoPlayerView()
|
||||
initializePlayerView(streams)
|
||||
if (!isLive) seekToWatchPosition()
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.play()
|
||||
exoPlayerView.useController = true
|
||||
initializePlayerNotification()
|
||||
if (sponsorBlockEnabled) fetchSponsorBlockSegments()
|
||||
// show comments if related streams disabled
|
||||
if (!relatedStreamsEnabled) toggleComments()
|
||||
// prepare for autoplay
|
||||
if (autoplayEnabled) setNextStream()
|
||||
if (watchHistoryEnabled) {
|
||||
PreferenceHelper.addToWatchHistory(videoId!!, streams)
|
||||
}
|
||||
}
|
||||
Globals.playingQueue += videoId!!
|
||||
lifecycleScope.launchWhenCreated {
|
||||
streams = try {
|
||||
RetrofitInstance.api.getStreams(videoId!!)
|
||||
} catch (e: IOException) {
|
||||
println(e)
|
||||
Log.e(TAG, "IOException, you might not have internet connection")
|
||||
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
|
||||
return@launchWhenCreated
|
||||
} catch (e: HttpException) {
|
||||
Log.e(TAG, "HttpException, unexpected response")
|
||||
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
|
||||
return@launchWhenCreated
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
// set media sources for the player
|
||||
setResolutionAndSubtitles(streams)
|
||||
prepareExoPlayerView()
|
||||
initializePlayerView(streams)
|
||||
if (!isLive) seekToWatchPosition()
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.play()
|
||||
exoPlayerView.useController = true
|
||||
initializePlayerNotification()
|
||||
if (sponsorBlockEnabled) fetchSponsorBlockSegments()
|
||||
// show comments if related streams disabled
|
||||
if (!relatedStreamsEnabled) toggleComments()
|
||||
// prepare for autoplay
|
||||
if (autoplayEnabled) setNextStream()
|
||||
if (watchHistoryEnabled) PreferenceHelper.addToWatchHistory(videoId!!, streams)
|
||||
}
|
||||
videoIds += videoId!!
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
/**
|
||||
* set the videoId of the next stream for autoplay
|
||||
*/
|
||||
private fun setNextStream() {
|
||||
// don't play a video if it got played before already
|
||||
var index = 0
|
||||
while (nextStreamId == null || nextStreamId == videoId!! ||
|
||||
(
|
||||
videoIds.contains(nextStreamId) &&
|
||||
videoIds.indexOf(videoId) > videoIds.indexOf(nextStreamId)
|
||||
)
|
||||
) {
|
||||
nextStreamId = streams.relatedStreams!![index].url.toID()
|
||||
if (index + 1 < streams.relatedStreams!!.size) index += 1
|
||||
else break
|
||||
}
|
||||
if (playlistId == null) return
|
||||
if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!)
|
||||
if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId)
|
||||
// search for the next videoId in the playlist
|
||||
lifecycleScope.launchWhenCreated {
|
||||
val nextId = autoPlayHelper.getNextPlaylistVideoId(videoId!!)
|
||||
if (nextId != null) nextStreamId = nextId
|
||||
nextStreamId = autoPlayHelper.getNextVideoId(videoId!!, streams.relatedStreams!!)
|
||||
}
|
||||
}
|
||||
|
||||
@ -845,6 +823,8 @@ class PlayerFragment : BaseFragment() {
|
||||
private fun playNextVideo() {
|
||||
if (nextStreamId == null) return
|
||||
// check whether there is a new video in the queue
|
||||
val nextQueueVideo = autoPlayHelper.getNextPlayingQueueVideoId(videoId!!)
|
||||
if (nextQueueVideo != null) nextStreamId = nextQueueVideo
|
||||
// by making sure that the next and the current video aren't the same
|
||||
saveWatchPosition()
|
||||
// forces the comments to reload for the new video
|
||||
@ -1073,13 +1053,13 @@ class PlayerFragment : BaseFragment() {
|
||||
|
||||
// next and previous buttons
|
||||
playerBinding.skipPrev.visibility = if (
|
||||
skipButtonsEnabled && videoIds.indexOf(videoId!!) != 0
|
||||
skipButtonsEnabled && Globals.playingQueue.indexOf(videoId!!) != 0
|
||||
) View.VISIBLE else View.INVISIBLE
|
||||
playerBinding.skipNext.visibility = if (skipButtonsEnabled) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
playerBinding.skipPrev.setOnClickListener {
|
||||
val index = videoIds.indexOf(videoId!!) - 1
|
||||
videoId = videoIds[index]
|
||||
val index = Globals.playingQueue.indexOf(videoId!!) - 1
|
||||
videoId = Globals.playingQueue[index]
|
||||
playVideo()
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ import android.os.Looper
|
||||
import android.widget.Toast
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.github.libretube.BACKGROUND_CHANNEL_ID
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.PLAYER_NOTIFICATION_ID
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.obj.Segment
|
||||
@ -29,7 +30,6 @@ import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -78,7 +78,7 @@ class BackgroundMode : Service() {
|
||||
/**
|
||||
* The [videoId] of the next stream for autoplay
|
||||
*/
|
||||
private lateinit var nextStreamId: String
|
||||
private var nextStreamId: String? = null
|
||||
|
||||
/**
|
||||
* Helper for finding the next video in the playlist
|
||||
@ -86,7 +86,12 @@ class BackgroundMode : Service() {
|
||||
private lateinit var autoPlayHelper: AutoPlayHelper
|
||||
|
||||
/**
|
||||
* Setting the required [notification] for running as a foreground service
|
||||
* Autoplay Preference
|
||||
*/
|
||||
private val autoplay = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_PLAY, true)
|
||||
|
||||
/**
|
||||
* Setting the required [Notification] for running as a foreground service
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@ -111,6 +116,9 @@ class BackgroundMode : Service() {
|
||||
*/
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
try {
|
||||
// clear the playing queue
|
||||
Globals.playingQueue.clear()
|
||||
|
||||
// get the intent arguments
|
||||
videoId = intent?.getStringExtra("videoId")!!
|
||||
playlistId = intent.getStringExtra("playlistId")
|
||||
@ -135,6 +143,8 @@ class BackgroundMode : Service() {
|
||||
videoId: String,
|
||||
seekToPosition: Long = 0
|
||||
) {
|
||||
// append the video to the playing queue
|
||||
Globals.playingQueue += videoId
|
||||
runBlocking {
|
||||
val job = launch {
|
||||
streams = RetrofitInstance.api.getStreams(videoId)
|
||||
@ -168,7 +178,7 @@ class BackgroundMode : Service() {
|
||||
|
||||
fetchSponsorBlockSegments()
|
||||
|
||||
setNextStream()
|
||||
if (autoplay) setNextStream()
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,7 +204,6 @@ class BackgroundMode : Service() {
|
||||
override fun onPlaybackStateChanged(@Player.State state: Int) {
|
||||
when (state) {
|
||||
Player.STATE_ENDED -> {
|
||||
val autoplay = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_PLAY, true)
|
||||
if (autoplay) playNextVideo()
|
||||
}
|
||||
Player.STATE_IDLE -> {
|
||||
@ -217,8 +226,7 @@ class BackgroundMode : Service() {
|
||||
if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!)
|
||||
// search for the next videoId in the playlist
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val nextId = autoPlayHelper.getNextPlaylistVideoId(videoId)
|
||||
if (nextId != null) nextStreamId = nextId
|
||||
nextStreamId = autoPlayHelper.getNextVideoId(videoId, streams!!.relatedStreams!!)
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,17 +234,18 @@ class BackgroundMode : Service() {
|
||||
* Plays the first related video to the current (used when the playback of the current video ended)
|
||||
*/
|
||||
private fun playNextVideo() {
|
||||
if (!this::nextStreamId.isInitialized || nextStreamId == videoId) return
|
||||
if (nextStreamId == null || nextStreamId == videoId) return
|
||||
val nextQueueVideo = autoPlayHelper.getNextPlayingQueueVideoId(videoId)
|
||||
if (nextQueueVideo != null) nextStreamId = nextQueueVideo
|
||||
|
||||
// play new video on background
|
||||
this.videoId = nextStreamId
|
||||
this.videoId = nextStreamId!!
|
||||
this.segmentData = null
|
||||
playAudio(videoId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the [MediaItem] with the [streams] into the [player]. Also creates a [MediaSessionConnector]
|
||||
* with the [mediaSession] and attach it to the [player].
|
||||
* Sets the [MediaItem] with the [streams] into the [player]
|
||||
*/
|
||||
private fun setMediaItem() {
|
||||
streams?.let {
|
||||
|
@ -1,17 +1,53 @@
|
||||
package com.github.libretube.util
|
||||
|
||||
import com.github.libretube.Globals
|
||||
import com.github.libretube.obj.StreamItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AutoPlayHelper(
|
||||
private val playlistId: String
|
||||
private val playlistId: String?
|
||||
) {
|
||||
private val TAG = "AutoPlayHelper"
|
||||
|
||||
private val playlistStreamIds = mutableListOf<String>()
|
||||
private var playlistNextPage: String? = null
|
||||
|
||||
suspend fun getNextPlaylistVideoId(currentVideoId: String): String? {
|
||||
suspend fun getNextVideoId(
|
||||
currentVideoId: String,
|
||||
relatedStreams: List<StreamItem>
|
||||
): String? {
|
||||
return if (Globals.playingQueue.last() != currentVideoId) {
|
||||
val currentVideoIndex = Globals.playingQueue.indexOf(currentVideoId)
|
||||
Globals.playingQueue[currentVideoIndex + 1]
|
||||
} else if (playlistId == null) getNextTrendingVideoId(
|
||||
currentVideoId,
|
||||
relatedStreams
|
||||
) else getNextPlaylistVideoId(
|
||||
currentVideoId
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNextTrendingVideoId(videoId: String, relatedStreams: List<StreamItem>): String? {
|
||||
// don't play a video if it got played before already
|
||||
var index = 0
|
||||
var nextStreamId: String? = null
|
||||
while (nextStreamId == null ||
|
||||
(
|
||||
Globals.playingQueue.contains(nextStreamId) &&
|
||||
Globals.playingQueue.indexOf(videoId) > Globals.playingQueue.indexOf(
|
||||
nextStreamId
|
||||
)
|
||||
)
|
||||
) {
|
||||
nextStreamId = relatedStreams[index].url.toID()
|
||||
if (index + 1 < relatedStreams.size) index += 1
|
||||
else break
|
||||
}
|
||||
return nextStreamId
|
||||
}
|
||||
|
||||
private suspend fun getNextPlaylistVideoId(currentVideoId: String): String? {
|
||||
// if the playlists contain the video, then save the next video as next stream
|
||||
if (playlistStreamIds.contains(currentVideoId)) {
|
||||
val index = playlistStreamIds.indexOf(currentVideoId)
|
||||
@ -24,9 +60,9 @@ class AutoPlayHelper(
|
||||
return withContext(Dispatchers.IO) {
|
||||
// fetch the playlists or its nextPage's videos
|
||||
val playlist =
|
||||
if (playlistNextPage == null) RetrofitInstance.authApi.getPlaylist(playlistId)
|
||||
if (playlistNextPage == null) RetrofitInstance.authApi.getPlaylist(playlistId!!)
|
||||
else RetrofitInstance.authApi.getPlaylistNextPage(
|
||||
playlistId,
|
||||
playlistId!!,
|
||||
playlistNextPage!!
|
||||
)
|
||||
// save the playlist urls to the list
|
||||
@ -39,4 +75,13 @@ class AutoPlayHelper(
|
||||
// return null when no nextPage is found
|
||||
return null
|
||||
}
|
||||
|
||||
fun getNextPlayingQueueVideoId(
|
||||
currentVideoId: String
|
||||
): String? {
|
||||
return if (Globals.playingQueue.last() != currentVideoId) {
|
||||
val currentVideoIndex = Globals.playingQueue.indexOf(currentVideoId)
|
||||
Globals.playingQueue[currentVideoIndex + 1]
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
@ -297,4 +297,5 @@
|
||||
<string name="history_size">Maximum history size</string>
|
||||
<string name="unlimited">Unlimited</string>
|
||||
<string name="background_mode">Background mode</string>
|
||||
<string name="add_to_queue">Add to queue</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user