mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Integrate with libass-android to support SSA/ASS subtitles in ExoPlayer (#1052)
## Description Add better ASS/SSA support to ExoPlayer by using [`libass-android`](https://github.com/peerless2012/libass-android). This is enabled with the "Use libass for ASS subtitles" advanced settings for ExoPlayer. It is enabled by default. ### Related issues Related to #22 Replaces reverted #569 Fixes #1107
This commit is contained in:
parent
95859be3c6
commit
b18416c954
13 changed files with 626 additions and 521 deletions
|
|
@ -171,6 +171,12 @@ android {
|
||||||
isUniversalApk = true
|
isUniversalApk = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
packaging {
|
||||||
|
jniLibs {
|
||||||
|
// Work around because libass-android & wholphin-mpv both (incorrectly) package libc++_shared.so
|
||||||
|
pickFirsts += "lib/*/libc++_shared.so"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("main") {
|
getByName("main") {
|
||||||
|
|
@ -269,6 +275,7 @@ dependencies {
|
||||||
implementation(libs.androidx.media3.exoplayer.dash)
|
implementation(libs.androidx.media3.exoplayer.dash)
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -431,16 +431,18 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summaryOn = R.string.enabled,
|
summaryOn = R.string.enabled,
|
||||||
summaryOff = R.string.disabled,
|
summaryOff = R.string.disabled,
|
||||||
)
|
)
|
||||||
val DirectPlayAss =
|
val AssSubtitleMode =
|
||||||
AppSwitchPreference<AppPreferences>(
|
AppChoicePreference<AppPreferences, AssPlaybackMode>(
|
||||||
title = R.string.direct_play_ass,
|
title = R.string.ass_subtitle_playback,
|
||||||
defaultValue = true,
|
defaultValue = AssPlaybackMode.ASS_LIBASS,
|
||||||
getter = { it.playbackPreferences.overrides.directPlayAss },
|
getter = { it.playbackPreferences.overrides.assPlaybackMode },
|
||||||
setter = { prefs, value ->
|
setter = { prefs, value ->
|
||||||
prefs.updatePlaybackOverrides { directPlayAss = value }
|
prefs.updatePlaybackOverrides { assPlaybackMode = value }
|
||||||
},
|
},
|
||||||
summaryOn = R.string.enabled,
|
displayValues = R.array.ass_subtitle_modes,
|
||||||
summaryOff = R.string.disabled,
|
subtitles = R.array.ass_subtitle_modes_summary,
|
||||||
|
indexToValue = { AssPlaybackMode.forNumber(it) },
|
||||||
|
valueToIndex = { if (it != AssPlaybackMode.UNRECOGNIZED) it.number else AssPlaybackMode.ASS_LIBASS.number },
|
||||||
)
|
)
|
||||||
val DirectPlayPgs =
|
val DirectPlayPgs =
|
||||||
AppSwitchPreference<AppPreferences>(
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
|
@ -1100,7 +1102,7 @@ private val ExoPlayerSettings =
|
||||||
AppPreference.FfmpegPreference,
|
AppPreference.FfmpegPreference,
|
||||||
AppPreference.DownMixStereo,
|
AppPreference.DownMixStereo,
|
||||||
AppPreference.Ac3Supported,
|
AppPreference.Ac3Supported,
|
||||||
AppPreference.DirectPlayAss,
|
AppPreference.AssSubtitleMode,
|
||||||
AppPreference.DirectPlayPgs,
|
AppPreference.DirectPlayPgs,
|
||||||
AppPreference.DirectPlayDoviProfile7,
|
AppPreference.DirectPlayDoviProfile7,
|
||||||
AppPreference.DecodeAv1,
|
AppPreference.DecodeAv1,
|
||||||
|
|
|
||||||
|
|
@ -61,10 +61,11 @@ class AppPreferencesSerializer
|
||||||
.apply {
|
.apply {
|
||||||
ac3Supported = AppPreference.Ac3Supported.defaultValue
|
ac3Supported = AppPreference.Ac3Supported.defaultValue
|
||||||
downmixStereo = AppPreference.DownMixStereo.defaultValue
|
downmixStereo = AppPreference.DownMixStereo.defaultValue
|
||||||
directPlayAss = AppPreference.DirectPlayAss.defaultValue
|
// directPlayAss = AppPreference.DirectPlayAss.defaultValue
|
||||||
directPlayPgs = AppPreference.DirectPlayPgs.defaultValue
|
directPlayPgs = AppPreference.DirectPlayPgs.defaultValue
|
||||||
mediaExtensionsEnabled =
|
mediaExtensionsEnabled =
|
||||||
AppPreference.FfmpegPreference.defaultValue
|
AppPreference.FfmpegPreference.defaultValue
|
||||||
|
assPlaybackMode = AppPreference.AssSubtitleMode.defaultValue
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
mpvOptions =
|
mpvOptions =
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ class AppUpgradeHandler
|
||||||
it.updatePlaybackOverrides {
|
it.updatePlaybackOverrides {
|
||||||
ac3Supported = AppPreference.Ac3Supported.defaultValue
|
ac3Supported = AppPreference.Ac3Supported.defaultValue
|
||||||
downmixStereo = AppPreference.DownMixStereo.defaultValue
|
downmixStereo = AppPreference.DownMixStereo.defaultValue
|
||||||
directPlayAss = AppPreference.DirectPlayAss.defaultValue
|
// directPlayAss = AppPreference.DirectPlayAss.defaultValue
|
||||||
directPlayPgs = AppPreference.DirectPlayPgs.defaultValue
|
directPlayPgs = AppPreference.DirectPlayPgs.defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -319,5 +319,13 @@ class AppUpgradeHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previous.isEqualOrBefore(Version.fromString("0.6.2-1-g0"))) {
|
||||||
|
appPreferences.updateData {
|
||||||
|
it.updatePlaybackOverrides {
|
||||||
|
assPlaybackMode = AppPreference.AssSubtitleMode.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.services
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.github.damontecres.wholphin.preferences.AssPlaybackMode
|
||||||
import com.github.damontecres.wholphin.preferences.PlaybackPreferences
|
import com.github.damontecres.wholphin.preferences.PlaybackPreferences
|
||||||
import com.github.damontecres.wholphin.util.profile.MediaCodecCapabilitiesTest
|
import com.github.damontecres.wholphin.util.profile.MediaCodecCapabilitiesTest
|
||||||
import com.github.damontecres.wholphin.util.profile.createDeviceProfile
|
import com.github.damontecres.wholphin.util.profile.createDeviceProfile
|
||||||
|
|
@ -43,7 +44,7 @@ class DeviceProfileService
|
||||||
maxBitrate = prefs.maxBitrate.toInt(),
|
maxBitrate = prefs.maxBitrate.toInt(),
|
||||||
isAC3Enabled = prefs.overrides.ac3Supported,
|
isAC3Enabled = prefs.overrides.ac3Supported,
|
||||||
downMixAudio = prefs.overrides.downmixStereo,
|
downMixAudio = prefs.overrides.downmixStereo,
|
||||||
assDirectPlay = prefs.overrides.directPlayAss,
|
assPlaybackMode = prefs.overrides.assPlaybackMode,
|
||||||
pgsDirectPlay = prefs.overrides.directPlayPgs,
|
pgsDirectPlay = prefs.overrides.directPlayPgs,
|
||||||
dolbyVisionELDirectPlay = prefs.overrides.directPlayDolbyVisionEL,
|
dolbyVisionELDirectPlay = prefs.overrides.directPlayDolbyVisionEL,
|
||||||
decodeAv1 = prefs.overrides.decodeAv1,
|
decodeAv1 = prefs.overrides.decodeAv1,
|
||||||
|
|
@ -58,7 +59,7 @@ class DeviceProfileService
|
||||||
maxBitrate = newConfig.maxBitrate,
|
maxBitrate = newConfig.maxBitrate,
|
||||||
isAC3Enabled = newConfig.isAC3Enabled,
|
isAC3Enabled = newConfig.isAC3Enabled,
|
||||||
downMixAudio = newConfig.downMixAudio,
|
downMixAudio = newConfig.downMixAudio,
|
||||||
assDirectPlay = newConfig.assDirectPlay,
|
assDirectPlay = newConfig.assPlaybackMode != AssPlaybackMode.ASS_TRANSCODE,
|
||||||
pgsDirectPlay = newConfig.pgsDirectPlay,
|
pgsDirectPlay = newConfig.pgsDirectPlay,
|
||||||
dolbyVisionELDirectPlay = newConfig.dolbyVisionELDirectPlay,
|
dolbyVisionELDirectPlay = newConfig.dolbyVisionELDirectPlay,
|
||||||
decodeAv1 = prefs.overrides.decodeAv1,
|
decodeAv1 = prefs.overrides.decodeAv1,
|
||||||
|
|
@ -77,7 +78,7 @@ data class DeviceProfileConfiguration(
|
||||||
val maxBitrate: Int,
|
val maxBitrate: Int,
|
||||||
val isAC3Enabled: Boolean,
|
val isAC3Enabled: Boolean,
|
||||||
val downMixAudio: Boolean,
|
val downMixAudio: Boolean,
|
||||||
val assDirectPlay: Boolean,
|
val assPlaybackMode: AssPlaybackMode,
|
||||||
val pgsDirectPlay: Boolean,
|
val pgsDirectPlay: Boolean,
|
||||||
val dolbyVisionELDirectPlay: Boolean,
|
val dolbyVisionELDirectPlay: Boolean,
|
||||||
val decodeAv1: Boolean,
|
val decodeAv1: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,29 @@ import androidx.datastore.core.DataStore
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
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.Renderer
|
import androidx.media3.exoplayer.Renderer
|
||||||
|
import androidx.media3.exoplayer.RenderersFactory
|
||||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
||||||
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer
|
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer
|
||||||
import androidx.media3.exoplayer.video.VideoRendererEventListener
|
import androidx.media3.exoplayer.video.VideoRendererEventListener
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.AssPlaybackMode
|
||||||
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
||||||
import com.github.damontecres.wholphin.preferences.PlaybackPreferences
|
import com.github.damontecres.wholphin.preferences.PlaybackPreferences
|
||||||
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.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.reflect.Constructor
|
import java.lang.reflect.Constructor
|
||||||
|
|
@ -46,71 +53,17 @@ class PlayerFactory
|
||||||
var currentPlayer: Player? = null
|
var currentPlayer: Player? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun createVideoPlayer(): Player {
|
|
||||||
if (currentPlayer?.isReleased == false) {
|
|
||||||
Timber.w("Player was not released before trying to create a new one!")
|
|
||||||
currentPlayer?.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
val prefs = runBlocking { appPreferences.data.firstOrNull()?.playbackPreferences }
|
|
||||||
val backend = prefs?.playerBackend ?: AppPreference.PlayerBackendPref.defaultValue
|
|
||||||
val newPlayer =
|
|
||||||
when (backend) {
|
|
||||||
PlayerBackend.PREFER_MPV,
|
|
||||||
PlayerBackend.MPV,
|
|
||||||
-> {
|
|
||||||
val enableHardwareDecoding =
|
|
||||||
prefs?.mpvOptions?.enableHardwareDecoding
|
|
||||||
?: AppPreference.MpvHardwareDecoding.defaultValue
|
|
||||||
val useGpuNext =
|
|
||||||
prefs?.mpvOptions?.useGpuNext
|
|
||||||
?: AppPreference.MpvGpuNext.defaultValue
|
|
||||||
MpvPlayer(context, enableHardwareDecoding, useGpuNext)
|
|
||||||
.apply {
|
|
||||||
playWhenReady = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerBackend.EXO_PLAYER,
|
|
||||||
PlayerBackend.UNRECOGNIZED,
|
|
||||||
-> {
|
|
||||||
val extensions = prefs?.overrides?.mediaExtensionsEnabled
|
|
||||||
val decodeAv1 = prefs?.overrides?.decodeAv1 == true
|
|
||||||
Timber.v("extensions=$extensions")
|
|
||||||
val rendererMode =
|
|
||||||
when (extensions) {
|
|
||||||
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
|
||||||
MediaExtensionStatus.MES_PREFERRED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
|
|
||||||
MediaExtensionStatus.MES_DISABLED -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
|
|
||||||
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
|
||||||
}
|
|
||||||
ExoPlayer
|
|
||||||
.Builder(context)
|
|
||||||
.setRenderersFactory(
|
|
||||||
WholphinRenderersFactory(context, decodeAv1)
|
|
||||||
.setEnableDecoderFallback(true)
|
|
||||||
.setExtensionRendererMode(rendererMode),
|
|
||||||
).build()
|
|
||||||
.apply {
|
|
||||||
playWhenReady = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentPlayer = newPlayer
|
|
||||||
return newPlayer
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createVideoPlayer(
|
suspend fun createVideoPlayer(
|
||||||
backend: PlayerBackend,
|
backend: PlayerBackend,
|
||||||
prefs: PlaybackPreferences,
|
prefs: PlaybackPreferences,
|
||||||
): Player {
|
): PlayerCreation {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
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 newPlayer =
|
val newPlayer =
|
||||||
when (backend) {
|
when (backend) {
|
||||||
PlayerBackend.PREFER_MPV,
|
PlayerBackend.PREFER_MPV,
|
||||||
|
|
@ -125,8 +78,14 @@ class PlayerFactory
|
||||||
PlayerBackend.UNRECOGNIZED,
|
PlayerBackend.UNRECOGNIZED,
|
||||||
-> {
|
-> {
|
||||||
val extensions = prefs.overrides.mediaExtensionsEnabled
|
val extensions = prefs.overrides.mediaExtensionsEnabled
|
||||||
|
val useLibAss =
|
||||||
|
prefs.overrides.assPlaybackMode == AssPlaybackMode.ASS_LIBASS
|
||||||
val decodeAv1 = prefs.overrides.decodeAv1
|
val decodeAv1 = prefs.overrides.decodeAv1
|
||||||
Timber.v("extensions=$extensions")
|
Timber.v(
|
||||||
|
"extensions=%s, assPlaybackMode=%s",
|
||||||
|
extensions,
|
||||||
|
prefs.overrides.assPlaybackMode,
|
||||||
|
)
|
||||||
val rendererMode =
|
val rendererMode =
|
||||||
when (extensions) {
|
when (extensions) {
|
||||||
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||||
|
|
@ -134,17 +93,42 @@ 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 =
|
||||||
WholphinRenderersFactory(context, decodeAv1)
|
WholphinRenderersFactory(context, decodeAv1)
|
||||||
.setEnableDecoderFallback(true)
|
.setEnableDecoderFallback(true)
|
||||||
.setExtensionRendererMode(rendererMode),
|
.setExtensionRendererMode(rendererMode)
|
||||||
).build()
|
val mediaSourceFactory =
|
||||||
|
if (useLibAss) {
|
||||||
|
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 {
|
||||||
|
assHandler?.init(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentPlayer = newPlayer
|
currentPlayer = newPlayer
|
||||||
return newPlayer
|
return PlayerCreation(newPlayer, assHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,6 +141,11 @@ val Player.isReleased: Boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class PlayerCreation(
|
||||||
|
val player: Player,
|
||||||
|
val assHandler: AssHandler? = null,
|
||||||
|
)
|
||||||
|
|
||||||
// Code is adapted from https://github.com/androidx/media/blob/release/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java#L436
|
// Code is adapted from https://github.com/androidx/media/blob/release/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRenderersFactory.java#L436
|
||||||
class WholphinRenderersFactory(
|
class WholphinRenderersFactory(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
|
||||||
|
|
@ -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,16 +43,18 @@ 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.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
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.Player
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.ui.SubtitleView
|
import androidx.media3.ui.SubtitleView
|
||||||
import androidx.media3.ui.compose.PlayerSurface
|
import androidx.media3.ui.compose.PlayerSurface
|
||||||
|
|
@ -61,6 +66,7 @@ import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.data.model.Playlist
|
import com.github.damontecres.wholphin.data.model.Playlist
|
||||||
|
import com.github.damontecres.wholphin.preferences.AssPlaybackMode
|
||||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.skipBackOnResume
|
import com.github.damontecres.wholphin.preferences.skipBackOnResume
|
||||||
|
|
@ -79,6 +85,7 @@ 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
|
||||||
|
|
@ -123,8 +130,7 @@ fun PlaybackPage(
|
||||||
LoadingState.Success -> {
|
LoadingState.Success -> {
|
||||||
val playerState by viewModel.currentPlayer.collectAsState()
|
val playerState by viewModel.currentPlayer.collectAsState()
|
||||||
PlaybackPageContent(
|
PlaybackPageContent(
|
||||||
player = playerState!!.player,
|
playerState = playerState!!,
|
||||||
playerBackend = playerState!!.backend,
|
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
|
|
@ -137,13 +143,15 @@ fun PlaybackPage(
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaybackPageContent(
|
fun PlaybackPageContent(
|
||||||
player: Player,
|
playerState: PlayerState,
|
||||||
playerBackend: PlayerBackend,
|
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: PlaybackViewModel,
|
viewModel: PlaybackViewModel,
|
||||||
) {
|
) {
|
||||||
|
val player = playerState.player
|
||||||
|
val playerBackend = playerState.backend
|
||||||
|
|
||||||
val prefs = preferences.appPreferences.playbackPreferences
|
val prefs = preferences.appPreferences.playbackPreferences
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
|
|
@ -316,10 +324,14 @@ fun PlaybackPageContent(
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.focusable(),
|
.focusable(),
|
||||||
) {
|
) {
|
||||||
|
var playerSurfaceSize 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.onSizeChanged {
|
||||||
|
playerSurfaceSize = it
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if (presentationState.coverSurface) {
|
if (presentationState.coverSurface) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -415,7 +427,7 @@ fun PlaybackPageContent(
|
||||||
remember(subtitleSettings) { subtitleSettings.imageSubtitleOpacity / 100f }
|
remember(subtitleSettings) { subtitleSettings.imageSubtitleOpacity / 100f }
|
||||||
|
|
||||||
// Subtitles
|
// Subtitles
|
||||||
if (skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled) {
|
if (skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled && !presentationState.coverSurface) {
|
||||||
val maxSize by animateFloatAsState(if (controllerViewState.controlsVisible) .7f else 1f)
|
val maxSize by animateFloatAsState(if (controllerViewState.controlsVisible) .7f else 1f)
|
||||||
val isImageSubtitles = remember(cues) { cues.firstOrNull()?.bitmap != null }
|
val isImageSubtitles = remember(cues) { cues.firstOrNull()?.bitmap != null }
|
||||||
AndroidView(
|
AndroidView(
|
||||||
|
|
@ -426,12 +438,42 @@ fun PlaybackPageContent(
|
||||||
setFixedTextSize(Dimension.SP, it.fontSize.toFloat())
|
setFixedTextSize(Dimension.SP, it.fontSize.toFloat())
|
||||||
setBottomPaddingFraction(it.margin.toFloat() / 100f)
|
setBottomPaddingFraction(it.margin.toFloat() / 100f)
|
||||||
}
|
}
|
||||||
|
playerState.assHandler?.let { assHandler ->
|
||||||
|
if (prefs.overrides.assPlaybackMode == AssPlaybackMode.ASS_LIBASS) {
|
||||||
|
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 = {
|
||||||
it.setCues(cues)
|
it.setCues(cues)
|
||||||
Media3SubtitleOverride(subtitleSettings.calculateEdgeSize(density))
|
Media3SubtitleOverride(subtitleSettings.calculateEdgeSize(density))
|
||||||
.apply(it)
|
.apply(it)
|
||||||
|
it.children.firstOrNull { it is AssSubtitleView }?.let {
|
||||||
|
(it as? AssSubtitleView)?.apply {
|
||||||
|
val resized =
|
||||||
|
layoutParams.let { it.width != playerSurfaceSize.width || it.height != playerSurfaceSize.height }
|
||||||
|
if (resized) {
|
||||||
|
Timber.v("Resizing AssSubtitleView: $playerSurfaceSize")
|
||||||
|
layoutParams =
|
||||||
|
FrameLayout
|
||||||
|
.LayoutParams(
|
||||||
|
playerSurfaceSize.width,
|
||||||
|
playerSurfaceSize.height,
|
||||||
|
).apply { gravity = Gravity.CENTER }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onReset = {
|
onReset = {
|
||||||
it.setCues(null)
|
it.setCues(null)
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
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
|
||||||
|
|
@ -221,7 +222,8 @@ class PlaybackViewModel
|
||||||
isHdr: Boolean,
|
isHdr: Boolean,
|
||||||
is4k: Boolean,
|
is4k: Boolean,
|
||||||
) {
|
) {
|
||||||
val softwareDecoding = !preferences.appPreferences.playbackPreferences.mpvOptions.enableHardwareDecoding
|
val softwareDecoding =
|
||||||
|
!preferences.appPreferences.playbackPreferences.mpvOptions.enableHardwareDecoding
|
||||||
val playerBackend =
|
val playerBackend =
|
||||||
when (preferences.appPreferences.playbackPreferences.playerBackend) {
|
when (preferences.appPreferences.playbackPreferences.playerBackend) {
|
||||||
PlayerBackend.UNRECOGNIZED,
|
PlayerBackend.UNRECOGNIZED,
|
||||||
|
|
@ -240,13 +242,14 @@ class PlaybackViewModel
|
||||||
disconnectPlayer()
|
disconnectPlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
player =
|
val playerCreation =
|
||||||
playerFactory.createVideoPlayer(
|
playerFactory.createVideoPlayer(
|
||||||
playerBackend,
|
playerBackend,
|
||||||
preferences.appPreferences.playbackPreferences,
|
preferences.appPreferences.playbackPreferences,
|
||||||
)
|
)
|
||||||
|
this.player = playerCreation.player
|
||||||
currentPlayer.update {
|
currentPlayer.update {
|
||||||
PlayerState(player, playerBackend)
|
PlayerState(playerCreation.player, playerBackend, playerCreation.assHandler)
|
||||||
}
|
}
|
||||||
configurePlayer()
|
configurePlayer()
|
||||||
}
|
}
|
||||||
|
|
@ -1450,6 +1453,7 @@ class PlaybackViewModel
|
||||||
data class PlayerState(
|
data class PlayerState(
|
||||||
val player: Player,
|
val player: Player,
|
||||||
val backend: PlayerBackend,
|
val backend: PlayerBackend,
|
||||||
|
val assHandler: AssHandler?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MediaSegmentState(
|
data class MediaSegmentState(
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ import com.github.damontecres.wholphin.ui.playback.isDirectionalDpad
|
||||||
import com.github.damontecres.wholphin.ui.playback.isDpad
|
import com.github.damontecres.wholphin.ui.playback.isDpad
|
||||||
import com.github.damontecres.wholphin.ui.playback.isEnterKey
|
import com.github.damontecres.wholphin.ui.playback.isEnterKey
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
@ -92,7 +93,20 @@ fun SlideshowPage(
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val loading by viewModel.loading.collectAsState()
|
||||||
|
|
||||||
|
when (val st = loading) {
|
||||||
|
is LoadingState.Error -> {
|
||||||
|
ErrorMessage(st, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> {
|
||||||
|
LoadingPage(modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadingState.Success -> {
|
||||||
val loadingState by viewModel.loadingState.observeAsState(ImageLoadingState.Loading)
|
val loadingState by viewModel.loadingState.observeAsState(ImageLoadingState.Loading)
|
||||||
val imageFilter by viewModel.imageFilter.observeAsState(VideoFilter())
|
val imageFilter by viewModel.imageFilter.observeAsState(VideoFilter())
|
||||||
val position by viewModel.position.observeAsState(0)
|
val position by viewModel.position.observeAsState(0)
|
||||||
|
|
@ -389,15 +403,15 @@ fun SlideshowPage(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
transformOrigin = TransformOrigin(xTransform, yTransform)
|
transformOrigin =
|
||||||
|
TransformOrigin(xTransform, yTransform)
|
||||||
}.rotate(rotateAnimation),
|
}.rotate(rotateAnimation),
|
||||||
model =
|
model =
|
||||||
ImageRequest
|
ImageRequest
|
||||||
.Builder(LocalContext.current)
|
.Builder(LocalContext.current)
|
||||||
.data(imageState.url)
|
.data(imageState.url)
|
||||||
.apply {
|
.size(Size.ORIGINAL)
|
||||||
if (isZoomed) size(Size.ORIGINAL)
|
.transitionFactory(CrossFadeFactory(750.milliseconds))
|
||||||
}.transitionFactory(CrossFadeFactory(750.milliseconds))
|
|
||||||
.useExistingImageAsPlaceholder(true)
|
.useExistingImageAsPlaceholder(true)
|
||||||
.build(),
|
.build(),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|
@ -491,4 +505,6 @@ fun SlideshowPage(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.PlaybackEffect
|
import com.github.damontecres.wholphin.data.model.PlaybackEffect
|
||||||
import com.github.damontecres.wholphin.data.model.VideoFilter
|
import com.github.damontecres.wholphin.data.model.VideoFilter
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||||
import com.github.damontecres.wholphin.services.PlayerFactory
|
import com.github.damontecres.wholphin.services.PlayerFactory
|
||||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||||
|
|
@ -30,6 +31,7 @@ import com.github.damontecres.wholphin.ui.util.ThrottledLiveData
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
@ -77,9 +79,8 @@ class SlideshowViewModel
|
||||||
fun create(slideshow: Destination.Slideshow): SlideshowViewModel
|
fun create(slideshow: Destination.Slideshow): SlideshowViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
val player by lazy {
|
lateinit var player: Player
|
||||||
playerFactory.createVideoPlayer()
|
private set
|
||||||
}
|
|
||||||
|
|
||||||
private var saveFilters = true
|
private var saveFilters = true
|
||||||
|
|
||||||
|
|
@ -96,7 +97,6 @@ class SlideshowViewModel
|
||||||
|
|
||||||
var slideshowDelay by Delegates.notNull<Long>()
|
var slideshowDelay by Delegates.notNull<Long>()
|
||||||
|
|
||||||
// private val album = MutableLiveData<BaseItem>()
|
|
||||||
private val _pager = MutableLiveData<ApiRequestPager<GetItemsRequest>>()
|
private val _pager = MutableLiveData<ApiRequestPager<GetItemsRequest>>()
|
||||||
val pager: LiveData<List<BaseItem?>> = _pager.map { it }
|
val pager: LiveData<List<BaseItem?>> = _pager.map { it }
|
||||||
val position = MutableLiveData(0)
|
val position = MutableLiveData(0)
|
||||||
|
|
@ -104,6 +104,8 @@ class SlideshowViewModel
|
||||||
private val _image = MutableLiveData<ImageState>()
|
private val _image = MutableLiveData<ImageState>()
|
||||||
val image: LiveData<ImageState> = _image
|
val image: LiveData<ImageState> = _image
|
||||||
|
|
||||||
|
val loading = MutableStateFlow<LoadingState>(LoadingState.Pending)
|
||||||
|
|
||||||
val loadingState = MutableLiveData<ImageLoadingState>(ImageLoadingState.Loading)
|
val loadingState = MutableLiveData<ImageLoadingState>(ImageLoadingState.Loading)
|
||||||
private val _imageFilter = MutableLiveData(VideoFilter())
|
private val _imageFilter = MutableLiveData(VideoFilter())
|
||||||
val imageFilter = ThrottledLiveData(_imageFilter, 500L)
|
val imageFilter = ThrottledLiveData(_imageFilter, 500L)
|
||||||
|
|
@ -113,22 +115,26 @@ class SlideshowViewModel
|
||||||
init {
|
init {
|
||||||
addCloseable {
|
addCloseable {
|
||||||
screensaverService.keepScreenOn(false)
|
screensaverService.keepScreenOn(false)
|
||||||
|
if (this@SlideshowViewModel::player.isInitialized) {
|
||||||
player.removeListener(this@SlideshowViewModel)
|
player.removeListener(this@SlideshowViewModel)
|
||||||
player.release()
|
player.release()
|
||||||
}
|
}
|
||||||
player.addListener(this@SlideshowViewModel)
|
}
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val photoPrefs = userPreferencesService.getCurrent().appPreferences.photoPreferences
|
try {
|
||||||
|
val appPreferences = userPreferencesService.getCurrent().appPreferences
|
||||||
|
val playerCreation =
|
||||||
|
playerFactory.createVideoPlayer(
|
||||||
|
backend = PlayerBackend.EXO_PLAYER,
|
||||||
|
appPreferences.playbackPreferences,
|
||||||
|
)
|
||||||
|
player = playerCreation.player
|
||||||
|
player.addListener(this@SlideshowViewModel)
|
||||||
|
|
||||||
|
val photoPrefs = appPreferences.photoPreferences
|
||||||
slideshowDelay =
|
slideshowDelay =
|
||||||
photoPrefs.slideshowDuration.takeIf { it >= AppPreference.SlideshowDuration.min }
|
photoPrefs.slideshowDuration.takeIf { it >= AppPreference.SlideshowDuration.min }
|
||||||
?: AppPreference.SlideshowDuration.defaultValue
|
?: AppPreference.SlideshowDuration.defaultValue
|
||||||
// val album =
|
|
||||||
// api.userLibraryApi
|
|
||||||
// .getItem(
|
|
||||||
// itemId = slideshowSettings.parentId,
|
|
||||||
// ).content
|
|
||||||
// .let { BaseItem(it, false) }
|
|
||||||
// this@SlideshowViewModel.album.setValueOnMain(album)
|
|
||||||
val includeItemTypes =
|
val includeItemTypes =
|
||||||
if (photoPrefs.slideshowPlayVideos) {
|
if (photoPrefs.slideshowPlayVideos) {
|
||||||
listOf(BaseItemKind.PHOTO, BaseItemKind.VIDEO)
|
listOf(BaseItemKind.PHOTO, BaseItemKind.VIDEO)
|
||||||
|
|
@ -163,8 +169,13 @@ class SlideshowViewModel
|
||||||
ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
|
ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope)
|
||||||
.init(slideshowSettings.index)
|
.init(slideshowSettings.index)
|
||||||
this@SlideshowViewModel._pager.setValueOnMain(pager)
|
this@SlideshowViewModel._pager.setValueOnMain(pager)
|
||||||
|
loading.update { LoadingState.Success }
|
||||||
updatePosition(slideshowSettings.index)?.join()
|
updatePosition(slideshowSettings.index)?.join()
|
||||||
if (slideshowSettings.startSlideshow) onMain { startSlideshow() }
|
if (slideshowSettings.startSlideshow) onMain { startSlideshow() }
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Timber.e(ex, "Error")
|
||||||
|
loading.update { LoadingState.Error(ex) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,14 +40,21 @@ message MpvOptions{
|
||||||
bool use_gpu_next = 2;
|
bool use_gpu_next = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AssPlaybackMode{
|
||||||
|
ASS_LIBASS = 0;
|
||||||
|
ASS_EXO_PLAYER = 1;
|
||||||
|
ASS_TRANSCODE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message PlaybackOverrides{
|
message PlaybackOverrides{
|
||||||
bool ac3_supported = 1;
|
bool ac3_supported = 1;
|
||||||
bool downmix_stereo = 2;
|
bool downmix_stereo = 2;
|
||||||
bool direct_play_ass = 3;
|
bool direct_play_ass = 3 [deprecated = true];
|
||||||
bool direct_play_pgs = 4;
|
bool direct_play_pgs = 4;
|
||||||
MediaExtensionStatus media_extensions_enabled = 5;
|
MediaExtensionStatus media_extensions_enabled = 5;
|
||||||
bool direct_play_dolby_vision_e_l = 6;
|
bool direct_play_dolby_vision_e_l = 6;
|
||||||
bool decode_av1 = 7;
|
bool decode_av1 = 7;
|
||||||
|
AssPlaybackMode ass_playback_mode = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PlaybackPreferences {
|
message PlaybackPreferences {
|
||||||
|
|
|
||||||
|
|
@ -312,7 +312,7 @@
|
||||||
<string name="check_for_updates">Check for updates</string>
|
<string name="check_for_updates">Check for updates</string>
|
||||||
<string name="combine_continue_next_summary">Applies to TV Series only</string>
|
<string name="combine_continue_next_summary">Applies to TV Series only</string>
|
||||||
<string name="combine_continue_next"><![CDATA[Combine Continue Watching & Next Up]]></string>
|
<string name="combine_continue_next"><![CDATA[Combine Continue Watching & Next Up]]></string>
|
||||||
<string name="direct_play_ass">Direct play ASS subtitles</string>
|
<string name="direct_play_ass">Use libass for ASS subtitles</string>
|
||||||
<string name="direct_play_pgs">Direct play PGS subtitles</string>
|
<string name="direct_play_pgs">Direct play PGS subtitles</string>
|
||||||
<string name="downmix_stereo">Always downmix to stereo</string>
|
<string name="downmix_stereo">Always downmix to stereo</string>
|
||||||
<string name="ffmpeg_extension_pref">Use FFmpeg decoder module</string>
|
<string name="ffmpeg_extension_pref">Use FFmpeg decoder module</string>
|
||||||
|
|
@ -757,4 +757,18 @@
|
||||||
<string name="discover_movies">Discover Movies</string>
|
<string name="discover_movies">Discover Movies</string>
|
||||||
<string name="studios_in">Studios in %1$s</string>
|
<string name="studios_in">Studios in %1$s</string>
|
||||||
|
|
||||||
|
<string name="ass_subtitle_mode_libass">Direct play with libass</string>
|
||||||
|
<string name="ass_subtitle_mode_exoplayer">Direct play with ExoPlayer built-in</string>
|
||||||
|
<string name="ass_subtitle_mode_transcode">Burn in/transcode on server</string>
|
||||||
|
<string name="ass_subtitle_playback">SSA/ASS subtitle playback</string>
|
||||||
|
<array name="ass_subtitle_modes">
|
||||||
|
<item>@string/ass_subtitle_mode_libass</item>
|
||||||
|
<item>@string/ass_subtitle_mode_exoplayer</item>
|
||||||
|
<item>@string/ass_subtitle_mode_transcode</item>
|
||||||
|
</array>
|
||||||
|
<array name="ass_subtitle_modes_summary">
|
||||||
|
<item>@string/default_track</item>
|
||||||
|
<item></item><!-- Intentionally blank -->
|
||||||
|
<item></item><!-- Intentionally blank -->
|
||||||
|
</array>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ preferenceKtx = "1.2.1"
|
||||||
tvprovider = "1.1.0"
|
tvprovider = "1.1.0"
|
||||||
workRuntimeKtx = "2.11.2"
|
workRuntimeKtx = "2.11.2"
|
||||||
paletteKtx = "1.0.0"
|
paletteKtx = "1.0.0"
|
||||||
|
assMedia = "0.4.0"
|
||||||
kotlinxCoroutinesTest = "1.10.2"
|
kotlinxCoroutinesTest = "1.10.2"
|
||||||
coreTesting = "2.2.0"
|
coreTesting = "2.2.0"
|
||||||
openapi-generator = "7.21.0"
|
openapi-generator = "7.21.0"
|
||||||
|
|
@ -139,6 +140,8 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t
|
||||||
androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" }
|
androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" }
|
||||||
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
|
androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" }
|
||||||
|
|
||||||
|
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-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue