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:
404errordeveloper 2026-10-06 00:25:35 +02:00
commit 2fa8f2435f
121 changed files with 17802 additions and 0 deletions

50
apps/pkg/mpd/kid.go Normal file
View file

@ -0,0 +1,50 @@
package mpd
import (
"encoding/xml"
"fmt"
"regexp"
"strings"
)
var reDefaultKID = regexp.MustCompile(`(?i)default_KID="([^"]+)"`)
// KIDToUUID normalizes any hex-ish KID spelling to dashed UUID form.
func KIDToUUID(kid string) (string, error) {
var b strings.Builder
for _, r := range kid {
if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') {
b.WriteRune(r)
}
}
h := strings.ToLower(b.String())
if len(h) != 32 {
return "", fmt.Errorf("invalid KID %q", kid)
}
return fmt.Sprintf("%s-%s-%s-%s-%s", h[0:8], h[8:12], h[12:16], h[16:20], h[20:32]), nil
}
// KIDHex strips dashes and lowercases a KID (the form used as a map key).
func KIDHex(kid string) string {
return strings.ToLower(strings.ReplaceAll(kid, "-", ""))
}
// ExtractDefaultKID pulls cenc:default_KID out of an MPD, as dashed UUID.
func ExtractDefaultKID(xmlText string) string {
m := reDefaultKID.FindStringSubmatch(xmlText)
if len(m) < 2 {
return ""
}
u, err := KIDToUUID(m[1])
if err != nil {
return ""
}
return u
}
// XMLEscape escapes text for inclusion in an XML element body.
func XMLEscape(s string) string {
var b strings.Builder
_ = xml.EscapeText(&b, []byte(s))
return b.String()
}

15
apps/pkg/mpd/mpd.go Normal file
View file

@ -0,0 +1,15 @@
package mpd
// Rewriter transforms an upstream live MPD into something N_m3u8DL-RE can
// refresh safely. Apps that need DAI/colon stripping or synthetic timelines
// implement this; others return Passthrough.
type Rewriter interface {
Name() string
Rewrite(upstreamXML []byte, kidHint string) (out []byte, err error)
}
// Passthrough leaves the MPD unchanged.
type Passthrough struct{}
func (Passthrough) Name() string { return "none" }
func (Passthrough) Rewrite(in []byte, _ string) ([]byte, error) { return in, nil }

59
apps/pkg/mpd/registry.go Normal file
View file

@ -0,0 +1,59 @@
package mpd
import (
"sort"
"strings"
"sync"
)
var (
regMu sync.RWMutex
registry = map[string]func() Rewriter{}
)
func init() {
Register("none", func() Rewriter { return Passthrough{} })
}
// Register makes a rewriter resolvable by name, so consumers (streamd) can pick
// one from stored config without importing the provider that implements it. A
// factory is registered rather than an instance because a rewriter may carry
// per-stream fallback state.
func Register(name string, newRewriter func() Rewriter) {
regMu.Lock()
defer regMu.Unlock()
registry[strings.ToLower(name)] = newRewriter
}
// Lookup builds a fresh rewriter for a name. An empty name, "none" or an unknown
// name yields Passthrough with ok=false, so callers can tell "no rewrite needed"
// from "rewrite with X".
func Lookup(name string) (Rewriter, bool) {
key := strings.ToLower(strings.TrimSpace(name))
if key == "" || key == "none" {
return Passthrough{}, false
}
regMu.RLock()
newRewriter, ok := registry[key]
regMu.RUnlock()
if !ok {
return Passthrough{}, false
}
rw := newRewriter()
if _, isPass := rw.(Passthrough); isPass {
return rw, false
}
return rw, true
}
// Names lists registered rewriter names.
func Names() []string {
regMu.RLock()
defer regMu.RUnlock()
out := make([]string, 0, len(registry))
for k := range registry {
out = append(out, k)
}
sort.Strings(out)
return out
}

104
apps/pkg/mpd/server.go Normal file
View file

@ -0,0 +1,104 @@
package mpd
import (
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
// KIDResolver is implemented by rewriters that can recover the live channel from
// a KID alone. The local server uses it so a rewrite still works after the
// upstream (ad-stitched) manifest session has expired and returns 410.
type KIDResolver interface {
ResolveKID(kidHex string) (channel, originBase string, ok bool)
}
// Primer is implemented by rewriters that want to be told the fallback channel
// and origin discovered on earlier requests.
type Primer interface {
Prime(channel, originBase string)
}
// LocalServer serves a rewritten live MPD on 127.0.0.1 for a downloader to
// refresh. Every GET re-fetches upstream and re-runs the rewriter.
type LocalServer struct {
URL string
Upstream string
Key string // KID:KEY
Headers map[string]string
rw Rewriter
kidHint string
ln net.Listener
httpServer *http.Server
}
// StartLocal starts the rewrite proxy. rw must not be nil; pass Passthrough{}
// to serve upstream unchanged.
func StartLocal(upstream, key string, headers map[string]string, rw Rewriter) (*LocalServer, error) {
if rw == nil {
rw = Passthrough{}
}
s := &LocalServer{Upstream: upstream, Key: key, Headers: headers, rw: rw}
if key != "" && strings.Contains(key, ":") {
s.kidHint = strings.SplitN(key, ":", 2)[0]
if r, ok := rw.(KIDResolver); ok {
if ch, base, found := r.ResolveKID(KIDHex(s.kidHint)); found {
if p, ok := rw.(Primer); ok {
p.Prime(ch, base)
}
}
}
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
s.ln = ln
mux := http.NewServeMux()
mux.HandleFunc("/", s.handle)
s.httpServer = &http.Server{Handler: mux}
s.URL = fmt.Sprintf("http://%s/manifest.mpd", ln.Addr().String())
go func() { _ = s.httpServer.Serve(ln) }()
return s, nil
}
func (s *LocalServer) Close() {
if s.httpServer != nil {
_ = s.httpServer.Close()
}
if s.ln != nil {
_ = s.ln.Close()
}
}
func (s *LocalServer) handle(w http.ResponseWriter, r *http.Request) {
body := ""
req, err := http.NewRequest(http.MethodGet, s.Upstream, nil)
if err == nil {
for k, v := range s.Headers {
req.Header.Set(k, v)
}
client := &http.Client{Timeout: 20 * time.Second}
if resp, err := client.Do(req); err == nil {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
_ = resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 && len(b) > 0 {
body = string(b)
}
}
}
// Upstream being gone is not fatal: a rewriter with a KID fallback can still
// publish the encoder timeline so the downloader keeps pulling segments.
payload, err := s.rw.Rewrite([]byte(body), s.kidHint)
if err != nil {
http.Error(w, err.Error(), 502)
return
}
w.Header().Set("Content-Type", "application/dash+xml")
w.WriteHeader(200)
_, _ = w.Write(payload)
}

142
apps/pkg/mpd/smooth.go Normal file
View file

@ -0,0 +1,142 @@
package mpd
import (
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
)
// DefaultMSSTimescale is the Smooth Streaming tick rate (100ns units).
const DefaultMSSTimescale = 10_000_000
var (
reSegDur = regexp.MustCompile(`\bd="(\d+)"`)
reSegT = regexp.MustCompile(`\bt="(\d+)"`)
reSegR = regexp.MustCompile(`\br="(\d+)"`)
reSegEntry = regexp.MustCompile(`<c\s+([^/]*)/>`)
)
// ScaleMSS converts a Smooth Streaming tick to another timescale without
// overflowing int64 (MSS times are ~1e16; mss*48000 does not fit).
func ScaleMSS(mss, fromScale, toScale int64) int64 {
if fromScale <= 0 {
fromScale = DefaultMSSTimescale
}
return mss/fromScale*toScale + (mss%fromScale)*toScale/fromScale
}
// Grid is the segment duration + phase of one encoder channel, per media type.
type Grid struct {
VDur, ADur int64
VMod, AMod int64
}
// GridSpec describes how to learn a Grid from an origin's Smooth manifest.
type GridSpec struct {
// ManifestPath is appended to the origin base (e.g. "Manifest").
ManifestPath string
// VideoName / AudioName are StreamIndex Name (or Type) attributes.
VideoName, AudioName string
// Timescales the DASH output uses.
VideoTimescale, AudioTimescale int64
MSSTimescale int64
// FallbackSegmentDuration in MSS ticks when the manifest has no d="".
FallbackSegmentDuration int64
}
var (
gridMu sync.Mutex
gridCache = map[string]Grid{}
)
// LearnGrid fetches the origin Smooth manifest once per channel and derives the
// segment duration and phase offset for video and audio.
func LearnGrid(channel, originBase string, spec GridSpec) (Grid, error) {
gridMu.Lock()
if g, ok := gridCache[channel]; ok {
gridMu.Unlock()
return g, nil
}
gridMu.Unlock()
if !strings.HasSuffix(originBase, "/") {
originBase += "/"
}
url := originBase + spec.ManifestPath
resp, err := http.Get(url)
if err != nil {
return Grid{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return Grid{}, err
}
text := string(body)
video := ParseMSSStreamTimes(text, spec.VideoName)
audio := ParseMSSStreamTimes(text, spec.AudioName)
mssDur := spec.FallbackSegmentDuration
if m := reSegDur.FindStringSubmatch(text); len(m) == 2 {
if v, e := strconv.ParseInt(m[1], 10, 64); e == nil {
mssDur = v
}
}
if len(video) == 0 || len(audio) == 0 {
return Grid{}, fmt.Errorf("encoder Manifest missing streams at %s", url)
}
ms := spec.MSSTimescale
vDur := ScaleMSS(mssDur, ms, spec.VideoTimescale)
aDur := ScaleMSS(mssDur, ms, spec.AudioTimescale)
g := Grid{
VDur: vDur,
ADur: aDur,
VMod: ScaleMSS(video[len(video)-1], ms, spec.VideoTimescale) % vDur,
AMod: ScaleMSS(audio[len(audio)-1], ms, spec.AudioTimescale) % aDur,
}
gridMu.Lock()
gridCache[channel] = g
gridMu.Unlock()
return g, nil
}
// ParseMSSStreamTimes expands the <c> timeline of one StreamIndex into
// absolute MSS tick times. streamName matches Name="" or Type="".
func ParseMSSStreamTimes(manifestXML, streamName string) []int64 {
reBlock := regexp.MustCompile(`(?is)<StreamIndex\b[^>]*\bName="` + regexp.QuoteMeta(streamName) + `"[^>]*>(.*?)</StreamIndex>`)
m := reBlock.FindStringSubmatch(manifestXML)
if m == nil {
reBlock = regexp.MustCompile(`(?is)<StreamIndex\b[^>]*\bType="` + regexp.QuoteMeta(streamName) + `"[^>]*>(.*?)</StreamIndex>`)
m = reBlock.FindStringSubmatch(manifestXML)
}
if m == nil {
return nil
}
var times []int64
tCur := int64(-1)
for _, c := range reSegEntry.FindAllStringSubmatch(m[1], -1) {
attrs := c[1]
tm := reSegT.FindStringSubmatch(attrs)
dm := reSegDur.FindStringSubmatch(attrs)
rm := reSegR.FindStringSubmatch(attrs)
if tm != nil {
tCur, _ = strconv.ParseInt(tm[1], 10, 64)
}
if tCur < 0 || dm == nil {
continue
}
d, _ := strconv.ParseInt(dm[1], 10, 64)
r := int64(0)
if rm != nil {
r, _ = strconv.ParseInt(rm[1], 10, 64)
}
for i := int64(0); i <= r; i++ {
times = append(times, tCur)
tCur += d
}
}
return times
}

280
apps/pkg/mpd/synthetic.go Normal file
View file

@ -0,0 +1,280 @@
package mpd
import (
"fmt"
"strings"
"sync"
"time"
)
// SyntheticConfig describes a live encoder's DASH grid well enough to publish a
// synthetic manifest for it. Every value a provider could differ on lives here;
// nothing in this package names a provider.
type SyntheticConfig struct {
// Timescales.
MSSTimescale int64 `yaml:"mss_timescale"`
VideoTimescale int64 `yaml:"video_timescale"`
AudioTimescale int64 `yaml:"audio_timescale"`
// Smooth Streaming stream names used to learn the segment grid.
ManifestPath string `yaml:"manifest_path"`
VideoName string `yaml:"video_name"`
AudioName string `yaml:"audio_name"`
// Segment path appended to the origin base for DASH segments.
SegmentPathSuffix string `yaml:"segment_path_suffix"`
// Representation attributes.
VideoBandwidth int `yaml:"video_bandwidth"`
AudioBandwidth int `yaml:"audio_bandwidth"`
VideoCodecs string `yaml:"video_codecs"`
AudioCodecs string `yaml:"audio_codecs"`
Width int `yaml:"width"`
Height int `yaml:"height"`
AudioSamplingRate int `yaml:"audio_sampling_rate"`
AudioChannels int `yaml:"audio_channels"`
Lang string `yaml:"lang"`
// Segment naming. "{channel}" is substituted; empty means derive as
// <channel>-<stream name>=<bandwidth>[-$Time$].<ext>.
VideoInitTemplate string `yaml:"video_init_template"`
VideoMediaTemplate string `yaml:"video_media_template"`
AudioInitTemplate string `yaml:"audio_init_template"`
AudioMediaTemplate string `yaml:"audio_media_template"`
SegmentExt string `yaml:"segment_ext"`
// Timeline shape.
LiveEdgeOffset float64 `yaml:"live_edge_offset_s"`
WindowSegments int64 `yaml:"window_segments"`
FallbackSegmentDuration int64 `yaml:"fallback_segment_duration"`
// MPD-level durations, as ISO-8601 strings.
MinUpdatePeriod string `yaml:"min_update_period"`
MinBufferTime string `yaml:"min_buffer_time"`
TimeShiftBufferDepth string `yaml:"time_shift_buffer_depth"`
MaxSegmentDuration string `yaml:"max_segment_duration"`
}
// WithDefaults fills anything left zero with the common Smooth-to-DASH values.
// These are container mechanics, not provider identity, so defaulting is safe.
func (c SyntheticConfig) WithDefaults() SyntheticConfig {
if c.MSSTimescale == 0 {
c.MSSTimescale = DefaultMSSTimescale
}
if c.VideoTimescale == 0 {
c.VideoTimescale = 600
}
if c.AudioTimescale == 0 {
c.AudioTimescale = 48_000
}
if c.ManifestPath == "" {
c.ManifestPath = "Manifest"
}
if c.VideoName == "" {
c.VideoName = "video"
}
if c.AudioName == "" {
c.AudioName = "audio_128k"
}
if c.SegmentPathSuffix == "" {
c.SegmentPathSuffix = "dash/"
}
if c.VideoBandwidth == 0 {
c.VideoBandwidth = 6_000_000
}
if c.AudioBandwidth == 0 {
c.AudioBandwidth = 128_000
}
if c.VideoCodecs == "" {
c.VideoCodecs = "avc1.640028"
}
if c.AudioCodecs == "" {
c.AudioCodecs = "mp4a.40.2"
}
if c.Width == 0 {
c.Width = 1920
}
if c.Height == 0 {
c.Height = 1080
}
if c.AudioSamplingRate == 0 {
c.AudioSamplingRate = 48_000
}
if c.AudioChannels == 0 {
c.AudioChannels = 2
}
if c.Lang == "" {
c.Lang = "en"
}
if c.SegmentExt == "" {
c.SegmentExt = "dash"
}
if c.LiveEdgeOffset == 0 {
c.LiveEdgeOffset = 12
}
if c.WindowSegments == 0 {
c.WindowSegments = 6
}
if c.FallbackSegmentDuration == 0 {
c.FallbackSegmentDuration = 38_400_000
}
if c.MinUpdatePeriod == "" {
c.MinUpdatePeriod = "PT2S"
}
if c.MinBufferTime == "" {
c.MinBufferTime = "PT8S"
}
if c.TimeShiftBufferDepth == "" {
c.TimeShiftBufferDepth = "PT1M"
}
if c.MaxSegmentDuration == "" {
c.MaxSegmentDuration = "PT4S"
}
return c
}
// GridSpec is the LearnGrid view of this config, exported so a provider module can
// learn the same encoder grid the builder uses.
func (c SyntheticConfig) GridSpec() GridSpec {
return GridSpec{
ManifestPath: c.ManifestPath,
VideoName: c.VideoName,
AudioName: c.AudioName,
VideoTimescale: c.VideoTimescale,
AudioTimescale: c.AudioTimescale,
MSSTimescale: c.MSSTimescale,
FallbackSegmentDuration: c.FallbackSegmentDuration,
}
}
func (c SyntheticConfig) segName(tmpl, channel, stream string, bandwidth int, withTime bool) string {
if tmpl != "" {
return strings.ReplaceAll(tmpl, "{channel}", channel)
}
if withTime {
return fmt.Sprintf("%s-%s=%d-$Time$.%s", channel, stream, bandwidth, c.SegmentExt)
}
return fmt.Sprintf("%s-%s=%d.%s", channel, stream, bandwidth, c.SegmentExt)
}
type anchor struct {
V, A int64
}
var (
anchorMu sync.Mutex
anchors = map[string]*anchor{}
)
const syntheticTemplate = `<?xml version="1.0" encoding="utf-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:cenc="urn:mpeg:cenc:2013"
profiles="urn:mpeg:dash:profile:isoff-live:2011" type="dynamic"
availabilityStartTime="1970-01-01T00:00:00Z" publishTime="%s"
minimumUpdatePeriod="%s" minBufferTime="%s" timeShiftBufferDepth="%s"
maxSegmentDuration="%s">
<BaseURL>%s</BaseURL>
<Period id="1" start="PT0S">
<AdaptationSet id="1" contentType="video" mimeType="video/mp4" lang="%s">
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="%s"/>
<Representation id="r0" bandwidth="%d" codecs="%s" width="%d" height="%d">
<SegmentTemplate timescale="%d" initialization="%s" media="%s" startNumber="1">
<SegmentTimeline><S t="%d" d="%d" r="%d"/></SegmentTimeline>
</SegmentTemplate>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="audio" mimeType="audio/mp4" lang="%s">
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="%s"/>
<Representation id="r1" bandwidth="%d" codecs="%s" audioSamplingRate="%d">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="%d"/>
<SegmentTemplate timescale="%d" initialization="%s" media="%s" startNumber="1">
<SegmentTimeline><S t="%d" d="%d" r="%d"/></SegmentTimeline>
</SegmentTemplate>
</Representation>
</AdaptationSet>
</Period>
</MPD>
`
// BuildSynthetic publishes a dynamic MPD on the encoder's own segment grid, so a
// downloader keeps fetching live segments even once the upstream (ad-stitched)
// manifest session has expired.
func BuildSynthetic(cfg SyntheticConfig, channel, originBase, kid string) ([]byte, error) {
cfg = cfg.WithDefaults()
if !strings.HasSuffix(originBase, "/") {
originBase += "/"
}
segBase := originBase + cfg.SegmentPathSuffix
kidU, err := KIDToUUID(kid)
if err != nil {
return nil, err
}
grid, err := LearnGrid(channel, originBase, cfg.GridSpec())
if err != nil {
return nil, err
}
// Float seconds so sub-second phase stays close to the encoder.
now := float64(time.Now().UTC().UnixNano())/1e9 - cfg.LiveEdgeOffset
align := func(ts, dur, mod int64) int64 {
raw := int64(now * float64(ts))
return raw - (raw-mod)%dur
}
vt := align(cfg.VideoTimescale, grid.VDur, grid.VMod)
at := align(cfg.AudioTimescale, grid.ADur, grid.AMod)
maxCount := cfg.WindowSegments
anchorMu.Lock()
a := anchors[channel]
if a == nil {
a = &anchor{
V: vt - (maxCount-1)*grid.VDur,
A: at - (maxCount-1)*grid.ADur,
}
anchors[channel] = a
}
vStart, aStart := a.V, a.A
if vt > vStart+(maxCount-1)*grid.VDur {
vStart = vt - (maxCount-1)*grid.VDur
aStart = at - (maxCount-1)*grid.ADur
a.V, a.A = vStart, aStart
} else if vt < vStart {
vStart = vt - (maxCount-1)*grid.VDur
aStart = at - (maxCount-1)*grid.ADur
a.V, a.A = vStart, aStart
}
anchorMu.Unlock()
vCount := (vt-vStart)/grid.VDur + 1
aCount := (at-aStart)/grid.ADur + 1
if vCount < 1 {
vCount = 1
}
if aCount < 1 {
aCount = 1
}
if vCount > maxCount {
vStart = vt - (maxCount-1)*grid.VDur
aStart = at - (maxCount-1)*grid.ADur
vCount, aCount = maxCount, maxCount
anchorMu.Lock()
anchors[channel] = &anchor{V: vStart, A: aStart}
anchorMu.Unlock()
}
published := time.Now().UTC().Format("2006-01-02T15:04:05.000000Z")
vInit := cfg.segName(cfg.VideoInitTemplate, channel, cfg.VideoName, cfg.VideoBandwidth, false)
vMedia := cfg.segName(cfg.VideoMediaTemplate, channel, cfg.VideoName, cfg.VideoBandwidth, true)
aInit := cfg.segName(cfg.AudioInitTemplate, channel, cfg.AudioName, cfg.AudioBandwidth, false)
aMedia := cfg.segName(cfg.AudioMediaTemplate, channel, cfg.AudioName, cfg.AudioBandwidth, true)
out := fmt.Sprintf(syntheticTemplate,
published, cfg.MinUpdatePeriod, cfg.MinBufferTime, cfg.TimeShiftBufferDepth, cfg.MaxSegmentDuration,
XMLEscape(segBase),
cfg.Lang, kidU,
cfg.VideoBandwidth, cfg.VideoCodecs, cfg.Width, cfg.Height,
cfg.VideoTimescale, vInit, vMedia, vStart, grid.VDur, vCount-1,
cfg.Lang, kidU,
cfg.AudioBandwidth, cfg.AudioCodecs, cfg.AudioSamplingRate, cfg.AudioChannels,
cfg.AudioTimescale, aInit, aMedia, aStart, grid.ADur, aCount-1,
)
return []byte(out), nil
}

View file

@ -0,0 +1,264 @@
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")
}
}