mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Integrate with libass-android
This commit is contained in:
parent
2ecc3f262d
commit
09ae259887
5 changed files with 98 additions and 13 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue