LibreTube/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt

166 lines
5.5 KiB
Kotlin

package com.github.libretube.services
import android.content.Intent
import android.os.Bundle
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.DownloadWithItems
import com.github.libretube.db.obj.filterByTab
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.serializable
import com.github.libretube.extensions.setMetadata
import com.github.libretube.extensions.toAndroidUri
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.activities.NoInternetActivity
import com.github.libretube.ui.fragments.DownloadTab
import com.github.libretube.util.PlayingQueue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.io.path.exists
/**
* A service to play downloaded audio in the background
*/
@OptIn(UnstableApi::class)
open class OfflinePlayerService : AbstractPlayerService() {
override val isOfflinePlayer: Boolean = true
override val isAudioOnlyPlayer: Boolean = true
private var noInternetService: Boolean = false
private var downloadWithItems: DownloadWithItems? = null
private lateinit var downloadTab: DownloadTab
private var shuffle: Boolean = false
private val scope = CoroutineScope(Dispatchers.Main)
private val playerListener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_ENDED && PlayerHelper.isAutoPlayEnabled()) {
playNextVideo(PlayingQueue.getNext() ?: return)
}
if (playbackState == Player.STATE_READY) {
scope.launch(Dispatchers.IO) {
val watchHistoryItem = downloadWithItems?.download?.toStreamItem()?.toWatchHistoryItem(videoId)
if (watchHistoryItem != null) {
DatabaseHelper.addToWatchHistory(watchHistoryItem)
}
}
}
}
}
override suspend fun onServiceCreated(args: Bundle) {
if (args.isEmpty) return
downloadTab = args.serializable(IntentData.downloadTab)!!
shuffle = args.getBoolean(IntentData.shuffle, false)
noInternetService = args.getBoolean(IntentData.noInternet, false)
val videoId = if (shuffle) {
runBlocking(Dispatchers.IO) {
Database.downloadDao().getRandomVideoIdByFileType(FileType.AUDIO)
}
} else {
args.getString(IntentData.videoId)
} ?: return
setVideoId(videoId)
PlayingQueue.clear()
exoPlayer?.addListener(playerListener)
fillQueue()
}
override fun getIntentActivity(): Class<*> {
return if (noInternetService) NoInternetActivity::class.java else MainActivity::class.java
}
/**
* Attempt to start an audio player with the given download items
*/
override suspend fun startPlayback() {
super.startPlayback()
val downloadWithItems = withContext(Dispatchers.IO) {
Database.downloadDao().findById(videoId)
}!!
this.downloadWithItems = downloadWithItems
PlayingQueue.updateCurrent(downloadWithItems.download.toStreamItem())
withContext(Dispatchers.Main) {
setMediaItem(downloadWithItems)
exoPlayer?.playWhenReady = PlayerHelper.playAutomatically
exoPlayer?.prepare()
if (watchPositionsEnabled) {
DatabaseHelper.getWatchPosition(videoId)?.let {
if (!DatabaseHelper.isVideoWatched(
it,
downloadWithItems.download.duration
)
) exoPlayer?.seekTo(it)
}
}
}
}
open fun setMediaItem(downloadWithItems: DownloadWithItems) {
val audioItem = downloadWithItems.downloadItems.filter { it.path.exists() }
.firstOrNull { it.type == FileType.AUDIO }
?: // in some rare cases, video files can contain audio
downloadWithItems.downloadItems.firstOrNull { it.type == FileType.VIDEO }
if (audioItem == null) {
stopSelf()
return
}
val mediaItem = MediaItem.Builder()
.setUri(audioItem.path.toAndroidUri())
.setMetadata(downloadWithItems)
.build()
exoPlayer?.setMediaItem(mediaItem)
}
private suspend fun fillQueue() {
val downloads = withContext(Dispatchers.IO) {
Database.downloadDao().getAll()
}
.filterByTab(downloadTab)
.toMutableList()
if (shuffle) downloads.shuffle()
PlayingQueue.insertRelatedStreams(downloads.map { it.download.toStreamItem() })
}
private fun playNextVideo(videoId: String) {
setVideoId(videoId)
scope.launch {
startPlayback()
}
}
/**
* Stop the service when app is removed from the task manager.
*/
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
onDestroy()
}
}