From 4b06bfb6871ab192844692c0e7703786d8bf4c6a Mon Sep 17 00:00:00 2001 From: Bnyro Date: Tue, 27 Feb 2024 14:38:15 +0100 Subject: [PATCH] feat: watch positions support for downloaded media --- .../github/libretube/helpers/PlayerHelper.kt | 18 ++++++++- .../services/OfflinePlayerService.kt | 40 +++++++++++++++++++ .../libretube/services/OnlinePlayerService.kt | 40 ++++++++++--------- .../ui/activities/OfflinePlayerActivity.kt | 40 +++++++++++++++++++ .../libretube/ui/fragments/PlayerFragment.kt | 20 ++-------- 5 files changed, 123 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt index b36fe9a55..c9e558779 100644 --- a/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt @@ -36,11 +36,15 @@ import com.github.libretube.api.obj.Segment import com.github.libretube.api.obj.Streams import com.github.libretube.constants.PreferenceKeys import com.github.libretube.db.DatabaseHolder +import com.github.libretube.db.obj.WatchPosition import com.github.libretube.enums.PlayerEvent import com.github.libretube.enums.SbSkipOptions import com.github.libretube.extensions.updateParameters import com.github.libretube.obj.VideoStats import com.github.libretube.util.TextUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.Locale import java.util.concurrent.Executors @@ -53,6 +57,7 @@ object PlayerHelper { const val SPONSOR_HIGHLIGHT_CATEGORY = "poi_highlight" const val ROLE_FLAG_AUTO_GEN_SUBTITLE = C.ROLE_FLAG_SUPPLEMENTARY private const val MINIMUM_BUFFER_DURATION = 1000 * 10 // exo default is 50s + const val WATCH_POSITION_TIMER_DELAY_MS = 1000L /** * The maximum amount of time to wait until the video starts playing: 10 minutes @@ -606,7 +611,7 @@ object PlayerHelper { } } - fun getPosition(videoId: String, duration: Long?): Long? { + fun getStoredWatchPosition(videoId: String, duration: Long?): Long? { if (duration == null) return null runCatching { @@ -780,4 +785,15 @@ object PlayerHelper { player.playbackState == Player.STATE_ENDED -> R.drawable.ic_restart else -> R.drawable.ic_play } + + fun saveWatchPosition(player: ExoPlayer, videoId: String) { + if (player.duration == C.TIME_UNSET || player.currentPosition in listOf(0L, C.TIME_UNSET)) { + return + } + + val watchPosition = WatchPosition(videoId, player.currentPosition) + CoroutineScope(Dispatchers.IO).launch { + DatabaseHolder.Database.watchPositionDao().insert(watchPosition) + } + } } 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 971203129..db1679dcb 100644 --- a/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OfflinePlayerService.kt @@ -1,7 +1,9 @@ package com.github.libretube.services import android.content.Intent +import android.os.Handler import android.os.IBinder +import android.os.Looper import androidx.annotation.OptIn import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat @@ -29,17 +31,39 @@ import kotlin.io.path.exists import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.Timer +import java.util.TimerTask /** * A service to play downloaded audio in the background */ class OfflinePlayerService : LifecycleService() { + val handler = Handler(Looper.getMainLooper()) + private var player: ExoPlayer? = null private var nowPlayingNotification: NowPlayingNotification? = null private lateinit var videoId: String private var downloadsWithItems: List = emptyList() + private var watchPositionTimer: Timer? = null + private val playerListener = object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + + // Start or pause watch position timer + if (isPlaying) { + watchPositionTimer = Timer() + watchPositionTimer!!.scheduleAtFixedRate(object : TimerTask() { + override fun run() { + handler.post(this@OfflinePlayerService::saveWatchPosition) + } + }, PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS, PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS) + } else { + watchPositionTimer?.cancel() + } + } + override fun onPlaybackStateChanged(playbackState: Int) { super.onPlaybackStateChanged(playbackState) @@ -142,10 +166,24 @@ class OfflinePlayerService : LifecycleService() { player?.playWhenReady = PlayerHelper.playAutomatically player?.prepare() + if (PlayerHelper.watchPositionsAudio) { + PlayerHelper.getStoredWatchPosition(videoId, downloadWithItems.download.duration)?.let { + player?.seekTo(it) + } + } + return true } + private fun saveWatchPosition() { + if (!PlayerHelper.watchPositionsVideo) return + + player?.let { PlayerHelper.saveWatchPosition(it, videoId) } + } + override fun onDestroy() { + saveWatchPosition() + nowPlayingNotification?.destroySelf() player?.stop() @@ -153,6 +191,8 @@ class OfflinePlayerService : LifecycleService() { player = null nowPlayingNotification = null + watchPositionTimer?.cancel() + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) stopSelf() 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 edc9dbe41..3c3721128 100644 --- a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt @@ -28,8 +28,6 @@ import com.github.libretube.api.obj.Segment import com.github.libretube.api.obj.Streams 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.WatchPosition import com.github.libretube.enums.NotificationId import com.github.libretube.extensions.parcelableExtra import com.github.libretube.extensions.setMetadata @@ -42,11 +40,12 @@ import com.github.libretube.obj.PlayerNotificationData import com.github.libretube.parcelable.PlayerData import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.PlayingQueue -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.encodeToString +import java.util.Timer +import java.util.TimerTask /** * Loads the selected videos audio in background mode with a notification area. @@ -103,10 +102,24 @@ class OnlinePlayerService : LifecycleService() { var onStateOrPlayingChanged: ((isPlaying: Boolean) -> Unit)? = null var onNewVideo: ((streams: Streams, videoId: String) -> Unit)? = null + private var watchPositionTimer: Timer? = null + 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 = Timer() + watchPositionTimer!!.scheduleAtFixedRate(object : TimerTask() { + override fun run() { + handler.post(this@OnlinePlayerService::saveWatchPosition) + } + }, PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS, PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS) + } else { + watchPositionTimer?.cancel() + } } override fun onPlaybackStateChanged(state: Int) { @@ -180,25 +193,14 @@ class OnlinePlayerService : LifecycleService() { PlayingQueue.setOnQueueTapListener { streamItem -> streamItem.url?.toID()?.let { playNextVideo(it) } } - - if (PlayerHelper.watchPositionsAudio) { - updateWatchPosition() - } } return super.onStartCommand(intent, flags, startId) } - private fun updateWatchPosition() { - player?.currentPosition?.let { - if (isTransitioning) return@let + private fun saveWatchPosition() { + if (isTransitioning || !PlayerHelper.watchPositionsAudio) return - val watchPosition = WatchPosition(videoId, it) - - CoroutineScope(Dispatchers.IO).launch { - Database.watchPositionDao().insert(watchPosition) - } - } - handler.postDelayed(this::updateWatchPosition, 500) + player?.let { PlayerHelper.saveWatchPosition(it, videoId) } } /** @@ -248,7 +250,7 @@ class OnlinePlayerService : LifecycleService() { if (seekToPosition != 0L) { player?.seekTo(seekToPosition) } else if (PlayerHelper.watchPositionsAudio) { - PlayerHelper.getPosition(videoId, streams?.duration)?.let { + PlayerHelper.getStoredWatchPosition(videoId, streams?.duration)?.let { player?.seekTo(it) } } @@ -392,6 +394,8 @@ class OnlinePlayerService : LifecycleService() { player?.stop() player?.release() + watchPositionTimer?.cancel() + // 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) diff --git a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt index d44c9dad2..4ebed3c17 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/OfflinePlayerActivity.kt @@ -4,6 +4,8 @@ import android.content.pm.ActivityInfo import android.media.session.PlaybackState import android.net.Uri import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.format.DateUtils import android.view.KeyEvent import androidx.activity.viewModels @@ -42,9 +44,13 @@ import kotlin.io.path.exists import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.Timer +import java.util.TimerTask @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) class OfflinePlayerActivity : BaseActivity() { + private val handler = Handler(Looper.getMainLooper()) + private lateinit var binding: ActivityOfflinePlayerBinding private lateinit var videoId: String private lateinit var player: ExoPlayer @@ -55,6 +61,8 @@ class OfflinePlayerActivity : BaseActivity() { private lateinit var playerBinding: ExoStyledPlayerControlViewBinding private val playerViewModel: PlayerViewModel by viewModels() + private var watchPositionTimer: Timer? = null + private val playerListener = object : Player.Listener { override fun onEvents(player: Player, events: Player.Events) { super.onEvents(player, events) @@ -64,6 +72,22 @@ class OfflinePlayerActivity : BaseActivity() { ) } + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + + // Start or pause watch position timer + if (isPlaying) { + watchPositionTimer = Timer() + watchPositionTimer!!.scheduleAtFixedRate(object : TimerTask() { + override fun run() { + handler.post(this@OfflinePlayerActivity::saveWatchPosition) + } + }, PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS, PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS) + } else { + watchPositionTimer?.cancel() + } + } + override fun onPlaybackStateChanged(playbackState: Int) { super.onPlaybackStateChanged(playbackState) // setup seekbar preview @@ -154,6 +178,12 @@ class OfflinePlayerActivity : BaseActivity() { player.playWhenReady = PlayerHelper.playAutomatically player.prepare() + + if (PlayerHelper.watchPositionsVideo) { + PlayerHelper.getStoredWatchPosition(videoId, downloadInfo.download.duration)?.let { + player.seekTo(it) + } + } } } @@ -205,6 +235,12 @@ class OfflinePlayerActivity : BaseActivity() { } } + private fun saveWatchPosition() { + if (!PlayerHelper.watchPositionsVideo) return + + PlayerHelper.saveWatchPosition(player, videoId) + } + override fun onResume() { playerViewModel.isFullscreen.value = true super.onResume() @@ -216,7 +252,11 @@ class OfflinePlayerActivity : BaseActivity() { } override fun onDestroy() { + saveWatchPosition() + player.release() + watchPositionTimer?.cancel() + super.onDestroy() } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 53e73a5d5..298c2cf80 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -62,8 +62,6 @@ import com.github.libretube.constants.IntentData import com.github.libretube.constants.PreferenceKeys import com.github.libretube.databinding.FragmentPlayerBinding import com.github.libretube.db.DatabaseHelper -import com.github.libretube.db.DatabaseHolder.Database -import com.github.libretube.db.obj.WatchPosition import com.github.libretube.enums.PlayerEvent import com.github.libretube.enums.ShareObjectType import com.github.libretube.extensions.formatShort @@ -114,7 +112,6 @@ import com.github.libretube.util.PlayingQueue import com.github.libretube.util.TextUtils import com.github.libretube.util.TextUtils.toTimeInSeconds import com.github.libretube.util.YoutubeHlsPlaylistParser -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -275,7 +272,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { override fun run() { handler.post(this@PlayerFragment::saveWatchPosition) } - }, 1000, 1000) + }, PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS, PlayerHelper.WATCH_POSITION_TIMER_DELAY_MS) } else { watchPositionTimer?.cancel() } @@ -861,17 +858,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { // save the watch position if video isn't finished and option enabled private fun saveWatchPosition() { - if (!this::exoPlayer.isInitialized || !PlayerHelper.watchPositionsVideo || isTransitioning || - exoPlayer.duration == C.TIME_UNSET || exoPlayer.currentPosition in listOf( - 0L, - C.TIME_UNSET - ) - ) { - return - } - val watchPosition = WatchPosition(videoId, exoPlayer.currentPosition) - CoroutineScope(Dispatchers.IO).launch { - Database.watchPositionDao().insert(watchPosition) + if (this::exoPlayer.isInitialized && !isTransitioning && PlayerHelper.watchPositionsVideo) { + PlayerHelper.saveWatchPosition(exoPlayer, videoId) } } @@ -1290,7 +1278,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions { timeStamp = 0L } else if (!streams.livestream) { // seek to the saved watch position - PlayerHelper.getPosition(videoId, streams.duration)?.let { + PlayerHelper.getStoredWatchPosition(videoId, streams.duration)?.let { exoPlayer.seekTo(it) } }