diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackOverlay.kt index 5fc2f3c6..db8b9965 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackOverlay.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackOverlay.kt @@ -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)) } } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt index df3ac97e..8df90178 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt @@ -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 { 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, + val playMethod: PlayMethod, + val playSessionId: String?, ) private val Format.idAsInt: Int? diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/TrackActivityPlaybackListener.kt b/app/src/main/java/com/github/damontecres/dolphin/util/TrackActivityPlaybackListener.kt index fe192222..736d9890 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/util/TrackActivityPlaybackListener.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/util/TrackActivityPlaybackListener.kt @@ -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 = - (if (position >= 0) position else player.currentPosition).milliseconds - Timber.Forest.v("saveActivity: itemId=$itemId, pos=$calcPosition") - // TODO parameters + withContext(Dispatchers.Main) { + (if (position >= 0) position else player.currentPosition).milliseconds + } +// 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, ), ) }