mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
Fix TV audio sink capability handling
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
package hu.bbara.purefin.core.data.client
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes as PlatformAudioAttributes
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioTrack
|
||||
import android.media.MediaCodecList
|
||||
import android.util.Log
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
@@ -20,7 +23,9 @@ import org.jellyfin.sdk.model.api.TranscodingProfile
|
||||
object AndroidDeviceProfile {
|
||||
|
||||
private const val TAG = "AndroidDeviceProfile"
|
||||
private const val DEFAULT_MAX_AUDIO_CHANNELS = 8
|
||||
private const val DEFAULT_MAX_AUDIO_CHANNELS = 2
|
||||
private const val PROBE_SAMPLE_RATE = 48_000
|
||||
private const val PROBE_ENCODING = AudioFormat.ENCODING_PCM_16BIT
|
||||
|
||||
@Volatile
|
||||
private var cachedSnapshot: CapabilitySnapshot? = null
|
||||
@@ -214,7 +219,7 @@ object AndroidDeviceProfile {
|
||||
val audioManager = context.getSystemService(AudioManager::class.java) ?: return DEFAULT_MAX_AUDIO_CHANNELS
|
||||
return runCatching {
|
||||
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||
val maxDeviceChannels = devices
|
||||
val reportedMaxChannels = devices
|
||||
.flatMap { device ->
|
||||
buildList {
|
||||
addAll(device.channelCounts.toList())
|
||||
@@ -223,15 +228,67 @@ object AndroidDeviceProfile {
|
||||
}
|
||||
}
|
||||
.maxOrNull()
|
||||
|
||||
maxDeviceChannels
|
||||
?.takeIf { it > 0 }
|
||||
?: DEFAULT_MAX_AUDIO_CHANNELS
|
||||
|
||||
val verifiedMaxChannels = verifyOutputChannelSupport(reportedMaxChannels)
|
||||
Log.d(TAG, "Reported max audio channels: $reportedMaxChannels, verified: $verifiedMaxChannels")
|
||||
verifiedMaxChannels
|
||||
}.getOrElse {
|
||||
DEFAULT_MAX_AUDIO_CHANNELS
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyOutputChannelSupport(reportedMaxChannels: Int): Int {
|
||||
val candidates = buildList {
|
||||
if (reportedMaxChannels >= 8) add(8)
|
||||
if (reportedMaxChannels >= 6) add(6)
|
||||
add(2)
|
||||
}.distinct()
|
||||
|
||||
return candidates.firstOrNull(::canCreateAudioTrackForChannelCount) ?: DEFAULT_MAX_AUDIO_CHANNELS
|
||||
}
|
||||
|
||||
private fun canCreateAudioTrackForChannelCount(channelCount: Int): Boolean {
|
||||
val channelMask = when (channelCount) {
|
||||
8 -> AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
|
||||
6 -> AudioFormat.CHANNEL_OUT_5POINT1
|
||||
2 -> AudioFormat.CHANNEL_OUT_STEREO
|
||||
else -> return false
|
||||
}
|
||||
|
||||
val minBufferSize = AudioTrack.getMinBufferSize(PROBE_SAMPLE_RATE, channelMask, PROBE_ENCODING)
|
||||
if (minBufferSize <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
val audioTrack = AudioTrack.Builder()
|
||||
.setAudioAttributes(
|
||||
PlatformAudioAttributes.Builder()
|
||||
.setUsage(PlatformAudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(PlatformAudioAttributes.CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
)
|
||||
.setAudioFormat(
|
||||
AudioFormat.Builder()
|
||||
.setSampleRate(PROBE_SAMPLE_RATE)
|
||||
.setEncoding(PROBE_ENCODING)
|
||||
.setChannelMask(channelMask)
|
||||
.build()
|
||||
)
|
||||
.setTransferMode(AudioTrack.MODE_STREAM)
|
||||
.setBufferSizeInBytes(minBufferSize * 2)
|
||||
.build()
|
||||
|
||||
try {
|
||||
audioTrack.state == AudioTrack.STATE_INITIALIZED
|
||||
} finally {
|
||||
audioTrack.release()
|
||||
}
|
||||
}.getOrElse { false }
|
||||
}
|
||||
|
||||
private fun channelMaskToCount(mask: Int): Int {
|
||||
return if (mask == 0) {
|
||||
0
|
||||
|
||||
@@ -18,6 +18,7 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import hu.bbara.purefin.core.data.client.AndroidDeviceProfile
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
@@ -27,6 +28,7 @@ object VideoPlayerModule {
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideVideoPlayer(application: Application, cacheDataSourceFactory: CacheDataSource.Factory): Player {
|
||||
val capabilitySnapshot = AndroidDeviceProfile.getSnapshot(application)
|
||||
val trackSelector = DefaultTrackSelector(application)
|
||||
val audioAttributes =
|
||||
AudioAttributes.Builder()
|
||||
@@ -37,7 +39,9 @@ object VideoPlayerModule {
|
||||
trackSelector.setParameters(
|
||||
trackSelector
|
||||
.buildUponParameters()
|
||||
.setTunnelingEnabled(true)
|
||||
.setMaxAudioChannelCount(capabilitySnapshot.maxAudioChannels)
|
||||
.setExceedAudioConstraintsIfNecessary(false)
|
||||
.setTunnelingEnabled(false)
|
||||
// .setPreferredAudioLanguage(
|
||||
// appPreferences.getValue(appPreferences.preferredAudioLanguage)
|
||||
// )
|
||||
|
||||
@@ -10,6 +10,10 @@ internal object PlaybackRetryPolicy {
|
||||
private val retryableErrorCodes = setOf(
|
||||
PlaybackException.ERROR_CODE_DECODER_INIT_FAILED,
|
||||
PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED,
|
||||
PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED,
|
||||
PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED,
|
||||
PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED,
|
||||
PlaybackException.ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED,
|
||||
PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED,
|
||||
PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED
|
||||
)
|
||||
@@ -45,6 +49,10 @@ internal object PlaybackRetryPolicy {
|
||||
}
|
||||
}.lowercase()
|
||||
|
||||
return "decoder" in detail || "codec" in detail || "unsupported" in detail
|
||||
return "decoder" in detail ||
|
||||
"codec" in detail ||
|
||||
"unsupported" in detail ||
|
||||
"audiotrack" in detail ||
|
||||
"audio sink" in detail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,22 @@ class PlaybackRetryPolicyTest {
|
||||
assertTrue(PlaybackRetryPolicy.shouldRetryWithTranscoding(error, playbackReportContext))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `audio track init failures are retried when transcoding is available`() {
|
||||
val error = PlayerError(
|
||||
summary = "Playback error",
|
||||
technicalDetail = "AudioTrack init failed",
|
||||
source = PlayerErrorSource.PLAYBACK,
|
||||
errorCode = PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED,
|
||||
errorCodeName = "ERROR_CODE_AUDIO_TRACK_INIT_FAILED",
|
||||
retryable = true
|
||||
)
|
||||
|
||||
val playbackReportContext = playbackReportContext(playMethod = PlayMethod.DIRECT_PLAY)
|
||||
|
||||
assertTrue(PlaybackRetryPolicy.shouldRetryWithTranscoding(error, playbackReportContext))
|
||||
}
|
||||
|
||||
private fun playbackReportContext(playMethod: PlayMethod): PlaybackReportContext {
|
||||
return PlaybackReportContext(
|
||||
playMethod = playMethod,
|
||||
|
||||
Reference in New Issue
Block a user