mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 14:20:30 +05:30
refactor: move common PlayerService code to AbstractPlayerService
This commit is contained in:
parent
ad1be01fcd
commit
db8ec51b12
@ -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<PlayerEvent>(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)
|
||||
}
|
@ -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<DownloadWithItems> = 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<PlayerEvent>(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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Segment>()
|
||||
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<PlayerEvent>(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<PlayerData>(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<PlayerData>(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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user