fix(playback): force a transcode when the webview cannot decode the audio (DR-149, 0.4.8)

Advertising a webview-shaped profile (DR-148) was necessary but not
sufficient. Probing the server directly showed Jellyfin 10.11.5 enforces a
DirectPlayProfile's Container and VideoCodec — excluding either returns
SupportsDirectPlay:false with TranscodeReasons=ContainerNotSupported /
VideoCodecNotSupported — but ignores its AudioCodec entirely: an E-AC-3
track is still offered for direct play against a profile listing only
aac,flac,mp3,opus,vorbis. Neither a VideoAudio CodecProfile forbidding the
codec nor MaxAudioChannels:2 against a 6-channel track changes the answer,
so no profile the client can send fixes this and the picture plays silent.

The client therefore stops delegating a question it can answer itself. The
negotiated source's audio is checked against what the webview decodes, and
an undecodable track forces the existing h264/aac HLS transcode regardless
of the server calling direct play fine; direct_play and needs_transcoding
are corrected to match so the frontend and the reporting path agree with
the URL actually used. The track judged is the one that would be served —
the default, else the first — since a supported track further down is not
the one that plays. A source with no audio, or a codec the server did not
name, is left alone rather than transcoded on a guess.

Test-first: the new tests failed against the old behaviour before the
decision existed. Verified on a motorola edge 30 by the audio HAL, not by
ear — the same E-AC-3 episode logged isMusicActive=true once and 58
ACDB-LOADER lines under this build, against 0 and 0 on 0.4.6, where an AAC
file in the same session produced 16 and 116. No FATAL EXCEPTION, so R8 on
the signed release build is unaffected.

Also carries in-flight subtitle-track work authored in a parallel session
(subtitleTracks, VideoPlayer, player/media, bindings) at the user's
request, so the tag matches the APK verified on device.
This commit is contained in:
2026-08-11 20:07:11 +02:00
parent 6a712c46cb
commit acddcdd6fa
8 changed files with 136 additions and 11 deletions
+27 -5
View File
@@ -1,9 +1,7 @@
//! TRACES: UR-002, UR-007 | DR-013 | IR-010
use async_trait::async_trait;
#[cfg(target_os = "android")]
use log::warn;
use log::{debug, error, info};
use log::{debug, error, info, warn};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
@@ -1342,6 +1340,9 @@ impl MediaRepository for OnlineRepository {
index: i32,
#[serde(default)]
codec: Option<String>,
/// The track the server serves when the client pins none.
#[serde(default)]
is_default: bool,
}
// Get detected codecs from Android MediaCodecList or use platform defaults
@@ -1475,9 +1476,29 @@ impl MediaRepository for OnlineRepository {
);
}
// Jellyfin 10.11.5 honours a DirectPlayProfile's container and video codec
// but ignores its audio codec, so it offers an E-AC-3 track for direct
// play even though DR-148 advertises only AAC — and the webview renders
// the picture in silence. Judge the track we would actually be served
// against what the webview can decode, and override the server's answer.
let audio_streams: Vec<(Option<&str>, bool)> = source
.media_streams
.iter()
.filter(|stream| stream.stream_type == "Audio")
.map(|stream| (stream.codec.as_deref(), stream.is_default))
.collect();
let audio_forces_transcode = super::device_profile::audio_forces_transcode(&audio_streams);
// Use TranscodingUrl from response if available (Streamyfin pattern)
let stream_url = if let Some(transcoding_url) = &source.transcoding_url {
format!("{}{}", self.server_url, transcoding_url)
} else if audio_forces_transcode {
warn!(
"[PlaybackInfo] Server offered direct play for audio the webview cannot decode ({:?}) — forcing an HLS transcode",
audio_streams.first().and_then(|(codec, _)| *codec)
);
self.get_video_stream_url(item_id, Some(&source.id), None, None)
.await?
} else {
// Fall back to direct stream URL. No audioStreamIndex: static=true
// serves the original file untouched, and pinning index 0 (the video
@@ -1498,8 +1519,9 @@ impl MediaRepository for OnlineRepository {
media_source_id: source.id.clone(),
play_session_id: response.play_session_id,
stream_url,
direct_play: source.supports_direct_play,
needs_transcoding: !source.supports_direct_play && source.supports_transcoding,
direct_play: source.supports_direct_play && !audio_forces_transcode,
needs_transcoding: audio_forces_transcode
|| (!source.supports_direct_play && source.supports_transcoding),
})
}