mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat(player): load external subtitles including ASS/SSA via SubtitleConfiguration
This commit is contained in:
@@ -20,6 +20,7 @@ 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.data.jellyfin.playback.SubtitleConfigurationMapper
|
||||
import hu.bbara.purefin.model.Episode
|
||||
import hu.bbara.purefin.model.MediaSegment
|
||||
import hu.bbara.purefin.model.PlayableMedia
|
||||
@@ -50,6 +51,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
private val userSessionRepository: UserSessionRepository,
|
||||
private val mediaDownloadController: MediaDownloadController,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val subtitleConfigurationMapper: SubtitleConfigurationMapper,
|
||||
@param:Offline private val offlineMediaRepository: LocalMediaRepository,
|
||||
private val offlineMediaManager: OfflineMediaManager,
|
||||
) : PlayableMediaRepository {
|
||||
@@ -198,6 +200,11 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
val serverUrl = userSessionRepository.serverUrl.first()
|
||||
val artworkUrl = ImageUrlBuilder.toImageUrl(serverUrl, mediaId, ArtworkKind.PRIMARY)
|
||||
|
||||
val subtitleConfigurations = subtitleConfigurationMapper.createSubtitleConfigurations(
|
||||
mediaSource = playbackSource.mediaSource,
|
||||
serverUrl = serverUrl,
|
||||
)
|
||||
|
||||
val mediaItem = createMediaItem(
|
||||
mediaId = mediaId.toString(),
|
||||
url = playbackSource.directPlayUrl,
|
||||
@@ -205,6 +212,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
subtitle = seasonEpisodeLabel(baseItem),
|
||||
artworkUrl = artworkUrl,
|
||||
playbackTag = playbackSource.toPlaybackMediaItemTag(),
|
||||
subtitleConfigurations = subtitleConfigurations,
|
||||
)
|
||||
|
||||
return@withContext mediaItem
|
||||
@@ -283,6 +291,7 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
subtitle: String?,
|
||||
artworkUrl: String,
|
||||
playbackTag: Any?,
|
||||
subtitleConfigurations: List<MediaItem.SubtitleConfiguration> = emptyList(),
|
||||
): MediaItem {
|
||||
val metadata = MediaMetadata.Builder()
|
||||
.setTitle(title)
|
||||
@@ -294,6 +303,9 @@ class DefaultPlayableMediaRepository @Inject constructor(
|
||||
.setMediaId(mediaId)
|
||||
.setMediaMetadata(metadata)
|
||||
.setTag(playbackTag)
|
||||
if (subtitleConfigurations.isNotEmpty()) {
|
||||
builder.setSubtitleConfigurations(subtitleConfigurations)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package hu.bbara.purefin.data.jellyfin.playback
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||
import javax.inject.Inject
|
||||
|
||||
class SubtitleConfigurationMapper @Inject constructor() {
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun createSubtitleConfigurations(
|
||||
mediaSource: MediaSourceInfo,
|
||||
serverUrl: String,
|
||||
): List<MediaItem.SubtitleConfiguration> {
|
||||
val streams = mediaSource.mediaStreams.orEmpty()
|
||||
if (streams.isEmpty()) return emptyList()
|
||||
|
||||
return streams.mapNotNull { stream ->
|
||||
if (stream.type != MediaStreamType.SUBTITLE) return@mapNotNull null
|
||||
if (stream.deliveryMethod != SubtitleDeliveryMethod.EXTERNAL) return@mapNotNull null
|
||||
|
||||
val deliveryUrl = stream.deliveryUrl?.trim()?.takeIf { it.isNotBlank() }
|
||||
?: return@mapNotNull null
|
||||
|
||||
val mimeType = mimeTypeForCodec(stream.codec)
|
||||
?: return@mapNotNull null
|
||||
|
||||
val resolvedUri = resolveUrl(serverUrl, deliveryUrl)
|
||||
|
||||
val selectionFlags = buildSelectionFlags(stream.isForced, stream.isDefault)
|
||||
|
||||
MediaItem.SubtitleConfiguration.Builder(resolvedUri.toUri())
|
||||
.setMimeType(mimeType)
|
||||
.setLanguage(stream.language)
|
||||
.setLabel(stream.displayTitle)
|
||||
.setSelectionFlags(selectionFlags)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun mimeTypeForCodec(codec: String?): String? {
|
||||
if (codec.isNullOrBlank()) return null
|
||||
return when (codec.lowercase().trim()) {
|
||||
"srt", "subrip" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"vtt", "webvtt" -> MimeTypes.TEXT_VTT
|
||||
"ttml" -> MimeTypes.APPLICATION_TTML
|
||||
"ass", "ssa" -> MimeTypes.TEXT_SSA
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun buildSelectionFlags(isForced: Boolean?, isDefault: Boolean?): Int {
|
||||
var flags = 0
|
||||
if (isForced == true) flags = flags or C.SELECTION_FLAG_FORCED
|
||||
if (isDefault == true) flags = flags or C.SELECTION_FLAG_DEFAULT
|
||||
return flags
|
||||
}
|
||||
|
||||
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 fun String.toUri() = android.net.Uri.parse(this)
|
||||
}
|
||||
Reference in New Issue
Block a user