Merge pull request #1008 from Bnyro/master

Playing queue
This commit is contained in:
Bnyro 2022-08-10 16:24:29 +02:00 committed by GitHub
commit d9c6a751fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 132 additions and 77 deletions

View File

@ -16,4 +16,7 @@ object Globals {
// for playlists // for playlists
var SELECTED_PLAYLIST_ID: String? = null var SELECTED_PLAYLIST_ID: String? = null
// history of played videos in the current lifecycle
val playingQueue = mutableListOf<String>()
} }

View File

@ -1,10 +1,14 @@
package com.github.libretube.dialogs package com.github.libretube.dialogs
import android.app.Dialog import android.app.Dialog
import android.app.NotificationManager
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.DialogFragment 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.R
import com.github.libretube.preferences.PreferenceHelper import com.github.libretube.preferences.PreferenceHelper
import com.github.libretube.util.BackgroundHelper 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. * 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.playOnBackground),
context?.getString(R.string.addToPlaylist), context?.getString(R.string.addToPlaylist),
context?.getString(R.string.share) 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()) return MaterialAlertDialogBuilder(requireContext())
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setAdapter( .setAdapter(
@ -68,6 +82,9 @@ class VideoOptionsDialog(
// using parentFragmentManager is important here // using parentFragmentManager is important here
shareDialog.show(parentFragmentManager, ShareDialog::class.java.name) shareDialog.show(parentFragmentManager, ShareDialog::class.java.name)
} }
context?.getString(R.string.add_to_queue) -> {
Globals.playingQueue += videoId
}
} }
} }
.show() .show()

View File

@ -27,7 +27,6 @@ import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.os.postDelayed
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@ -176,11 +175,6 @@ class PlayerFragment : BaseFragment() {
*/ */
private lateinit var nowPlayingNotification: NowPlayingNotification private lateinit var nowPlayingNotification: NowPlayingNotification
/**
* history of played videos in the current lifecycle
*/
val videoIds = mutableListOf<String>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
arguments?.let { arguments?.let {
@ -207,6 +201,9 @@ class PlayerFragment : BaseFragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.hideKeyboard(view) context?.hideKeyboard(view)
// clear the playing queue
Globals.playingQueue.clear()
setUserPrefs() setUserPrefs()
if (autoplayEnabled) playerBinding.autoplayIV.setImageResource(R.drawable.ic_toggle_on) if (autoplayEnabled) playerBinding.autoplayIV.setImageResource(R.drawable.ic_toggle_on)
@ -719,68 +716,49 @@ class PlayerFragment : BaseFragment() {
} }
private fun playVideo() { private fun playVideo() {
fun run() { Globals.playingQueue += videoId!!
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
streams = try { streams = try {
RetrofitInstance.api.getStreams(videoId!!) RetrofitInstance.api.getStreams(videoId!!)
} catch (e: IOException) { } catch (e: IOException) {
println(e) println(e)
Log.e(TAG, "IOException, you might not have internet connection") Log.e(TAG, "IOException, you might not have internet connection")
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated return@launchWhenCreated
} catch (e: HttpException) { } catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response") Log.e(TAG, "HttpException, unexpected response")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated return@launchWhenCreated
} }
runOnUiThread { runOnUiThread {
// set media sources for the player // set media sources for the player
setResolutionAndSubtitles(streams) setResolutionAndSubtitles(streams)
prepareExoPlayerView() prepareExoPlayerView()
initializePlayerView(streams) initializePlayerView(streams)
if (!isLive) seekToWatchPosition() if (!isLive) seekToWatchPosition()
exoPlayer.prepare() exoPlayer.prepare()
exoPlayer.play() exoPlayer.play()
exoPlayerView.useController = true exoPlayerView.useController = true
initializePlayerNotification() initializePlayerNotification()
if (sponsorBlockEnabled) fetchSponsorBlockSegments() if (sponsorBlockEnabled) fetchSponsorBlockSegments()
// show comments if related streams disabled // show comments if related streams disabled
if (!relatedStreamsEnabled) toggleComments() if (!relatedStreamsEnabled) toggleComments()
// prepare for autoplay // prepare for autoplay
if (autoplayEnabled) setNextStream() if (autoplayEnabled) setNextStream()
if (watchHistoryEnabled) { if (watchHistoryEnabled) PreferenceHelper.addToWatchHistory(videoId!!, streams)
PreferenceHelper.addToWatchHistory(videoId!!, streams)
}
}
} }
videoIds += videoId!!
} }
run()
} }
/** /**
* set the videoId of the next stream for autoplay * set the videoId of the next stream for autoplay
*/ */
private fun setNextStream() { private fun setNextStream() {
// don't play a video if it got played before already if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId)
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!!)
// search for the next videoId in the playlist // search for the next videoId in the playlist
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val nextId = autoPlayHelper.getNextPlaylistVideoId(videoId!!) nextStreamId = autoPlayHelper.getNextVideoId(videoId!!, streams.relatedStreams!!)
if (nextId != null) nextStreamId = nextId
} }
} }
@ -845,6 +823,8 @@ class PlayerFragment : BaseFragment() {
private fun playNextVideo() { private fun playNextVideo() {
if (nextStreamId == null) return if (nextStreamId == null) return
// check whether there is a new video in the queue // 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 // by making sure that the next and the current video aren't the same
saveWatchPosition() saveWatchPosition()
// forces the comments to reload for the new video // forces the comments to reload for the new video
@ -1073,13 +1053,13 @@ class PlayerFragment : BaseFragment() {
// next and previous buttons // next and previous buttons
playerBinding.skipPrev.visibility = if ( playerBinding.skipPrev.visibility = if (
skipButtonsEnabled && videoIds.indexOf(videoId!!) != 0 skipButtonsEnabled && Globals.playingQueue.indexOf(videoId!!) != 0
) View.VISIBLE else View.INVISIBLE ) View.VISIBLE else View.INVISIBLE
playerBinding.skipNext.visibility = if (skipButtonsEnabled) View.VISIBLE else View.INVISIBLE playerBinding.skipNext.visibility = if (skipButtonsEnabled) View.VISIBLE else View.INVISIBLE
playerBinding.skipPrev.setOnClickListener { playerBinding.skipPrev.setOnClickListener {
val index = videoIds.indexOf(videoId!!) - 1 val index = Globals.playingQueue.indexOf(videoId!!) - 1
videoId = videoIds[index] videoId = Globals.playingQueue[index]
playVideo() playVideo()
} }

View File

@ -12,6 +12,7 @@ import android.os.Looper
import android.widget.Toast import android.widget.Toast
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.github.libretube.BACKGROUND_CHANNEL_ID import com.github.libretube.BACKGROUND_CHANNEL_ID
import com.github.libretube.Globals
import com.github.libretube.PLAYER_NOTIFICATION_ID import com.github.libretube.PLAYER_NOTIFICATION_ID
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.obj.Segment 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.MediaItem
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -78,7 +78,7 @@ class BackgroundMode : Service() {
/** /**
* The [videoId] of the next stream for autoplay * 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 * Helper for finding the next video in the playlist
@ -86,7 +86,12 @@ class BackgroundMode : Service() {
private lateinit var autoPlayHelper: AutoPlayHelper 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() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -111,6 +116,9 @@ class BackgroundMode : Service() {
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
try { try {
// clear the playing queue
Globals.playingQueue.clear()
// get the intent arguments // get the intent arguments
videoId = intent?.getStringExtra("videoId")!! videoId = intent?.getStringExtra("videoId")!!
playlistId = intent.getStringExtra("playlistId") playlistId = intent.getStringExtra("playlistId")
@ -135,6 +143,8 @@ class BackgroundMode : Service() {
videoId: String, videoId: String,
seekToPosition: Long = 0 seekToPosition: Long = 0
) { ) {
// append the video to the playing queue
Globals.playingQueue += videoId
runBlocking { runBlocking {
val job = launch { val job = launch {
streams = RetrofitInstance.api.getStreams(videoId) streams = RetrofitInstance.api.getStreams(videoId)
@ -168,7 +178,7 @@ class BackgroundMode : Service() {
fetchSponsorBlockSegments() fetchSponsorBlockSegments()
setNextStream() if (autoplay) setNextStream()
} }
} }
@ -194,7 +204,6 @@ class BackgroundMode : Service() {
override fun onPlaybackStateChanged(@Player.State state: Int) { override fun onPlaybackStateChanged(@Player.State state: Int) {
when (state) { when (state) {
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
val autoplay = PreferenceHelper.getBoolean(PreferenceKeys.AUTO_PLAY, true)
if (autoplay) playNextVideo() if (autoplay) playNextVideo()
} }
Player.STATE_IDLE -> { Player.STATE_IDLE -> {
@ -217,8 +226,7 @@ class BackgroundMode : Service() {
if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!) if (!this::autoPlayHelper.isInitialized) autoPlayHelper = AutoPlayHelper(playlistId!!)
// search for the next videoId in the playlist // search for the next videoId in the playlist
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val nextId = autoPlayHelper.getNextPlaylistVideoId(videoId) nextStreamId = autoPlayHelper.getNextVideoId(videoId, streams!!.relatedStreams!!)
if (nextId != null) nextStreamId = nextId
} }
} }
@ -226,17 +234,18 @@ class BackgroundMode : Service() {
* Plays the first related video to the current (used when the playback of the current video ended) * Plays the first related video to the current (used when the playback of the current video ended)
*/ */
private fun playNextVideo() { 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 // play new video on background
this.videoId = nextStreamId this.videoId = nextStreamId!!
this.segmentData = null this.segmentData = null
playAudio(videoId) playAudio(videoId)
} }
/** /**
* Sets the [MediaItem] with the [streams] into the [player]. Also creates a [MediaSessionConnector] * Sets the [MediaItem] with the [streams] into the [player]
* with the [mediaSession] and attach it to the [player].
*/ */
private fun setMediaItem() { private fun setMediaItem() {
streams?.let { streams?.let {

View File

@ -1,17 +1,53 @@
package com.github.libretube.util package com.github.libretube.util
import com.github.libretube.Globals
import com.github.libretube.obj.StreamItem
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class AutoPlayHelper( class AutoPlayHelper(
private val playlistId: String private val playlistId: String?
) { ) {
private val TAG = "AutoPlayHelper" private val TAG = "AutoPlayHelper"
private val playlistStreamIds = mutableListOf<String>() private val playlistStreamIds = mutableListOf<String>()
private var playlistNextPage: String? = null 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 the playlists contain the video, then save the next video as next stream
if (playlistStreamIds.contains(currentVideoId)) { if (playlistStreamIds.contains(currentVideoId)) {
val index = playlistStreamIds.indexOf(currentVideoId) val index = playlistStreamIds.indexOf(currentVideoId)
@ -24,9 +60,9 @@ class AutoPlayHelper(
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
// fetch the playlists or its nextPage's videos // fetch the playlists or its nextPage's videos
val playlist = val playlist =
if (playlistNextPage == null) RetrofitInstance.authApi.getPlaylist(playlistId) if (playlistNextPage == null) RetrofitInstance.authApi.getPlaylist(playlistId!!)
else RetrofitInstance.authApi.getPlaylistNextPage( else RetrofitInstance.authApi.getPlaylistNextPage(
playlistId, playlistId!!,
playlistNextPage!! playlistNextPage!!
) )
// save the playlist urls to the list // save the playlist urls to the list
@ -39,4 +75,13 @@ class AutoPlayHelper(
// return null when no nextPage is found // return null when no nextPage is found
return null 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
}
} }

View File

@ -297,4 +297,5 @@
<string name="history_size">Maximum history size</string> <string name="history_size">Maximum history size</string>
<string name="unlimited">Unlimited</string> <string name="unlimited">Unlimited</string>
<string name="background_mode">Background mode</string> <string name="background_mode">Background mode</string>
<string name="add_to_queue">Add to queue</string>
</resources> </resources>