refactor: move common PlayerService code to AbstractPlayerService

This commit is contained in:
Bnyro 2024-10-06 12:57:43 +02:00
parent ad1be01fcd
commit db8ec51b12
3 changed files with 349 additions and 417 deletions

View File

@ -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)
}

View File

@ -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,87 +21,20 @@ 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 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)
}
}
}
}
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 {
override suspend fun onServiceCreated(intent: Intent) {
downloadsWithItems = withContext(Dispatchers.IO) {
DatabaseHolder.Database.downloadDao().getAll()
}
if (downloadsWithItems.isEmpty()) {
onDestroy()
return@launch
return
}
val videoId = intent?.getStringExtra(IntentData.videoId)
val videoId = intent.getStringExtra(IntentData.videoId)
val downloadToPlay = if (videoId == null) {
downloadsWithItems = downloadsWithItems.shuffled()
@ -130,41 +44,18 @@ class OfflinePlayerService : LifecycleService() {
}
this@OfflinePlayerService.videoId = downloadToPlay.download.videoId
createPlayerAndNotification()
// destroy the service if there was no success playing the selected audio/video
if (!startAudioPlayer(downloadToPlay)) 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!!,
NowPlayingNotification.Companion.NowPlayingNotificationType.AUDIO_OFFLINE
)
}
/**
* 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()
}
}
}
}
}

View File

@ -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,156 +89,43 @@ 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 ->
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
// play the audio in the background
loadAudio(playerData)
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()
} catch (e: Exception) {
val errorMessage = StreamsExtractor.getExtractorErrorMessageString(this@OnlinePlayerService, e)
this@OnlinePlayerService.toastFromMainDispatcher(errorMessage)
return@withContext null
}
} ?: return
if (PlayingQueue.isEmpty()) {
PlayingQueue.updateQueue(
@ -280,10 +147,8 @@ class OnlinePlayerService : LifecycleService() {
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
fun seekToPosition(position: Long) = player?.seekTo(position)
when (playbackState) {
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) }
}
}
}
}
}