Improvement to playback activity tracking

This commit is contained in:
Damontecres 2025-10-12 21:11:43 -04:00
parent 29e13cba0f
commit 763589fa0d
No known key found for this signature in database
3 changed files with 68 additions and 33 deletions

View file

@ -12,7 +12,9 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
@ -375,8 +377,10 @@ fun Controller(
Text(
text = "Chapters",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(start = 16.dp),
modifier = Modifier.padding(start = 16.dp, top = 16.dp),
)
} else {
Spacer(Modifier.height(32.dp))
}
}
}

View file

@ -53,6 +53,7 @@ import org.jellyfin.sdk.model.api.MediaSegmentDto
import org.jellyfin.sdk.model.api.MediaSegmentType
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.PlayMethod
import org.jellyfin.sdk.model.api.PlaybackInfoDto
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
import org.jellyfin.sdk.model.api.TrickplayInfo
@ -74,7 +75,7 @@ enum class TranscodeType {
data class StreamDecision(
val itemId: UUID,
val type: TranscodeType,
val type: PlayMethod,
val url: String,
)
@ -188,7 +189,7 @@ class PlaybackViewModel
}?.sortedWith(compareBy<AudioStream> { it.language }.thenByDescending { it.channels })
.orEmpty()
// TODO audio selection based on channel layout, etc
// TODO audio selection based on channel layout or preferences or default
val audioLanguage = preferences.userConfig.audioLanguagePreference
val audioIndex =
if (audioLanguage != null) {
@ -236,22 +237,14 @@ class PlaybackViewModel
SubtitlePlaybackMode.NONE -> null
}
Timber.v("base.mediaStreams=${base.mediaStreams}")
Timber.v("subtitleTracks=$subtitleStreams")
Timber.v("audioStreams=$audioStreams")
// Timber.v("base.mediaStreams=${base.mediaStreams}")
// Timber.v("subtitleTracks=$subtitleStreams")
// Timber.v("audioStreams=$audioStreams")
Timber.d("Selected audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
withContext(Dispatchers.Main) {
this@PlaybackViewModel.audioStreams.value = audioStreams
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
this@PlaybackViewModel.activityListener?.let {
it.release()
player.removeListener(it)
}
val activityListener =
TrackActivityPlaybackListener(api, itemId, player)
player.addListener(activityListener)
this@PlaybackViewModel.activityListener = activityListener
changeStreams(
itemId,
@ -328,9 +321,9 @@ class PlaybackViewModel
}
val transcodeType =
when {
source.supportsDirectPlay -> TranscodeType.DIRECT_PLAY
source.supportsDirectStream -> TranscodeType.DIRECT_STREAM
source.supportsTranscoding -> TranscodeType.TRANSCODE
source.supportsDirectPlay -> PlayMethod.DIRECT_PLAY
source.supportsDirectStream -> PlayMethod.DIRECT_STREAM
source.supportsTranscoding -> PlayMethod.TRANSCODE
else -> throw Exception("No supported playback method")
}
val decision = StreamDecision(itemId, transcodeType, mediaUrl)
@ -370,9 +363,24 @@ class PlaybackViewModel
subtitleIndex,
source.id?.toUUIDOrNull(),
listOf(),
playMethod = transcodeType,
playSessionId = response.playSessionId,
)
withContext(Dispatchers.Main) {
this@PlaybackViewModel.activityListener?.let {
it.release()
player.removeListener(it)
}
val activityListener =
TrackActivityPlaybackListener(
api = api,
player = player,
playback = playback,
)
player.addListener(activityListener)
this@PlaybackViewModel.activityListener = activityListener
duration.value = source.runTimeTicks?.ticks
stream.value = decision
currentPlayback.value = playback
@ -581,6 +589,8 @@ data class CurrentPlayback(
val subtitleIndex: Int?,
val mediaSourceId: UUID?,
val tracks: List<TrackSupport>,
val playMethod: PlayMethod,
val playSessionId: String?,
)
private val Format.idAsInt: Int?

View file

@ -3,21 +3,22 @@ package com.github.damontecres.dolphin.util
import androidx.annotation.OptIn
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import com.github.damontecres.dolphin.ui.playback.CurrentPlayback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.playStateApi
import org.jellyfin.sdk.model.api.PlayMethod
import org.jellyfin.sdk.model.api.PlaybackOrder
import org.jellyfin.sdk.model.api.PlaybackProgressInfo
import org.jellyfin.sdk.model.api.PlaybackStartInfo
import org.jellyfin.sdk.model.api.PlaybackStopInfo
import org.jellyfin.sdk.model.api.RepeatMode
import org.jellyfin.sdk.model.extensions.inWholeTicks
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import kotlin.time.Duration.Companion.milliseconds
@ -29,8 +30,8 @@ import kotlin.time.Duration.Companion.seconds
@OptIn(UnstableApi::class)
class TrackActivityPlaybackListener(
private val api: ApiClient,
private val itemId: UUID,
private val player: Player,
var playback: CurrentPlayback,
) : Player.Listener {
private val coroutineScope = CoroutineScope(Dispatchers.Main)
private val task: TimerTask
@ -41,6 +42,21 @@ class TrackActivityPlaybackListener(
private var incrementedPlayCount = AtomicBoolean(false)
init {
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
api.playStateApi.reportPlaybackStart(
PlaybackStartInfo(
canSeek = true,
itemId = playback.itemId,
isPaused = withContext(Dispatchers.Main) { !player.isPlaying },
playMethod = playback.playMethod,
repeatMode = RepeatMode.REPEAT_NONE,
playbackOrder = PlaybackOrder.DEFAULT,
isMuted = false,
audioStreamIndex = playback.audioIndex,
subtitleStreamIndex = playback.subtitleIndex,
),
)
}
val saveActivityInterval = 10.seconds.inWholeMilliseconds
val delay = 1.seconds.inWholeMilliseconds
// Every x seconds, check if the video is playing
@ -74,12 +90,13 @@ class TrackActivityPlaybackListener(
fun release() {
task.cancel()
TIMER.purge()
coroutineScope.launch(ExceptionHandler()) {
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
api.playStateApi.reportPlaybackStopped(
PlaybackStopInfo(
itemId = itemId,
positionTicks = player.currentPosition.milliseconds.inWholeTicks,
itemId = playback.itemId,
positionTicks = withContext(Dispatchers.Main) { player.currentPosition.milliseconds.inWholeTicks },
failed = false,
playSessionId = playback.playSessionId,
),
)
}
@ -98,28 +115,32 @@ class TrackActivityPlaybackListener(
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_ENDED) {
Timber.Forest.v("onPlaybackStateChanged STATE_ENDED")
// TODO mark as watched
Timber.v("onPlaybackStateChanged STATE_ENDED")
saveActivity(player.duration)
}
}
private fun saveActivity(position: Long) {
coroutineScope.launch(ExceptionHandler()) {
val totalDuration = totalPlayDurationMilliseconds.get()
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
// val totalDuration = totalPlayDurationMilliseconds.get()
val calcPosition =
withContext(Dispatchers.Main) {
(if (position >= 0) position else player.currentPosition).milliseconds
Timber.Forest.v("saveActivity: itemId=$itemId, pos=$calcPosition")
// TODO parameters
}
// Timber.v("saveActivity: itemId=$itemId, pos=$calcPosition")
api.playStateApi.reportPlaybackProgress(
PlaybackProgressInfo(
itemId = itemId,
itemId = playback.itemId,
positionTicks = calcPosition.inWholeTicks,
canSeek = true,
isPaused = !player.isPlaying,
isPaused = withContext(Dispatchers.Main) { !player.isPlaying },
isMuted = false,
playMethod = PlayMethod.DIRECT_PLAY,
playMethod = playback.playMethod,
repeatMode = RepeatMode.REPEAT_NONE,
playbackOrder = PlaybackOrder.DEFAULT,
playSessionId = playback.playSessionId,
audioStreamIndex = playback.audioIndex,
subtitleStreamIndex = playback.subtitleIndex,
),
)
}