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.
This commit is contained in:
commit
2fa8f2435f
121 changed files with 17802 additions and 0 deletions
130
apps/modules/rte/live_test.go
Normal file
130
apps/modules/rte/live_test.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
//go:build live
|
||||
|
||||
// Live tests hit the real origin and need this module's local module.yaml.
|
||||
// They are excluded from the normal test run; enable with:
|
||||
//
|
||||
// go -C apps/modules test -tags live ./rte/ -v
|
||||
package rte
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"drmdecryption/mpd"
|
||||
)
|
||||
|
||||
// The rewriter must publish a usable manifest from the KID alone — this is the
|
||||
// path taken when the ad-stitched upstream session has expired (HTTP 410) and all
|
||||
// that is left is the key from an earlier capture.
|
||||
func TestLiveRewriteFromKIDOnly(t *testing.T) {
|
||||
cfg, res, err := loadConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.Loaded {
|
||||
t.Skipf("no local values at %s", res.Path)
|
||||
}
|
||||
if len(cfg.KIDs) == 0 {
|
||||
t.Skip("no kids configured")
|
||||
}
|
||||
|
||||
for channel, kid := range cfg.KIDs {
|
||||
t.Run(channel, func(t *testing.T) {
|
||||
resetCaches(channel)
|
||||
r := NewRewriterWith(cfg)
|
||||
|
||||
// Empty upstream = expired session; only the KID hint is available.
|
||||
out, err := r.Rewrite(nil, kid)
|
||||
if err != nil {
|
||||
t.Fatalf("rewrite from KID: %v", err)
|
||||
}
|
||||
got := string(out)
|
||||
|
||||
origin := cfg.Origins[channel]
|
||||
if origin == "" {
|
||||
t.Fatalf("no origin configured for %s", channel)
|
||||
}
|
||||
for _, want := range []string{
|
||||
`type="dynamic"`,
|
||||
origin + cfg.Synthetic.SegmentPathSuffix,
|
||||
channel + "-" + cfg.Synthetic.VideoName,
|
||||
channel + "-" + cfg.Synthetic.AudioName,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("manifest missing %q", want)
|
||||
}
|
||||
}
|
||||
u, _ := mpd.KIDToUUID(kid)
|
||||
if !strings.Contains(got, `cenc:default_KID="`+u+`"`) {
|
||||
t.Errorf("manifest does not carry KID %s", u)
|
||||
}
|
||||
|
||||
// The timeline must be non-empty and on the encoder grid, or the
|
||||
// downloader asks the origin for segments that do not exist.
|
||||
re := regexp.MustCompile(`<S t="(\d+)" d="(\d+)" r="(\d+)"/>`)
|
||||
ms := re.FindAllStringSubmatch(got, -1)
|
||||
if len(ms) != 2 {
|
||||
t.Fatalf("want 2 timelines, got %d", len(ms))
|
||||
}
|
||||
grid, err := mpd.LearnGrid(channel, origin, cfg.Synthetic.GridSpec())
|
||||
if err != nil {
|
||||
t.Fatalf("learn grid: %v", err)
|
||||
}
|
||||
for i, m := range ms {
|
||||
start, _ := strconv.ParseInt(m[1], 10, 64)
|
||||
dur, _ := strconv.ParseInt(m[2], 10, 64)
|
||||
rep, _ := strconv.Atoi(m[3])
|
||||
wantDur, mod := grid.VDur, grid.VMod
|
||||
if i == 1 {
|
||||
wantDur, mod = grid.ADur, grid.AMod
|
||||
}
|
||||
if dur != wantDur {
|
||||
t.Errorf("timeline %d: d=%d, want %d (from the live origin)", i, dur, wantDur)
|
||||
}
|
||||
if int64(rep+1) != cfg.Synthetic.WindowSegments {
|
||||
t.Errorf("timeline %d: %d segments, want %d", i, rep+1, cfg.Synthetic.WindowSegments)
|
||||
}
|
||||
if off := ((start-mod)%wantDur + wantDur) % wantDur; off != 0 {
|
||||
t.Errorf("timeline %d: start %d is off the encoder grid by %d", i, start, off)
|
||||
}
|
||||
if start <= 0 {
|
||||
t.Errorf("timeline %d: non-positive start %d", i, start)
|
||||
}
|
||||
}
|
||||
t.Logf("%s: %d bytes, vdur=%d adur=%d", channel, len(out), grid.VDur, grid.ADur)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The KID→channel→origin lookup is what makes the expired-session path work.
|
||||
func TestLiveKIDResolvesToConfiguredOrigin(t *testing.T) {
|
||||
cfg, res, err := loadConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !res.Loaded {
|
||||
t.Skipf("no local values at %s", res.Path)
|
||||
}
|
||||
r := NewRewriterWith(cfg)
|
||||
for channel, kid := range cfg.KIDs {
|
||||
gotCh, gotBase, ok := r.ResolveKID(mpd.KIDHex(kid))
|
||||
if !ok {
|
||||
t.Errorf("%s: KID %s did not resolve", channel, kid)
|
||||
continue
|
||||
}
|
||||
if gotCh != channel {
|
||||
t.Errorf("KID %s resolved to %q, want %q", kid, gotCh, channel)
|
||||
}
|
||||
if gotBase != cfg.Origins[channel] {
|
||||
t.Errorf("%s: origin %q, want %q", channel, gotBase, cfg.Origins[channel])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resetCaches(channel string) {
|
||||
// The grid/anchor caches live in pkg/mpd and are keyed by channel; a fresh
|
||||
// process per test run is enough, so nothing to do here beyond documenting it.
|
||||
_ = channel
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue