From 09ae2598878102b55366e108d12fc33f6a3f597b Mon Sep 17 00:00:00 2001 From: Damontecres Date: Wed, 24 Dec 2025 17:48:29 -0500 Subject: [PATCH] Integrate with libass-android --- app/build.gradle.kts | 1 + .../wholphin/services/PlayerFactory.kt | 61 +++++++++++++++---- .../wholphin/ui/playback/PlaybackPage.kt | 40 +++++++++++- .../wholphin/ui/playback/PlaybackViewModel.kt | 6 +- gradle/libs.versions.toml | 3 + 5 files changed, 98 insertions(+), 13 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f564ca84..c8ae975b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -197,6 +197,7 @@ dependencies { implementation(libs.androidx.media3.exoplayer.hls) implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui.compose) + implementation(libs.ass.media) implementation(libs.coil.core) implementation(libs.coil.compose) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt index c59ba81d..5850f05a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt @@ -7,14 +7,23 @@ import androidx.annotation.OptIn import androidx.datastore.core.DataStore import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultDataSource import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.RenderersFactory +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.extractor.DefaultExtractorsFactory import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.MediaExtensionStatus import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.util.mpv.MpvPlayer import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.peerless2012.ass.media.AssHandler +import io.github.peerless2012.ass.media.factory.AssRenderersFactory +import io.github.peerless2012.ass.media.kt.withAssMkvSupport +import io.github.peerless2012.ass.media.parser.AssSubtitleParserFactory +import io.github.peerless2012.ass.media.type.AssRenderType import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.runBlocking import timber.log.Timber @@ -35,12 +44,12 @@ class PlayerFactory var currentPlayer: Player? = null private set - fun createVideoPlayer(): Player { + fun createVideoPlayer(): PlayerCreation { if (currentPlayer?.isReleased == false) { Timber.w("Player was not released before trying to create a new one!") currentPlayer?.release() } - + var assHandler: AssHandler? = null val prefs = runBlocking { appPreferences.data.firstOrNull()?.playbackPreferences } val backend = prefs?.playerBackend ?: AppPreference.PlayerBackendPref.defaultValue val newPlayer = @@ -61,9 +70,11 @@ class PlayerFactory PlayerBackend.EXO_PLAYER, PlayerBackend.UNRECOGNIZED, -> { - val extensions = - runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled - Timber.v("extensions=$extensions") + val extensions = prefs?.overrides?.mediaExtensionsEnabled + val directPlayAss = + prefs?.overrides?.directPlayAss + ?: AppPreference.DirectPlayAss.defaultValue + Timber.v("extensions=$extensions, directPlayAss=$directPlayAss") val rendererMode = when (extensions) { MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON @@ -71,20 +82,43 @@ class PlayerFactory MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON } + val dataSourceFactory = DefaultDataSource.Factory(context) + val extractorsFactory = DefaultExtractorsFactory() + var renderersFactory: RenderersFactory = + DefaultRenderersFactory(context) + .setEnableDecoderFallback(true) + .setExtensionRendererMode(rendererMode) + val mediaSourceFactory = + if (directPlayAss) { + assHandler = AssHandler(AssRenderType.OVERLAY_OPEN_GL) + val assSubtitleParserFactory = AssSubtitleParserFactory(assHandler) + renderersFactory = AssRenderersFactory(assHandler, renderersFactory) + DefaultMediaSourceFactory( + dataSourceFactory, + extractorsFactory.withAssMkvSupport( + assSubtitleParserFactory, + assHandler, + ), + ).setSubtitleParserFactory(assSubtitleParserFactory) + } else { + DefaultMediaSourceFactory( + dataSourceFactory, + extractorsFactory, + ) + } ExoPlayer .Builder(context) - .setRenderersFactory( - DefaultRenderersFactory(context) - .setEnableDecoderFallback(true) - .setExtensionRendererMode(rendererMode), - ).build() + .setMediaSourceFactory(mediaSourceFactory) + .setRenderersFactory(renderersFactory) + .build() .apply { + assHandler?.init(this) playWhenReady = true } } } currentPlayer = newPlayer - return newPlayer + return PlayerCreation(newPlayer, assHandler) } } @@ -96,3 +130,8 @@ val Player.isReleased: Boolean else -> throw IllegalStateException("Unknown Player type: ${this::class.qualifiedName}") } } + +data class PlayerCreation( + val player: Player, + val assHandler: AssHandler? = null, +) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index e27ea11f..2955b469 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -1,5 +1,8 @@ package com.github.damontecres.wholphin.ui.playback +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout import androidx.activity.compose.BackHandler import androidx.annotation.Dimension import androidx.annotation.OptIn @@ -40,13 +43,16 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.core.view.children import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleStartEffect import androidx.media3.common.util.UnstableApi @@ -80,10 +86,12 @@ import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.Media3SubtitleOverride import com.github.damontecres.wholphin.util.mpv.MpvPlayer +import io.github.peerless2012.ass.media.widget.AssSubtitleView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.jellyfin.sdk.model.extensions.ticks +import timber.log.Timber import java.util.UUID import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -286,10 +294,14 @@ fun PlaybackPage( .focusRequester(focusRequester) .focusable(), ) { + var playerSize by remember { mutableStateOf(IntSize.Zero) } PlayerSurface( player = player, surfaceType = SURFACE_TYPE_SURFACE_VIEW, - modifier = scaledModifier, + modifier = + scaledModifier.onGloballyPositioned { + playerSize = it.size + }, ) if (presentationState.coverSurface) { val isLoading by rememberPlayerLoadingState(player) @@ -383,6 +395,21 @@ fun PlaybackPage( setFixedTextSize(Dimension.SP, it.fontSize.toFloat()) setBottomPaddingFraction(it.margin.toFloat() / 100f) } + viewModel.assHandler?.let { assHandler -> + if (prefs.overrides.directPlayAss) { + Timber.v("Adding AssSubtitleView") + addView( + AssSubtitleView(context, assHandler).apply { + layoutParams = + FrameLayout + .LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ).apply { gravity = Gravity.CENTER } + }, + ) + } + } } }, update = { @@ -391,6 +418,17 @@ fun PlaybackPage( preferences.appPreferences.interfacePreferences.subtitlesPreferences .calculateEdgeSize(density), ).apply(it) + it.children.firstOrNull { it is AssSubtitleView }?.let { + (it as? AssSubtitleView)?.apply { + Timber.v("Resize: $playerSize") + layoutParams = + FrameLayout + .LayoutParams( + playerSize.width, + playerSize.height, + ).apply { gravity = Gravity.CENTER } + } + } }, onReset = { it.setCues(null) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 1d380e08..4e71a1e8 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -63,6 +63,7 @@ import com.github.damontecres.wholphin.util.subtitleMimeTypes import com.github.damontecres.wholphin.util.supportItemKinds import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.peerless2012.ass.media.AssHandler import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -130,8 +131,11 @@ class PlaybackViewModel ) : ViewModel(), Player.Listener, AnalyticsListener { + var assHandler: AssHandler? = null val player by lazy { - playerFactory.createVideoPlayer() + val creation = playerFactory.createVideoPlayer() + assHandler = creation.assHandler + creation.player } internal val mutex = Mutex() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 79d1ae5c..7a5ffa42 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ hilt = "2.57.2" room = "2.8.4" preferenceKtx = "1.2.1" paletteKtx = "1.0.0" +assMedia = "0.4.0-alpha01" [libraries] aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } @@ -103,6 +104,8 @@ androidx-preference-ktx = { group = "androidx.preference", name = "preference-kt androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" } androidx-palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "paletteKtx" } +ass-media = { group = "io.github.peerless2012", name = "ass-media", version.ref = "assMedia" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }