mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
feat: improve subtitle handling by implementing coroutine-based MIME type resolution and job management
This commit is contained in:
parent
6bdd907f13
commit
37855a9981
1 changed files with 109 additions and 28 deletions
|
|
@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
|
|
@ -40,7 +41,13 @@ import androidx.media3.ui.PlayerView
|
|||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.media3.ui.CaptionStyleCompat
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
private const val TAG = "NuvioPlayer"
|
||||
|
||||
|
|
@ -59,6 +66,7 @@ actual fun PlatformPlayerSurface(
|
|||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val latestOnSnapshot = rememberUpdatedState(onSnapshot)
|
||||
val latestOnError = rememberUpdatedState(onError)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val playerSettings = remember {
|
||||
PlayerSettingsRepository.ensureLoaded()
|
||||
|
|
@ -109,6 +117,7 @@ actual fun PlatformPlayerSurface(
|
|||
val pendingSubtitleTrackIndex = remember { mutableListOf<Int>() }
|
||||
var playerViewRef by remember { mutableStateOf<PlayerView?>(null) }
|
||||
var currentSubtitleStyle by remember { mutableStateOf(SubtitleStyleState.DEFAULT) }
|
||||
var subtitleSelectionJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
DisposableEffect(exoPlayer) {
|
||||
val listener = object : Player.Listener {
|
||||
|
|
@ -156,10 +165,12 @@ actual fun PlatformPlayerSurface(
|
|||
}
|
||||
latestOnSnapshot.value(exoPlayer.snapshot())
|
||||
}
|
||||
|
||||
}
|
||||
exoPlayer.addListener(listener)
|
||||
onDispose {
|
||||
exoPlayer.removeListener(listener)
|
||||
subtitleSelectionJob?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -249,35 +260,43 @@ actual fun PlatformPlayerSurface(
|
|||
|
||||
override fun setSubtitleUri(url: String) {
|
||||
Log.d(TAG, "setSubtitleUri: url=$url")
|
||||
Log.d(TAG, "setSubtitleUri: mime=${guessSubtitleMime(url)}")
|
||||
val currentPosition = exoPlayer.currentPosition
|
||||
val wasPlaying = exoPlayer.isPlaying
|
||||
val currentMediaItem = exoPlayer.currentMediaItem ?: run {
|
||||
Log.e(TAG, "setSubtitleUri: currentMediaItem is null, aborting")
|
||||
return
|
||||
subtitleSelectionJob?.cancel()
|
||||
subtitleSelectionJob = coroutineScope.launch {
|
||||
val currentPosition = exoPlayer.currentPosition
|
||||
val wasPlaying = exoPlayer.isPlaying
|
||||
val currentMediaItem = exoPlayer.currentMediaItem ?: run {
|
||||
Log.e(TAG, "setSubtitleUri: currentMediaItem is null, aborting")
|
||||
return@launch
|
||||
}
|
||||
val resolvedMime = withContext(Dispatchers.IO) {
|
||||
resolveSubtitleMimeType(url)
|
||||
}
|
||||
Log.d(TAG, "setSubtitleUri: currentPosition=$currentPosition, wasPlaying=$wasPlaying")
|
||||
val subtitleConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(url))
|
||||
.setMimeType(resolvedMime)
|
||||
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
||||
.setRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.build()
|
||||
Log.d(
|
||||
TAG,
|
||||
"setSubtitleUri: subtitleConfig built, uri=${subtitleConfig.uri}, mime=${subtitleConfig.mimeType}, selectionFlags=${subtitleConfig.selectionFlags}"
|
||||
)
|
||||
val newMediaItem = currentMediaItem.buildUpon()
|
||||
.setSubtitleConfigurations(listOf(subtitleConfig))
|
||||
.build()
|
||||
Log.d(TAG, "setSubtitleUri: newMediaItem subtitleConfigs count=${newMediaItem.localConfiguration?.subtitleConfigurations?.size}")
|
||||
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
||||
.clearOverridesOfType(C.TRACK_TYPE_TEXT)
|
||||
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.build()
|
||||
Log.d(TAG, "setSubtitleUri: track params set before prepare, textDisabled=${exoPlayer.trackSelectionParameters.disabledTrackTypes.contains(C.TRACK_TYPE_TEXT)}")
|
||||
exoPlayer.setMediaItem(newMediaItem, currentPosition)
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.playWhenReady = wasPlaying
|
||||
Log.d(TAG, "setSubtitleUri: prepare() called, waiting for STATE_READY")
|
||||
}
|
||||
Log.d(TAG, "setSubtitleUri: currentPosition=$currentPosition, wasPlaying=$wasPlaying")
|
||||
val subtitleConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(url))
|
||||
.setMimeType(guessSubtitleMime(url))
|
||||
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
||||
.setRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.build()
|
||||
Log.d(TAG, "setSubtitleUri: subtitleConfig built, uri=${subtitleConfig.uri}, mime=${subtitleConfig.mimeType}, selectionFlags=${subtitleConfig.selectionFlags}")
|
||||
val newMediaItem = currentMediaItem.buildUpon()
|
||||
.setSubtitleConfigurations(listOf(subtitleConfig))
|
||||
.build()
|
||||
Log.d(TAG, "setSubtitleUri: newMediaItem subtitleConfigs count=${newMediaItem.localConfiguration?.subtitleConfigurations?.size}")
|
||||
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
||||
.clearOverridesOfType(C.TRACK_TYPE_TEXT)
|
||||
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.build()
|
||||
Log.d(TAG, "setSubtitleUri: track params set before prepare, textDisabled=${exoPlayer.trackSelectionParameters.disabledTrackTypes.contains(C.TRACK_TYPE_TEXT)}")
|
||||
exoPlayer.setMediaItem(newMediaItem, currentPosition)
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.playWhenReady = wasPlaying
|
||||
Log.d(TAG, "setSubtitleUri: prepare() called, waiting for STATE_READY")
|
||||
}
|
||||
|
||||
override fun clearExternalSubtitle() {
|
||||
|
|
@ -474,6 +493,68 @@ private fun ExoPlayer.logCurrentTracks(context: String) {
|
|||
Log.d(TAG, "--- end logCurrentTracks ---")
|
||||
}
|
||||
|
||||
private fun resolveSubtitleMimeType(url: String): String {
|
||||
probeSubtitleHeaders(url)?.let { (contentType, contentDisposition) ->
|
||||
mapSubtitleMime(contentType)?.let { return it }
|
||||
filenameFromContentDisposition(contentDisposition)?.let(::guessSubtitleMime)?.let { return it }
|
||||
}
|
||||
return guessSubtitleMime(url)
|
||||
}
|
||||
|
||||
private fun probeSubtitleHeaders(url: String): Pair<String?, String?>? {
|
||||
val methods = listOf("HEAD", "GET")
|
||||
methods.forEach { method ->
|
||||
runCatching {
|
||||
val connection = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = method
|
||||
connectTimeout = 5_000
|
||||
readTimeout = 5_000
|
||||
instanceFollowRedirects = true
|
||||
setRequestProperty("Accept", "*/*")
|
||||
}
|
||||
try {
|
||||
connection.responseCode
|
||||
connection.contentType to connection.getHeaderField("Content-Disposition")
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}.getOrNull()?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun mapSubtitleMime(contentType: String?): String? {
|
||||
val normalized = contentType
|
||||
?.substringBefore(';')
|
||||
?.trim()
|
||||
?.lowercase()
|
||||
?: return null
|
||||
|
||||
return when (normalized) {
|
||||
"application/x-subrip",
|
||||
"application/srt",
|
||||
"text/srt",
|
||||
"text/plain" -> MimeTypes.APPLICATION_SUBRIP
|
||||
"text/vtt",
|
||||
"application/vtt" -> MimeTypes.TEXT_VTT
|
||||
"text/x-ssa",
|
||||
"text/ssa",
|
||||
"text/ass",
|
||||
"application/x-ssa" -> MimeTypes.TEXT_SSA
|
||||
"application/ttml+xml",
|
||||
"text/xml",
|
||||
"application/xml" -> MimeTypes.APPLICATION_TTML
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun filenameFromContentDisposition(contentDisposition: String?): String? =
|
||||
contentDisposition
|
||||
?.substringAfter("filename=", missingDelimiterValue = "")
|
||||
?.trim()
|
||||
?.trim('"')
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
private fun guessSubtitleMime(url: String): String {
|
||||
val lower = url.lowercase()
|
||||
return when {
|
||||
|
|
|
|||
Loading…
Reference in a new issue