mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Improvement to playback activity tracking
This commit is contained in:
parent
29e13cba0f
commit
763589fa0d
3 changed files with 68 additions and 33 deletions
|
|
@ -12,7 +12,9 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
|
@ -375,8 +377,10 @@ fun Controller(
|
||||||
Text(
|
Text(
|
||||||
text = "Chapters",
|
text = "Chapters",
|
||||||
style = MaterialTheme.typography.titleLarge,
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import org.jellyfin.sdk.model.api.MediaSegmentDto
|
||||||
import org.jellyfin.sdk.model.api.MediaSegmentType
|
import org.jellyfin.sdk.model.api.MediaSegmentType
|
||||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
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.PlaybackInfoDto
|
||||||
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
|
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
|
||||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||||
|
|
@ -74,7 +75,7 @@ enum class TranscodeType {
|
||||||
|
|
||||||
data class StreamDecision(
|
data class StreamDecision(
|
||||||
val itemId: UUID,
|
val itemId: UUID,
|
||||||
val type: TranscodeType,
|
val type: PlayMethod,
|
||||||
val url: String,
|
val url: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -188,7 +189,7 @@ class PlaybackViewModel
|
||||||
}?.sortedWith(compareBy<AudioStream> { it.language }.thenByDescending { it.channels })
|
}?.sortedWith(compareBy<AudioStream> { it.language }.thenByDescending { it.channels })
|
||||||
.orEmpty()
|
.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 audioLanguage = preferences.userConfig.audioLanguagePreference
|
||||||
val audioIndex =
|
val audioIndex =
|
||||||
if (audioLanguage != null) {
|
if (audioLanguage != null) {
|
||||||
|
|
@ -236,22 +237,14 @@ class PlaybackViewModel
|
||||||
SubtitlePlaybackMode.NONE -> null
|
SubtitlePlaybackMode.NONE -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.v("base.mediaStreams=${base.mediaStreams}")
|
// Timber.v("base.mediaStreams=${base.mediaStreams}")
|
||||||
Timber.v("subtitleTracks=$subtitleStreams")
|
// Timber.v("subtitleTracks=$subtitleStreams")
|
||||||
Timber.v("audioStreams=$audioStreams")
|
// Timber.v("audioStreams=$audioStreams")
|
||||||
Timber.d("Selected audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
Timber.d("Selected audioIndex=$audioIndex, subtitleIndex=$subtitleIndex")
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@PlaybackViewModel.audioStreams.value = audioStreams
|
this@PlaybackViewModel.audioStreams.value = audioStreams
|
||||||
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
|
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(
|
changeStreams(
|
||||||
itemId,
|
itemId,
|
||||||
|
|
@ -328,9 +321,9 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
val transcodeType =
|
val transcodeType =
|
||||||
when {
|
when {
|
||||||
source.supportsDirectPlay -> TranscodeType.DIRECT_PLAY
|
source.supportsDirectPlay -> PlayMethod.DIRECT_PLAY
|
||||||
source.supportsDirectStream -> TranscodeType.DIRECT_STREAM
|
source.supportsDirectStream -> PlayMethod.DIRECT_STREAM
|
||||||
source.supportsTranscoding -> TranscodeType.TRANSCODE
|
source.supportsTranscoding -> PlayMethod.TRANSCODE
|
||||||
else -> throw Exception("No supported playback method")
|
else -> throw Exception("No supported playback method")
|
||||||
}
|
}
|
||||||
val decision = StreamDecision(itemId, transcodeType, mediaUrl)
|
val decision = StreamDecision(itemId, transcodeType, mediaUrl)
|
||||||
|
|
@ -370,9 +363,24 @@ class PlaybackViewModel
|
||||||
subtitleIndex,
|
subtitleIndex,
|
||||||
source.id?.toUUIDOrNull(),
|
source.id?.toUUIDOrNull(),
|
||||||
listOf(),
|
listOf(),
|
||||||
|
playMethod = transcodeType,
|
||||||
|
playSessionId = response.playSessionId,
|
||||||
)
|
)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
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
|
duration.value = source.runTimeTicks?.ticks
|
||||||
stream.value = decision
|
stream.value = decision
|
||||||
currentPlayback.value = playback
|
currentPlayback.value = playback
|
||||||
|
|
@ -581,6 +589,8 @@ data class CurrentPlayback(
|
||||||
val subtitleIndex: Int?,
|
val subtitleIndex: Int?,
|
||||||
val mediaSourceId: UUID?,
|
val mediaSourceId: UUID?,
|
||||||
val tracks: List<TrackSupport>,
|
val tracks: List<TrackSupport>,
|
||||||
|
val playMethod: PlayMethod,
|
||||||
|
val playSessionId: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val Format.idAsInt: Int?
|
private val Format.idAsInt: Int?
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,22 @@ package com.github.damontecres.dolphin.util
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import com.github.damontecres.dolphin.ui.playback.CurrentPlayback
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
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.PlaybackOrder
|
||||||
import org.jellyfin.sdk.model.api.PlaybackProgressInfo
|
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.PlaybackStopInfo
|
||||||
import org.jellyfin.sdk.model.api.RepeatMode
|
import org.jellyfin.sdk.model.api.RepeatMode
|
||||||
import org.jellyfin.sdk.model.extensions.inWholeTicks
|
import org.jellyfin.sdk.model.extensions.inWholeTicks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
import java.util.TimerTask
|
import java.util.TimerTask
|
||||||
import java.util.UUID
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
@ -29,8 +30,8 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
class TrackActivityPlaybackListener(
|
class TrackActivityPlaybackListener(
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
private val itemId: UUID,
|
|
||||||
private val player: Player,
|
private val player: Player,
|
||||||
|
var playback: CurrentPlayback,
|
||||||
) : Player.Listener {
|
) : Player.Listener {
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||||
private val task: TimerTask
|
private val task: TimerTask
|
||||||
|
|
@ -41,6 +42,21 @@ class TrackActivityPlaybackListener(
|
||||||
private var incrementedPlayCount = AtomicBoolean(false)
|
private var incrementedPlayCount = AtomicBoolean(false)
|
||||||
|
|
||||||
init {
|
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 saveActivityInterval = 10.seconds.inWholeMilliseconds
|
||||||
val delay = 1.seconds.inWholeMilliseconds
|
val delay = 1.seconds.inWholeMilliseconds
|
||||||
// Every x seconds, check if the video is playing
|
// Every x seconds, check if the video is playing
|
||||||
|
|
@ -74,12 +90,13 @@ class TrackActivityPlaybackListener(
|
||||||
fun release() {
|
fun release() {
|
||||||
task.cancel()
|
task.cancel()
|
||||||
TIMER.purge()
|
TIMER.purge()
|
||||||
coroutineScope.launch(ExceptionHandler()) {
|
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
api.playStateApi.reportPlaybackStopped(
|
api.playStateApi.reportPlaybackStopped(
|
||||||
PlaybackStopInfo(
|
PlaybackStopInfo(
|
||||||
itemId = itemId,
|
itemId = playback.itemId,
|
||||||
positionTicks = player.currentPosition.milliseconds.inWholeTicks,
|
positionTicks = withContext(Dispatchers.Main) { player.currentPosition.milliseconds.inWholeTicks },
|
||||||
failed = false,
|
failed = false,
|
||||||
|
playSessionId = playback.playSessionId,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -98,28 +115,32 @@ class TrackActivityPlaybackListener(
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
if (playbackState == Player.STATE_ENDED) {
|
if (playbackState == Player.STATE_ENDED) {
|
||||||
Timber.Forest.v("onPlaybackStateChanged STATE_ENDED")
|
Timber.v("onPlaybackStateChanged STATE_ENDED")
|
||||||
// TODO mark as watched
|
saveActivity(player.duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveActivity(position: Long) {
|
private fun saveActivity(position: Long) {
|
||||||
coroutineScope.launch(ExceptionHandler()) {
|
coroutineScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
val totalDuration = totalPlayDurationMilliseconds.get()
|
// val totalDuration = totalPlayDurationMilliseconds.get()
|
||||||
val calcPosition =
|
val calcPosition =
|
||||||
(if (position >= 0) position else player.currentPosition).milliseconds
|
withContext(Dispatchers.Main) {
|
||||||
Timber.Forest.v("saveActivity: itemId=$itemId, pos=$calcPosition")
|
(if (position >= 0) position else player.currentPosition).milliseconds
|
||||||
// TODO parameters
|
}
|
||||||
|
// Timber.v("saveActivity: itemId=$itemId, pos=$calcPosition")
|
||||||
api.playStateApi.reportPlaybackProgress(
|
api.playStateApi.reportPlaybackProgress(
|
||||||
PlaybackProgressInfo(
|
PlaybackProgressInfo(
|
||||||
itemId = itemId,
|
itemId = playback.itemId,
|
||||||
positionTicks = calcPosition.inWholeTicks,
|
positionTicks = calcPosition.inWholeTicks,
|
||||||
canSeek = true,
|
canSeek = true,
|
||||||
isPaused = !player.isPlaying,
|
isPaused = withContext(Dispatchers.Main) { !player.isPlaying },
|
||||||
isMuted = false,
|
isMuted = false,
|
||||||
playMethod = PlayMethod.DIRECT_PLAY,
|
playMethod = playback.playMethod,
|
||||||
repeatMode = RepeatMode.REPEAT_NONE,
|
repeatMode = RepeatMode.REPEAT_NONE,
|
||||||
playbackOrder = PlaybackOrder.DEFAULT,
|
playbackOrder = PlaybackOrder.DEFAULT,
|
||||||
|
playSessionId = playback.playSessionId,
|
||||||
|
audioStreamIndex = playback.audioIndex,
|
||||||
|
subtitleStreamIndex = playback.subtitleIndex,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue