From db8ec51b1294d93c3ecc317f353803ed3eacb4aa Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 6 Oct 2024 12:57:43 +0200 Subject: [PATCH] refactor: move common PlayerService code to AbstractPlayerService --- .../services/AbstractPlayerService.kt | 233 +++++++++++++ .../services/OfflinePlayerService.kt | 206 +++-------- .../libretube/services/OnlinePlayerService.kt | 327 ++++-------------- 3 files changed, 349 insertions(+), 417 deletions(-) create mode 100644 app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt diff --git a/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt b/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt new file mode 100644 index 000000000..7214615be --- /dev/null +++ b/app/src/main/java/com/github/libretube/services/AbstractPlayerService.kt @@ -0,0 +1,233 @@ +package com.github.libretube.services + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.widget.Toast +import androidx.annotation.OptIn +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector +import com.github.libretube.LibreTubeApp.Companion.PLAYER_CHANNEL_NAME +import com.github.libretube.R +import com.github.libretube.constants.IntentData +import com.github.libretube.db.DatabaseHolder +import com.github.libretube.db.obj.DownloadWithItems +import com.github.libretube.enums.FileType +import com.github.libretube.enums.NotificationId +import com.github.libretube.enums.PlayerEvent +import com.github.libretube.extensions.serializableExtra +import com.github.libretube.extensions.toAndroidUri +import com.github.libretube.extensions.updateParameters +import com.github.libretube.helpers.PlayerHelper +import com.github.libretube.obj.PlayerNotificationData +import com.github.libretube.util.NowPlayingNotification +import com.github.libretube.util.PauseableTimer +import com.github.libretube.util.PlayingQueue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.io.path.exists + +@UnstableApi +abstract class AbstractPlayerService : LifecycleService() { + var player: ExoPlayer? = null + var nowPlayingNotification: NowPlayingNotification? = null + var trackSelector: DefaultTrackSelector? = null + + lateinit var videoId: String + var isTransitioning = true + + val handler = Handler(Looper.getMainLooper()) + + private val watchPositionTimer = PauseableTimer( + onTick = ::saveWatchPosition, + delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS + ) + + private val playerListener = object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + + // Start or pause watch position timer + if (isPlaying) { + watchPositionTimer.resume() + } else { + watchPositionTimer.pause() + } + } + + override fun onPlaybackStateChanged(playbackState: Int) { + super.onPlaybackStateChanged(playbackState) + + this@AbstractPlayerService.onPlaybackStateChanged(playbackState) + } + + override fun onPlayerError(error: PlaybackException) { + // show a toast on errors + Handler(Looper.getMainLooper()).post { + Toast.makeText( + applicationContext, + error.localizedMessage, + Toast.LENGTH_SHORT + ).show() + } + } + + override fun onEvents(player: Player, events: Player.Events) { + super.onEvents(player, events) + + if (events.contains(Player.EVENT_TRACKS_CHANGED)) { + PlayerHelper.setPreferredAudioQuality(this@AbstractPlayerService, player, trackSelector ?: return) + } + } + } + + private val playerActionReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val event = intent.serializableExtra(PlayerHelper.CONTROL_TYPE) ?: return + val player = player ?: return + + if (PlayerHelper.handlePlayerAction(player, event)) return + + when (event) { + PlayerEvent.Next -> { + PlayingQueue.navigateNext() + } + PlayerEvent.Prev -> { + PlayingQueue.navigatePrev() + } + PlayerEvent.Stop -> { + onDestroy() + } + else -> Unit + } + } + } + + override fun onCreate() { + super.onCreate() + + val notification = NotificationCompat.Builder(this, PLAYER_CHANNEL_NAME) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.playingOnBackground)) + .setSmallIcon(R.drawable.ic_launcher_lockscreen) + .build() + + startForeground(NotificationId.PLAYER_PLAYBACK.id, notification) + + ContextCompat.registerReceiver( + this, + playerActionReceiver, + IntentFilter(PlayerHelper.getIntentActionName(this)), + ContextCompat.RECEIVER_NOT_EXPORTED + ) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + lifecycleScope.launch { + if (intent != null) { + createPlayerAndNotification() + onServiceCreated(intent) + startPlaybackAndUpdateNotification() + } + else stopSelf() + } + + return super.onStartCommand(intent, flags, startId) + } + + abstract suspend fun onServiceCreated(intent: Intent) + + @OptIn(UnstableApi::class) + private fun createPlayerAndNotification() { + val trackSelector = DefaultTrackSelector(this) + this.trackSelector = trackSelector + + trackSelector.updateParameters { + setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) + } + + player = PlayerHelper.createPlayer(this, trackSelector, true) + // prevent android from putting LibreTube to sleep when locked + player!!.setWakeMode(C.WAKE_MODE_LOCAL) + player!!.addListener(playerListener) + + PlayerHelper.setPreferredCodecs(trackSelector) + + nowPlayingNotification = NowPlayingNotification( + this, + player!!, + NowPlayingNotification.Companion.NowPlayingNotificationType.AUDIO_OFFLINE + ) + } + + abstract suspend fun startPlaybackAndUpdateNotification() + + fun saveWatchPosition() { + if (isTransitioning || !PlayerHelper.watchPositionsVideo) return + + player?.let { PlayerHelper.saveWatchPosition(it, videoId) } + } + + override fun onDestroy() { + PlayingQueue.resetToDefaults() + + saveWatchPosition() + + nowPlayingNotification?.destroySelf() + nowPlayingNotification = null + watchPositionTimer.destroy() + + handler.removeCallbacksAndMessages(null) + + runCatching { + player?.stop() + player?.release() + } + player = null + + runCatching { + unregisterReceiver(playerActionReceiver) + } + + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + stopSelf() + + super.onDestroy() + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return null + } + + /** + * Stop the service when app is removed from the task manager. + */ + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + onDestroy() + } + + abstract fun onPlaybackStateChanged(playbackState: Int) + + fun getCurrentPosition() = player?.currentPosition + + fun getDuration() = player?.duration + + fun seekToPosition(position: Long) = player?.seekTo(position) +} diff --git a/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt b/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt index 94ed1eed5..669b5e7c7 100644 --- a/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt @@ -1,37 +1,18 @@ package com.github.libretube.services -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.os.IBinder -import androidx.annotation.OptIn -import androidx.core.app.NotificationCompat -import androidx.core.app.ServiceCompat -import androidx.core.content.ContextCompat -import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope -import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.trackselection.DefaultTrackSelector -import com.github.libretube.LibreTubeApp.Companion.PLAYER_CHANNEL_NAME -import com.github.libretube.R import com.github.libretube.constants.IntentData import com.github.libretube.db.DatabaseHolder import com.github.libretube.db.obj.DownloadWithItems import com.github.libretube.enums.FileType -import com.github.libretube.enums.NotificationId -import com.github.libretube.enums.PlayerEvent -import com.github.libretube.extensions.serializableExtra import com.github.libretube.extensions.toAndroidUri -import com.github.libretube.extensions.updateParameters import com.github.libretube.helpers.PlayerHelper import com.github.libretube.obj.PlayerNotificationData -import com.github.libretube.util.NowPlayingNotification -import com.github.libretube.util.PauseableTimer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -40,131 +21,41 @@ import kotlin.io.path.exists /** * A service to play downloaded audio in the background */ -class OfflinePlayerService : LifecycleService() { - private var player: ExoPlayer? = null - private var nowPlayingNotification: NowPlayingNotification? = null - private lateinit var videoId: String +@UnstableApi +class OfflinePlayerService : AbstractPlayerService() { private var downloadsWithItems: List = emptyList() - private val watchPositionTimer = PauseableTimer( - onTick = this::saveWatchPosition, - delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS - ) - - private val playerListener = object : Player.Listener { - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - - // Start or pause watch position timer - if (isPlaying) { - watchPositionTimer.resume() - } else { - watchPositionTimer.pause() - } + override suspend fun onServiceCreated(intent: Intent) { + downloadsWithItems = withContext(Dispatchers.IO) { + DatabaseHolder.Database.downloadDao().getAll() + } + if (downloadsWithItems.isEmpty()) { + onDestroy() + return } - override fun onPlaybackStateChanged(playbackState: Int) { - super.onPlaybackStateChanged(playbackState) + val videoId = intent.getStringExtra(IntentData.videoId) - // automatically go to the next video/audio when the current one ended - if (playbackState == Player.STATE_ENDED) { - val currentIndex = downloadsWithItems.indexOfFirst { it.download.videoId == videoId } - downloadsWithItems.getOrNull(currentIndex + 1)?.let { - this@OfflinePlayerService.videoId = it.download.videoId - startAudioPlayer(it) - } - } - } - } - - private val playerActionReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val event = intent.serializableExtra(PlayerHelper.CONTROL_TYPE) ?: return - val player = player ?: return - - if (PlayerHelper.handlePlayerAction(player, event)) return - - when (event) { - PlayerEvent.Stop -> onDestroy() - else -> Unit - } - } - } - - override fun onCreate() { - super.onCreate() - - val notification = NotificationCompat.Builder(this, PLAYER_CHANNEL_NAME) - .setContentTitle(getString(R.string.app_name)) - .setContentText(getString(R.string.playingOnBackground)) - .setSmallIcon(R.drawable.ic_launcher_lockscreen) - .build() - - startForeground(NotificationId.PLAYER_PLAYBACK.id, notification) - - ContextCompat.registerReceiver( - this, - playerActionReceiver, - IntentFilter(PlayerHelper.getIntentActionName(this)), - ContextCompat.RECEIVER_NOT_EXPORTED - ) - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - lifecycleScope.launch { - downloadsWithItems = withContext(Dispatchers.IO) { - DatabaseHolder.Database.downloadDao().getAll() - } - if (downloadsWithItems.isEmpty()) { - onDestroy() - return@launch - } - - val videoId = intent?.getStringExtra(IntentData.videoId) - - val downloadToPlay = if (videoId == null) { - downloadsWithItems = downloadsWithItems.shuffled() - downloadsWithItems.first() - } else { - downloadsWithItems.first { it.download.videoId == videoId } - } - - this@OfflinePlayerService.videoId = downloadToPlay.download.videoId - - createPlayerAndNotification() - - // destroy the service if there was no success playing the selected audio/video - if (!startAudioPlayer(downloadToPlay)) onDestroy() + val downloadToPlay = if (videoId == null) { + downloadsWithItems = downloadsWithItems.shuffled() + downloadsWithItems.first() + } else { + downloadsWithItems.first { it.download.videoId == videoId } } - return super.onStartCommand(intent, flags, startId) - } - - @OptIn(UnstableApi::class) - private fun createPlayerAndNotification() { - val trackSelector = DefaultTrackSelector(this@OfflinePlayerService) - trackSelector.updateParameters { - setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) - } - - player = PlayerHelper.createPlayer(this@OfflinePlayerService, trackSelector, true) - // prevent android from putting LibreTube to sleep when locked - player!!.setWakeMode(C.WAKE_MODE_LOCAL) - player!!.addListener(playerListener) - - nowPlayingNotification = NowPlayingNotification( - this, - player!!, - NowPlayingNotification.Companion.NowPlayingNotificationType.AUDIO_OFFLINE - ) + this@OfflinePlayerService.videoId = downloadToPlay.download.videoId } /** * Attempt to start an audio player with the given download items - * @param downloadWithItems The database download to play from - * @return whether starting the audio player succeeded */ - private fun startAudioPlayer(downloadWithItems: DownloadWithItems): Boolean { + override suspend fun startPlaybackAndUpdateNotification() { + val downloadWithItems = downloadsWithItems.firstOrNull { it.download.videoId == videoId } + if (downloadWithItems == null) { + stopSelf() + return + } + val notificationData = PlayerNotificationData( title = downloadWithItems.download.title, uploaderName = downloadWithItems.download.uploader, @@ -176,7 +67,11 @@ class OfflinePlayerService : LifecycleService() { .firstOrNull { it.type == FileType.AUDIO } ?: // in some rare cases, video files can contain audio downloadWithItems.downloadItems.firstOrNull { it.type == FileType.VIDEO } - ?: return false + + if (audioItem == null) { + stopSelf() + return + } val mediaItem = MediaItem.Builder() .setUri(audioItem.path.toAndroidUri()) @@ -191,37 +86,6 @@ class OfflinePlayerService : LifecycleService() { player?.seekTo(it) } } - - return true - } - - private fun saveWatchPosition() { - if (!PlayerHelper.watchPositionsVideo) return - - player?.let { PlayerHelper.saveWatchPosition(it, videoId) } - } - - override fun onDestroy() { - saveWatchPosition() - - nowPlayingNotification?.destroySelf() - nowPlayingNotification = null - watchPositionTimer.destroy() - - runCatching { - player?.stop() - player?.release() - } - player = null - - runCatching { - unregisterReceiver(playerActionReceiver) - } - - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - stopSelf() - - super.onDestroy() } override fun onBind(intent: Intent): IBinder? { @@ -236,4 +100,18 @@ class OfflinePlayerService : LifecycleService() { super.onTaskRemoved(rootIntent) onDestroy() } + + override fun onPlaybackStateChanged(playbackState: Int) { + // automatically go to the next video/audio when the current one ended + if (playbackState == Player.STATE_ENDED) { + val currentIndex = downloadsWithItems.indexOfFirst { it.download.videoId == videoId } + downloadsWithItems.getOrNull(currentIndex + 1)?.let { + this@OfflinePlayerService.videoId = it.download.videoId + + lifecycleScope.launch { + startPlaybackAndUpdateNotification() + } + } + } + } } diff --git a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt index 9e73ea40d..8db5458b5 100644 --- a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt @@ -39,6 +39,7 @@ import com.github.libretube.extensions.parcelableExtra import com.github.libretube.extensions.serializableExtra import com.github.libretube.extensions.setMetadata import com.github.libretube.extensions.toID +import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.extensions.updateParameters import com.github.libretube.helpers.PlayerHelper import com.github.libretube.helpers.PlayerHelper.checkForSegments @@ -57,17 +58,13 @@ import kotlinx.serialization.encodeToString * Loads the selected videos audio in background mode with a notification area. */ @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) -class OnlinePlayerService : LifecycleService() { - /** - * VideoId of the video - */ - private lateinit var videoId: String - +class OnlinePlayerService : AbstractPlayerService() { /** * PlaylistId/ChannelId for autoplay */ private var playlistId: String? = null private var channelId: String? = null + private var startTimestamp: Long? = null /** * The response that gets when called the Api. @@ -75,29 +72,12 @@ class OnlinePlayerService : LifecycleService() { var streams: Streams? = null private set - /** - * The [ExoPlayer] player. Followed tutorial [here](https://developer.android.com/codelabs/exoplayer-intro) - */ - var player: ExoPlayer? = null - private var trackSelector: DefaultTrackSelector? = null - private var isTransitioning = true - /** * SponsorBlock Segment data */ private var segments = listOf() private var sponsorBlockConfig = PlayerHelper.getSponsorBlockCategories() - /** - * [Notification] for the player - */ - private lateinit var nowPlayingNotification: NowPlayingNotification - - /** - * Autoplay Preference - */ - private val handler = Handler(Looper.getMainLooper()) - /** * Used for connecting to the AudioPlayerFragment */ @@ -109,181 +89,66 @@ class OnlinePlayerService : LifecycleService() { var onStateOrPlayingChanged: ((isPlaying: Boolean) -> Unit)? = null var onNewVideo: ((streams: Streams, videoId: String) -> Unit)? = null - private val watchPositionTimer = PauseableTimer( - onTick = this::saveWatchPosition, - delayMillis = PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS - ) - - private val playerListener = object : Player.Listener { - override fun onIsPlayingChanged(isPlaying: Boolean) { - super.onIsPlayingChanged(isPlaying) - onStateOrPlayingChanged?.invoke(isPlaying) - - // Start or pause watch position timer - if (isPlaying) { - watchPositionTimer.resume() - } else { - watchPositionTimer.pause() - } - } - - override fun onPlaybackStateChanged(state: Int) { - onStateOrPlayingChanged?.invoke(player?.isPlaying ?: false) - - when (state) { - Player.STATE_ENDED -> { - if (!isTransitioning) playNextVideo() - } - - Player.STATE_IDLE -> { - onDestroy() - } - - Player.STATE_BUFFERING -> {} - Player.STATE_READY -> { - isTransitioning = false - - // save video to watch history when the video starts playing or is being resumed - // waiting for the player to be ready since the video can't be claimed to be watched - // while it did not yet start actually, but did buffer only so far - lifecycleScope.launch(Dispatchers.IO) { - streams?.let { DatabaseHelper.addToWatchHistory(videoId, it) } - } - } - } - } - - override fun onPlayerError(error: PlaybackException) { - // show a toast on errors - Handler(Looper.getMainLooper()).post { - Toast.makeText( - this@OnlinePlayerService.applicationContext, - error.localizedMessage, - Toast.LENGTH_SHORT - ).show() - } - } - - override fun onEvents(player: Player, events: Player.Events) { - super.onEvents(player, events) - - if (events.contains(Player.EVENT_TRACKS_CHANGED)) { - PlayerHelper.setPreferredAudioQuality(this@OnlinePlayerService, player, trackSelector ?: return) - } - } - } - - private val playerActionReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val event = intent.serializableExtra(PlayerHelper.CONTROL_TYPE) ?: return - val player = player ?: return - - if (PlayerHelper.handlePlayerAction(player, event)) return - - when (event) { - PlayerEvent.Next -> { - PlayingQueue.navigateNext() - } - PlayerEvent.Prev -> { - PlayingQueue.navigatePrev() - } - PlayerEvent.Stop -> { - onDestroy() - } - else -> Unit - } - } - } - - /** - * Setting the required [Notification] for running as a foreground service - */ - override fun onCreate() { - super.onCreate() - - val notification = NotificationCompat.Builder(this, PLAYER_CHANNEL_NAME) - .setContentTitle(getString(R.string.app_name)) - .setContentText(getString(R.string.playingOnBackground)) - .setSmallIcon(R.drawable.ic_launcher_lockscreen) - .build() - - startForeground(NotificationId.PLAYER_PLAYBACK.id, notification) - - ContextCompat.registerReceiver( - this, - playerActionReceiver, - IntentFilter(PlayerHelper.getIntentActionName(this)), - ContextCompat.RECEIVER_NOT_EXPORTED - ) - } - - /** - * Initializes the [player] with the [MediaItem]. - */ - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + override suspend fun onServiceCreated(intent: Intent) { // reset the playing queue listeners PlayingQueue.resetToDefaults() - intent?.parcelableExtra(IntentData.playerData)?.let { playerData -> - // get the intent arguments - videoId = playerData.videoId - playlistId = playerData.playlistId - - // play the audio in the background - loadAudio(playerData) - - PlayingQueue.setOnQueueTapListener { streamItem -> - streamItem.url?.toID()?.let { playNextVideo(it) } - } + val playerData = intent.parcelableExtra(IntentData.playerData) + if (playerData == null) { + stopSelf() + return + } + + // get the intent arguments + videoId = playerData.videoId + playlistId = playerData.playlistId + startTimestamp = playerData.timestamp + + if (!playerData.keepQueue) PlayingQueue.clear() + + PlayingQueue.setOnQueueTapListener { streamItem -> + streamItem.url?.toID()?.let { playNextVideo(it) } } - return super.onStartCommand(intent, flags, startId) } - private fun saveWatchPosition() { - if (isTransitioning || !PlayerHelper.watchPositionsAudio) return + override suspend fun startPlaybackAndUpdateNotification() { + val timestamp = startTimestamp ?: 0L + startTimestamp = null - player?.let { PlayerHelper.saveWatchPosition(it, videoId) } - } - - /** - * Gets the video data and prepares the [player]. - */ - private fun loadAudio(playerData: PlayerData) { - val (videoId, _, _, keepQueue, timestamp) = playerData isTransitioning = true - lifecycleScope.launch(Dispatchers.IO) { - streams = runCatching { + streams = withContext(Dispatchers.IO) { + try { StreamsExtractor.extractStreams(videoId) - }.getOrNull() ?: return@launch - - // clear the queue if it shouldn't be kept explicitly - if (!keepQueue) PlayingQueue.clear() - - if (PlayingQueue.isEmpty()) { - PlayingQueue.updateQueue( - streams!!.toStreamItem(videoId), - playlistId, - channelId, - streams!!.relatedStreams - ) - } else if (PlayingQueue.isLast() && playlistId == null && channelId == null) { - PlayingQueue.insertRelatedStreams(streams!!.relatedStreams) + } catch (e: Exception) { + val errorMessage = StreamsExtractor.getExtractorErrorMessageString(this@OnlinePlayerService, e) + this@OnlinePlayerService.toastFromMainDispatcher(errorMessage) + return@withContext null } + } ?: return - // save the current stream to the queue - streams?.toStreamItem(videoId)?.let { - PlayingQueue.updateCurrent(it) - } + if (PlayingQueue.isEmpty()) { + PlayingQueue.updateQueue( + streams!!.toStreamItem(videoId), + playlistId, + channelId, + streams!!.relatedStreams + ) + } else if (PlayingQueue.isLast() && playlistId == null && channelId == null) { + PlayingQueue.insertRelatedStreams(streams!!.relatedStreams) + } - withContext(Dispatchers.Main) { - playAudio(timestamp) - } + // save the current stream to the queue + streams?.toStreamItem(videoId)?.let { + PlayingQueue.updateCurrent(it) + } + + withContext(Dispatchers.Main) { + playAudio(timestamp) } } private fun playAudio(seekToPosition: Long) { - initializePlayer() lifecycleScope.launch(Dispatchers.IO) { setMediaItem() @@ -299,20 +164,12 @@ class OnlinePlayerService : LifecycleService() { } } - // create the notification - if (!this@OnlinePlayerService::nowPlayingNotification.isInitialized) { - nowPlayingNotification = NowPlayingNotification( - this@OnlinePlayerService, - player!!, - NowPlayingNotification.Companion.NowPlayingNotificationType.AUDIO_ONLINE - ) - } val playerNotificationData = PlayerNotificationData( streams?.title, streams?.uploader, streams?.thumbnailUrl ) - nowPlayingNotification.updatePlayerNotification(videoId, playerNotificationData) + nowPlayingNotification?.updatePlayerNotification(videoId, playerNotificationData) streams?.let { onNewVideo?.invoke(it, videoId) } player?.apply { @@ -323,28 +180,6 @@ class OnlinePlayerService : LifecycleService() { if (PlayerHelper.sponsorBlockEnabled) fetchSponsorBlockSegments() } - /** - * create the player - */ - private fun initializePlayer() { - if (player != null) return - - trackSelector = DefaultTrackSelector(this) - trackSelector!!.updateParameters { - setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) - } - - player = PlayerHelper.createPlayer(this, trackSelector!!, true) - // prevent android from putting LibreTube to sleep when locked - player!!.setWakeMode(WAKE_MODE_NETWORK) - - // Listens for changed playbackStates (e.g. pause, end) - // Plays the next video when the current one ended - player?.addListener(playerListener) - - PlayerHelper.setPreferredCodecs(trackSelector!!) - } - /** * Plays the next video from the queue */ @@ -364,7 +199,10 @@ class OnlinePlayerService : LifecycleService() { this.videoId = nextVideo this.streams = null this.segments = emptyList() - loadAudio(PlayerData(videoId, keepQueue = true)) + + lifecycleScope.launch { + startPlaybackAndUpdateNotification() + } } /** @@ -414,43 +252,6 @@ class OnlinePlayerService : LifecycleService() { player?.checkForSegments(this, segments, sponsorBlockConfig) } - /** - * Stop the service when app is removed from the task manager. - */ - override fun onTaskRemoved(rootIntent: Intent?) { - super.onTaskRemoved(rootIntent) - onDestroy() - } - - /** - * destroy the [OnlinePlayerService] foreground service - */ - override fun onDestroy() { - // reset the playing queue - PlayingQueue.resetToDefaults() - - if (this::nowPlayingNotification.isInitialized) nowPlayingNotification.destroySelf() - watchPositionTimer.destroy() - handler.removeCallbacksAndMessages(null) - - runCatching { - player?.stop() - player?.release() - } - - runCatching { - unregisterReceiver(playerActionReceiver) - } - - // called when the user pressed stop in the notification - // stop the service from being in the foreground and remove the notification - ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) - // destroy the service - stopSelf() - - super.onDestroy() - } - inner class LocalBinder : Binder() { // Return this instance of [BackgroundMode] so clients can call public methods fun getService(): OnlinePlayerService = this@OnlinePlayerService @@ -461,9 +262,29 @@ class OnlinePlayerService : LifecycleService() { return binder } - fun getCurrentPosition() = player?.currentPosition + override fun onPlaybackStateChanged(playbackState: Int) { + onStateOrPlayingChanged?.invoke(player?.isPlaying ?: false) - fun getDuration() = player?.duration + when (playbackState) { + Player.STATE_ENDED -> { + if (!isTransitioning) playNextVideo() + } - fun seekToPosition(position: Long) = player?.seekTo(position) + Player.STATE_IDLE -> { + onDestroy() + } + + Player.STATE_BUFFERING -> {} + Player.STATE_READY -> { + isTransitioning = false + + // save video to watch history when the video starts playing or is being resumed + // waiting for the player to be ready since the video can't be claimed to be watched + // while it did not yet start actually, but did buffer only so far + lifecycleScope.launch(Dispatchers.IO) { + streams?.let { DatabaseHelper.addToWatchHistory(videoId, it) } + } + } + } + } }