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:
+14
-1
@@ -2235,7 +2235,20 @@ type: string;
|
||||
/**
|
||||
* Provider-neutral stream classification — replaces `stream_type`.
|
||||
*/
|
||||
kind?: StreamKind; codec?: string | null; language?: string | null; displayTitle?: string | null; index: number; isDefault: boolean; isForced: boolean }
|
||||
kind?: StreamKind; codec?: string | null; language?: string | null; displayTitle?: string | null; index: number; isDefault: boolean; isForced: boolean;
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
supportsExternalDelivery?: boolean | null }
|
||||
export type MediaType = "audio" | "video"
|
||||
/**
|
||||
* Lightweight media item for merged playback state
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { videoFitClass } from "./videoFit";
|
||||
import { fatalNetworkErrorAction } from "./hlsRecovery";
|
||||
import {
|
||||
subtitleStreamsOf,
|
||||
resolveSubtitleTracks,
|
||||
reconcileSelectedSubtitle,
|
||||
videoCrossOriginMode,
|
||||
@@ -333,13 +334,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Get available subtitle tracks from media streams
|
||||
// The subtitle streams the menu offers — the same list the <track> children
|
||||
// and the native play request are built from, so the menu can never name a
|
||||
// track the player was never given. subtitleStreamsOf() also drops the ones
|
||||
// the backend says it cannot deliver as a sidecar (image-based PGS/DVD/DVB,
|
||||
// which only server burn-in could show and we never ask for — DR-176).
|
||||
// TRACES: UR-020 | DR-176 | UT-168
|
||||
const subtitleTracks = $derived(() => {
|
||||
if (!media || !media.mediaStreams) {
|
||||
console.log("[VideoPlayer] No media or mediaStreams available for subtitles");
|
||||
return [];
|
||||
}
|
||||
const tracks = media.mediaStreams.filter(stream => stream.kind === "subtitle");
|
||||
const tracks = subtitleStreamsOf(media.mediaStreams);
|
||||
console.log("[VideoPlayer] Found subtitle tracks:", tracks.length, tracks);
|
||||
return tracks;
|
||||
});
|
||||
|
||||
@@ -53,6 +53,68 @@ describe("subtitleStreamsOf", () => {
|
||||
expect(subtitleStreamsOf(null)).toEqual([]);
|
||||
expect(subtitleStreamsOf(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
/**
|
||||
* A subtitle the app cannot draw must not reach the picker. Image-based
|
||||
* tracks (PGS/DVD/DVB) are bitmaps: the only way to show one is for the server
|
||||
* to composite it into the video, which this app deliberately never asks for
|
||||
* (DR-176). Offering it anyway produced the reported symptom's twin — a menu
|
||||
* entry that selects, ticks, and shows nothing.
|
||||
*
|
||||
* The verdict is the backend's (`supportsExternalDelivery`); the codec
|
||||
* vocabulary behind it stays in Rust.
|
||||
*
|
||||
* TRACES: UR-020 | DR-176 | UT-168
|
||||
*/
|
||||
it("drops subtitles the backend says it cannot deliver as a sidecar", () => {
|
||||
const streams: SubtitleStreamLike[] = [
|
||||
{ index: 2, kind: "subtitle", displayTitle: "English PGS SDH", supportsExternalDelivery: false },
|
||||
{ index: 3, kind: "subtitle", displayTitle: "English Text SDH", supportsExternalDelivery: true },
|
||||
];
|
||||
|
||||
expect(subtitleStreamsOf(streams).map((s) => s.index)).toEqual([3]);
|
||||
});
|
||||
|
||||
/**
|
||||
* Only an explicit "no" hides a track. A stream that carries no verdict at all
|
||||
* predates the field (or came from somewhere that does not set it), and
|
||||
* hiding those would silently empty the menu for sources that work today.
|
||||
*
|
||||
* TRACES: UR-020 | DR-176 | UT-168
|
||||
*/
|
||||
it("keeps subtitles that carry no verdict", () => {
|
||||
const streams: SubtitleStreamLike[] = [
|
||||
{ index: 2, kind: "subtitle", displayTitle: "English" },
|
||||
{ index: 3, kind: "subtitle", displayTitle: "French", supportsExternalDelivery: null },
|
||||
];
|
||||
|
||||
expect(subtitleStreamsOf(streams).map((s) => s.index)).toEqual([2, 3]);
|
||||
});
|
||||
|
||||
/**
|
||||
* The same list feeds the `<track>` children and the native play request, so
|
||||
* an undeliverable track must not even have its URL fetched — that request is
|
||||
* the one that 404s, and the sideloaded track it would produce is the dead
|
||||
* entry all over again.
|
||||
*
|
||||
* TRACES: UR-020 | DR-176 | UT-168
|
||||
*/
|
||||
it("never resolves a URL for a subtitle it dropped", async () => {
|
||||
const asked: number[] = [];
|
||||
const tracks = await resolveSubtitleTracks(
|
||||
[
|
||||
{ index: 2, kind: "subtitle", displayTitle: "PGS", supportsExternalDelivery: false },
|
||||
{ index: 3, kind: "subtitle", displayTitle: "SRT", supportsExternalDelivery: true },
|
||||
],
|
||||
async (index) => {
|
||||
asked.push(index);
|
||||
return url(index);
|
||||
},
|
||||
);
|
||||
|
||||
expect(asked).toEqual([3]);
|
||||
expect(tracks.map((t) => t.streamIndex)).toEqual([3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("subtitleTrackLabel", () => {
|
||||
|
||||
@@ -32,6 +32,13 @@ export interface SubtitleStreamLike {
|
||||
displayTitle?: string | null;
|
||||
isDefault?: boolean;
|
||||
isForced?: boolean;
|
||||
/**
|
||||
* The backend's verdict on whether this track can arrive as a sidecar the app
|
||||
* renders itself. `false` means only the server could have shown it, by
|
||||
* burning it into the picture — which the app never asks for. Absent means no
|
||||
* verdict was given, which is not the same as "no".
|
||||
*/
|
||||
supportsExternalDelivery?: boolean | null;
|
||||
}
|
||||
|
||||
/** A subtitle stream whose URL resolved — i.e. one we can actually render. */
|
||||
@@ -46,12 +53,32 @@ export interface RenderableSubtitleTrack {
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
/** Subtitle streams of a media item, in stream order. */
|
||||
export function subtitleStreamsOf(
|
||||
streams: readonly SubtitleStreamLike[] | null | undefined,
|
||||
): SubtitleStreamLike[] {
|
||||
/**
|
||||
* Subtitle streams of a media item that the app can actually show, in stream
|
||||
* order. This is the one list behind everything: the picker, the `<track>`
|
||||
* children, and the array sent to the native backend.
|
||||
*
|
||||
* Image-based subtitles (PGS/DVD/DVB) are filtered out here rather than at each
|
||||
* consumer. They are bitmaps — a client can only display one if the server
|
||||
* composites it into the video, and the app deliberately asks for no burn-in at
|
||||
* all (DR-176), so such a track is one it can never draw. Leaving it in the
|
||||
* picker produced a control that ticked and showed nothing.
|
||||
*
|
||||
* The judgement is the backend's: `supportsExternalDelivery` arrives already
|
||||
* decided, because *which formats are bitmaps* is domain vocabulary and belongs
|
||||
* in Rust. Only an explicit `false` drops a stream; a stream carrying no verdict
|
||||
* is kept, so a source that never sets the field behaves exactly as before.
|
||||
*
|
||||
* Generic in the stream type so callers keep their own richer fields (the menu
|
||||
* reads `codec` off the result).
|
||||
*
|
||||
* TRACES: UR-020 | DR-176 | UT-168
|
||||
*/
|
||||
export function subtitleStreamsOf<T extends SubtitleStreamLike>(
|
||||
streams: readonly T[] | null | undefined,
|
||||
): T[] {
|
||||
if (!streams) return [];
|
||||
return streams.filter((s) => s.kind === "subtitle");
|
||||
return streams.filter((s) => s.kind === "subtitle" && s.supportsExternalDelivery !== false);
|
||||
}
|
||||
|
||||
/** Human label for a subtitle stream, matching the menu's own fallback chain. */
|
||||
|
||||
Reference in New Issue
Block a user