fix(overlays): derive dynamic-range badges from Dolby Vision metadata (#365)

* fix(overlays): derive dynamic-range badges from Dolby Vision metadata

Dolby Vision files were labeled with a generic "HDR" badge everywhere
outside the Media Info dialog, because badge sites only consulted the
bare FileVersion.hdr boolean even though the payload carries
video_tracks[].dolby_vision / dv_profile / video_range_type /
color_transfer.

Web: add a shared helper (web/src/lib/videoRange.ts) that derives a
display label ("DV", "DV HDR10", "DV HLG", "HDR10+", "HDR10", "HLG")
from the probed video tracks, mirroring the server-side normalizeHDR
vocabulary, with the hdr boolean kept as the last-resort fallback for
stale pre-DV probe rows. Use it in QualityBadges, VersionDropdown,
VersionFlyout, and the player HUD (playback-info) in place of the
hardcoded "HDR" literal. Badge styling is unchanged.

Server: in internal/overlays/summary.go, break bestFile resolution
ties by richness of dynamic-range metadata (DV > explicit HDR10/HLG
via color_transfer > bare hdr boolean > SDR) so a first-scanned
generic-HDR file no longer masks a Dolby Vision sibling on card
overlays. Full ties still keep the earliest file, so the selection
stays deterministic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(overlays): detect Dolby Vision via dv_profile and DOVI range type

Probed rows set DolbyVision and DVProfile together, but catalog-seeded
or imported tracks can carry only dv_profile or a DOVI* video_range_type
with an empty dolby_vision string. Share one hasDolbyVision predicate
between rangeRank and normalizeHDR so those rows rank and label as DV,
matching the web videoRange helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(overlays): read video_range_type and hdr10_plus in hdrTypeFromTracks

Review follow-up: hdrTypeFromTracks only inspected color_transfer, so
server card overlays could never label HDR10+ and a track whose only
signal is video_range_type (e.g. catalog-seeded rows without probed
color metadata) fell through to the bare-boolean tier of rangeRank —
inconsistent with hasDolbyVision, which already reads the range type.
The server now shares the web helper's detection order exactly, making
the web/server vocabulary mirror claim true.

Also drops the unused video_range field from the web VideoRangeTrack
interface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Quick104 <31828688+Quick104@users.noreply.github.com>
This commit is contained in:
RXWatcher
2026-07-16 10:53:30 -04:00
committed by GitHub
co-authored by rxwatcher Claude Opus 4.8 Quick104
parent f377be9d6e
commit 0f9584ef42
9 changed files with 419 additions and 22 deletions
+52 -12
View File
@@ -51,20 +51,53 @@ func BuildSummary(files []*models.MediaFile) *Summary {
func bestFile(files []*models.MediaFile) *models.MediaFile {
var best *models.MediaFile
bestRank := -1
bestRes := -1
bestRange := -1
for _, file := range files {
if file == nil {
continue
}
rank := resolutionRank(file.Resolution)
if best == nil || rank > bestRank {
res := resolutionRank(file.Resolution)
rng := rangeRank(file)
if best == nil || res > bestRes || (res == bestRes && rng > bestRange) {
best = file
bestRank = rank
bestRes = res
bestRange = rng
}
}
return best
}
// rangeRank orders files with equal resolution by the richness of their
// dynamic-range metadata so a Dolby Vision version is not masked by a
// first-scanned file that only carries the bare HDR boolean (e.g. a stale
// pre-DV probe row). Ties keep the earliest file in the slice.
func rangeRank(file *models.MediaFile) int {
if hasDolbyVision(file.VideoTracks) {
return 3
}
if hdrTypeFromTracks(file.VideoTracks) != "" {
return 2
}
if file.HDR {
return 1
}
return 0
}
// hasDolbyVision reports whether any track carries Dolby Vision metadata.
// Probed rows set DolbyVision and DVProfile together, but seeded/imported
// rows may carry only dv_profile or a DOVI* video_range_type.
func hasDolbyVision(tracks []models.VideoTrack) bool {
for _, track := range tracks {
if track.DolbyVision != "" || track.DVProfile > 0 ||
strings.HasPrefix(track.VideoRangeType, "DOVI") {
return true
}
}
return false
}
func normalizeResolution(value string) string {
cleaned := strings.ToLower(strings.TrimSpace(value))
switch cleaned {
@@ -92,13 +125,7 @@ func resolutionRank(value string) int {
}
func normalizeHDR(file *models.MediaFile) string {
hasDV := false
for _, track := range file.VideoTracks {
if track.DolbyVision != "" {
hasDV = true
break
}
}
hasDV := hasDolbyVision(file.VideoTracks)
hdrType := hdrTypeFromTracks(file.VideoTracks)
switch {
@@ -115,9 +142,22 @@ func normalizeHDR(file *models.MediaFile) string {
}
}
// hdrTypeFromTracks inspects video track color transfer to distinguish HDR variants.
// hdrTypeFromTracks distinguishes HDR variants from the scanner-derived
// video_range_type enum (HDR10, HDR10Plus, HLG, DOVIWith*) with color
// transfer as a fallback, so rows without probed color metadata (e.g.
// catalog-seeded imports) still resolve. Keep in sync with trackHdrType in
// web/src/lib/videoRange.ts.
func hdrTypeFromTracks(tracks []models.VideoTrack) string {
for _, track := range tracks {
rangeType := strings.TrimSpace(track.VideoRangeType)
switch {
case track.HDR10Plus || strings.Contains(rangeType, "HDR10Plus"):
return "HDR10+"
case rangeType == "HDR10" || strings.HasSuffix(rangeType, "WithHDR10"):
return "HDR10"
case rangeType == "HLG" || strings.HasSuffix(rangeType, "WithHLG"):
return "HLG"
}
ct := strings.ToLower(track.ColorTransfer)
switch {
case strings.Contains(ct, "smpte2084"):
+159
View File
@@ -212,6 +212,165 @@ func TestNormalizeReleaseType(t *testing.T) {
}
}
func TestNormalizeHDR(t *testing.T) {
cases := []struct {
name string
file *models.MediaFile
want string
}{
{"sdr", &models.MediaFile{}, ""},
{"bare boolean", &models.MediaFile{HDR: true}, "HDR"},
{"hdr10 via color transfer", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{ColorTransfer: "smpte2084"}},
}, "HDR10"},
{"hlg via color transfer", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{ColorTransfer: "arib-std-b67"}},
}, "HLG"},
{"dv only", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{DolbyVision: "Profile 5"}},
}, "DV"},
{"dv with hdr10 base layer", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{
DolbyVision: "Profile 8",
ColorTransfer: "smpte2084",
}},
}, "DV HDR10"},
{"dv via profile number only", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{DVProfile: 5}},
}, "DV"},
{"dv via DOVI range type only", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{
VideoRangeType: "DOVIWithHDR10",
ColorTransfer: "smpte2084",
}},
}, "DV HDR10"},
{"hdr10 via range type without color transfer", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{VideoRangeType: "HDR10"}},
}, "HDR10"},
{"hlg via range type without color transfer", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{VideoRangeType: "HLG"}},
}, "HLG"},
{"hdr10+ via flag", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{
HDR10Plus: true,
ColorTransfer: "smpte2084",
}},
}, "HDR10+"},
{"hdr10+ via range type", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{VideoRangeType: "HDR10Plus"}},
}, "HDR10+"},
{"dv with hdr10+ base layer", &models.MediaFile{
HDR: true,
VideoTracks: []models.VideoTrack{{VideoRangeType: "DOVIWithELHDR10Plus"}},
}, "DV HDR10+"},
{"sdr range type stays sdr", &models.MediaFile{
VideoTracks: []models.VideoTrack{{VideoRangeType: "SDR"}},
}, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := normalizeHDR(tc.file); got != tc.want {
t.Errorf("normalizeHDR = %q, want %q", got, tc.want)
}
})
}
}
func TestBestFileRangeTieBreaking(t *testing.T) {
genericHDR := &models.MediaFile{Resolution: "2160p", HDR: true}
explicitHDR10 := &models.MediaFile{
Resolution: "2160p",
HDR: true,
VideoTracks: []models.VideoTrack{{ColorTransfer: "smpte2084"}},
}
dv := &models.MediaFile{
Resolution: "2160p",
HDR: true,
VideoTracks: []models.VideoTrack{{
DolbyVision: "Profile 8",
ColorTransfer: "smpte2084",
}},
}
sdr := &models.MediaFile{Resolution: "2160p"}
dvProfileOnly := &models.MediaFile{
Resolution: "2160p",
HDR: true,
VideoTracks: []models.VideoTrack{{DVProfile: 5}},
}
lowResDV := &models.MediaFile{
Resolution: "1080p",
HDR: true,
VideoTracks: []models.VideoTrack{{DolbyVision: "Profile 5"}},
}
rangeTypeHDR10 := &models.MediaFile{
Resolution: "2160p",
HDR: true,
VideoTracks: []models.VideoTrack{{VideoRangeType: "HDR10"}},
}
cases := []struct {
name string
files []*models.MediaFile
want *models.MediaFile
}{
{"nil files ignored", []*models.MediaFile{nil, genericHDR}, genericHDR},
{"dv beats first-scanned generic hdr at same resolution",
[]*models.MediaFile{genericHDR, dv}, dv},
{"dv beats explicit hdr10 at same resolution",
[]*models.MediaFile{explicitHDR10, dv}, dv},
{"dv via profile number only still outranks generic hdr",
[]*models.MediaFile{genericHDR, dvProfileOnly}, dvProfileOnly},
{"range-type-only hdr10 outranks bare boolean",
[]*models.MediaFile{genericHDR, rangeTypeHDR10}, rangeTypeHDR10},
{"explicit hdr10 beats bare boolean at same resolution",
[]*models.MediaFile{genericHDR, explicitHDR10}, explicitHDR10},
{"bare boolean beats sdr at same resolution",
[]*models.MediaFile{sdr, genericHDR}, genericHDR},
{"higher resolution still wins over lower-res dv",
[]*models.MediaFile{lowResDV, genericHDR}, genericHDR},
{"full tie keeps earliest file",
[]*models.MediaFile{genericHDR, {Resolution: "2160p", HDR: true}}, genericHDR},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := bestFile(tc.files); got != tc.want {
t.Errorf("bestFile picked %+v, want %+v", got, tc.want)
}
})
}
}
func TestBuildSummaryPrefersDolbyVisionSibling(t *testing.T) {
genericHDR := &models.MediaFile{Resolution: "2160p", HDR: true, CodecVideo: "hevc"}
dv := &models.MediaFile{
Resolution: "2160p",
HDR: true,
CodecVideo: "hevc",
VideoTracks: []models.VideoTrack{{
Codec: "hevc",
DolbyVision: "Profile 8",
ColorTransfer: "smpte2084",
}},
}
got := BuildSummary([]*models.MediaFile{genericHDR, dv})
if got == nil {
t.Fatal("expected non-nil summary")
}
if got.HDR != "DV HDR10" {
t.Errorf("HDR = %q, want %q", got.HDR, "DV HDR10")
}
}
func TestBuildSummaryAggregatesNewFields(t *testing.T) {
file := &models.MediaFile{
Resolution: "1080p",
+103
View File
@@ -0,0 +1,103 @@
import { describe, expect, it } from "vitest";
import { bestVideoRangeLabel, videoRangeLabel, type VideoRangeSource } from "./videoRange";
describe("videoRangeLabel", () => {
it("returns empty for SDR / unprobed", () => {
expect(videoRangeLabel({ hdr: false })).toBe("");
expect(videoRangeLabel({ hdr: false, video_tracks: [{}] })).toBe("");
});
it("falls back to generic HDR from the bare boolean", () => {
expect(videoRangeLabel({ hdr: true })).toBe("HDR");
expect(videoRangeLabel({ hdr: true, video_tracks: [{}] })).toBe("HDR");
});
it("detects Dolby Vision from the dolby_vision string", () => {
expect(videoRangeLabel({ hdr: true, video_tracks: [{ dolby_vision: "Profile 5" }] })).toBe(
"DV",
);
});
it("detects Dolby Vision from dv_profile alone", () => {
expect(videoRangeLabel({ hdr: true, video_tracks: [{ dv_profile: 8 }] })).toBe("DV");
});
it("detects Dolby Vision from a DOVI video_range_type", () => {
expect(
videoRangeLabel({ hdr: true, video_tracks: [{ video_range_type: "DOVIWithSDR" }] }),
).toBe("DV");
});
it("combines DV with HDR10 base-layer compatibility", () => {
expect(
videoRangeLabel({
hdr: true,
video_tracks: [{ dolby_vision: "Profile 8", video_range_type: "DOVIWithHDR10" }],
}),
).toBe("DV HDR10");
expect(
videoRangeLabel({
hdr: true,
video_tracks: [{ dolby_vision: "Profile 7", color_transfer: "smpte2084" }],
}),
).toBe("DV HDR10");
});
it("combines DV with HLG compatibility", () => {
expect(
videoRangeLabel({ hdr: true, video_tracks: [{ video_range_type: "DOVIWithHLG" }] }),
).toBe("DV HLG");
});
it("labels HDR10+ from the flag or range type", () => {
expect(videoRangeLabel({ hdr: true, video_tracks: [{ hdr10_plus: true }] })).toBe("HDR10+");
expect(videoRangeLabel({ hdr: true, video_tracks: [{ video_range_type: "HDR10Plus" }] })).toBe(
"HDR10+",
);
expect(
videoRangeLabel({
hdr: true,
video_tracks: [{ video_range_type: "DOVIWithELHDR10Plus" }],
}),
).toBe("DV HDR10+");
});
it("labels HDR10 and HLG from video_range_type or color_transfer", () => {
expect(videoRangeLabel({ hdr: true, video_tracks: [{ video_range_type: "HDR10" }] })).toBe(
"HDR10",
);
expect(videoRangeLabel({ hdr: true, video_tracks: [{ color_transfer: "smpte2084" }] })).toBe(
"HDR10",
);
expect(videoRangeLabel({ hdr: true, video_tracks: [{ video_range_type: "HLG" }] })).toBe("HLG");
expect(videoRangeLabel({ hdr: true, video_tracks: [{ color_transfer: "arib-std-b67" }] })).toBe(
"HLG",
);
});
});
describe("bestVideoRangeLabel", () => {
it("prefers DV over a generic-HDR sibling version", () => {
const versions: VideoRangeSource[] = [
{ hdr: true },
{
hdr: true,
video_tracks: [{ dolby_vision: "Profile 8", video_range_type: "DOVIWithHDR10" }],
},
];
expect(bestVideoRangeLabel(versions)).toBe("DV HDR10");
});
it("prefers explicit HDR10 over the bare boolean", () => {
const versions: VideoRangeSource[] = [
{ hdr: true },
{ hdr: true, video_tracks: [{ color_transfer: "smpte2084" }] },
];
expect(bestVideoRangeLabel(versions)).toBe("HDR10");
});
it("returns empty when every version is SDR", () => {
expect(bestVideoRangeLabel([{ hdr: false }, { hdr: false }])).toBe("");
});
});
+89
View File
@@ -0,0 +1,89 @@
// Derives a compact dynamic-range badge label ("DV", "DV HDR10", "HDR10+",
// "HDR10", "HLG", "HDR") from a file version's probed video tracks. Mirrors
// the server-side vocabulary in internal/overlays/summary.go (normalizeHDR)
// so card overlays, detail badges, version pickers, and the player HUD all
// agree. The bare `hdr` boolean is kept as the last-resort fallback for
// files probed before Dolby Vision / video-range metadata existed.
/** Minimal structural shape shared by VersionVideoTrack and PlayerVideoTrack. */
export interface VideoRangeTrack {
dolby_vision?: string;
dv_profile?: number;
hdr10_plus?: boolean;
video_range_type?: string;
color_transfer?: string;
}
/** Minimal structural shape shared by FileVersion and PlayerFileVersion. */
export interface VideoRangeSource {
hdr?: boolean;
video_tracks?: VideoRangeTrack[];
}
function trackHasDolbyVision(track: VideoRangeTrack): boolean {
if (track.dolby_vision?.trim()) return true;
if ((track.dv_profile ?? 0) > 0) return true;
return (track.video_range_type?.trim() ?? "").startsWith("DOVI");
}
// Distinguishes HDR variants from the scanner-derived video_range_type enum
// (HDR10, HDR10Plus, HLG, DOVIWith*) with color_transfer as a fallback,
// matching the server's hdrTypeFromTracks.
function trackHdrType(track: VideoRangeTrack): string {
const rangeType = track.video_range_type?.trim() ?? "";
if (track.hdr10_plus || rangeType.includes("HDR10Plus")) return "HDR10+";
if (rangeType === "HDR10" || rangeType.endsWith("WithHDR10")) return "HDR10";
if (rangeType === "HLG" || rangeType.endsWith("WithHLG")) return "HLG";
const transfer = track.color_transfer?.toLowerCase() ?? "";
if (transfer.includes("smpte2084")) return "HDR10";
if (transfer.includes("arib-std-b67")) return "HLG";
return "";
}
/**
* Display label for a file version's dynamic range: "DV", "DV HDR10",
* "DV HDR10+", "DV HLG", "HDR10+", "HDR10", "HLG", "HDR" (boolean-only
* fallback), or "" for SDR/unknown.
*/
export function videoRangeLabel(source: VideoRangeSource): string {
let hasDV = false;
let hdrType = "";
for (const track of source.video_tracks ?? []) {
if (trackHasDolbyVision(track)) hasDV = true;
if (!hdrType) hdrType = trackHdrType(track);
}
if (hasDV) return hdrType ? `DV ${hdrType}` : "DV";
if (hdrType) return hdrType;
if (source.hdr) return "HDR";
return "";
}
// Rollup preference when a badge summarizes several versions: any DV variant
// beats explicit HDR10+/HDR10/HLG, which beat the generic boolean "HDR".
const LABEL_RANK: Record<string, number> = {
"DV HDR10+": 8,
"DV HDR10": 7,
"DV HLG": 6,
DV: 5,
"HDR10+": 4,
HDR10: 3,
HLG: 2,
HDR: 1,
};
/** Best (most specific) dynamic-range label across a set of versions. */
export function bestVideoRangeLabel(sources: VideoRangeSource[]): string {
let best = "";
let bestRank = 0;
for (const source of sources) {
const label = videoRangeLabel(source);
const rank = LABEL_RANK[label] ?? 0;
if (rank > bestRank) {
best = label;
bestRank = rank;
}
}
return best;
}
@@ -1,4 +1,5 @@
import type { FileVersion } from "@/api/types";
import { bestVideoRangeLabel } from "@/lib/videoRange";
import { pickBestAttributes } from "./versionRankingUtils";
interface QualityBadgesProps {
@@ -11,7 +12,8 @@ export default function QualityBadges({ versions }: QualityBadgesProps) {
const badges: string[] = [];
if (best.resolution) badges.push(best.resolution);
if (best.hdr) badges.push("HDR");
const rangeLabel = bestVideoRangeLabel(versions);
if (rangeLabel) badges.push(rangeLabel);
if (best.audioLabel) badges.push(best.audioLabel);
if (badges.length === 0) return null;
@@ -5,6 +5,7 @@ import type { FileVersion, PlaybackVariant } from "@/api/types";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { videoRangeLabel } from "@/lib/videoRange";
import { sortPlaybackVariantsByEditionPreference } from "./versionRankingUtils";
import { buildDetailLine, buildQualitySummary, sortByResolution } from "./VersionFlyout";
@@ -128,6 +129,7 @@ export default function VersionDropdown({
const isSelected = version.file_id === activeVersion?.file_id;
const summary = buildQualitySummary(version);
const detail = buildDetailLine(version);
const rangeLabel = videoRangeLabel(version);
return (
<button
@@ -146,9 +148,9 @@ export default function VersionDropdown({
<span className="truncate text-sm font-medium">
{summary || `Version ${version.file_id}`}
</span>
{version.hdr ? (
{rangeLabel ? (
<Badge variant="secondary" className="px-1.5 py-0 text-[10px] uppercase">
HDR
{rangeLabel}
</Badge>
) : null}
</div>
@@ -6,6 +6,7 @@ import {
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { formatFileSize, mapAudioLabel } from "@/lib/mediaFormat";
import { videoRangeLabel } from "@/lib/videoRange";
import { extractSourceHint } from "./versionFormatUtils";
import { resolutionScore } from "./versionRankingUtils";
@@ -18,7 +19,8 @@ export function buildQualitySummary(version: FileVersion): string {
if (version.resolution) parts.push(version.resolution);
if (version.codec_video) parts.push(version.codec_video.toUpperCase());
if (version.hdr) parts.push("HDR");
const rangeLabel = videoRangeLabel(version);
if (rangeLabel) parts.push(rangeLabel);
if (version.codec_audio) parts.push(mapAudioLabel(version.codec_audio));
if (parts.length === 0 && version.container) {
parts.push(version.container.toUpperCase());
+3 -1
View File
@@ -170,7 +170,9 @@ describe("playback info helpers", () => {
runtimeStats: {},
});
expect(rowValue(sections, "Player", "Auto-switched from")).toBe("2160p HEVC HDR");
// The default fixture is a Dolby Vision (Profile 8.1) file, so the range
// badge reads "DV" instead of the old generic boolean-derived "HDR".
expect(rowValue(sections, "Player", "Auto-switched from")).toBe("2160p HEVC DV");
expect(rowValue(sections, "Current Source File", "Video codec")).toBe("H.264 High");
});
+3 -5
View File
@@ -6,6 +6,7 @@ import {
formatMbpsFromKbps,
formatSampleRate,
} from "@/lib/mediaFormat";
import { videoRangeLabel } from "@/lib/videoRange";
import type {
PlaybackSessionPlaybackInfo,
PlayMethod,
@@ -223,7 +224,7 @@ function formatRequestedSourceVersion(version: PlayerFileVersion): string {
const parts = [
version.resolution?.trim(),
formatCodecLabel(version.codec_video),
version.hdr ? "HDR" : null,
videoRangeLabel(version) || null,
].filter(Boolean);
return parts.join(" ");
}
@@ -333,11 +334,8 @@ export function formatVideoRangeType(
if (track?.video_range) {
return track.video_range;
}
if (version?.hdr) {
return "HDR";
}
if (version) {
return "SDR";
return videoRangeLabel(version) || "SDR";
}
return "—";
}