fix(playback): fall back to transcoding on direct play errors

This commit is contained in:
2026-06-01 21:25:21 +02:00
parent 72cb574753
commit 38f1f94956
4 changed files with 159 additions and 15 deletions

View File

@@ -9,6 +9,8 @@ import hu.bbara.purefin.core.Offline
import hu.bbara.purefin.core.data.LocalMediaRepository
import hu.bbara.purefin.core.data.NetworkMonitor
import hu.bbara.purefin.core.data.PlayableMediaRepository
import hu.bbara.purefin.core.data.PlaybackMediaItemTag
import hu.bbara.purefin.core.data.PlaybackMethod
import hu.bbara.purefin.core.data.PlaybackReportContext
import hu.bbara.purefin.core.data.UserSessionRepository
import hu.bbara.purefin.core.download.MediaDownloadController
@@ -17,6 +19,7 @@ import hu.bbara.purefin.core.image.ImageUrlBuilder
import hu.bbara.purefin.core.player.preference.TrackPreferencesRepository
import hu.bbara.purefin.data.jellyfin.client.JellyfinApiClient
import hu.bbara.purefin.data.jellyfin.playback.JellyfinPlaybackResolver
import hu.bbara.purefin.data.jellyfin.playback.PlaybackSource
import hu.bbara.purefin.model.Episode
import hu.bbara.purefin.model.MediaSegment
import hu.bbara.purefin.model.PlayableMedia
@@ -87,7 +90,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
val playbackSource = jellyfinPlaybackResolver.getPlaybackSource(mediaId) ?: return null
val mediaItem = if (downloadedMediaItem == null) {
getMediaItem(baseItem, playbackSource.directPlayUrl)
getMediaItem(baseItem, playbackSource)
} else {
getDownloadedMediaItem(baseItem, downloadedMediaItem)
}
@@ -170,7 +173,10 @@ class DefaultPlayableMediaRepository @Inject constructor(
return null
}
private suspend fun getMediaItem(baseItem: BaseItemDto, url: String): MediaItem = withContext(Dispatchers.IO) {
private suspend fun getMediaItem(
baseItem: BaseItemDto,
playbackSource: PlaybackSource,
): MediaItem = withContext(Dispatchers.IO) {
val mediaId = baseItem.id
val baseItem = jellyfinApiClient.getItemInfo(mediaId)
@@ -179,12 +185,11 @@ class DefaultPlayableMediaRepository @Inject constructor(
val mediaItem = createMediaItem(
mediaId = mediaId.toString(),
url = url,
url = playbackSource.directPlayUrl,
title = baseItem?.name ?: "Unknown",
subtitle = seasonEpisodeLabel(baseItem),
artworkUrl = artworkUrl,
// TODO
playbackReportContext = null,
playbackTag = playbackSource.toPlaybackMediaItemTag(),
)
return@withContext mediaItem
@@ -243,7 +248,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
title: String,
subtitle: String?,
artworkUrl: String,
playbackReportContext: PlaybackReportContext?,
playbackTag: Any?,
): MediaItem {
val metadata = MediaMetadata.Builder()
.setTitle(title)
@@ -254,10 +259,20 @@ class DefaultPlayableMediaRepository @Inject constructor(
.setUri(url.toUri())
.setMediaId(mediaId)
.setMediaMetadata(metadata)
.setTag(playbackReportContext)
.setTag(playbackTag)
return builder.build()
}
private fun PlaybackSource.toPlaybackMediaItemTag(): PlaybackMediaItemTag {
return PlaybackMediaItemTag(
playbackReportContext = playbackReportContext,
transcodingFallbackUrl = transcodingUrl,
transcodingFallbackReportContext = transcodingUrl?.let {
playbackReportContext.copy(playMethod = PlaybackMethod.TRANSCODE)
},
)
}
private fun calculateResumePosition(
baseItem: BaseItemDto?,
mediaSource: MediaSourceInfo,

View File

@@ -48,20 +48,36 @@ class JellyfinPlaybackResolver @Inject constructor(
return@withContext null
}
val mediaSource = playbackInfo.mediaSources.first()
val transcodingUrl = mediaSource.transcodingUrl
?.trim()
?.takeIf { it.isNotBlank() }
?.let { url -> resolveUrl(serverUrl, url) }
PlaybackSource(
mediaSource = playbackInfo.mediaSources.first(),
mediaSource = mediaSource,
directPlayUrl = directPlayUrl,
transcodingUrl = playbackInfo.mediaSources.firstOrNull()?.transcodingUrl,
transcodingUrl = transcodingUrl,
playbackReportContext = PlaybackReportContext(
playMethod = PlaybackMethod.DIRECT_PLAY,
mediaSourceId = playbackInfo.mediaSources.firstOrNull()?.id,
audioStreamIndex = playbackInfo.mediaSources.firstOrNull()?.defaultAudioStreamIndex,
subtitleStreamIndex = playbackInfo.mediaSources.firstOrNull()?.defaultSubtitleStreamIndex,
mediaSourceId = mediaSource.id,
audioStreamIndex = mediaSource.defaultAudioStreamIndex,
subtitleStreamIndex = mediaSource.defaultSubtitleStreamIndex,
playSessionId = playbackInfo.playSessionId,
),
)
}
private fun resolveUrl(serverUrl: String, url: String): String {
if (
url.startsWith("http://", ignoreCase = true) ||
url.startsWith("https://", ignoreCase = true)
) {
return url
}
return "${serverUrl.trimEnd('/')}/${url.trimStart('/')}"
}
private companion object {
private const val TAG = "PlaybackResolver"
}