mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
fix: correct subtitle display after seeking by preferring embedded delivery
Subtitles were not showing the active cue after rewinding/seeking because they were delivered externally and Media3 only picks up forward-starting cues. Changed device profile to prefer EMBED delivery so subtitles stay in the container where Media3's extraction-time parser handles them correctly. Also added support for attaching external subtitle tracks to MediaItem when embedding isn't possible.
This commit is contained in:
@@ -51,6 +51,14 @@ object AndroidDeviceProfile {
|
||||
codecProfiles = emptyList(),
|
||||
|
||||
subtitleProfiles = listOf(
|
||||
// Prefer EMBED so subtitles stay in the container — this gives
|
||||
// correct cues after seeking (Media3 parses them at extraction time).
|
||||
SubtitleProfile("srt", SubtitleDeliveryMethod.EMBED),
|
||||
SubtitleProfile("ass", SubtitleDeliveryMethod.EMBED),
|
||||
SubtitleProfile("ssa", SubtitleDeliveryMethod.EMBED),
|
||||
SubtitleProfile("subrip", SubtitleDeliveryMethod.EMBED),
|
||||
SubtitleProfile("sub", SubtitleDeliveryMethod.EMBED),
|
||||
// EXTERNAL fallback for when embedding isn't possible
|
||||
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
package hu.bbara.purefin.player.data
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import hu.bbara.purefin.client.JellyfinApiClient
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import androidx.core.net.toUri
|
||||
import hu.bbara.purefin.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.session.UserSessionRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
class PlayerMediaRepository @Inject constructor(
|
||||
@@ -37,12 +44,15 @@ class PlayerMediaRepository @Inject constructor(
|
||||
val serverUrl = userSessionRepository.serverUrl.first()
|
||||
val artworkUrl = JellyfinImageHelper.toImageUrl(serverUrl, mediaId, ImageType.PRIMARY)
|
||||
|
||||
val subtitleConfigs = buildExternalSubtitleConfigs(serverUrl, mediaId, selectedMediaSource)
|
||||
|
||||
val mediaItem = createMediaItem(
|
||||
mediaId = mediaId.toString(),
|
||||
playbackUrl = playbackUrl,
|
||||
title = baseItem?.name ?: selectedMediaSource.name!!,
|
||||
subtitle = "S${baseItem!!.parentIndexNumber}:E${baseItem.indexNumber}",
|
||||
artworkUrl = artworkUrl
|
||||
artworkUrl = artworkUrl,
|
||||
subtitleConfigurations = subtitleConfigs
|
||||
)
|
||||
|
||||
Pair(mediaItem, resumePositionMs)
|
||||
@@ -88,22 +98,81 @@ class PlayerMediaRepository @Inject constructor(
|
||||
mediaSource = selectedMediaSource
|
||||
) ?: return@mapNotNull null
|
||||
val artworkUrl = JellyfinImageHelper.toImageUrl(serverUrl, id, ImageType.PRIMARY)
|
||||
val subtitleConfigs = buildExternalSubtitleConfigs(serverUrl, id, selectedMediaSource)
|
||||
createMediaItem(
|
||||
mediaId = stringId,
|
||||
playbackUrl = playbackUrl,
|
||||
title = episode.name ?: selectedMediaSource.name!!,
|
||||
subtitle = "S${episode.parentIndexNumber}:E${episode.indexNumber}",
|
||||
artworkUrl = artworkUrl
|
||||
artworkUrl = artworkUrl,
|
||||
subtitleConfigurations = subtitleConfigs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun buildExternalSubtitleConfigs(
|
||||
serverUrl: String,
|
||||
mediaId: UUID,
|
||||
mediaSource: MediaSourceInfo
|
||||
): List<MediaItem.SubtitleConfiguration> {
|
||||
val streams = mediaSource.mediaStreams ?: return emptyList()
|
||||
val mediaSourceId = mediaSource.id ?: return emptyList()
|
||||
val baseUrl = serverUrl.trimEnd('/')
|
||||
|
||||
return streams
|
||||
.filter { it.type == MediaStreamType.SUBTITLE && it.deliveryMethod == SubtitleDeliveryMethod.EXTERNAL }
|
||||
.mapNotNull { stream ->
|
||||
val codec = stream.codec ?: return@mapNotNull null
|
||||
val mimeType = subtitleCodecToMimeType(codec) ?: return@mapNotNull null
|
||||
// Use deliveryUrl from server if available, otherwise construct it
|
||||
val url = if (!stream.deliveryUrl.isNullOrBlank()) {
|
||||
if (stream.deliveryUrl!!.startsWith("http")) {
|
||||
stream.deliveryUrl!!
|
||||
} else {
|
||||
"$baseUrl${stream.deliveryUrl}"
|
||||
}
|
||||
} else {
|
||||
val format = if (codec == "subrip") "srt" else codec
|
||||
"$baseUrl/Videos/$mediaId/$mediaSourceId/Subtitles/${stream.index}/0/Stream.$format"
|
||||
}
|
||||
|
||||
Log.d("PlayerMediaRepo", "External subtitle: ${stream.displayTitle} ($codec) -> $url")
|
||||
|
||||
MediaItem.SubtitleConfiguration.Builder(url.toUri())
|
||||
.setMimeType(mimeType)
|
||||
.setLanguage(stream.language)
|
||||
.setLabel(stream.displayTitle ?: stream.language ?: "Track ${stream.index}")
|
||||
.setSelectionFlags(
|
||||
if (stream.isForced) C.SELECTION_FLAG_FORCED
|
||||
else if (stream.isDefault) C.SELECTION_FLAG_DEFAULT
|
||||
else 0
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun subtitleCodecToMimeType(codec: String): String? = when (codec.lowercase()) {
|
||||
"srt", "subrip" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"ass", "ssa" -> MimeTypes.TEXT_SSA
|
||||
"vtt", "webvtt" -> MimeTypes.TEXT_VTT
|
||||
"ttml", "dfxp" -> MimeTypes.APPLICATION_TTML
|
||||
"sub", "microdvd" -> MimeTypes.APPLICATION_SUBRIP // sub often converted to srt by Jellyfin
|
||||
"pgs", "pgssub" -> MimeTypes.APPLICATION_PGS
|
||||
else -> {
|
||||
Log.w("PlayerMediaRepo", "Unknown subtitle codec: $codec")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createMediaItem(
|
||||
mediaId: String,
|
||||
playbackUrl: String,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
artworkUrl: String
|
||||
artworkUrl: String,
|
||||
subtitleConfigurations: List<MediaItem.SubtitleConfiguration> = emptyList()
|
||||
): MediaItem {
|
||||
val metadata = MediaMetadata.Builder()
|
||||
.setTitle(title)
|
||||
@@ -114,6 +183,7 @@ class PlayerMediaRepository @Inject constructor(
|
||||
.setUri(playbackUrl.toUri())
|
||||
.setMediaId(mediaId)
|
||||
.setMediaMetadata(metadata)
|
||||
.setSubtitleConfigurations(subtitleConfigurations)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user