Null-DRM-Official/apps/pkg/mpd/synthetic_test.go
404errordeveloper 2fa8f2435f Initial commit: Null DRM Official
Capture, decrypt, and restream toolkit with compiled-in app modules
(RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www.
BBC module.yaml is published (clear streams); other module values stay local.
2026-10-06 00:25:35 +02:00

264 lines
8.5 KiB
Go

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 = `<?xml version="1.0" encoding="utf-8"?>
<SmoothStreamingMedia MajorVersion="2" MinorVersion="0" TimeScale="10000000" IsLive="TRUE">
<StreamIndex Type="video" Name="video" Chunks="0" QualityLevels="1" Url="QualityLevels({bitrate})/Fragments(video={start time})">
<QualityLevel Index="0" Bitrate="6000000" FourCC="H264" MaxWidth="1920" MaxHeight="1080"/>
<c t="17000000000000" d="40000000" r="3"/>
</StreamIndex>
<StreamIndex Type="audio" Name="audio_128k" Chunks="0" QualityLevels="1" Url="QualityLevels({bitrate})/Fragments(audio_128k={start time})">
<QualityLevel Index="0" Bitrate="128000" FourCC="AACL" SamplingRate="48000"/>
<c t="17000000000000" d="40000000" r="3"/>
</StreamIndex>
</SmoothStreamingMedia>`
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(`<S t="(\d+)" d="(\d+)" r="(\d+)"/>`)
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(`<S t="(\d+)" d="(\d+)" r="\d+"/>`)
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(`<ContentProtection cenc:default_KID="%s"/>`, "DF163382-1DDD-FDD5-BEC9-822C1EC0F052")
if got := ExtractDefaultKID(xml); got != "df163382-1ddd-fdd5-bec9-822c1ec0f052" {
t.Errorf("ExtractDefaultKID = %q", got)
}
if ExtractDefaultKID(`<MPD/>`) != "" {
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")
}
}