Fixed resolution showing the raw cropped height (e.g. 804p) instead of the nominal class Plex/Jellyfin display (1080p) - letterboxed/cinemascope sources are shorter than their resolution tier, so classify off the width-derived 16:9 height instead

This commit is contained in:
Bergasha committed 2026-08-19 14:54:48 +10:00
1 parent 90fa9e8741
commit a4bbed0cb8
2 files changed
+39 -2

No files matched your search

@@ -9,7 +9,12 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import type { StreamSession } from "@homarr/integrations";
import { getMediaServerColumnVisibility, getSeasonEpisodeParams, SessionDetailsPopover } from "./component";
import {
getMediaServerColumnVisibility,
getResolutionLabel,
getSeasonEpisodeParams,
SessionDetailsPopover,
} from "./component";
vi.mock("@homarr/translation/client", () => ({
useScopedI18n: () => (key: string) => key,
@@ -124,3 +129,24 @@ describe("getSeasonEpisodeParams", () => {
expect(getSeasonEpisodeParams(null, undefined)).toBeNull();
});
});
describe("getResolutionLabel", () => {
test("labels a standard 16:9 resolution by its exact height", () => {
expect(getResolutionLabel(1920, 1080)).toBe("1080p");
expect(getResolutionLabel(1280, 720)).toBe("720p");
});
test("classifies a cinemascope source by its width, not its cropped height", () => {
// A 1920x804 encode (2.35:1, cropped top and bottom) is still a 1080p source.
expect(getResolutionLabel(1920, 804)).toBe("1080p");
expect(getResolutionLabel(1280, 640)).toBe("720p");
});
test("rounds a near-standard resolution up to its nearest tier", () => {
expect(getResolutionLabel(1912, 796)).toBe("1080p");
});
test("falls back to the raw height when it doesn't clear any known tier", () => {
expect(getResolutionLabel(100, 100)).toBe("100p");
});
});
@@ -77,6 +77,17 @@ function formatBitrate(bitrateKbps: number | null | undefined): string | null {
return bitrateKbps >= 1000 ? `${(bitrateKbps / 1000).toFixed(1)} Mbps` : `${Math.round(bitrateKbps)} kbps`;
}
const RESOLUTION_TIER_HEIGHTS = [4320, 2160, 1440, 1080, 720, 480, 360, 240] as const;
export function getResolutionLabel(width: number, height: number): string {
// Letterboxed/cinemascope sources are cropped shorter than their nominal resolution class
// (a 1920x804 encode is still "1080p"), so classify off the width-derived 16:9 height instead
// of the raw (possibly cropped) height - matches how Plex/Jellyfin label these themselves.
const effectiveHeight = Math.max(height, Math.round((width * 9) / 16));
const tier = RESOLUTION_TIER_HEIGHTS.find((candidate) => effectiveHeight >= candidate * 0.9);
return `${tier ?? effectiveHeight}p`;
}
export function getSeasonEpisodeParams(
seasonNumber: number | null | undefined,
episodeNumber: number | null | undefined,
@@ -305,7 +316,7 @@ export default function MediaServerWidget({
// touches audio/container and leaves the video resolution untouched).
const resolution =
currentlyPlaying?.metadata?.transcoding.resolution ?? currentlyPlaying?.metadata?.video.resolution;
const resolutionLabel = resolution ? `${resolution.height}p` : null;
const resolutionLabel = resolution ? getResolutionLabel(resolution.width, resolution.height) : null;
const toggleDetails = () => setSelectedRowId((current) => (current === rowId ? null : rowId));
return (