diff --git a/app/src/main/java/com/github/libretube/Globals.kt b/app/src/main/java/com/github/libretube/Globals.kt index 6d3b43a11..5db9eabea 100644 --- a/app/src/main/java/com/github/libretube/Globals.kt +++ b/app/src/main/java/com/github/libretube/Globals.kt @@ -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() } diff --git a/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt b/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt index a90e270cf..f10d1d254 100644 --- a/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt +++ b/app/src/main/java/com/github/libretube/dialogs/VideoOptionsDialog.kt @@ -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() diff --git a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt index d29c500e4..f8413534b 100644 --- a/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/fragments/PlayerFragment.kt @@ -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() - 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() } diff --git a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt index c3ae20dac..0ad0f53b8 100644 --- a/app/src/main/java/com/github/libretube/services/BackgroundMode.kt +++ b/app/src/main/java/com/github/libretube/services/BackgroundMode.kt @@ -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 { diff --git a/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt b/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt index ca48fd1d2..d747681ea 100644 --- a/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt +++ b/app/src/main/java/com/github/libretube/util/AutoPlayHelper.kt @@ -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() private var playlistNextPage: String? = null - suspend fun getNextPlaylistVideoId(currentVideoId: String): String? { + suspend fun getNextVideoId( + currentVideoId: String, + relatedStreams: List + ): 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): 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 + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc4981bce..6cc64f7a6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -297,4 +297,5 @@ Maximum history size Unlimited Background mode + Add to queue