Compare commits

...
2 Commits
Author SHA1 Message Date
zoriya 7578fe944e fix(exo-sub): fix exoplayer crash with karaoke ass 2026-08-06 12:37:28 +02:00
zoriya 1789d00fdc fix(subtitles): fix external subtitles 2026-08-06 12:37:28 +02:00
2 changed files with 61 additions and 4 deletions
@@ -1,6 +1,8 @@
package dev.zoriya.omni
import android.annotation.SuppressLint
import android.content.Context
import android.os.Looper
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -13,11 +15,16 @@ import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.Player
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.Renderer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.text.TextOutput
import androidx.media3.exoplayer.text.TextRenderer
import com.margelo.nitro.NitroModules
import com.margelo.nitro.omni.AndroidBackend
import com.margelo.nitro.omni.CastStatus
@@ -62,9 +69,26 @@ class OmniPlayer(
AndroidBackend.EXOPLAYER -> {
val http = DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true)
httpDataSourceFactory = http
ExoPlayer.Builder(ctx)
// karaoke subs make exoplayer crash without this...
val renderersFactory = object : DefaultRenderersFactory(ctx) {
override fun buildTextRenderers(
context: Context,
output: TextOutput,
outputLooper: Looper,
extensionRendererMode: Int,
out: ArrayList<Renderer>,
) {
out.add(
TextRenderer(output, outputLooper).apply {
experimentalSetLegacyDecodingEnabled(true)
}
)
}
}
ExoPlayer.Builder(ctx, renderersFactory)
.setMediaSourceFactory(
DefaultMediaSourceFactory(DefaultDataSource.Factory(ctx, http))
.experimentalParseSubtitlesDuringExtraction(false)
)
.build()
}
@@ -187,6 +211,19 @@ class OmniPlayer(
super.dispose()
}
private fun inferSubtitleMimeType(link: String): String? {
val ext = link.toUri().lastPathSegment?.substringAfterLast('.', "")?.lowercase()
return when (ext) {
"vtt", "webvtt" -> MimeTypes.TEXT_VTT
"srt" -> MimeTypes.APPLICATION_SUBRIP
"ass", "ssa" -> MimeTypes.TEXT_SSA
"ttml", "dfxp", "xml" -> MimeTypes.APPLICATION_TTML
"sup" -> MimeTypes.APPLICATION_PGS
"sub" -> MimeTypes.APPLICATION_VOBSUB
else -> null
}
}
private fun buildMediaItem(
src: com.margelo.nitro.omni.VideoSrc,
metadata: com.margelo.nitro.omni.Metadata?,
@@ -212,7 +249,10 @@ class OmniPlayer(
.setId(subtitle.id)
.setLanguage(subtitle.language)
.setLabel(subtitle.label)
.setMimeType(subtitle.mimeType)
// exoplayer requires a mime type to pick a subtitle parser.
// fall back to inferring it from the link's extension so
// consumers don't have to provide one.
.setMimeType(subtitle.mimeType ?: inferSubtitleMimeType(subtitle.link))
.build()
})
.setRequestMetadata(
@@ -46,6 +46,7 @@ import org.videolan.libvlc.MediaPlayer
import org.videolan.libvlc.interfaces.IMedia
import org.videolan.libvlc.interfaces.IMedia.VideoTrack
import org.videolan.libvlc.interfaces.IVLCVout
import java.security.MessageDigest
@SuppressLint("UnsafeOptInUsageError")
class VlcPlayer(ctx: Context) :
@@ -387,6 +388,20 @@ class VlcPlayer(ctx: Context) :
}
}
// vlc keys each slave's tracks by the md5 of its uri ("<md5(uri)>/spu/<n>")
private fun subtitleSlaveForTrackId(trackId: String): MediaItem.SubtitleConfiguration? {
val hash = trackId.substringBefore("/spu/", "")
if (hash.isEmpty()) return null
return mediaItems.getOrNull(currentMediaItemIndex)
?.localConfiguration?.subtitleConfigurations
?.firstOrNull { md5(it.uri.toString()) == hash }
}
private fun md5(value: String): String =
MessageDigest.getInstance("MD5")
.digest(value.toByteArray())
.joinToString("") { "%02x".format(it.toInt() and 0xFF) }
// vlc doesn't allow arbitrary headers :c
private fun applyRequestHeaders(media: Media, extras: Bundle?) {
if (extras == null) return
@@ -599,10 +614,12 @@ class VlcPlayer(ctx: Context) :
}
player.getTracks(IMedia.Track.Type.Text)?.forEach { track ->
// external subs don't have label/language, match them back to surface them
val slave = subtitleSlaveForTrackId(track.id)
val format = Format.Builder()
.setId(track.id)
.setLabel(track.name)
.setLanguage(track.language)
.setLabel(slave?.label ?: track.name)
.setLanguage(slave?.language ?: track.language)
.setSampleMimeType("text/x-unknown")
.build()
val group = TrackGroup("vlc-sub-${track.id}", format)