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
|
package hu.bbara.purefin.core.data.client
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.AudioAttributes as PlatformAudioAttributes
|
||||||
|
import android.media.AudioFormat
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
import android.media.AudioTrack
|
||||||
import android.media.MediaCodecList
|
import android.media.MediaCodecList
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
@@ -20,7 +23,9 @@ import org.jellyfin.sdk.model.api.TranscodingProfile
|
|||||||
object AndroidDeviceProfile {
|
object AndroidDeviceProfile {
|
||||||
|
|
||||||
private const val TAG = "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
|
@Volatile
|
||||||
private var cachedSnapshot: CapabilitySnapshot? = null
|
private var cachedSnapshot: CapabilitySnapshot? = null
|
||||||
@@ -214,7 +219,7 @@ object AndroidDeviceProfile {
|
|||||||
val audioManager = context.getSystemService(AudioManager::class.java) ?: return DEFAULT_MAX_AUDIO_CHANNELS
|
val audioManager = context.getSystemService(AudioManager::class.java) ?: return DEFAULT_MAX_AUDIO_CHANNELS
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||||
val maxDeviceChannels = devices
|
val reportedMaxChannels = devices
|
||||||
.flatMap { device ->
|
.flatMap { device ->
|
||||||
buildList {
|
buildList {
|
||||||
addAll(device.channelCounts.toList())
|
addAll(device.channelCounts.toList())
|
||||||
@@ -223,15 +228,67 @@ object AndroidDeviceProfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.maxOrNull()
|
.maxOrNull()
|
||||||
|
|
||||||
maxDeviceChannels
|
|
||||||
?.takeIf { it > 0 }
|
?.takeIf { it > 0 }
|
||||||
?: DEFAULT_MAX_AUDIO_CHANNELS
|
?: DEFAULT_MAX_AUDIO_CHANNELS
|
||||||
|
|
||||||
|
val verifiedMaxChannels = verifyOutputChannelSupport(reportedMaxChannels)
|
||||||
|
Log.d(TAG, "Reported max audio channels: $reportedMaxChannels, verified: $verifiedMaxChannels")
|
||||||
|
verifiedMaxChannels
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
DEFAULT_MAX_AUDIO_CHANNELS
|
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 {
|
private fun channelMaskToCount(mask: Int): Int {
|
||||||
return if (mask == 0) {
|
return if (mask == 0) {
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ViewModelComponent
|
import dagger.hilt.android.components.ViewModelComponent
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
|
import hu.bbara.purefin.core.data.client.AndroidDeviceProfile
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(ViewModelComponent::class)
|
@InstallIn(ViewModelComponent::class)
|
||||||
@@ -27,6 +28,7 @@ object VideoPlayerModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
fun provideVideoPlayer(application: Application, cacheDataSourceFactory: CacheDataSource.Factory): Player {
|
fun provideVideoPlayer(application: Application, cacheDataSourceFactory: CacheDataSource.Factory): Player {
|
||||||
|
val capabilitySnapshot = AndroidDeviceProfile.getSnapshot(application)
|
||||||
val trackSelector = DefaultTrackSelector(application)
|
val trackSelector = DefaultTrackSelector(application)
|
||||||
val audioAttributes =
|
val audioAttributes =
|
||||||
AudioAttributes.Builder()
|
AudioAttributes.Builder()
|
||||||
@@ -37,7 +39,9 @@ object VideoPlayerModule {
|
|||||||
trackSelector.setParameters(
|
trackSelector.setParameters(
|
||||||
trackSelector
|
trackSelector
|
||||||
.buildUponParameters()
|
.buildUponParameters()
|
||||||
.setTunnelingEnabled(true)
|
.setMaxAudioChannelCount(capabilitySnapshot.maxAudioChannels)
|
||||||
|
.setExceedAudioConstraintsIfNecessary(false)
|
||||||
|
.setTunnelingEnabled(false)
|
||||||
// .setPreferredAudioLanguage(
|
// .setPreferredAudioLanguage(
|
||||||
// appPreferences.getValue(appPreferences.preferredAudioLanguage)
|
// appPreferences.getValue(appPreferences.preferredAudioLanguage)
|
||||||
// )
|
// )
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ internal object PlaybackRetryPolicy {
|
|||||||
private val retryableErrorCodes = setOf(
|
private val retryableErrorCodes = setOf(
|
||||||
PlaybackException.ERROR_CODE_DECODER_INIT_FAILED,
|
PlaybackException.ERROR_CODE_DECODER_INIT_FAILED,
|
||||||
PlaybackException.ERROR_CODE_DECODER_QUERY_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_MALFORMED,
|
||||||
PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED
|
PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED
|
||||||
)
|
)
|
||||||
@@ -45,6 +49,10 @@ internal object PlaybackRetryPolicy {
|
|||||||
}
|
}
|
||||||
}.lowercase()
|
}.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))
|
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 {
|
private fun playbackReportContext(playMethod: PlayMethod): PlaybackReportContext {
|
||||||
return PlaybackReportContext(
|
return PlaybackReportContext(
|
||||||
playMethod = playMethod,
|
playMethod = playMethod,
|
||||||
|
|||||||
Reference in New Issue
Block a user