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
This commit is contained in:
Ray 2026-04-19 15:10:15 -04:00 committed by GitHub
parent 790069d818
commit 08836e3c5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 77 additions and 26 deletions

View file

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

View file

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