fix(player): restore the subtitle sidecar work dropped by the previous commit

The previous commit was assembled from a tree read before 13264e22 landed,
so committing it reverted that commit's changes: the image-based subtitle
filtering in device_profile/types, subtitleTracks and its tests, the
regenerated bindings, and the VideoPlayer menu wiring.

Nothing was lost — the working tree held both changes throughout. This
restores those files to the merged state, leaving both the subtitle fix and
the play-session fix in place.

TRACES: UR-020, UR-004 | DR-176 | UT-168
This commit is contained in:
2026-08-16 10:20:22 +02:00
parent 2d67b0e4f5
commit 5096c01960
9 changed files with 5442 additions and 4416 deletions
@@ -122,6 +122,21 @@ pub fn playback_subtitle_stream_index() -> i32 {
NO_SUBTITLE_STREAM
}
/// Whether a subtitle in this format can reach the app as a sidecar it draws
/// itself — the same verdict as [`subtitle_forces_burn_in`], from the reader's
/// side, and the one a subtitle picker needs.
///
/// Since the app asks for burn-in nowhere (see
/// [`playback_subtitle_stream_index`]), a format that only burn-in could deliver
/// is one it can never display. An unnamed format is treated as undeliverable
/// rather than guessed at: offering a track and drawing nothing is worse than
/// not offering it.
///
/// TRACES: UR-020 | DR-176 | UT-168
pub fn subtitle_supports_external_delivery(codec: Option<&str>) -> bool {
codec.is_some_and(|codec| !subtitle_forces_burn_in(codec))
}
/// Narrow a detected audio-codec list to what the renderer that will actually
/// play the **video** can decode.
///
+34 -9
View File
@@ -555,6 +555,18 @@ impl OnlineRepository {
("SegmentContainer", "ts".to_string()),
("TranscodingContainer", "ts".to_string()),
("TranscodingProtocol", "hls".to_string()),
// Say "no subtitle" rather than leaving the choice open. An omitted
// index is not neutral: the server then picks the source's own
// default/forced track, and an image-based one can only be delivered
// by burning it into the picture (DR-176). The negotiation already
// sends this sentinel, but most streams are opened by rebuilding
// *this* URL — a quality switch, a transcoded seek, an audio-track
// switch — so it has to hold here too, independently of whatever
// session state the server still holds.
(
"SubtitleStreamIndex",
super::device_profile::playback_subtitle_stream_index().to_string(),
),
];
// Scale the picture down to what the budget can carry. Omitted for the
@@ -988,15 +1000,28 @@ impl JellyfinItem {
media_streams: self.media_streams.map(|streams| {
streams
.into_iter()
.map(|s| crate::repository::types::MediaStream {
kind: crate::domain::stream_kind_from_jellyfin(&s.stream_type),
stream_type: s.stream_type,
codec: s.codec,
language: s.language,
display_title: s.display_title,
index: s.index,
is_default: s.is_default,
is_forced: s.is_forced,
.map(|s| {
let kind = crate::domain::stream_kind_from_jellyfin(&s.stream_type);
// Only a subtitle can be a sidecar; asked of anything
// else the question has no answer. TRACES: UR-020 |
// DR-176 | UT-168
let supports_external_delivery =
(kind == crate::domain::StreamKind::Subtitle).then(|| {
super::device_profile::subtitle_supports_external_delivery(
s.codec.as_deref(),
)
});
crate::repository::types::MediaStream {
kind,
stream_type: s.stream_type,
codec: s.codec,
language: s.language,
display_title: s.display_title,
index: s.index,
is_default: s.is_default,
is_forced: s.is_forced,
supports_external_delivery,
}
})
.collect()
}),
+12
View File
@@ -253,6 +253,18 @@ pub struct MediaStream {
pub index: i32,
pub is_default: bool,
pub is_forced: bool,
/// Whether this stream can reach the app as a sidecar it renders itself.
///
/// `None` for anything that is not a subtitle — the question does not apply,
/// and `false` there would read like a verdict. For a subtitle it is the
/// difference between a track the app can draw and one only the server could
/// have shown, by burning it into the picture (DR-176) — which this app never
/// asks it to do. The vocabulary of *which formats those are* stays in Rust;
/// the frontend only reads the answer.
///
/// TRACES: UR-020 | DR-176 | UT-168
#[serde(default)]
pub supports_external_delivery: Option<bool>,
}
/// Media source information