mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
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:
parent
790069d818
commit
08836e3c5d
2 changed files with 77 additions and 26 deletions
|
|
@ -5,20 +5,14 @@ import androidx.annotation.OptIn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.media3.common.AudioAttributes
|
|
||||||
import androidx.media3.common.C
|
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Timeline
|
import androidx.media3.common.Timeline
|
||||||
import androidx.media3.common.util.UnstableApi
|
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 androidx.media3.session.MediaSession
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.AudioItem
|
import com.github.damontecres.wholphin.data.model.AudioItem
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
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.services.hilt.DefaultCoroutineScope
|
||||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||||
import com.github.damontecres.wholphin.ui.main.settings.MoveDirection
|
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.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.extensions.instantMixApi
|
import org.jellyfin.sdk.api.client.extensions.instantMixApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.universalAudioApi
|
import org.jellyfin.sdk.api.client.extensions.universalAudioApi
|
||||||
|
|
@ -74,9 +67,9 @@ class MusicService
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
@param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient,
|
|
||||||
@param:DefaultCoroutineScope private val defaultScope: CoroutineScope,
|
@param:DefaultCoroutineScope private val defaultScope: CoroutineScope,
|
||||||
private val api: ApiClient,
|
private val api: ApiClient,
|
||||||
|
private val playerFactory: PlayerFactory,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
private val imageUrlService: ImageUrlService,
|
private val imageUrlService: ImageUrlService,
|
||||||
) {
|
) {
|
||||||
|
|
@ -86,21 +79,9 @@ class MusicService
|
||||||
private val audioFormats by lazy { listOf(*supportedAudioCodecs) }
|
private val audioFormats by lazy { listOf(*supportedAudioCodecs) }
|
||||||
|
|
||||||
val player: Player by lazy {
|
val player: Player by lazy {
|
||||||
ExoPlayer
|
playerFactory
|
||||||
.Builder(context)
|
.createAudioPlayer()
|
||||||
.setMediaSourceFactory(
|
|
||||||
DefaultMediaSourceFactory(
|
|
||||||
OkHttpDataSource.Factory(authOkHttpClient),
|
|
||||||
),
|
|
||||||
).build()
|
|
||||||
.also {
|
.also {
|
||||||
it.setAudioAttributes(
|
|
||||||
AudioAttributes
|
|
||||||
.Builder()
|
|
||||||
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
|
||||||
.build(),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
it.addListener(MusicPlayerListener(it, _state))
|
it.addListener(MusicPlayerListener(it, _state))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,29 @@ import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.media3.common.AudioAttributes
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences
|
||||||
import androidx.media3.common.util.ExperimentalApi
|
import androidx.media3.common.util.ExperimentalApi
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.datasource.DefaultDataSource
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
|
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.exoplayer.Renderer
|
import androidx.media3.exoplayer.Renderer
|
||||||
import androidx.media3.exoplayer.RenderersFactory
|
import androidx.media3.exoplayer.RenderersFactory
|
||||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer
|
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer
|
||||||
import androidx.media3.exoplayer.video.VideoRendererEventListener
|
import androidx.media3.exoplayer.video.VideoRendererEventListener
|
||||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
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.AssPlaybackMode
|
||||||
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus
|
||||||
import com.github.damontecres.wholphin.preferences.PlaybackPreferences
|
import com.github.damontecres.wholphin.preferences.PlaybackPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||||
|
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||||
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
import com.github.damontecres.wholphin.util.mpv.MpvPlayer
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import io.github.peerless2012.ass.media.AssHandler
|
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 io.github.peerless2012.ass.media.type.AssRenderType
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.lang.reflect.Constructor
|
import java.lang.reflect.Constructor
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -48,7 +52,7 @@ class PlayerFactory
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
private val appPreferences: DataStore<AppPreferences>,
|
@param:AuthOkHttpClient private val authOkHttpClient: OkHttpClient,
|
||||||
) {
|
) {
|
||||||
@Volatile
|
@Volatile
|
||||||
var currentPlayer: Player? = null
|
var currentPlayer: Player? = null
|
||||||
|
|
@ -95,7 +99,7 @@ class PlayerFactory
|
||||||
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
else -> DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
|
||||||
}
|
}
|
||||||
val dataSourceFactory = DefaultDataSource.Factory(context)
|
val dataSourceFactory = DefaultDataSource.Factory(context)
|
||||||
val extractorsFactory = DefaultExtractorsFactory()
|
val extractorsFactory = createExtractorsFactory()
|
||||||
var renderersFactory: RenderersFactory =
|
var renderersFactory: RenderersFactory =
|
||||||
WholphinRenderersFactory(context, decodeAv1)
|
WholphinRenderersFactory(context, decodeAv1)
|
||||||
.setEnableDecoderFallback(true)
|
.setEnableDecoderFallback(true)
|
||||||
|
|
@ -118,13 +122,25 @@ class PlayerFactory
|
||||||
extractorsFactory,
|
extractorsFactory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val trackSelector = createTrackSelector()
|
||||||
|
|
||||||
ExoPlayer
|
ExoPlayer
|
||||||
.Builder(context)
|
.Builder(context)
|
||||||
.setMediaSourceFactory(mediaSourceFactory)
|
.setMediaSourceFactory(mediaSourceFactory)
|
||||||
.setRenderersFactory(renderersFactory)
|
.setRenderersFactory(renderersFactory)
|
||||||
|
.setTrackSelector(trackSelector)
|
||||||
.build()
|
.build()
|
||||||
.apply {
|
.apply {
|
||||||
assHandler?.init(this)
|
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
|
currentPlayer = newPlayer
|
||||||
return PlayerCreation(newPlayer, assHandler)
|
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
|
val Player.isReleased: Boolean
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue