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.
130 lines
3.8 KiB
Go
130 lines
3.8 KiB
Go
//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
|
|
}
|