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:
co-authored by
rxwatcher
Claude Opus 4.8
Quick104
parent
f377be9d6e
commit
0f9584ef42
@@ -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"):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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("");
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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 "—";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user