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
<!-- Please include screenshots if the PR alters any UI elements -->
<img width="4284" height="5712" alt="IMG_9486"
src="https://github.com/user-attachments/assets/ddf95e68-6ebd-42f9-9565-568de4fbfc42"
/>

## AI or LLM usage
Developed in pair with Gemini and human-reviewed, tested.

---------

Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
Nehereus 2026-08-12 01:36:03 +08:00 • committed by GitHub
parent de30c2ccac
commit 1c0e9a5853
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 61 additions and 18 deletions

View file

@ -182,6 +182,8 @@ class AppPreferencesSerializer
.apply {
enabled = false
videoTunnelingEnabled = false
preferAc3Surround = ExperimentalPreference.PreferAc3ForSurround.defaultValue
disableAudioOffload = ExperimentalPreference.DisableAudioOffload.defaultValue
}.build()
}.build()

View file

@ -60,6 +60,17 @@ object ExperimentalPreference {
}
},
)
val DisableAudioOffload =
AppSwitchPreference<AppPreferences>(
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,
),
),
)

View file

@ -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))
}

View file

@ -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

View file

@ -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 {

View file

@ -404,6 +404,8 @@
<string name="will_remove_pin">Will remove PIN</string>
<string name="pin_digits_hint">[0–9]</string>
<string name="image_cache_size">Image disk cache size (MB)</string>
<string name="disable_audio_offload">Disable audio hardware offload</string>
<string name="disable_audio_offload_summary">Disabling offloading audio decoding may fix playback freezes on some devices</string>
<string name="view_options">View options</string>
<string name="columns">Columns</string>
<string name="spacing">Spacing</string>