From e93fdf16c394968b82e895d10d3a4bd66e667e0b Mon Sep 17 00:00:00 2001 From: Lawtro <98205608+Lawtro37@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:07:14 +1000 Subject: [PATCH 1/3] Update dependencies to add libass support --- app/build.gradle.kts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd26dffce..6f5804354 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -271,9 +271,12 @@ ksp { // Hilt 2.57 can process Kotlin 2.3.0 metadata without moving to Hilt 2.59 (AGP 9). configurations.all { resolutionStrategy { - force("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0") + force("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3") force("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + force("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3") force("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + force("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.3") + force("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.7.3") } } @@ -364,6 +367,9 @@ ksp { // (libffmpegJNI.so) is the likely source of the Play Console warning. add("sideloadImplementation", "org.jellyfin.media3:media3-ffmpeg-decoder:1.9.0+1") + // ASS/SSA subtitle rendering for Media3. + implementation("io.github.peerless2012:ass-media:0.5.1") + // Networking - Retrofit + OkHttp implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") @@ -449,7 +455,7 @@ ksp { // Unit Testing testImplementation("junit:junit:4.13.2") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") testImplementation("io.mockk:mockk:1.13.8") testImplementation("app.cash.turbine:turbine:1.0.0") // Flow testing testImplementation("com.google.truth:truth:1.1.5") // Better assertions @@ -464,7 +470,7 @@ ksp { androidTestImplementation("androidx.compose.ui:ui-test-junit4") androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0") androidTestImplementation("io.mockk:mockk-android:1.13.8") - androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") } secrets { @@ -595,5 +601,3 @@ dependencies { add("sideloadImplementation", "org.mozilla:rhino:1.8.1") add("sideloadImplementation", "com.google.re2j:re2j:1.8") } - - From 60060050275f3d2d6d5d81be811b000138f2a0df Mon Sep 17 00:00:00 2001 From: Lawtro <98205608+Lawtro37@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:10:29 +1000 Subject: [PATCH 2/3] Modify ExoPlayer builder to use "buildWithAssSupport" --- .../kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt index 215369912..0bff4fdc5 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt @@ -137,6 +137,8 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.ui.AspectRatioFrameLayout import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Text +import io.github.peerless2012.ass.media.kt.buildWithAssSupport +import io.github.peerless2012.ass.media.type.AssRenderType import coil.compose.AsyncImage import com.arflix.tv.ArflixApplication import com.arflix.tv.network.OkHttpProvider @@ -1152,7 +1154,10 @@ fun PlayerScreen( .build(), /* handleAudioFocus = */ true ) - .build().apply { + .buildWithAssSupport( // Enable ASS subtitle rendering for stylized subtitles + context, + AssRenderType.OVERLAY_OPEN_GL + ).apply { // Ensure volume is at maximum volume = 1.0f From 31e90eb470a4e79090f5465408b198a556290b80 Mon Sep 17 00:00:00 2001 From: Lawtro <98205608+Lawtro37@users.noreply.github.com> Date: Sat, 12 Sep 2026 23:56:59 +1000 Subject: [PATCH 3/3] Enhance subtitle mime type detection logic Updated subtitle mime type detection to include query string checks. --- .../tv/ui/screens/player/PlayerScreen.kt | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt index 0bff4fdc5..96bb7ad98 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt @@ -1156,7 +1156,7 @@ fun PlayerScreen( ) .buildWithAssSupport( // Enable ASS subtitle rendering for stylized subtitles context, - AssRenderType.OVERLAY_OPEN_GL + AssRenderType.OVERLAY_OPEN_GL, ).apply { // Ensure volume is at maximum volume = 1.0f @@ -6111,20 +6111,45 @@ private fun buildExternalSubtitleConfigurations(subtitles: List): List } private fun subtitleMimeTypeFromUrl(url: String): String { - // Trailing slash before the query is real-world (AIOStreams: ".../sub.vtt/?lang=…") — trim it - // so the extension check still sees ".vtt"; the SRT fallback would silently fail on WEBVTT. - val cleanUrl = url.substringBefore('?').trimEnd('/').lowercase() + // idealy the subtitle format should be retrived from the metadata of the subtitle file. + // though that would require downloading the subtitle file first, which is not ideal. + // maybe you could do some download magic where you download the first few bytes of the subtitle file to get the metadata and then determine the format from that. + // this works for now though I suppose but it should really be updated to use the metadata later. + + val lower = url.lowercase() + + val path = lower + .substringBefore('?') + .trimEnd('/') + + val query = lower + .substringAfter('?', "") + return when { - cleanUrl.endsWith(".vtt") -> MimeTypes.TEXT_VTT - cleanUrl.endsWith(".srt") || cleanUrl.endsWith(".srt.gz") -> MimeTypes.APPLICATION_SUBRIP - cleanUrl.endsWith(".ass") || cleanUrl.endsWith(".ssa") -> MimeTypes.TEXT_SSA - cleanUrl.endsWith(".ttml") || cleanUrl.endsWith(".dfxp") -> MimeTypes.APPLICATION_TTML - // OpenSubtitles serves SRT through extensionless URLs - use SRT as default - // since it's the dominant format from subtitle addons (OpenSubtitles, Comet). - // SRT and VTT are similar but SRT uses comma for milliseconds (00:01:23,456) - // while VTT uses period and requires a WEBVTT header. Using SRT avoids silent - // parse failures when the actual content is SRT. - else -> MimeTypes.APPLICATION_SUBRIP + // Detection by file name + path.endsWith(".vtt") -> + MimeTypes.TEXT_VTT + + path.endsWith(".srt") || path.endsWith(".srt.gz") -> + MimeTypes.APPLICATION_SUBRIP + + path.endsWith(".ass") || path.endsWith(".ssa") -> + MimeTypes.TEXT_SSA + + // detection by query string + query.contains("format=ass") || + query.contains("format=ssa") || + query.contains("type=ass") || + query.contains("type=ssa") || + query.contains("mime=text/x-ass") || + query.contains("mime=text/x-ssa") -> + MimeTypes.TEXT_SSA + + path.endsWith(".ttml") || path.endsWith(".dfxp") -> + MimeTypes.APPLICATION_TTML + + else -> + MimeTypes.APPLICATION_SUBRIP } }