diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt index 4053fc3..13b32d3 100644 --- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt @@ -13,6 +13,7 @@ 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 @@ -187,6 +188,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 +226,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( diff --git a/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt b/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt index 16942bc..3a63d51 100644 --- a/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt +++ b/android/src/main/java/dev/zoriya/omni/VlcPlayer.kt @@ -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 ("/spu/") + 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)