Integrate with libass-android

This commit is contained in:
Damontecres 2025-12-24 17:48:29 -05:00
parent 2ecc3f262d
commit 09ae259887
No known key found for this signature in database
5 changed files with 98 additions and 13 deletions

View file

@ -197,6 +197,7 @@ dependencies {
implementation(libs.androidx.media3.exoplayer.hls) implementation(libs.androidx.media3.exoplayer.hls)
implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.ui.compose) implementation(libs.androidx.media3.ui.compose)
implementation(libs.ass.media)
implementation(libs.coil.core) implementation(libs.coil.core)
implementation(libs.coil.compose) implementation(libs.coil.compose)

View file

@ -7,14 +7,23 @@ import androidx.annotation.OptIn
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer 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.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.util.mpv.MpvPlayer import com.github.damontecres.wholphin.util.mpv.MpvPlayer
import dagger.hilt.android.qualifiers.ApplicationContext 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.flow.firstOrNull
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
@ -35,12 +44,12 @@ class PlayerFactory
var currentPlayer: Player? = null var currentPlayer: Player? = null
private set private set
fun createVideoPlayer(): Player { fun createVideoPlayer(): PlayerCreation {
if (currentPlayer?.isReleased == false) { if (currentPlayer?.isReleased == false) {
Timber.w("Player was not released before trying to create a new one!") Timber.w("Player was not released before trying to create a new one!")
currentPlayer?.release() currentPlayer?.release()
} }
var assHandler: AssHandler? = null
val prefs = runBlocking { appPreferences.data.firstOrNull()?.playbackPreferences } val prefs = runBlocking { appPreferences.data.firstOrNull()?.playbackPreferences }
val backend = prefs?.playerBackend ?: AppPreference.PlayerBackendPref.defaultValue val backend = prefs?.playerBackend ?: AppPreference.PlayerBackendPref.defaultValue
val newPlayer = val newPlayer =
@ -61,9 +70,11 @@ class PlayerFactory
PlayerBackend.EXO_PLAYER, PlayerBackend.EXO_PLAYER,
PlayerBackend.UNRECOGNIZED, PlayerBackend.UNRECOGNIZED,
-> { -> {
val extensions = val extensions = prefs?.overrides?.mediaExtensionsEnabled
runBlocking { appPreferences.data.firstOrNull() }?.playbackPreferences?.overrides?.mediaExtensionsEnabled val directPlayAss =
Timber.v("extensions=$extensions") prefs?.overrides?.directPlayAss
?: AppPreference.DirectPlayAss.defaultValue
Timber.v("extensions=$extensions, directPlayAss=$directPlayAss")
val rendererMode = val rendererMode =
when (extensions) { when (extensions) {
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
@ -71,20 +82,43 @@ class PlayerFactory
MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
} }
ExoPlayer val dataSourceFactory = DefaultDataSource.Factory(context)
.Builder(context) val extractorsFactory = DefaultExtractorsFactory()
.setRenderersFactory( var renderersFactory: RenderersFactory =
DefaultRenderersFactory(context) DefaultRenderersFactory(context)
.setEnableDecoderFallback(true) .setEnableDecoderFallback(true)
.setExtensionRendererMode(rendererMode), .setExtensionRendererMode(rendererMode)
).build() 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)
.setMediaSourceFactory(mediaSourceFactory)
.setRenderersFactory(renderersFactory)
.build()
.apply { .apply {
assHandler?.init(this)
playWhenReady = true playWhenReady = true
} }
} }
} }
currentPlayer = newPlayer 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}") else -> throw IllegalStateException("Unknown Player type: ${this::class.qualifiedName}")
} }
} }
data class PlayerCreation(
val player: Player,
val assHandler: AssHandler? = null,
)

View file

@ -1,5 +1,8 @@
package com.github.damontecres.wholphin.ui.playback 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.activity.compose.BackHandler
import androidx.annotation.Dimension import androidx.annotation.Dimension
import androidx.annotation.OptIn 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.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.view.children
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.media3.common.util.UnstableApi 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.LoadingState
import com.github.damontecres.wholphin.util.Media3SubtitleOverride import com.github.damontecres.wholphin.util.Media3SubtitleOverride
import com.github.damontecres.wholphin.util.mpv.MpvPlayer import com.github.damontecres.wholphin.util.mpv.MpvPlayer
import io.github.peerless2012.ass.media.widget.AssSubtitleView
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber
import java.util.UUID import java.util.UUID
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -286,10 +294,14 @@ fun PlaybackPage(
.focusRequester(focusRequester) .focusRequester(focusRequester)
.focusable(), .focusable(),
) { ) {
var playerSize by remember { mutableStateOf(IntSize.Zero) }
PlayerSurface( PlayerSurface(
player = player, player = player,
surfaceType = SURFACE_TYPE_SURFACE_VIEW, surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = scaledModifier, modifier =
scaledModifier.onGloballyPositioned {
playerSize = it.size
},
) )
if (presentationState.coverSurface) { if (presentationState.coverSurface) {
val isLoading by rememberPlayerLoadingState(player) val isLoading by rememberPlayerLoadingState(player)
@ -383,6 +395,21 @@ fun PlaybackPage(
setFixedTextSize(Dimension.SP, it.fontSize.toFloat()) setFixedTextSize(Dimension.SP, it.fontSize.toFloat())
setBottomPaddingFraction(it.margin.toFloat() / 100f) 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 = { update = {
@ -391,6 +418,17 @@ fun PlaybackPage(
preferences.appPreferences.interfacePreferences.subtitlesPreferences preferences.appPreferences.interfacePreferences.subtitlesPreferences
.calculateEdgeSize(density), .calculateEdgeSize(density),
).apply(it) ).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 = { onReset = {
it.setCues(null) it.setCues(null)

View file

@ -63,6 +63,7 @@ import com.github.damontecres.wholphin.util.subtitleMimeTypes
import com.github.damontecres.wholphin.util.supportItemKinds import com.github.damontecres.wholphin.util.supportItemKinds
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.peerless2012.ass.media.AssHandler
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -130,8 +131,11 @@ class PlaybackViewModel
) : ViewModel(), ) : ViewModel(),
Player.Listener, Player.Listener,
AnalyticsListener { AnalyticsListener {
var assHandler: AssHandler? = null
val player by lazy { val player by lazy {
playerFactory.createVideoPlayer() val creation = playerFactory.createVideoPlayer()
assHandler = creation.assHandler
creation.player
} }
internal val mutex = Mutex() internal val mutex = Mutex()

View file

@ -33,6 +33,7 @@ hilt = "2.57.2"
room = "2.8.4" room = "2.8.4"
preferenceKtx = "1.2.1" preferenceKtx = "1.2.1"
paletteKtx = "1.0.0" paletteKtx = "1.0.0"
assMedia = "0.4.0-alpha01"
[libraries] [libraries]
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } 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-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
androidx-palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "paletteKtx" } 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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }