Add toggles to override direct playback (#24)

Fixes #10 

Adds toggles to disable direct play of AC3 audio, ASS subtitles, and PGS
subtitles. Also a toggle to force down mixing audio to stereo.

This copies the behavior from the official client. I'm not sure if there
is a way to determine if direct playing AC3 is possible because it can
depend on devices other than the Android TV device such as a sound bar.
So it's up to the user to toggle off support if necessary.

Attribution: This PR uses on [code from the official
client](c775603df4/app/src/main/java/org/jellyfin/androidtv/util/profile/deviceProfile.kt).
This commit is contained in:
damontecres 2025-10-17 12:46:55 -04:00 committed by GitHub
parent bd4d4d5092
commit f220aeee44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 190 additions and 10 deletions

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.lifecycle.lifecycleScope
import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.runtime.rememberNavBackStack
import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
@ -34,9 +35,13 @@ import com.github.damontecres.wholphin.ui.nav.ApplicationContent
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.util.AppUpgradeHandler
import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.UpdateChecker import com.github.damontecres.wholphin.util.UpdateChecker
import com.github.damontecres.wholphin.util.profile.createDeviceProfile import com.github.damontecres.wholphin.util.profile.createDeviceProfile
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -59,10 +64,16 @@ class MainActivity : AppCompatActivity() {
@Inject @Inject
lateinit var updateChecker: UpdateChecker lateinit var updateChecker: UpdateChecker
@Inject
lateinit var appUpgradeHandler: AppUpgradeHandler
@OptIn(ExperimentalTvMaterial3Api::class) @OptIn(ExperimentalTvMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Timber.i("MainActivity.onCreate") Timber.i("MainActivity.onCreate")
lifecycleScope.launch(Dispatchers.IO + ExceptionHandler()) {
appUpgradeHandler.run()
}
setContent { setContent {
CoilConfig(okHttpClient, false) CoilConfig(okHttpClient, false)
val appPreferences by userPreferencesDataStore.data.collectAsState(null) val appPreferences by userPreferencesDataStore.data.collectAsState(null)

View file

@ -313,6 +313,51 @@ sealed interface AppPreference<T> {
}, },
) )
val Ac3Supported =
AppSwitchPreference(
title = R.string.ac3_supported,
defaultValue = true,
getter = { it.playbackPreferences.overrides.ac3Supported },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { ac3Supported = value }
},
summaryOn = R.string.enabled,
summaryOff = R.string.disabled,
)
val DownMixStereo =
AppSwitchPreference(
title = R.string.downmix_stereo,
defaultValue = false,
getter = { it.playbackPreferences.overrides.downmixStereo },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { downmixStereo = value }
},
summaryOn = R.string.enabled,
summaryOff = R.string.disabled,
)
val DirectPlayAss =
AppSwitchPreference(
title = R.string.direct_play_ass,
defaultValue = true,
getter = { it.playbackPreferences.overrides.directPlayAss },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { directPlayAss = value }
},
summaryOn = R.string.enabled,
summaryOff = R.string.disabled,
)
val DirectPlayPgs =
AppSwitchPreference(
title = R.string.direct_play_pgs,
defaultValue = true,
getter = { it.playbackPreferences.overrides.directPlayPgs },
setter = { prefs, value ->
prefs.updatePlaybackOverrides { directPlayPgs = value }
},
summaryOn = R.string.enabled,
summaryOff = R.string.disabled,
)
val RememberSelectedTab = val RememberSelectedTab =
AppSwitchPreference( AppSwitchPreference(
title = R.string.remember_selected_tab, title = R.string.remember_selected_tab,
@ -521,6 +566,16 @@ val advancedPreferences =
AppPreference.PlaybackDebugInfo, AppPreference.PlaybackDebugInfo,
), ),
), ),
PreferenceGroup(
title = R.string.playback_overrides,
preferences =
listOf(
AppPreference.DownMixStereo,
AppPreference.Ac3Supported,
AppPreference.DirectPlayAss,
AppPreference.DirectPlayPgs,
),
),
PreferenceGroup( PreferenceGroup(
title = R.string.updates, title = R.string.updates,
preferences = preferences =

View file

@ -42,6 +42,16 @@ class AppPreferencesSerializer
skipCommercials = AppPreference.SkipCommercials.defaultValue skipCommercials = AppPreference.SkipCommercials.defaultValue
skipPreviews = AppPreference.SkipPreviews.defaultValue skipPreviews = AppPreference.SkipPreviews.defaultValue
skipRecaps = AppPreference.SkipRecaps.defaultValue skipRecaps = AppPreference.SkipRecaps.defaultValue
overrides =
PlaybackOverrides
.newBuilder()
.apply {
ac3Supported = AppPreference.Ac3Supported.defaultValue
downmixStereo = AppPreference.DownMixStereo.defaultValue
directPlayAss = AppPreference.DirectPlayAss.defaultValue
directPlayPgs = AppPreference.DirectPlayPgs.defaultValue
}.build()
}.build() }.build()
homePagePreferences = homePagePreferences =
HomePagePreferences HomePagePreferences
@ -81,6 +91,11 @@ inline fun AppPreferences.updatePlaybackPreferences(block: PlaybackPreferences.B
playbackPreferences = playbackPreferences.toBuilder().apply(block).build() playbackPreferences = playbackPreferences.toBuilder().apply(block).build()
} }
inline fun AppPreferences.updatePlaybackOverrides(block: PlaybackOverrides.Builder.() -> Unit): AppPreferences =
updatePlaybackPreferences {
overrides = overrides.toBuilder().apply(block).build()
}
inline fun AppPreferences.updateHomePagePreferences(block: HomePagePreferences.Builder.() -> Unit): AppPreferences = inline fun AppPreferences.updateHomePagePreferences(block: HomePagePreferences.Builder.() -> Unit): AppPreferences =
update { update {
homePagePreferences = homePagePreferences.toBuilder().apply(block).build() homePagePreferences = homePagePreferences.toBuilder().apply(block).build()

View file

@ -0,0 +1,84 @@
package com.github.damontecres.wholphin.util
import android.content.Context
import android.os.Build
import androidx.core.content.edit
import androidx.datastore.core.DataStore
import androidx.preference.PreferenceManager
import com.github.damontecres.wholphin.WholphinApplication
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AppUpgradeHandler
@Inject
constructor(
@param:ApplicationContext private val context: Context,
private val appPreferences: DataStore<AppPreferences>,
) {
suspend fun run() {
val pkgInfo = WholphinApplication.instance.packageManager.getPackageInfo(WholphinApplication.instance.packageName, 0)
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val previousVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null)
val previousVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1)
val newVersion = pkgInfo.versionName!!
val newVersionCode =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
pkgInfo.longVersionCode
} else {
pkgInfo.versionCode.toLong()
}
if (newVersion != previousVersion || newVersionCode != previousVersionCode) {
Timber.i(
"App updated: $previousVersion=>$newVersion, $previousVersionCode=>$newVersionCode",
)
prefs.edit(true) {
putString(VERSION_NAME_PREVIOUS_KEY, previousVersion)
putLong(VERSION_CODE_PREVIOUS_KEY, previousVersionCode)
putString(VERSION_NAME_CURRENT_KEY, newVersion)
putLong(VERSION_CODE_CURRENT_KEY, newVersionCode)
}
try {
upgradeApp(
context,
Version.fromString(previousVersion ?: "0.0.0"),
Version.fromString(newVersion),
appPreferences,
)
} catch (ex: Exception) {
Timber.e(ex, "Exception during app upgrade")
}
}
}
companion object {
const val VERSION_NAME_PREVIOUS_KEY = "version.previous.name"
const val VERSION_CODE_PREVIOUS_KEY = "version.previous.code"
const val VERSION_NAME_CURRENT_KEY = "version.current.name"
const val VERSION_CODE_CURRENT_KEY = "version.current.code"
}
}
suspend fun upgradeApp(
context: Context,
previous: Version,
current: Version,
appPreferences: DataStore<AppPreferences>,
) {
if (previous.isEqualOrBefore(Version.fromString("0.1.0-2-g0"))) {
appPreferences.updateData {
it.updatePlaybackOverrides {
ac3Supported = AppPreference.Ac3Supported.defaultValue
downmixStereo = AppPreference.DownMixStereo.defaultValue
directPlayAss = AppPreference.DirectPlayAss.defaultValue
directPlayPgs = AppPreference.DirectPlayPgs.defaultValue
}
}
}
}

View file

@ -45,20 +45,21 @@ private val supportedAudioCodecs =
Codec.Audio.VORBIS, Codec.Audio.VORBIS,
) )
// TODO use preferences
fun createDeviceProfile( fun createDeviceProfile(
context: Context, context: Context,
userPreferences: UserPreferences, userPreferences: UserPreferences,
disableDirectPlay: Boolean = false, disableDirectPlay: Boolean = false,
) = createDeviceProfile( ) = userPreferences.appPreferences.playbackPreferences.let { prefs ->
mediaTest = MediaCodecCapabilitiesTest(context), createDeviceProfile(
maxBitrate = 100_000_000, mediaTest = MediaCodecCapabilitiesTest(context),
disableDirectPlay = disableDirectPlay, maxBitrate = 100_000_000,
isAC3Enabled = true, disableDirectPlay = disableDirectPlay,
downMixAudio = false, isAC3Enabled = prefs.overrides.ac3Supported,
assDirectPlay = true, downMixAudio = prefs.overrides.downmixStereo,
pgsDirectPlay = true, assDirectPlay = prefs.overrides.directPlayAss,
) pgsDirectPlay = prefs.overrides.directPlayPgs,
)
}
fun createDeviceProfile( fun createDeviceProfile(
mediaTest: MediaCodecCapabilitiesTest, mediaTest: MediaCodecCapabilitiesTest,

View file

@ -9,6 +9,13 @@ enum SkipSegmentBehavior{
ASK_TO_SKIP = 2; ASK_TO_SKIP = 2;
} }
message PlaybackOverrides{
bool ac3_supported = 1;
bool downmix_stereo = 2;
bool direct_play_ass = 3;
bool direct_play_pgs = 4;
}
message PlaybackPreferences { message PlaybackPreferences {
int64 skip_forward_ms = 1; int64 skip_forward_ms = 1;
int64 skip_back_ms = 2; int64 skip_back_ms = 2;
@ -25,6 +32,8 @@ message PlaybackPreferences {
SkipSegmentBehavior skip_commercials = 12; SkipSegmentBehavior skip_commercials = 12;
SkipSegmentBehavior skip_recaps = 13; SkipSegmentBehavior skip_recaps = 13;
SkipSegmentBehavior skip_previews = 14; SkipSegmentBehavior skip_previews = 14;
PlaybackOverrides overrides = 15;
} }
message HomePagePreferences{ message HomePagePreferences{

View file

@ -77,5 +77,10 @@
<string name="clear_image_cache">Clear image cache</string> <string name="clear_image_cache">Clear image cache</string>
<string name="shuffle">Shuffle</string> <string name="shuffle">Shuffle</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="ac3_supported">Device supports AC3/Dolby Digital</string>
<string name="downmix_stereo">Always downmix to stereo</string>
<string name="playback_overrides">Playback overrides</string>
<string name="direct_play_ass">Direct play ASS subtitles</string>
<string name="direct_play_pgs">Direct play PGS subtitles</string>
</resources> </resources>