diff --git a/client/src/types.ts b/client/src/types.ts index e513eb4..71d5e85 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -36,6 +36,11 @@ export interface SeasonDto { export interface PlaybackTrackOption { index: number; label: string; + // Subtitle tracks only (undefined for audio) - false for an image/ + // bitmap subtitle (PGS/VobSub/DVB - a Blu-ray/DVD rip's own subtitles, + // typically), which this player's forced burn-in (SubtitleMethod=Encode) + // cannot actually render - see Player.ts's subtitle ` (`views/player/Player.ts`) shows these +as disabled options labeled "(image, non disponible)" rather than hiding +them outright - the track still exists and is still listed, just not +pickable, so it's clear *why* rather than silently missing. This answers +the "is item 2 also PGS" question directly from the app itself instead of +needing another round of server-log archaeology, and prevents anyone +from picking a subtitle that's guaranteed to break playback the same way +again, for any item, without needing to know any of this history. + ## Assumptions to verify **Confirmed against the real server** (not a guess — actually exercised, diff --git a/server/src/services/CatalogService.ts b/server/src/services/CatalogService.ts index 782e9be..30da661 100644 --- a/server/src/services/CatalogService.ts +++ b/server/src/services/CatalogService.ts @@ -23,6 +23,18 @@ const WATCH_PARTY_TYPES = new Set(['Movie', 'Episode', 'Video', 'MusicVideo', 'A // the same check. const VISIBLE_COLLECTION_TYPES = new Set(['movies', 'tvshows', 'music', 'musicvideos']); +// Image/bitmap subtitle codecs - pre-rendered pictures (from a Blu-ray/DVD +// rip), not text. This app forces SubtitleMethod=Encode whenever a +// subtitle is picked (see applyPlaybackOverrides), which asks ffmpeg's +// `subtitles` filter (libass) to burn it in - libass renders TEXT via a +// font, it has no code path for compositing a bitmap at all. Confirmed +// against a real server (see docs/architecture.md's player-track- +// selection section, "Round 4/5"): picking one of these doesn't produce a +// slow-but-working transcode, it makes Emby's own segment generation +// throw outright. Used by getPlaybackOptions to flag these in the picker +// instead of letting someone pick a track that's guaranteed to fail. +const IMAGE_SUBTITLE_CODECS = new Set(['hdmv_pgs_subtitle', 'dvd_subtitle', 'dvb_subtitle']); + interface EmbyItemsResponse { Items?: EmbyItem[]; } @@ -31,7 +43,7 @@ interface PlaybackInfoResponse { MediaSources?: Array<{ Id?: string; TranscodingUrl?: string; - MediaStreams?: Array<{ Type: string; Index: number; DisplayTitle?: string; Language?: string }>; + MediaStreams?: Array<{ Type: string; Index: number; DisplayTitle?: string; Language?: string; Codec?: string }>; }>; PlaySessionId?: string; } @@ -178,7 +190,11 @@ class CatalogServiceClass { const label = (s: { Index: number; DisplayTitle?: string; Language?: string }) => s.DisplayTitle || s.Language || `Piste ${s.Index}`; return { audioTracks: streams.filter((s) => s.Type === 'Audio').map((s) => ({ index: s.Index, label: label(s) })), - subtitleTracks: streams.filter((s) => s.Type === 'Subtitle').map((s) => ({ index: s.Index, label: label(s) })) + subtitleTracks: streams.filter((s) => s.Type === 'Subtitle').map((s) => ({ + index: s.Index, + label: label(s), + supportsBurnIn: !IMAGE_SUBTITLE_CODECS.has((s.Codec || '').toLowerCase()) + })) }; } diff --git a/server/src/types.ts b/server/src/types.ts index fb92dce..405af57 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -138,6 +138,11 @@ export interface SeasonDto { export interface PlaybackTrackOption { index: number; label: string; + // Subtitle tracks only (undefined for audio - the concept doesn't apply). + // false means this app's forced SubtitleMethod=Encode (see + // CatalogService.applyPlaybackOverrides) cannot actually work for this + // track - see IMAGE_SUBTITLE_CODECS's own comment for why. + supportsBurnIn?: boolean; } export interface PlaybackOptionsDto { diff --git a/server/test/catalogService.test.ts b/server/test/catalogService.test.ts index d60fe4f..ae428d5 100644 --- a/server/test/catalogService.test.ts +++ b/server/test/catalogService.test.ts @@ -86,4 +86,24 @@ describe('CatalogService', () => { (err: Error & { status?: number }) => err.status === 404 ); }); + + it('getPlaybackOptions flags an image/bitmap subtitle (PGS) as not burn-in-able, text subtitles as fine', async (t) => { + t.mock.method(EmbyClient, 'fetch', async () => jsonResponse({ + MediaSources: [{ + Id: 'src1', + MediaStreams: [ + { Type: 'Audio', Index: 1, Language: 'fre' }, + { Type: 'Subtitle', Index: 2, Language: 'eng', Codec: 'subrip' }, + { Type: 'Subtitle', Index: 3, Language: 'eng', Codec: 'hdmv_pgs_subtitle' }, + { Type: 'Subtitle', Index: 4, Language: 'fre', Codec: 'ass' } + ] + }] + })); + + const options = await CatalogService.getPlaybackOptions(session, 'item1'); + + assert.equal(options.subtitleTracks.find((t) => t.index === 2)?.supportsBurnIn, true, 'subrip is text - burns in fine'); + assert.equal(options.subtitleTracks.find((t) => t.index === 3)?.supportsBurnIn, false, 'PGS is a bitmap - this player\'s forced Encode can\'t render it'); + assert.equal(options.subtitleTracks.find((t) => t.index === 4)?.supportsBurnIn, true, 'ass is text too'); + }); });