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
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
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()

View File

@ -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()
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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>