Skip to content

Commit

Permalink
Merge pull request #5669 from Bnyro/master
Browse files Browse the repository at this point in the history
feat: watch positions support for downloaded media
  • Loading branch information
Bnyro committed Feb 27, 2024
2 parents 19bc802 + 4b06bfb commit 698c0ff
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 35 deletions.
18 changes: 17 additions & 1 deletion app/src/main/java/com/github/libretube/helpers/PlayerHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<DownloadWithItems> = 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)

Expand Down Expand Up @@ -142,17 +166,33 @@ 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()
player?.release()
player = null
nowPlayingNotification = null

watchPositionTimer?.cancel()

ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
stopSelf()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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

val watchPosition = WatchPosition(videoId, it)
private fun saveWatchPosition() {
if (isTransitioning || !PlayerHelper.watchPositionsAudio) return

CoroutineScope(Dispatchers.IO).launch {
Database.watchPositionDao().insert(watchPosition)
}
}
handler.postDelayed(this::updateWatchPosition, 500)
player?.let { PlayerHelper.saveWatchPosition(it, videoId) }
}

/**
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
}
}

Expand Down Expand Up @@ -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()
Expand All @@ -216,7 +252,11 @@ class OfflinePlayerActivity : BaseActivity() {
}

override fun onDestroy() {
saveWatchPosition()

player.release()
watchPositionTimer?.cancel()

super.onDestroy()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}
Expand Down

0 comments on commit 698c0ff

Please sign in to comment.