Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -595,5 +601,3 @@ dependencies {
add("sideloadImplementation", "org.mozilla:rhino:1.8.1")
add("sideloadImplementation", "com.google.re2j:re2j:1.8")
}


58 changes: 44 additions & 14 deletions app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -6106,20 +6111,45 @@ private fun buildExternalSubtitleConfigurations(subtitles: List<Subtitle>): 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
}
}

Expand Down
Loading