From 08836e3c5d8a43acbb2b08f21980eb7dd8bdd9c2 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:10:15 -0400 Subject: [PATCH] Update ExoPlayer with better audio configuration (#1258) ## Description Makes some changes when building ExoPlayer to help improve pass through audio. The major changes are enabled constant bitrate seeking and audio offloading. Documentation is thin on these features, but [this blog post](https://www.qualcomm.com/developer/blog/2023/08/audio-offload-support-exoplayer) while focused on ExoPlayer2 and power usage, does explain a bit about audio offloading. Small section on constant bitrate seeking: https://developer.android.com/media/media3/exoplayer/customization#enabling-constant-bitrate-seeking ### Related issues N/A ### Testing Emulator, nvidia shield w/ AVR to check pass through ## Screenshots N/A ## AI or LLM usage None --- .../wholphin/services/MusicService.kt | 25 +----- .../wholphin/services/PlayerFactory.kt | 78 ++++++++++++++++++- 2 files changed, 77 insertions(+), 26 deletions(-) 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 ff7699a6..6eddb51c 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 @@ -5,20 +5,14 @@ import androidx.annotation.OptIn import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember -import androidx.media3.common.AudioAttributes -import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Timeline import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.okhttp.OkHttpDataSource -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory 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.services.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope import com.github.damontecres.wholphin.ui.DefaultItemFields import com.github.damontecres.wholphin.ui.main.settings.MoveDirection @@ -44,7 +38,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.instantMixApi import org.jellyfin.sdk.api.client.extensions.universalAudioApi @@ -74,9 +67,9 @@ class MusicService @Inject constructor( @param:ApplicationContext private val context: Context, - @param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient, @param:DefaultCoroutineScope private val defaultScope: CoroutineScope, private val api: ApiClient, + private val playerFactory: PlayerFactory, private val serverRepository: ServerRepository, private val imageUrlService: ImageUrlService, ) { @@ -86,21 +79,9 @@ class MusicService private val audioFormats by lazy { listOf(*supportedAudioCodecs) } val player: Player by lazy { - ExoPlayer - .Builder(context) - .setMediaSourceFactory( - DefaultMediaSourceFactory( - OkHttpDataSource.Factory(authOkHttpClient), - ), - ).build() + playerFactory + .createAudioPlayer() .also { - it.setAudioAttributes( - AudioAttributes - .Builder() - .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) - .build(), - false, - ) 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 f8d5ad2b..da00fd3d 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 @@ -6,26 +6,29 @@ import android.content.Context import android.os.Build import android.os.Handler import androidx.annotation.OptIn -import androidx.datastore.core.DataStore +import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.Player +import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences import androidx.media3.common.util.ExperimentalApi import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.DefaultDataSource +import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.Renderer import androidx.media3.exoplayer.RenderersFactory import androidx.media3.exoplayer.mediacodec.MediaCodecSelector import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.video.MediaCodecVideoRenderer import androidx.media3.exoplayer.video.VideoRendererEventListener import androidx.media3.extractor.DefaultExtractorsFactory -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.PlaybackPreferences import com.github.damontecres.wholphin.preferences.PlayerBackend +import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient import com.github.damontecres.wholphin.util.mpv.MpvPlayer import dagger.hilt.android.qualifiers.ApplicationContext import io.github.peerless2012.ass.media.AssHandler @@ -35,6 +38,7 @@ import io.github.peerless2012.ass.media.parser.AssSubtitleParserFactory import io.github.peerless2012.ass.media.type.AssRenderType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient import timber.log.Timber import java.lang.reflect.Constructor import javax.inject.Inject @@ -48,7 +52,7 @@ class PlayerFactory @Inject constructor( @param:ApplicationContext private val context: Context, - private val appPreferences: DataStore, + @param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient, ) { @Volatile var currentPlayer: Player? = null @@ -95,7 +99,7 @@ class PlayerFactory else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON } val dataSourceFactory = DefaultDataSource.Factory(context) - val extractorsFactory = DefaultExtractorsFactory() + val extractorsFactory = createExtractorsFactory() var renderersFactory: RenderersFactory = WholphinRenderersFactory(context, decodeAv1) .setEnableDecoderFallback(true) @@ -118,13 +122,25 @@ class PlayerFactory extractorsFactory, ) } + val trackSelector = createTrackSelector() + ExoPlayer .Builder(context) .setMediaSourceFactory(mediaSourceFactory) .setRenderersFactory(renderersFactory) + .setTrackSelector(trackSelector) .build() .apply { assHandler?.init(this) + withContext(Dispatchers.Main) { + setAudioAttributes( + AudioAttributes + .Builder() + .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE) + .build(), + false, + ) + } } } @@ -135,6 +151,60 @@ class PlayerFactory currentPlayer = newPlayer return PlayerCreation(newPlayer, assHandler) } + + fun createAudioPlayer(extensions: MediaExtensionStatus = MediaExtensionStatus.MES_FALLBACK): ExoPlayer { + 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 + } + val extractorsFactory = createExtractorsFactory() + val renderersFactory: RenderersFactory = + WholphinRenderersFactory(context, false) + .setEnableDecoderFallback(true) + .setExtensionRendererMode(rendererMode) + val mediaSourceFactory = + DefaultMediaSourceFactory( + OkHttpDataSource.Factory(authOkHttpClient), + extractorsFactory, + ) + val trackSelector = createTrackSelector() + return ExoPlayer + .Builder(context) + .setMediaSourceFactory(mediaSourceFactory) + .setRenderersFactory(renderersFactory) + .setTrackSelector(trackSelector) + .build() + .also { + it.setAudioAttributes( + AudioAttributes + .Builder() + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .build(), + false, + ) + } + } + + private fun createExtractorsFactory() = + DefaultExtractorsFactory() + .setConstantBitrateSeekingEnabled(true) + .setConstantBitrateSeekingAlwaysEnabled(true) + + private fun createTrackSelector() = + DefaultTrackSelector(context).apply { + setParameters( + buildUponParameters() + .setAudioOffloadPreferences( + AudioOffloadPreferences + .Builder() + .setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) + .build(), + ), + ) + } } val Player.isReleased: Boolean