fix: name the real HDR format on the player and detail pages, and the playing audio codec in the player

Merged
BL
5d ago
+500-76

[!NOTE]
Merge after #139. I tested this on top of #139's player and want it to land on that, not the current one.

Problem and reproduction

The Apple TV player's Info tab and the detail pages didn't name a file's real format:

  • The player's dynamic-range badge said "HDR" for any HDR file, so a Dolby Vision file showed "HDR" and HDR10+, HDR10 and HLG weren't told apart.
  • The player's audio badge showed the file version's main audio codec, not the track that's playing. With a second audio track selected the badges read 4K HDR HEVC TRUEHD while the Audio row said AC3 · 5.1.
  • The detail pages only said DV or HDR. The Apple TV detail page added a separate HDR10 badge for profile 7 and 8.1, and Emby and Jellyfin files only ever showed HDR, because nothing read their range or Dolby Vision fields.

To reproduce: open a Dolby Vision title, note the badges on its detail page, play it, switch to an audio track with a different codec, then open the Info tab. Root cause: the player only checked the file's hdr flag and built its badges once at load from FileVersion.codecAudio, and each detail screen had its own DV/HDR logic that only read Silo's dolby_vision and video_range.

Implementation and impact

  • New PlayerStreamBadges works out one dynamic-range label from the selected file's video tracks: DV P<profile> when the profile is known (DV P5, DV P7, DV P8.1), DV when it isn't, then HDR10+, HDR10 or HLG, HDR only when the file is HDR but the type is unknown, and nothing for SDR. The comment at the top of the file sets out where each provider's values come from.
  • The label names the file as it is. Profile 7 stays DV P7 even though Vivid plays it as 8.1 on a Dolby Vision display (and as its HDR10 base layer on other displays); the player's stats show the actual output.
  • What each provider sends, and what Vivid maps:
    • Silo (/api/v1/watch): dolby_vision is only ever Profile <n>, so the .1 comes from dv_bl_compat_id, which is left out when it's 0. video_range is only DolbyVision, HDR or empty, so HDR10+, HDR10 and HLG come from video_range_type, hdr10_plus and, for older probes, color_transfer. VideoTrack now decodes video_range_type, dv_profile, dv_bl_compat_id and hdr10_plus.
    • Jellyfin: the adapter now maps VideoRangeType, DvProfile, DvBlSignalCompatibilityId and Hdr10PlusPresentFlag into the same new fields. DOVIInvalid (Dolby Vision outside the spec, played as its base layer) isn't shown as DV.
    • Emby: the adapter translates ExtendedVideoType (DolbyVision, Hdr10, Hdr10Plus, HyperLogGamma) and ExtendedVideoSubtype into the same fields, and falls back to VideoRange text on servers that don't send them. Emby's ExtendedVideoSubTypes reference only has two-digit values (DoviProfile02 to DoviProfile92, so DoviProfile81 is profile 8 with compatibility ID 1). Its API reference spells the field ExtendedVideoSubType while server JSON uses ExtendedVideoSubtype, so both are read.
  • Where the label now shows:
    • the Apple TV player's Info tab (Stream badges);
    • the Apple TV detail page, as one badge (4K · DV P8.1 · ATMOS · CC instead of DV and HDR10);
    • the version pickers on both platforms (DetailPlaybackFormatting.dynamicRangeLabel);
    • Continue Watching in the Apple TV Home focus info (badge text only);
    • the iPhone and iPad detail page's hero chip, which now also shows for Dolby Vision with an SDR base layer (profile 8.2), and its Video range fact, which reads every video stream like the hero.
  • The player's audio badge comes from the selected PlayerTrack with the same text as the Audio row (one shared helper), and PlayerViewModel.streamBadges reads audioTracks and selectedAudioId, so the badge changes when you switch tracks. Until the engine reports a selected track, the file's main codec stands in as before.
  • Deliberately unchanged: card overlays (poster badges) and the catalogue-summary rows in the Home focus info have no video-track details, so they keep their DV and HDR marks; the Continue Watching overlay values aren't touched either. Downloads don't keep their video tracks, so a downloaded HDR file still shows HDR. The player's stats and diagnostics keep reporting the engine's output. Emby and Jellyfin still leave videoRange and dolbyVision empty, because the phone's Dolby Vision fact and the Apple TV Continue Watching card overlays read them.
  • iPhone and iPad have no player Info badges, so the player part of this is Apple TV only.
  • Risk: the new VideoTrack fields are optional and type-checked in the adapters, so a server that doesn't send them falls back to DV or HDR.

Validation

  • iosApp/Tests/PlayerStreamBadgesTests.swift, in both VividTests and VividTVTests (listed in iosApp/project.yml), covers Silo Dolby Vision profiles 5, 7, 8.1, 8.2, 8.4 and 10, compatibility from the range type, older Silo labels, HDR10+, HDR10 and HLG (including colour transfer only), the HDR fallback and SDR, Jellyfin and Emby through their real version() mapping and decoder, that the detail label matches the player for all three providers (and, on tvOS, that the detail page shows DV P8.1 as one badge with no HDR10), and the audio badge following the playing track.
  • DetailVersionSelectionTests.testVersionSelectorUsesTheDolbyVisionProfile now expects DV P8.1 in the version picker labels.
  • CI: Player regression on dad60da passed, iOS and tvOS (run 38484418131). Results for the latest head are in the checks below.
  • Local: signed Release tvOS builds of #139's head plus this PR built and installed for device testing (see below). No local test run.

Performance

Not applicable. This changes label text only; no benchmark was run.

Testing status

  • Tested? Yes, on Apple TV. Not on iPhone or iPad.
  • Environment: Apple TV 4K (3rd generation), signed Release build of #139's head (experiment/vivid-player-bridge) with this PR on top; GitHub Actions simulators (iOS and tvOS).
  • Revision: first device build dad60da on #139 ccc9c42; second device build this PR at 1c2ec42 on #139 ead3ead. Later commits only change comments, docs and the iPhone and iPad Video range fact.
  • Checks and results: I checked the first build on Apple TV: the player badge showed DV P7 for a profile 7 file, which led to the detail page changes here. On the second build the player badges and the detail page labels worked as intended. CI unit tests passed on dad60da.
  • Limitations: I haven't checked every provider and format on a device (Emby and Jellyfin titles, HDR10+, HLG and SDR files are covered by the unit tests). The iPhone and iPad detail page changes are untested on a device.

Design impact and approval

[!IMPORTANT]
Only blurbery can approve design changes. Obtain prior, explicit approval and reference it here. Unapproved design changes must not be merged.

  • Design impact: label text only, no layout, style, order or spacing change. The player's dynamic-range and audio badges name the real format and the playing codec. The detail pages, version pickers, Home focus info (Continue Watching) and the iPhone and iPad hero chip and Video range fact use the same dynamic-range label, and the Apple TV detail page shows it as one badge instead of DV plus HDR10. Profile 7 is labelled DV P7.
  • Approval from blurbery: in chat on 14 October 2026: first the player badges (real HDR format, audio badge matching the playing track, badge text only), then, after device testing, the file's real format everywhere the file's video details exist, one badge on the Apple TV detail page, and profile 7 labelled as DV P7.

Documentation impact

  • docs/playback/architecture.md: what the Info tab's Stream badges show, the brief file-codec fallback, that profile 7 shows as DV P7, where else the label appears, and what keeps the old marks.

  • docs/apple-tv-browsing.md: the detail page's dynamic-range badge and Version pill show the file's format as one label.

  • docs/cores/emby.md and docs/cores/jellyfin.md: which stream fields the label reads, and that the detail pages, version pickers and Home focus info show it.

  • I checked the docs this change affects and updated them in this PR, or explained above why none needed changes.

AI disclosure

[!IMPORTANT]
Required for every PR. Incomplete disclosures are not ready for review or merge.

I implemented these changes and used Claude Opus as a tool.