From 1c0e9a585353cea54f6627c9f9865fade06bb2a7 Mon Sep 17 00:00:00 2001
From: Nehereus <55364637+Nehereus@users.noreply.github.com>
Date: Wed, 12 Aug 2026 01:36:03 +0800
Subject: [PATCH] Add a preference option to disable hardware audio offloading
(#1564)
## Description
By default, Wholphin plays music with hardware audio offloading enabled.
This delegates audio decoding to the device's hardware DSP, making
playback stability highly dependent on the manufacturer's system audio
implementation. On certain hardware (such as my TCL TVs, e.g., the
Chinese Model Q9L Pro), this implementation causes music playback to
hang indefinitely at 0 seconds.
### Related issues
#1372
### Testing
Tested on a TCL TV (Q9L Pro):
- Some musics playback hangs at 0s with offloading enabled.
- Disabling audio offloading fixes the issues for all medias.
## Screenshots
## AI or LLM usage
Developed in pair with Gemini and human-reviewed, tested.
---------
Co-authored-by: Damontecres
---
.../preferences/AppPreferencesSerializer.kt | 2 +
.../preferences/ExperimentalPreference.kt | 12 +++++
.../wholphin/services/MusicService.kt | 11 +++-
.../wholphin/services/PlayerFactory.kt | 51 ++++++++++++-------
app/src/main/proto/WholphinDataStore.proto | 1 +
app/src/main/res/values/strings.xml | 2 +
6 files changed, 61 insertions(+), 18 deletions(-)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt
index abcee865..c6dcd8ec 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt
@@ -182,6 +182,8 @@ class AppPreferencesSerializer
.apply {
enabled = false
videoTunnelingEnabled = false
+ preferAc3Surround = ExperimentalPreference.PreferAc3ForSurround.defaultValue
+ disableAudioOffload = ExperimentalPreference.DisableAudioOffload.defaultValue
}.build()
}.build()
diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt
index 3950d626..e5923659 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/ExperimentalPreference.kt
@@ -60,6 +60,17 @@ object ExperimentalPreference {
}
},
)
+
+ val DisableAudioOffload =
+ AppSwitchPreference(
+ title = R.string.disable_audio_offload,
+ defaultValue = false,
+ getter = { it.experimentalPreferences.disableAudioOffload },
+ setter = { prefs, value ->
+ prefs.updateExperimentalPreferences { disableAudioOffload = value }
+ },
+ summary = R.string.disable_audio_offload_summary,
+ )
}
val experimentalPreferences =
@@ -71,6 +82,7 @@ val experimentalPreferences =
listOf(
ExperimentalPreference.VideoTunneling,
ExperimentalPreference.PreferAc3ForSurround,
+ ExperimentalPreference.DisableAudioOffload,
),
),
)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt
index eea66307..38888485 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt
@@ -13,6 +13,7 @@ import androidx.media3.session.MediaSession
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.AudioItem
import com.github.damontecres.wholphin.data.model.BaseItem
+import com.github.damontecres.wholphin.preferences.get
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.gt
@@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
@@ -83,8 +85,15 @@ class MusicService
private val audioFormats by lazy { listOf(*supportedAudioCodecs) }
val player: Player by lazy {
+ val disableAudioOffload =
+ runBlocking {
+ userPreferencesService
+ .getCurrent()
+ .appPreferences.experimentalPreferences
+ .get { disableAudioOffload } ?: false
+ }
playerFactory
- .createAudioPlayer()
+ .createAudioPlayer(disableAudioOffload = disableAudioOffload)
.also {
it.addListener(MusicPlayerListener(it, _state))
}
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 24736015..cec0a205 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
@@ -132,9 +132,15 @@ class PlayerFactory
extractorsFactory,
)
}
+ val disableAudioOffload =
+ appPreferences.experimentalPreferences.get { disableAudioOffload } ?: false
val tunneling =
appPreferences.experimentalPreferences.get { videoTunnelingEnabled }
- val trackSelector = createTrackSelector(tunneling)
+ val trackSelector =
+ createTrackSelector(
+ tunneling = tunneling,
+ disableAudioOffload = disableAudioOffload,
+ )
ExoPlayer
.Builder(context)
@@ -164,7 +170,10 @@ class PlayerFactory
return PlayerCreation(newPlayer, assHandler)
}
- fun createAudioPlayer(extensions: MediaExtensionStatus = MediaExtensionStatus.MES_FALLBACK): ExoPlayer {
+ fun createAudioPlayer(
+ disableAudioOffload: Boolean,
+ extensions: MediaExtensionStatus = MediaExtensionStatus.MES_FALLBACK,
+ ): ExoPlayer {
val rendererMode =
when (extensions) {
MediaExtensionStatus.MES_FALLBACK -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
@@ -182,7 +191,7 @@ class PlayerFactory
OkHttpDataSource.Factory(authOkHttpClient),
extractorsFactory,
)
- val trackSelector = createTrackSelector()
+ val trackSelector = createTrackSelector(disableAudioOffload = disableAudioOffload)
return ExoPlayer
.Builder(context)
.setMediaSourceFactory(mediaSourceFactory)
@@ -205,20 +214,28 @@ class PlayerFactory
.setConstantBitrateSeekingEnabled(true)
.setConstantBitrateSeekingAlwaysEnabled(true)
- private fun createTrackSelector(tunneling: Boolean? = null) =
- DefaultTrackSelector(context).apply {
- setParameters(
- buildUponParameters()
- .apply {
- tunneling?.let { setTunnelingEnabled(tunneling) }
- }.setAudioOffloadPreferences(
- AudioOffloadPreferences
- .Builder()
- .setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
- .build(),
- ),
- )
- }
+ private fun createTrackSelector(
+ tunneling: Boolean? = null,
+ disableAudioOffload: Boolean = false,
+ ) = DefaultTrackSelector(context).apply {
+ val offloadMode =
+ if (disableAudioOffload) {
+ AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_DISABLED
+ } else {
+ AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED
+ }
+ setParameters(
+ buildUponParameters()
+ .apply {
+ tunneling?.let { setTunnelingEnabled(tunneling) }
+ }.setAudioOffloadPreferences(
+ AudioOffloadPreferences
+ .Builder()
+ .setAudioOffloadMode(offloadMode)
+ .build(),
+ ),
+ )
+ }
fun createMediaSession(player: Player) =
MediaSession
diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto
index 2db6201b..f190b43b 100644
--- a/app/src/main/proto/WholphinDataStore.proto
+++ b/app/src/main/proto/WholphinDataStore.proto
@@ -225,6 +225,7 @@ message ExperimentalPreferences {
bool enabled = 1;
bool video_tunneling_enabled = 2;
bool prefer_ac3_surround = 3;
+ bool disable_audio_offload = 4;
}
message AppPreferences {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bf37ea15..3b1bd402 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -404,6 +404,8 @@
Will remove PIN
[0–9]
Image disk cache size (MB)
+ Disable audio hardware offload
+ Disabling offloading audio decoding may fix playback freezes on some devices
View options
Columns
Spacing