fix(playback): ask the renderer what it can decode, in one place
Four bugs, one cause. "What can this device decode" was answered in five
places, four of which assumed the webview was decoding:
- the device profile's direct-play codecs (cfg per platform, inline)
- the transcoding targets (hardcoded "h264,hevc")
- the direct-play audio narrowing (webview list, all platforms)
- the client-side audio override (webview list, all platforms)
- get_video_stream_url's VideoCodec (hardcoded "h264")
On Android the decoder is ExoPlayer, so four of those were simply wrong there,
and the costs were invisible without a device:
- dts is in the tablet's own codec list, gets stripped from the profile, and
is then forced to transcode by a rule about a renderer that is not playing
it.
- An hevc source whose *audio* is eac3 had its **picture fully re-encoded**.
The server's own transcoding URL got this right — VideoCodec=h264,hevc,
TranscodeReasons=AudioCodecNotSupported, video copied — but the moment a
quality change or track switch re-opened the stream through our builder,
the hardcoded h264 turned a cheap audio remux into a full transcode. That
is a quality change silently making playback more expensive, on the exact
path a viewer uses when playback is already struggling.
`renderer_codecs()` and `renderer_can_decode_audio()` are now the single
source, and all five sites read them. On the webview path every value resolves
exactly as before, so desktop behaviour is unchanged by construction; on
Android the profile becomes the device's own.
The list is also what lets the server *copy* rather than re-encode: naming
every codec the renderer can decode is what turns a transcode into a
passthrough when the source is already playable. That is the whole of "use the
best format available".
Also corrects this branch's headline number where it is asserted — the
architecture doc, the desktop-native-video spec and the spike. The measured 85%
Android direct-play rate used a profile containing ac3/eac3; the device it was
later verified on reports neither, so eac3 content correctly transcodes there.
It is a ceiling for an ExoPlayer-appropriate profile, not what the app achieves,
and realising any of it depends on this change. Left in place with the caveat
rather than deleted, because the measurement is real — it just measures
something narrower than it was quoted as measuring.
Unverified: this changes what Android negotiates and has not been exercised on
the tablet yet. Desktop is unchanged by construction but also unre-tested.
This commit is contained in:
@@ -212,6 +212,16 @@ pub fn subtitle_supports_external_delivery(codec: Option<&str>) -> bool {
|
||||
///
|
||||
/// TRACES: UR-004 | DR-148 | UT-142
|
||||
pub fn video_audio_codecs(detected: &str) -> String {
|
||||
// Where the video renderer decodes the audio itself (ExoPlayer), the
|
||||
// platform list *is* the answer and narrowing it to the webview's throws
|
||||
// away codecs the device genuinely plays — dts, on the tablet this was
|
||||
// found on. TRACES: UR-004, UR-080 | DR-233
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
return detected.to_string();
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
let kept: Vec<&str> = detected
|
||||
.split(',')
|
||||
.filter_map(|codec| {
|
||||
@@ -233,6 +243,83 @@ pub fn video_audio_codecs(detected: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the renderer that will actually decode video on this platform can play.
|
||||
///
|
||||
/// Returns `(video_codecs, audio_codecs)` as Jellyfin-style comma lists.
|
||||
///
|
||||
/// This exists because the answer was previously derived in four places and
|
||||
/// hardcoded in a fifth, each of them assuming the *webview* was decoding:
|
||||
/// the device profile, the transcoding targets, the direct-play audio
|
||||
/// narrowing, the client-side audio override, and `get_video_stream_url`'s
|
||||
/// `VideoCodec`. On Android the decoder is ExoPlayer, so every one of those was
|
||||
/// wrong there — the observed cost being an hevc source re-encoded to h264
|
||||
/// because its *audio* was eac3, and dts forced to transcode though the device
|
||||
/// decodes it.
|
||||
///
|
||||
/// One source, so the copies cannot disagree again.
|
||||
///
|
||||
/// TRACES: UR-004, UR-080 | DR-233
|
||||
pub fn renderer_codecs() -> (String, String) {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
// ExoPlayer, and the device itself answers via MediaCodecList.
|
||||
crate::player::get_detected_codecs()
|
||||
.map(|(video, audio, _channels)| (video, audio))
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!(
|
||||
"[DeviceProfile] Codec detection not complete, using conservative defaults"
|
||||
);
|
||||
("h264,hevc".to_string(), "aac,mp3".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
// Linux desktop draws video in the WebKitGTK HTML5 <video> element, which
|
||||
// cannot reliably decode HEVC/AV1/VP9. Claim only what it decodes, so
|
||||
// Jellyfin transcodes the rest to h264 HLS. (Audio-only playback goes
|
||||
// through MPV and is unaffected — that is a different renderer and a
|
||||
// different profile.)
|
||||
//
|
||||
// When mpv draws the picture here this stops being a platform constant and
|
||||
// becomes a question about the active renderer — which is the whole point of
|
||||
// returning it from a function rather than a `cfg` block.
|
||||
#[cfg(all(not(target_os = "android"), target_os = "linux"))]
|
||||
{
|
||||
("h264".to_string(), "aac,mp3,opus,vorbis,flac".to_string())
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "android"), not(target_os = "linux")))]
|
||||
{
|
||||
(
|
||||
"h264,hevc,vp8,vp9,av1,mpeg4".to_string(),
|
||||
"aac,mp3,opus,vorbis,flac".to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the renderer that decodes *video* on this platform can also decode
|
||||
/// this audio codec.
|
||||
///
|
||||
/// On a webview platform this is the webview's narrow list, because the element
|
||||
/// decodes both halves. On Android it is the device's own list: ExoPlayer plays
|
||||
/// the audio, so judging it against the webview's capabilities transcodes files
|
||||
/// that would have played.
|
||||
///
|
||||
/// TRACES: UR-004, UR-080 | DR-233
|
||||
pub fn renderer_can_decode_audio(codec: &str) -> bool {
|
||||
let codec = codec.trim();
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let (_video, audio) = renderer_codecs();
|
||||
return audio
|
||||
.split(',')
|
||||
.any(|supported| supported.trim().eq_ignore_ascii_case(codec));
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
webview_can_decode_audio(codec)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the webview `<video>` element can decode this audio codec.
|
||||
///
|
||||
/// TRACES: UR-004 | DR-149 | UT-148
|
||||
@@ -260,7 +347,9 @@ pub fn webview_can_decode_audio(codec: &str) -> bool {
|
||||
/// TRACES: UR-004 | DR-149 | UT-148
|
||||
pub fn audio_forces_transcode(streams: &[(Option<&str>, bool)]) -> bool {
|
||||
match served_audio_codec(streams) {
|
||||
Some(codec) => !webview_can_decode_audio(codec),
|
||||
// The renderer that will decode it, not always the webview — see
|
||||
// `renderer_can_decode_audio`. TRACES: UR-004, UR-080 | DR-233
|
||||
Some(codec) => !renderer_can_decode_audio(codec),
|
||||
// No audio at all, or a codec the server did not name: leave it alone.
|
||||
None => false,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user