Merge pull request #5286 from Bnyro/master

feat: automatically play the next downloaded video in offline audio mode
This commit is contained in:
Bnyro 2023-12-04 16:20:59 +01:00 committed by GitHub
commit e61be4e3ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 57 additions and 31 deletions

View File

@ -2,12 +2,15 @@ package com.github.libretube.services
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import androidx.annotation.OptIn
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem 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.ExoPlayer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.github.libretube.LibreTubeApp.Companion.PLAYER_CHANNEL_NAME import com.github.libretube.LibreTubeApp.Companion.PLAYER_CHANNEL_NAME
@ -33,6 +36,23 @@ import kotlinx.coroutines.withContext
class OfflinePlayerService : LifecycleService() { class OfflinePlayerService : LifecycleService() {
private var player: ExoPlayer? = null private var player: ExoPlayer? = null
private var nowPlayingNotification: NowPlayingNotification? = null private var nowPlayingNotification: NowPlayingNotification? = null
private lateinit var videoId: String
private var downloadsWithItems: List<DownloadWithItems> = emptyList()
private val playerListener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
// 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)
}
}
}
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -47,52 +67,55 @@ class OfflinePlayerService : LifecycleService() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val videoId = intent?.getStringExtra(IntentData.videoId) videoId = intent?.getStringExtra(IntentData.videoId)!!
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch {
val downloadWithItems = DatabaseHolder.Database.downloadDao().findById(videoId!!) downloadsWithItems = withContext(Dispatchers.IO) {
withContext(Dispatchers.Main) { DatabaseHolder.Database.downloadDao().getAll()
if (startAudioPlayer(downloadWithItems)) { }
nowPlayingNotification = NowPlayingNotification( val downloadWithItems = downloadsWithItems.first { it.download.videoId == videoId }
this@OfflinePlayerService,
player!!, createPlayerAndNotification()
true
) // destroy the service if there was no success playing the selected audio/video
if (!startAudioPlayer(downloadWithItems)) onDestroy()
}
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!!, true)
}
/**
* 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 {
val notificationData = PlayerNotificationData( val notificationData = PlayerNotificationData(
title = downloadWithItems.download.title, title = downloadWithItems.download.title,
uploaderName = downloadWithItems.download.uploader, uploaderName = downloadWithItems.download.uploader,
thumbnailPath = downloadWithItems.download.thumbnailPath thumbnailPath = downloadWithItems.download.thumbnailPath
) )
nowPlayingNotification?.updatePlayerNotification(videoId, notificationData) nowPlayingNotification?.updatePlayerNotification(videoId, notificationData)
} else {
onDestroy()
}
}
}
return super.onStartCommand(intent, flags, startId) val audioItem = downloadWithItems.downloadItems.filter { it.path.exists() }
}
/**
* Attempt to start an audio player with the given download items
* @param downloadWithItem The database download to play from
* @return whether starting the audio player succeeded
*/
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
private fun startAudioPlayer(downloadWithItem: DownloadWithItems): Boolean {
val 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(C.WAKE_MODE_LOCAL)
val audioItem = downloadWithItem.downloadItems.filter { it.path.exists() }
.firstOrNull { it.type == FileType.AUDIO } .firstOrNull { it.type == FileType.AUDIO }
?: // in some rare cases, video files can contain audio ?: // in some rare cases, video files can contain audio
downloadWithItem.downloadItems.firstOrNull { it.type == FileType.VIDEO } downloadWithItems.downloadItems.firstOrNull { it.type == FileType.VIDEO }
?: return false ?: return false
val mediaItem = MediaItem.Builder() val mediaItem = MediaItem.Builder()
@ -102,6 +125,7 @@ class OfflinePlayerService : LifecycleService() {
player?.setMediaItem(mediaItem) player?.setMediaItem(mediaItem)
player?.playWhenReady = PlayerHelper.playAutomatically player?.playWhenReady = PlayerHelper.playAutomatically
player?.prepare() player?.prepare()
return true return true
} }

View File

@ -138,7 +138,9 @@ class DownloadsAdapter(
items.forEach { items.forEach {
it.path.deleteIfExists() it.path.deleteIfExists()
} }
runCatching {
download.thumbnailPath?.deleteIfExists() download.thumbnailPath?.deleteIfExists()
}
runBlocking(Dispatchers.IO) { runBlocking(Dispatchers.IO) {
DatabaseHolder.Database.downloadDao().deleteDownload(download) DatabaseHolder.Database.downloadDao().deleteDownload(download)