Files
silo-server/internal/playback/remux_dv_test.go
T
ee9356aab3 fix(playback): claim Safari DV/HDR10 decode evidence and preserve dvvC in remuxes (#617)
* fix(playback): claim Safari DV/HDR10 decode evidence and preserve dvvC

Follow-up to #613 for #609: Safari 26 reports dynamic-range: standard even
on an XDR display, and answers canPlayType "probably" only for dvh1/hvc1
sample entries, never dvhe/hev1. The web probe gated every structured HDR
claim on that media query and probed only dvhe, so the planner still saw
empty hdr_details and terminated with hdr_transcode_unsupported.

- Run the Dolby Vision and HDR10 shape probes unconditionally; the
  dynamic-range query survives only as the best-effort hdr output boolean.
- Probe dvh1+dvhe per DV profile and hvc1+hev1 for the exact HDR10
  Media Capabilities shape; either definitive answer earns the claim.
- Tag preserved-DV remuxes dvh1 with -strict unofficial: FFmpeg omits the
  dvvC configuration record under either tag without it, so the previous
  dvhe output carried no DV signaling at all.
- Scale the screen-derived max_resolution by devicePixelRatio so 2x
  panels stop advertising 1080p.

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

* fix(playback): align DV/HDR10 claims with the sample entries the remux emits

Review follow-ups on #617 (Codex P1s):

- Probe only dvh1: a browser answering "probably" solely for dvhe has
  given no evidence for the dvh1-tagged file the preserve remux delivers,
  so that answer no longer earns a Dolby Vision claim; such browsers keep
  the validated HDR10 fallback.
- Label the explicit v3 HDR10 strip output hvc1 so the file matches the
  hvc1 evidence the web probe accepts; legacy/auto strips keep hev1.
- Cover preserve tagging for profiles 5 and 8, and exercise the
  mode-to-argument mapping through StartRemuxWithDVMode instead of
  passing the tag flag directly.

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

* fix(playback): name the subtitle when burn-in alone forces a refused transcode

Selecting a PGS/VOBSUB track on a client without a bitmap renderer forces
a burn-in transcode. When that transcode cannot run, the terminal blamed
the HDR pipeline (hdr_transcode_unsupported) or the 4K policy
(no_alternate_version) — problems that were not blocking playback, since
deselecting the subtitle restores the previous route.

- When the burn requirement is the sole trigger of the adaptation, the
  HDR and 4K-policy terminals emit subtitle_conversion_unsupported with
  a message naming the burn requirement and the actual blocker. A range
  the client genuinely cannot take keeps the HDR terminal.
- describePlanTerminal passes the server's subtitle message through
  instead of flattening every subtitle_* reason to one generic sentence.
- The web player toasts the refusal when it rolls the subtitle selection
  back; previously the only surface was the quality menu, which a user
  who just picked a subtitle has no reason to open.

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

* fix(playback): require decode evidence for the exact hvc1 entry and gate the disabled-transcode subtitle terminal

Review follow-ups on #617:

- The HDR10 probe accepts only hvc1.2.4.L153.B0: the explicit v3 strip
  remux labels its output hvc1, so an hev1-only decodingInfo answer is
  evidence for bytes Silo never sends and earns no claim (Codex P1).
- The disabled-transcode branch blames the subtitle only when the burn
  requirement was the sole adaptation trigger; other causes keep
  transcoding_disabled. In practice a bitmap selection without transcode
  terminals in the subtitle policy before this branch, but the guard
  keeps the sole-cause invariant if that ordering ever changes
  (CodeRabbit / Codex P2).
- Protocol doc scopes the hvc1 labeling to the explicit v3 strip path;
  legacy/auto strips keep hev1 (CodeRabbit).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 21:14:52 -04:00

289 lines
12 KiB
Go

package playback
import (
"context"
"io"
"os"
"path/filepath"
"strings"
"testing"
)
func argsContainPair(args []string, a, b string) bool {
for i := 0; i < len(args)-1; i++ {
if args[i] == a && args[i+1] == b {
return true
}
}
return false
}
// Profile 7 remuxes drop the enhancement-layer track (-map 0:v:0 keeps only
// the base layer), which leaves dangling dual-layer RPU metadata on the BL.
// Stripping the RPUs yields a clean HDR10 stream — both a correctness fix and
// the Apple-parity fallback presentation for devices without a P7 decoder.
func TestBuildRemuxArgsStripsDolbyVisionRPUForProfile7(t *testing.T) {
args := buildRemuxArgs("/x.mkv", "mp4", 0, false, -1, 7, false, false)
if !argsContainPair(args, "-bsf:v", "dovi_rpu=strip=1") {
t.Fatalf("profile 7 remux must strip DV RPUs from the base layer, args=%v", strings.Join(args, " "))
}
}
func TestBuildRemuxArgsExcludesAttachedPictures(t *testing.T) {
args := buildRemuxArgs("/book.m4b", "mp4", 0, true, -1, 0, false, true)
joined := strings.Join(args, " ")
if !strings.Contains(joined, "-map 0:V:0?") {
t.Fatalf("remux args must exclude attached-picture video streams: %s", joined)
}
}
func TestBuildRemuxArgsHonorsPlannedAACOutput(t *testing.T) {
args := buildRemuxArgsWithAudioV3("/book.m4b", "mp4", 0, true, -1, 0, false, true, 1, 96)
if !argsContainPair(args, "-ac", "1") || !argsContainPair(args, "-b:a", "96k") {
t.Fatalf("planned mono bitrate missing from remux args: %s", strings.Join(args, " "))
}
}
func TestBuildRemuxArgsRequiresVideoForVideoPlans(t *testing.T) {
args := buildRemuxArgs("/movie.mkv", "mp4", 0, false, -1, 0, false, false)
if !argsContainPair(args, "-map", "0:V:0") || argsContainPair(args, "-map", "0:V:0?") {
t.Fatalf("video remux must use a mandatory video map, args=%s", strings.Join(args, " "))
}
}
// An ffmpeg without the dovi_rpu bitstream filter (pre-7.1) would abort on
// the unknown filter before producing any output. The profile must be
// neutralized so the remux still starts, keeping the pre-strip behavior.
func TestRemuxDVProfileFallsBackWithoutFilterSupport(t *testing.T) {
if got := remuxDVProfile(7, false); got != 0 {
t.Errorf("remuxDVProfile(7, false) = %d, want 0 (no strip without filter support)", got)
}
if got := remuxDVProfile(7, true); got != 7 {
t.Errorf("remuxDVProfile(7, true) = %d, want 7", got)
}
for _, profile := range []int{0, 5, 8} {
for _, canStrip := range []bool{false, true} {
if got := remuxDVProfile(profile, canStrip); got != profile {
t.Errorf("remuxDVProfile(%d, %t) = %d, want %d (pass through)", profile, canStrip, got, profile)
}
}
}
}
// Profile 8 base layers are self-contained: the RPU stays valid without an
// enhancement layer and DV-capable clients can render it. Never strip.
func TestBuildRemuxArgsKeepsRPUForProfile8AndPlainFiles(t *testing.T) {
for _, profile := range []int{0, 5, 8} {
args := buildRemuxArgs("/x.mkv", "mp4", 0, false, -1, profile, false, false)
if argsContainPair(args, "-bsf:v", "dovi_rpu=strip=1") {
t.Fatalf("profile %d remux must not strip DV RPUs, args=%v", profile, strings.Join(args, " "))
}
}
}
// The DV sample entry is an explicit opt-in for the v3 preserve recipe:
// Media3 keys decoder selection from it, but legacy web/jellycompat consumers
// rely on the pre-v3 hev1 labeling their demuxers accept.
func TestBuildRemuxArgsTagsPreservedDolbyVisionOnlyWhenRequested(t *testing.T) {
for _, profile := range []int{5, 8} {
args := buildRemuxArgs("/x.mkv", "mp4", 0, false, -1, profile, true, false)
if !argsContainPair(args, "-tag:v", "dvh1") {
t.Fatalf("preserved profile %d must retain a DV sample entry, args=%v", profile, strings.Join(args, " "))
}
// Without -strict unofficial FFmpeg drops the dvvC configuration record
// and the output is an untagged HEVC stream no DV decoder will select.
if !argsContainPair(args, "-strict", "unofficial") {
t.Fatalf("preserved profile %d must allow the dvvC box, args=%v", profile, strings.Join(args, " "))
}
legacy := buildRemuxArgs("/x.mkv", "mp4", 0, false, -1, profile, false, false)
if argsContainPair(legacy, "-tag:v", "dvh1") || argsContainPair(legacy, "-strict", "unofficial") {
t.Fatalf("legacy profile %d remux must keep hev1 labeling, args=%v", profile, strings.Join(legacy, " "))
}
}
}
// The explicit v3 strip recipe labels its HDR10 output hvc1 — the sample entry
// the web probe collected evidence for — while the legacy/auto strip keeps
// ffmpeg's default hev1 for pre-v3 consumers. Neither carries a DOVI record,
// so neither needs the -strict relaxation.
func TestBuildRemuxArgsTagsStrippedHDR10OnlyWhenRequested(t *testing.T) {
tagged := buildRemuxArgs("/x.mkv", "mp4", 0, false, -1, 7, true, false)
if !argsContainPair(tagged, "-bsf:v", "dovi_rpu=strip=1") || !argsContainPair(tagged, "-tag:v", "hvc1") {
t.Fatalf("explicit strip must emit an hvc1-labeled HDR10 stream, args=%v", strings.Join(tagged, " "))
}
if argsContainPair(tagged, "-tag:v", "dvh1") || argsContainPair(tagged, "-strict", "unofficial") {
t.Fatalf("stripped output must not carry DV signaling, args=%v", strings.Join(tagged, " "))
}
legacy := buildRemuxArgs("/x.mkv", "mp4", 0, false, -1, 7, false, false)
if argsContainPair(legacy, "-tag:v", "hvc1") || argsContainPair(legacy, "-tag:v", "dvh1") {
t.Fatalf("legacy strip consumers must keep hev1 labeling, args=%v", strings.Join(legacy, " "))
}
}
// Dual-layer Profile 7 cannot be preserved by a base-layer-only remux; the
// explicit preserve recipe must fail fast instead of emitting a stream with
// dangling RPUs and no enhancement layer.
func TestStartRemuxRejectsPreservedProfile7(t *testing.T) {
if _, err := StartRemuxWithDVMode(t.Context(), "/nonexistent.mkv", "mp4", 0, false, -1, 7, RemuxDVPreserveV3, ""); err == nil {
t.Fatal("preserve mode accepted a profile 7 source")
}
}
// An unknown mode must fail for every profile, not only Profile 7 sources.
func TestStartRemuxRejectsUnknownModeForAllProfiles(t *testing.T) {
for _, profile := range []int{0, 5, 7, 8} {
if _, err := StartRemuxWithDVMode(t.Context(), "/nonexistent.mkv", "mp4", 0, false, -1, profile, RemuxDVMode("bogus"), ""); err == nil {
t.Fatalf("unknown remux DV mode accepted for profile %d", profile)
}
}
}
func TestBuildRemuxArgsDelaysMoovForCopiedAtmosConfiguration(t *testing.T) {
args := buildRemuxArgs("/x.mkv", "mp4", 0, false, -1, 8, false, false)
if !argsContainPair(args, "-movflags", "frag_keyframe+delay_moov+default_base_moof") {
t.Fatalf("remux must delay moov until copied audio is parsed, args=%v", strings.Join(args, " "))
}
}
// writeProbeAwareFFmpeg stands in for an ffmpeg that carries the dovi_rpu
// filter but cannot parse this source's RPU: it advertises the filter, fails
// the probe the way the real one does (rejecting packets while exiting 0), and
// records the arguments of every non-probe invocation.
func writeProbeAwareFFmpeg(t *testing.T) (bin, argLog string) {
t.Helper()
dir := t.TempDir()
bin = filepath.Join(dir, "ffmpeg")
argLog = filepath.Join(dir, "args")
script := "#!/bin/sh\n" +
"case \"$*\" in\n" +
" *-bsfs*) echo dovi_rpu; exit 0;;\n" +
" *'-f null'*) echo '[dovi_rpu @ 0x55] Failed to read unit 1 (type 39).' >&2; exit 0;;\n" +
"esac\n" +
"echo \"$*\" >> " + argLog + "\n" +
"exit 0\n"
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
t.Fatalf("write fake ffmpeg: %v", err)
}
return bin, argLog
}
// writeRecordingFFmpeg stands in for an ffmpeg whose dovi_rpu probe succeeds,
// recording the arguments of every non-probe invocation so tests can assert
// what the remux mode actually mapped onto the command line.
func writeRecordingFFmpeg(t *testing.T) (bin, argLog string) {
t.Helper()
dir := t.TempDir()
bin = filepath.Join(dir, "ffmpeg")
argLog = filepath.Join(dir, "args")
script := "#!/bin/sh\n" +
"case \"$*\" in\n" +
" *-bsfs*) echo dovi_rpu; exit 0;;\n" +
" *'-f null'*) exit 0;;\n" +
"esac\n" +
"echo \"$*\" >> " + argLog + "\n" +
"exit 0\n"
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
t.Fatalf("write fake ffmpeg: %v", err)
}
return bin, argLog
}
func recordedRemuxArgs(t *testing.T, session *RemuxSession, argLog string) string {
t.Helper()
_, _ = io.ReadAll(session)
_ = session.Close()
recorded, err := os.ReadFile(argLog)
if err != nil {
t.Fatalf("the remux never ran: %v", err)
}
return string(recorded)
}
// The preserve mode, not the caller, decides the sample-entry tagging:
// buildRemuxArgs cannot be handed the flag directly by production code, so the
// mapping from RemuxDVPreserveV3 must be exercised through the mode switch.
func TestPreserveModeTagsTheSampleEntry(t *testing.T) {
for _, profile := range []int{5, 8} {
bin, argLog := writeRecordingFFmpeg(t)
session, err := StartRemuxWithDVMode(t.Context(), remuxSourceFile(t), "mp4", 0, false, -1, profile, RemuxDVPreserveV3, bin)
if err != nil {
t.Fatalf("preserve remux refused profile %d: %v", profile, err)
}
recorded := recordedRemuxArgs(t, session, argLog)
if !strings.Contains(recorded, "-tag:v dvh1") || !strings.Contains(recorded, "-strict unofficial") {
t.Fatalf("preserve mode must map to dvh1 + -strict unofficial for profile %d: %s", profile, recorded)
}
}
}
func TestExplicitStripModeTagsHVC1(t *testing.T) {
for _, profile := range []int{7, 8} {
bin, argLog := writeRecordingFFmpeg(t)
session, err := StartRemuxWithDVMode(t.Context(), remuxSourceFile(t), "mp4", 0, false, -1, profile, RemuxDVStripToHDR10V3, bin)
if err != nil {
t.Fatalf("strip remux refused profile %d: %v", profile, err)
}
recorded := recordedRemuxArgs(t, session, argLog)
if !strings.Contains(recorded, "dovi_rpu=strip=1") || !strings.Contains(recorded, "-tag:v hvc1") {
t.Fatalf("explicit strip must map to dovi_rpu + hvc1 for profile %d: %s", profile, recorded)
}
if strings.Contains(recorded, "dvh1") || strings.Contains(recorded, "-strict unofficial") {
t.Fatalf("stripped output must not carry DV signaling for profile %d: %s", profile, recorded)
}
}
}
func remuxSourceFile(t *testing.T) string {
t.Helper()
path := filepath.Join(t.TempDir(), "movie.mkv")
if err := os.WriteFile(path, []byte("not really a movie"), 0o600); err != nil {
t.Fatalf("write remux fixture: %v", err)
}
return path
}
// The legacy/auto mode derives the strip from the profile alone, with no plan
// to consult — the web player, jellycompat and pre-v3 stream tokens all arrive
// here. An unparseable RPU has to neutralize the profile the same way a missing
// dovi_rpu filter does, or the filter rejects every packet and the response
// never produces a byte.
func TestLegacyRemuxDropsTheStripForAnUnstrippableSource(t *testing.T) {
bin, argLog := writeProbeAwareFFmpeg(t)
path := remuxSourceFile(t)
session, err := StartRemuxWithDVMode(context.Background(), path, "mp4", 0, false, -1, 7, RemuxDVLegacyAutoV3, bin)
if err != nil {
t.Fatalf("legacy remux refused to start: %v", err)
}
// Drain to EOF so the stand-in has finished recording before it is killed.
_, _ = io.ReadAll(session)
session.Close()
recorded, err := os.ReadFile(argLog)
if err != nil {
t.Fatalf("the remux never ran: %v", err)
}
if strings.Contains(string(recorded), DV7ToHDR10BitstreamFilter) {
t.Fatalf("the hanging filter survived into the remux: %s", recorded)
}
}
// The explicit v3 recipe has already promised the client HDR10. Reaching it
// with an unstrippable source means a session or stream token minted before
// the verdict was known, and neither honouring nor silently dropping the strip
// is right: fail so the request gets a definite error instead of a stalled
// stream, and so the next start re-plans onto a route that works.
func TestExplicitStripRecipeRefusesAnUnstrippableSource(t *testing.T) {
bin, _ := writeProbeAwareFFmpeg(t)
path := remuxSourceFile(t)
session, err := StartRemuxWithDVMode(context.Background(), path, "mp4", 0, false, -1, 7, RemuxDVStripToHDR10V3, bin)
if err == nil {
session.Close()
t.Fatal("the explicit HDR10 strip accepted a source it cannot strip")
}
if !strings.Contains(err.Error(), "cannot be stripped") {
t.Fatalf("error = %v, want it to name the unstrippable source", err)
}
}