package mpd import ( "fmt" "net/http" "net/http/httptest" "regexp" "strconv" "strings" "testing" ) // A Smooth Streaming manifest shaped like a live encoder serves: 4s segments at // the 10MHz MSS tick rate. const smoothManifest = ` ` func originServer(t *testing.T) string { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(r.URL.Path, "/Manifest") { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/xml") _, _ = w.Write([]byte(smoothManifest)) })) t.Cleanup(srv.Close) return srv.URL + "/live/test.isml/" } // resetGridCache keeps tests independent — the grid and anchor caches are global // so a live stream learns its encoder phase only once. func resetGridCache(channel string) { gridMu.Lock() delete(gridCache, channel) gridMu.Unlock() anchorMu.Lock() delete(anchors, channel) anchorMu.Unlock() } func TestBuildSyntheticShape(t *testing.T) { resetGridCache("vctest") base := originServer(t) out, err := BuildSynthetic(SyntheticConfig{}, "vctest", base, "df163382-1ddd-fdd5-bec9-822c1ec0f052") if err != nil { t.Fatal(err) } got := string(out) // The default grid is the one the live RTE pipeline was tuned with. for _, want := range []string{ `type="dynamic"`, `minimumUpdatePeriod="PT2S"`, `minBufferTime="PT8S"`, `timeShiftBufferDepth="PT1M"`, `maxSegmentDuration="PT4S"`, `cenc:default_KID="df163382-1ddd-fdd5-bec9-822c1ec0f052"`, `timescale="600"`, `timescale="48000"`, `bandwidth="6000000"`, `bandwidth="128000"`, `codecs="avc1.640028"`, `codecs="mp4a.40.2"`, `width="1920" height="1080"`, `audioSamplingRate="48000"`, `initialization="vctest-video=6000000.dash"`, `media="vctest-video=6000000-$Time$.dash"`, `initialization="vctest-audio_128k=128000.dash"`, `media="vctest-audio_128k=128000-$Time$.dash"`, base + "dash/", } { if !strings.Contains(got, want) { t.Errorf("synthetic MPD missing %q\n---\n%s", want, got) } } } // The published window must be WindowSegments long: r = count-1. func TestBuildSyntheticWindowSize(t *testing.T) { resetGridCache("vcwin") base := originServer(t) out, err := BuildSynthetic(SyntheticConfig{WindowSegments: 6}, "vcwin", base, "df1633821dddfdd5bec9822c1ec0f052") if err != nil { t.Fatal(err) } re := regexp.MustCompile(``) ms := re.FindAllStringSubmatch(string(out), -1) if len(ms) != 2 { t.Fatalf("want 2 SegmentTimelines, got %d", len(ms)) } for i, m := range ms { r, _ := strconv.Atoi(m[3]) if r != 5 { t.Errorf("timeline %d: r=%d, want 5 (6 segments)", i, r) } d, _ := strconv.Atoi(m[2]) // 4s segments: 4*600=2400 video ticks, 4*48000=192000 audio ticks. want := []int{2400, 192000}[i] if d != want { t.Errorf("timeline %d: d=%d, want %d", i, d, want) } } } // Segment times must land on the encoder's grid, i.e. share its phase offset, // or the downloader requests segments the origin does not have. func TestBuildSyntheticAlignsToEncoderPhase(t *testing.T) { resetGridCache("vcphase") base := originServer(t) grid, err := LearnGrid("vcphase", base, SyntheticConfig{}.WithDefaults().GridSpec()) if err != nil { t.Fatal(err) } out, err := BuildSynthetic(SyntheticConfig{}, "vcphase", base, "df1633821dddfdd5bec9822c1ec0f052") if err != nil { t.Fatal(err) } re := regexp.MustCompile(``) ms := re.FindAllStringSubmatch(string(out), -1) vStart, _ := strconv.ParseInt(ms[0][1], 10, 64) if got := ((vStart-grid.VMod)%grid.VDur + grid.VDur) % grid.VDur; got != 0 { t.Errorf("video start %d is off-grid by %d (dur=%d mod=%d)", vStart, got, grid.VDur, grid.VMod) } aStart, _ := strconv.ParseInt(ms[1][1], 10, 64) if got := ((aStart-grid.AMod)%grid.ADur + grid.ADur) % grid.ADur; got != 0 { t.Errorf("audio start %d is off-grid by %d (dur=%d mod=%d)", aStart, got, grid.ADur, grid.AMod) } } // Every provider-shaped literal must be overridable from config. func TestBuildSyntheticHonoursConfig(t *testing.T) { resetGridCache("ch9") base := originServer(t) cfg := SyntheticConfig{ VideoName: "video", AudioName: "audio_128k", VideoBandwidth: 3_000_000, AudioBandwidth: 96_000, VideoCodecs: "hvc1.1.6.L93.B0", AudioCodecs: "mp4a.40.5", Width: 1280, Height: 720, Lang: "ga", SegmentPathSuffix: "cmaf/", VideoInitTemplate: "{channel}-v-init.m4s", VideoMediaTemplate: "{channel}-v-$Time$.m4s", MinUpdatePeriod: "PT4S", } out, err := BuildSynthetic(cfg, "ch9", base, "df1633821dddfdd5bec9822c1ec0f052") if err != nil { t.Fatal(err) } got := string(out) for _, want := range []string{ `bandwidth="3000000"`, `bandwidth="96000"`, `codecs="hvc1.1.6.L93.B0"`, `codecs="mp4a.40.5"`, `width="1280" height="720"`, `lang="ga"`, `minimumUpdatePeriod="PT4S"`, base + "cmaf/", `initialization="ch9-v-init.m4s"`, `media="ch9-v-$Time$.m4s"`, // audio templates were left empty, so they derive from name + bandwidth `initialization="ch9-audio_128k=96000.dash"`, } { if !strings.Contains(got, want) { t.Errorf("missing %q\n---\n%s", want, got) } } if strings.Contains(got, `width="1920"`) || strings.Contains(got, `codecs="avc1`) { t.Error("a default leaked past the config override") } } func TestBuildSyntheticRejectsBadKID(t *testing.T) { resetGridCache("vcbad") base := originServer(t) if _, err := BuildSynthetic(SyntheticConfig{}, "vcbad", base, "not-a-kid"); err == nil { t.Fatal("expected an error for a malformed KID") } } func TestScaleMSSDoesNotOverflow(t *testing.T) { // A real MSS time (~1.7e16); mss*48000 overflows int64 if done naively. const mss = int64(17_000_000_000_000_000) got := ScaleMSS(mss, DefaultMSSTimescale, 48_000) want := int64(17_000_000_000_000_000 / 10_000_000 * 48_000) if got != want { t.Errorf("ScaleMSS = %d, want %d", got, want) } if got < 0 { t.Error("ScaleMSS overflowed to a negative value") } } func TestParseMSSStreamTimesExpandsRepeats(t *testing.T) { times := ParseMSSStreamTimes(smoothManifest, "video") if len(times) != 4 { t.Fatalf("got %d segment times, want 4 (r=3)", len(times)) } for i, ts := range times { want := int64(17_000_000_000_000) + int64(i)*40_000_000 if ts != want { t.Errorf("time[%d] = %d, want %d", i, ts, want) } } if ParseMSSStreamTimes(smoothManifest, "nope") != nil { t.Error("unknown stream name should yield no times") } } func TestKIDToUUIDAndHex(t *testing.T) { u, err := KIDToUUID("DF1633821DDDFDD5BEC9822C1EC0F052") if err != nil { t.Fatal(err) } if u != "df163382-1ddd-fdd5-bec9-822c1ec0f052" { t.Errorf("KIDToUUID = %q", u) } if KIDHex(u) != "df1633821dddfdd5bec9822c1ec0f052" { t.Errorf("KIDHex = %q", KIDHex(u)) } if _, err := KIDToUUID("abc"); err == nil { t.Error("short KID should error") } } func TestExtractDefaultKID(t *testing.T) { xml := fmt.Sprintf(``, "DF163382-1DDD-FDD5-BEC9-822C1EC0F052") if got := ExtractDefaultKID(xml); got != "df163382-1ddd-fdd5-bec9-822c1ec0f052" { t.Errorf("ExtractDefaultKID = %q", got) } if ExtractDefaultKID(``) != "" { t.Error("no KID should yield empty string") } } // An unregistered or "none" rewriter must report that no rewrite is needed. func TestRewriterRegistry(t *testing.T) { if _, needed := Lookup("none"); needed { t.Error(`"none" must not need a rewrite`) } if _, needed := Lookup(""); needed { t.Error("empty name must not need a rewrite") } if _, needed := Lookup("nosuchprovider"); needed { t.Error("unknown name must not need a rewrite") } Register("testrw", func() Rewriter { return Passthrough{} }) // Registering a Passthrough still means "no rewrite needed". if _, needed := Lookup("testrw"); needed { t.Error("a passthrough rewriter must not report needing a rewrite") } }