fix(playback): bound the device profile by the audio route's channels (DR-141)
MediaCodecList answers "can this device decode 5.1", which is not the question that decides whether the user hears anything: a phone decodes an AC-3 5.1 track happily and still has two channels to play it out of. The DeviceProfile carried no MaxAudioChannels, so Jellyfin was free to direct-play the multichannel track to a two-channel sink — silence or dialogue folded into surround channels that go nowhere, depending on the device. Report media3 AudioCapabilities.maxChannelCount for the current route over JNI alongside the codec lists, and bound the direct-play and transcoding profiles (and the HLS URL's TranscodingMaxAudioChannels, previously hardcoded to 2) by it. No codec is ever removed, so a device with genuine surround output keeps direct-playing it. A missing or zero reading means "route not yet established", not "no audio", and falls back to stereo.
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
package com.dtourolle.jellytau.player
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaCodecList
|
||||
import android.util.Log
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.audio.AudioCapabilities
|
||||
|
||||
/**
|
||||
* Detects hardware codec capabilities using MediaCodecList.
|
||||
@@ -75,6 +79,36 @@ object CodecDetector {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Report how many channels the *current audio output route* can voice.
|
||||
*
|
||||
* This is a different question from "can this device decode 5.1", which the
|
||||
* codec list already answers: a phone decodes a 5.1 AC-3 track happily and
|
||||
* still has only two channels to play it out of. Left unreported, Jellyfin
|
||||
* is free to direct-play the multichannel track, and what the user hears is
|
||||
* device dependent — a failed AudioSink configuration (silence) or dialogue
|
||||
* folded into the surround channels and lost.
|
||||
*
|
||||
* Returns 0 when there is no answer; Rust reads that as "unknown" and falls
|
||||
* back to stereo rather than claiming a capability we have not observed.
|
||||
*/
|
||||
@UnstableApi
|
||||
fun detectMaxAudioChannels(context: Context): Int {
|
||||
return try {
|
||||
val capabilities = AudioCapabilities.getCapabilities(
|
||||
context,
|
||||
AudioAttributes.DEFAULT,
|
||||
/* routedDevice= */ null
|
||||
)
|
||||
val channels = capabilities.maxChannelCount
|
||||
Log.i(TAG, "Audio route max channel count: $channels")
|
||||
channels.coerceAtLeast(0)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error querying audio capabilities", e)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Android MIME types to Jellyfin codec names.
|
||||
*
|
||||
|
||||
@@ -130,7 +130,7 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
}
|
||||
|
||||
// Detect and report hardware codec capabilities to Rust
|
||||
detectAndReportCodecs()
|
||||
detectAndReportCodecs(context.applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,23 +141,31 @@ class JellyTauPlayer(private val appContext: Context) {
|
||||
* Called during player initialization.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun detectAndReportCodecs() {
|
||||
fun detectAndReportCodecs(context: Context) {
|
||||
val capabilities = CodecDetector.detectHardwareCodecs()
|
||||
|
||||
// Convert lists to comma-separated strings for JNI transfer
|
||||
val videoCodecsStr = capabilities.videoCodecs.joinToString(",")
|
||||
val audioCodecsStr = capabilities.audioCodecs.joinToString(",")
|
||||
|
||||
android.util.Log.i("JellyTauPlayer", "Reporting codecs to Rust: video=$videoCodecsStr, audio=$audioCodecsStr")
|
||||
// What the route can *voice*, which the codec list does not answer.
|
||||
// 0 means "no answer"; Rust falls back to stereo.
|
||||
val maxAudioChannels = CodecDetector.detectMaxAudioChannels(context)
|
||||
|
||||
android.util.Log.i("JellyTauPlayer", "Reporting codecs to Rust: video=$videoCodecsStr, audio=$audioCodecsStr, maxAudioChannels=$maxAudioChannels")
|
||||
|
||||
// Call native method to store in Rust
|
||||
nativeOnCodecsDetected(videoCodecsStr, audioCodecsStr)
|
||||
nativeOnCodecsDetected(videoCodecsStr, audioCodecsStr, maxAudioChannels)
|
||||
}
|
||||
|
||||
/**
|
||||
* Native method to report detected codecs to Rust.
|
||||
*/
|
||||
private external fun nativeOnCodecsDetected(videoCodecs: String, audioCodecs: String)
|
||||
private external fun nativeOnCodecsDetected(
|
||||
videoCodecs: String,
|
||||
audioCodecs: String,
|
||||
maxAudioChannels: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if the player is initialized.
|
||||
|
||||
Reference in New Issue
Block a user