Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.lostf1sh.pixelplayeross.data.service.player

import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import com.lostf1sh.pixelplayeross.data.model.AudioOutputMode
import java.util.Locale

@androidx.annotation.OptIn(UnstableApi::class)
Expand All @@ -13,12 +14,21 @@ internal object AudioDecoderPolicy {
AUDIO_MIDI
)

fun shouldUseExtensionRenderer(mimeType: String): Boolean {
return extensionOnlyMimeTypes.any { it.equals(mimeType, ignoreCase = true) }
fun shouldUseExtensionRenderer(mimeType: String, outputMode: AudioOutputMode): Boolean {
// Some platform FLAC decoders produce corrupt audio when Media3 requests PCM_FLOAT
// (reported on the Galaxy S25 Ultra, issue #122). FFmpeg can decode FLAC directly to
// float PCM without that platform negotiation. Keep the normal decoder order in the
// integer output modes, where the reporter confirmed playback works.
return (outputMode.usesFloatOutput && MimeTypes.AUDIO_FLAC.equals(mimeType, ignoreCase = true)) ||
extensionOnlyMimeTypes.any { it.equals(mimeType, ignoreCase = true) }
}

fun <T> selectPlatformDecoders(mimeType: String, decoderInfos: List<T>): List<T> {
return if (shouldUseExtensionRenderer(mimeType)) {
fun <T> selectPlatformDecoders(
mimeType: String,
decoderInfos: List<T>,
outputMode: AudioOutputMode
): List<T> {
return if (shouldUseExtensionRenderer(mimeType, outputMode)) {
emptyList()
} else {
decoderInfos
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -990,22 +990,25 @@ class DualPlayerEngine @Inject constructor(
}

private fun buildPlayer(): ExoPlayer {
// Decoder selection happens on the playback thread. Keep it tied to this player's sink
// even while a mode change is rebuilding the players.
val outputMode = audioOutputMode
val mediaCodecSelector = MediaCodecSelector { mimeType, requiresSecureDecoder, requiresTunnelingDecoder ->
val decoderInfos = MediaCodecSelector.DEFAULT.getDecoderInfos(
mimeType,
requiresSecureDecoder,
requiresTunnelingDecoder
)

AudioDecoderPolicy.selectPlatformDecoders(mimeType, decoderInfos)
AudioDecoderPolicy.selectPlatformDecoders(mimeType, decoderInfos, outputMode)
}
val renderersFactory = object : DefaultRenderersFactory(context) {
override fun buildAudioSink(
context: Context,
enableFloatOutput: Boolean,
enableAudioOutputPlaybackParams: Boolean
): AudioSink {
if (audioOutputMode.usesUnmodifiedMedia3AudioSink) {
if (outputMode.usesUnmodifiedMedia3AudioSink) {
// Android's audio policy may
// grant a DIRECT thread for a compatible device/format, or safely fall back
// to the mixed path. This deliberately does not promise exclusive output.
Expand All @@ -1018,7 +1021,7 @@ class DualPlayerEngine @Inject constructor(
) { "Media3 did not create its default AudioSink" }
}
return DefaultAudioSink.Builder(context)
.setEnableFloatOutput(audioOutputMode.usesFloatOutput)
.setEnableFloatOutput(outputMode.usesFloatOutput)
.setEnableAudioOutputPlaybackParameters(enableAudioOutputPlaybackParams)
.setAudioProcessorChain(
DefaultAudioSink.DefaultAudioProcessorChain(
Expand Down Expand Up @@ -1056,7 +1059,7 @@ class DualPlayerEngine @Inject constructor(
out: ArrayList<Renderer>
) {
}
}.setEnableAudioFloatOutput(audioOutputMode.usesFloatOutput)
}.setEnableAudioFloatOutput(outputMode.usesFloatOutput)
.setMediaCodecSelector(mediaCodecSelector)
.setEnableDecoderFallback(true)
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
Expand Down Expand Up @@ -1126,7 +1129,7 @@ class DualPlayerEngine @Inject constructor(
.setMediaSourceFactory(DefaultMediaSourceFactory(resolvingFactory, extractorsFactory))
.setLoadControl(loadControl)
.build().apply {
if (!audioOutputMode.usesUnmodifiedMedia3AudioSink) {
if (!outputMode.usesUnmodifiedMedia3AudioSink) {
sharedAudioSessionIdOrNull()?.let { setAudioSessionId(it) }
}
setAudioAttributes(audioAttributes, false)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,74 @@
package com.lostf1sh.pixelplayeross.data.service.player

import androidx.media3.common.MimeTypes
import com.lostf1sh.pixelplayeross.data.model.AudioOutputMode
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource

class AudioDecoderPolicyTest {

@Test
fun selectPlatformDecoders_routesAlacToExtensionRenderer() {
@ParameterizedTest
@EnumSource(AudioOutputMode::class)
fun selectPlatformDecoders_routesAlacToExtensionRenderer(outputMode: AudioOutputMode) {
val decoders = listOf("c2.qti.alac.decoder", "c2.android.alac.decoder")

val selected = AudioDecoderPolicy.selectPlatformDecoders(MimeTypes.AUDIO_ALAC, decoders)
val selected = AudioDecoderPolicy.selectPlatformDecoders(MimeTypes.AUDIO_ALAC, decoders, outputMode)

assertThat(selected).isEmpty()
}

@Test
fun selectPlatformDecoders_routesMidiToExtensionRenderer() {
@ParameterizedTest
@EnumSource(AudioOutputMode::class)
fun selectPlatformDecoders_routesMidiToExtensionRenderer(outputMode: AudioOutputMode) {
val decoders = listOf("platform-midi-decoder")

val selected = AudioDecoderPolicy.selectPlatformDecoders(MimeTypes.AUDIO_EXOPLAYER_MIDI, decoders)
val selected = AudioDecoderPolicy.selectPlatformDecoders(MimeTypes.AUDIO_EXOPLAYER_MIDI, decoders, outputMode)

assertThat(selected).isEmpty()
}

@Test
fun selectPlatformDecoders_preservesMedia3OrderForCoreFormats() {
@ParameterizedTest
@EnumSource(AudioOutputMode::class)
fun selectPlatformDecoders_preservesMedia3OrderForCoreFormats(outputMode: AudioOutputMode) {
val decoders = listOf("c2.android.aac.decoder", "c2.qti.aac.decoder")

val selected = AudioDecoderPolicy.selectPlatformDecoders(MimeTypes.AUDIO_AAC, decoders)
val selected = AudioDecoderPolicy.selectPlatformDecoders(MimeTypes.AUDIO_AAC, decoders, outputMode)

assertThat(selected).containsExactlyElementsIn(decoders).inOrder()
}

@Test
fun selectPlatformDecoders_routesFlacToExtensionRendererForFloatOutput() {
val decoders = listOf("c2.qti.flac.decoder", "c2.android.flac.decoder")

val selected = AudioDecoderPolicy.selectPlatformDecoders(
MimeTypes.AUDIO_FLAC, decoders, AudioOutputMode.PCM_FLOAT
)

assertThat(selected).isEmpty()
}

@ParameterizedTest
@EnumSource(value = AudioOutputMode::class, names = ["SYSTEM_DEFAULT", "DIRECT"])
fun selectPlatformDecoders_preservesFlacDecodersForIntegerOutput(outputMode: AudioOutputMode) {
val decoders = listOf("c2.qti.flac.decoder", "c2.android.flac.decoder")

val selected = AudioDecoderPolicy.selectPlatformDecoders(MimeTypes.AUDIO_FLAC, decoders, outputMode)

assertThat(selected).containsExactlyElementsIn(decoders).inOrder()
}

@Test
fun selectPlatformDecoders_matchesFlacMimeTypeIgnoringCase() {
val selected = AudioDecoderPolicy.selectPlatformDecoders(
"AUDIO/FLAC", listOf("c2.qti.flac.decoder"), AudioOutputMode.PCM_FLOAT
)

assertThat(selected).isEmpty()
}

@Test
fun isLikelyHardwareDecoder_marksSoftwareRenderersAsSoftware() {
assertThat(AudioDecoderPolicy.isLikelyHardwareDecoder("OMX.google.aac.decoder")).isFalse()
Expand Down