fix(player): stop the server burning subtitles into the picture
A transcoded episode stalled every few seconds and seeking took five to nine seconds to produce a frame. Neither was a seek bug: both seeks in the capture landed correctly. The stream itself could not keep up. The episode was HEVC video, E-AC-3 audio, and a PGSSUB subtitle track. Only the audio needed transcoding — the device profile supports HEVC and the server would have remuxed the video untouched. But the PlaybackInfo request omitted SubtitleStreamIndex, and omitting it does not mean "no subtitles": the server then honours the source's default/forced flag and picks a track itself. It picked the PGS one. PGS is a bitmap, and the profile advertised only srt/vtt as External, so it could not go out as a sidecar — leaving SubtitleMethod=Encode, burn-in. Burn-in is a video cost, not a subtitle cost. Compositing rules out remuxing, so the whole HEVC stream was re-encoded to h264 frame by frame. The server could not sustain that in real time: the buffer never grew past one segment and playback ran waiting -> HLS error -> canplay -> three seconds of picture, indefinitely, while each seek restarted the encoder from scratch. TranscodeReasons named it — SubtitleCodecNotSupported — but nothing in the log connected that to the stall, so the diagnostic now says which track it is declining and why. Ask for SubtitleStreamIndex=-1 explicitly, and advertise every text format we can render (srt/subrip/ass/ssa/vtt) as External so a subtitle can only ever arrive as a sidecar. Nothing is lost: the app already fetches subtitle tracks itself and draws them over the video (UR-020), so the server's composited copy was always redundant. Image-based tracks are consequently not offered, which is honest rather than a regression — the renderer cannot composite a bitmap, and the previous behaviour paid for them by making the stream unwatchable. The policy lives beside the other device-profile rules in Rust, where it is testable without a device. TRACES: UR-020, UR-004 | DR-176 | UT-168
This commit is contained in:
@@ -1530,23 +1530,27 @@ impl MediaRepository for OnlineRepository {
|
||||
max_audio_channels: max_audio_channels.clone(),
|
||||
},
|
||||
],
|
||||
subtitle_profiles: vec![
|
||||
SubtitleProfile {
|
||||
format: "srt".to_string(),
|
||||
method: "External".to_string(),
|
||||
},
|
||||
SubtitleProfile {
|
||||
format: "vtt".to_string(),
|
||||
method: "External".to_string(),
|
||||
},
|
||||
],
|
||||
subtitle_profiles: super::device_profile::subtitle_profiles()
|
||||
.into_iter()
|
||||
.map(|(format, method)| SubtitleProfile {
|
||||
format: format.to_string(),
|
||||
method: method.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
// POST to PlaybackInfo with device profile containing detected codecs
|
||||
let request_body = PlaybackInfoRequest {
|
||||
user_id: self.user_id.clone(),
|
||||
audio_stream_index: None, // Let the server pick the source default
|
||||
subtitle_stream_index: None,
|
||||
// Never let the server choose a subtitle track for us. Omitting this
|
||||
// makes it honour the source's default/forced flag, and an image-based
|
||||
// default (PGS) it cannot send as a sidecar becomes SubtitleMethod=Encode
|
||||
// — burn-in, which forces a full video re-encode of a stream that would
|
||||
// otherwise be remuxed. The app renders subtitles itself (UR-020).
|
||||
//
|
||||
// TRACES: UR-020, UR-004 | DR-176 | UT-168
|
||||
subtitle_stream_index: Some(super::device_profile::playback_subtitle_stream_index()),
|
||||
start_time_ticks: 0,
|
||||
is_playback: true,
|
||||
auto_open_live_stream: true,
|
||||
@@ -1573,6 +1577,23 @@ impl MediaRepository for OnlineRepository {
|
||||
);
|
||||
}
|
||||
|
||||
// Name the tracks we are declining to have the server composite. Burn-in
|
||||
// rules out remuxing the video, so a single image-based track can turn a
|
||||
// free passthrough into a full re-encode; when that used to happen there
|
||||
// was nothing in the log connecting the stall to the subtitle.
|
||||
for stream in &source.media_streams {
|
||||
if stream.stream_type == "Subtitle" {
|
||||
if let Some(codec) = stream.codec.as_deref() {
|
||||
if super::device_profile::subtitle_forces_burn_in(codec) {
|
||||
info!(
|
||||
" Subtitle index={} ({}) is image-based — not requested; the app renders text tracks itself rather than have the server burn it in (which would force a video re-encode)",
|
||||
stream.index, codec
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user