package engine import ( "encoding/json" "strings" "testing" ) // TestRegistryHasBuiltins pins the engines a client can name out of the box. func TestRegistryHasBuiltins(t *testing.T) { for _, id := range []string{HTML5ID, "mpv", "vlc", "mdk"} { if _, ok := Get(id); !ok { t.Errorf("engine %q is not registered", id) } } if _, ok := Get("nope"); ok { t.Error("Get returned an unknown engine") } // Get is case/space tolerant so a client's casing never matters. if _, ok := Get(" HTML5 "); !ok { t.Error("Get should trim and lower-case the id") } } // TestHTML5BaselineIsHonest guards the measured facts: the browser baseline must // NOT claim HEVC or mpeg4, which Chrome cannot decode, and must not claim a // container it does not handle. func TestHTML5BaselineIsHonest(t *testing.T) { html5, _ := Get(HTML5ID) if contains(html5.VCodecs, "hevc") || contains(html5.VCodecs, "h265") { t.Error("html5 baseline claims HEVC, which browsers cannot decode") } if contains(html5.VCodecs, "mpeg4") { t.Error("html5 baseline claims mpeg4, which browsers cannot decode") } if !contains(html5.VCodecs, "h264") || !contains(html5.VCodecs, "vp9") || !contains(html5.VCodecs, "av1") { t.Errorf("html5 baseline is missing a measured-playable codec: %v", html5.VCodecs) } if !contains(html5.Containers, "mkv") { t.Error("html5 baseline must accept mkv (h264-in-mkv plays)") } if contains(html5.Containers, "avi") { t.Error("html5 baseline must not claim avi") } } // TestEngineJSONMatchesContract pins the frozen wire shape (web/src/lib/api.ts). func TestEngineJSONMatchesContract(t *testing.T) { e, _ := Get(HTML5ID) b, err := json.Marshal(e) if err != nil { t.Fatal(err) } var raw map[string]json.RawMessage if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } for _, key := range []string{"id", "name", "kind", "containers", "vcodecs", "acodecs"} { if _, ok := raw[key]; !ok { t.Errorf("engine JSON is missing %q", key) } } if len(raw) != 6 { t.Errorf("engine JSON has %d keys, want exactly 6: %s", len(raw), b) } } func TestResolve(t *testing.T) { tests := []struct { name string facts []string engines []string playable bool engine string // expected engine id, or "" for nil reason string msgSubstr string // must appear in Message when not playable // canTranscode is whether the server itself can re-encode (ADR-008). canTranscode bool }{ { name: "mp4 h264 aac plays in html5", facts: Facts("mp4", "h264", "aac"), engines: []string{"html5"}, playable: true, engine: "html5", }, { name: "mkv h264 plays despite the container", facts: Facts("mkv", "h264", "aac"), engines: []string{"html5"}, playable: true, engine: "html5", }, { name: "mp4 hevc aac is not playable", facts: Facts("mp4", "hevc", "aac"), engines: []string{"html5"}, playable: false, reason: "vcodec:hevc", msgSubstr: "HEVC", }, { name: "avi mpeg4 is a container blocker", facts: Facts("avi", "mpeg4", "mp3"), engines: []string{"html5"}, playable: false, reason: "container:avi", msgSubstr: "AVI", }, { name: "unsupported audio codec", facts: Facts("mp4", "h264", "dts"), engines: []string{"html5"}, playable: false, reason: "acodec:dts", msgSubstr: "DTS", }, { name: "audio-only mp3 is playable", facts: Facts("mp3", "", "mp3"), engines: []string{"html5"}, playable: true, engine: "html5", }, { name: "audio-only flac is playable", facts: Facts("flac", "", "flac"), engines: []string{"html5"}, playable: true, engine: "html5", }, { name: "audio-only dts is not playable", facts: Facts("m4a", "", "dts"), engines: []string{"html5"}, playable: false, reason: "acodec:dts", msgSubstr: "DTS", }, { name: "empty engine list falls back to html5 baseline", facts: Facts("mp4", "h264", "aac"), engines: nil, playable: true, engine: "html5", }, { name: "empty engine list still rejects hevc", facts: Facts("mp4", "hevc", "aac"), engines: nil, playable: false, reason: "vcodec:hevc", msgSubstr: "mpv", }, { name: "native engine changes the answer for hevc", facts: Facts("mp4", "hevc", "aac"), engines: []string{"html5", "mpv"}, playable: true, engine: "mpv", }, { name: "native engine handles avi", facts: Facts("avi", "mpeg4", "mp3"), engines: []string{"html5", "vlc"}, playable: true, engine: "vlc", }, { name: "client ordering wins among capable engines", facts: Facts("mkv", "h264", "aac"), engines: []string{"mpv", "html5"}, playable: true, engine: "mpv", }, { name: "capable engine beats an earlier incapable one", facts: Facts("mp4", "hevc", "aac"), engines: []string{"html5", "vlc", "mpv"}, playable: true, engine: "vlc", }, { name: "unknown engines are skipped, reason uses fallback", facts: Facts("mp4", "hevc", "aac"), engines: []string{"bogus"}, playable: false, reason: "vcodec:hevc", msgSubstr: "mpv", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Resolve(tt.facts, tt.engines, tt.canTranscode) if got.Playable != tt.playable { t.Fatalf("playable = %v, want %v (reason %q)", got.Playable, tt.playable, got.Reason) } if tt.playable { if got.Engine == nil || *got.Engine != tt.engine { t.Errorf("engine = %v, want %q", got.Engine, tt.engine) } if got.Reason != "" || got.Message != "" { t.Errorf("playable result carried reason=%q message=%q", got.Reason, got.Message) } return } if got.Engine != nil { t.Errorf("engine = %q, want null", *got.Engine) } if got.Reason != tt.reason { t.Errorf("reason = %q, want %q", got.Reason, tt.reason) } if tt.msgSubstr != "" && !strings.Contains(got.Message, tt.msgSubstr) { t.Errorf("message = %q, want it to mention %q", got.Message, tt.msgSubstr) } if strings.Contains(strings.ToLower(got.Message), "you ") { t.Errorf("message blames the user: %q", got.Message) } }) } } // TestResolutionJSONEngineNull pins that a non-playable answer serialises // engine as JSON null, exactly as PlaybackResolution declares. func TestResolutionJSONEngineNull(t *testing.T) { got := Resolve(Facts("mp4", "hevc", "aac"), []string{"html5"}, false) b, err := json.Marshal(got) if err != nil { t.Fatal(err) } var raw struct { Playable bool `json:"playable"` Engine json.RawMessage `json:"engine"` Reason string `json:"reason"` Message string `json:"message"` } if err := json.Unmarshal(b, &raw); err != nil { t.Fatal(err) } if string(raw.Engine) != "null" { t.Errorf("engine = %s, want null", raw.Engine) } if raw.Playable || raw.Reason != "vcodec:hevc" || raw.Message == "" { t.Errorf("unexpected resolution JSON: %s", b) } } // A file nothing can direct-play is still playable when the SERVER can re-encode // (ADR-008). The ordering matters more than the feature: transcoding is the last // resort, so a file that plays directly must never pay for it. func TestResolveTranscodeIsTheLastResort(t *testing.T) { hevc := Facts("mp4", "hevc", "aac") // Nothing the client declared can decode it, so the server re-encodes. got := Resolve(hevc, []string{"html5"}, true) if !got.Playable || got.Engine == nil || *got.Engine != TranscodeID { t.Errorf("Resolve(hevc, html5, canTranscode) = %+v, want playable via %q", got, TranscodeID) } // A file the client CAN decode gets direct play, never a transcode. This is // the assertion that keeps a working file from being re-encoded for nothing. got = Resolve(Facts("mp4", "h264", "aac"), []string{"html5"}, true) if !got.Playable || got.Engine == nil || *got.Engine == TranscodeID { t.Errorf("a direct-playable file chose %v; direct play must always win", got.Engine) } // With no capability the answer is exactly what it was before ADR-008: the // blocker, with a machine-readable reason for the UI. got = Resolve(hevc, []string{"html5"}, false) if got.Playable { t.Errorf("Resolve(hevc, html5, no transcode) = playable; want the blocker") } if got.Reason == "" || got.Message == "" { t.Errorf("want a reason and a message when nothing can play it, got %+v", got) } }