diff --git a/.prettierignore b/.prettierignore index 14e68779..b1bcc99c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,7 @@ node_modules dist runtime-scripts *.min.js + +# Vendored third-party sources — reformatting them would destroy the diff against upstream, +# which is the only thing that makes a checked-in library reviewable. See the README beside it. +src/workspaces/officerdev/src/apps/Jellyfin/vendor/hls.mjs diff --git a/src/servers/sidecar/jellyfin/routes.ts b/src/servers/sidecar/jellyfin/routes.ts index 4b2137ec..9c7fb67d 100644 --- a/src/servers/sidecar/jellyfin/routes.ts +++ b/src/servers/sidecar/jellyfin/routes.ts @@ -261,17 +261,49 @@ function transcodeQuality(source: MediaSource): { videoBitrate: number; maxWidth return { videoBitrate: ENCODE_BITRATE, maxWidth: ENCODE_MAX_WIDTH }; } +/** + * Fix up the HLS URL Jellyfin handed back so it carries the same quality decision as the progressive one. + * + * `TranscodingUrl` is built by the server from our `DeviceProfile`, which is why it is taken rather than + * reinvented: it already carries the segment container, the audio codec list, the play session and the + * transcode reasons, and every one of those is the server's business rather than ours. What it gets wrong for + * this machine is exactly two values — it resolves `VideoBitrate` from `MaxStreamingBitrate` (~119 Mbit, the + * ceiling that exists to let a stream COPY through) and never sets a width. For a file that can be copied that + * is right. For a file that must be re-encoded it asks a CPU-only container to encode 4K, which cannot keep up + * with playback. So those two are overwritten from `transcodeQuality`, and nothing else is touched. + * + * Verified against a real answer from this server: the URL comes back with `VideoBitrate=119616000` and no + * width, on an item whose transcode reason was `ContainerNotSupported` alone. + */ +function hlsPath(source: MediaSource): string { + const sanitized = sanitizeUpstreamPath(source.TranscodingUrl!); + const [path, search = ''] = sanitized.split('?'); + const params = new URLSearchParams(search); + const quality = transcodeQuality(source); + + // Jellyfin's own casing here is PascalCase, and its query parsing is case-insensitive — but a duplicate key + // under a different case would still be two values, so delete before setting rather than trusting that. + for (const key of [...params.keys()]) { + const lower = key.toLowerCase(); + if (lower === 'videobitrate' || lower === 'maxwidth' || lower === 'maxheight') params.delete(key); + } + params.set('VideoBitrate', String(quality.videoBitrate)); + if (quality.maxWidth) params.set('MaxWidth', String(quality.maxWidth)); + + return `${path}?${params.toString()}`; +} + /** * Build the PROGRESSIVE transcode URL — one continuous mp4 the `