Null-DRM-Official/apps/modules/rte/mpd.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

146 lines
4.1 KiB
Go

package rte
import (
"fmt"
"regexp"
"strings"
"sync"
"drmdecryption/mpd"
)
func init() {
mpd.Register(Name, func() mpd.Rewriter { return NewRewriter() })
}
var reBaseURL = regexp.MustCompile(`(?i)<BaseURL[^>]*>([^<]+)</BaseURL>`)
// Rewriter turns an ad-stitched upstream MPD into a synthetic manifest on the
// origin encoder's own segment grid, so a downloader keeps pulling live segments
// after the upstream session expires. All origins and KIDs come from config.
type Rewriter struct {
cfg Config
mu sync.Mutex
fallbackCh string
fallbackBase string
}
// NewRewriter loads the module config and returns a rewriter. A config error is
// deferred to Rewrite so registry lookup never fails at startup.
func NewRewriter() *Rewriter {
cfg, _, err := loadConfig()
if err != nil {
return &Rewriter{cfg: Config{}.withDefaults()}
}
return &Rewriter{cfg: cfg}
}
// NewRewriterWith builds a rewriter from an already-loaded config.
func NewRewriterWith(cfg Config) *Rewriter {
return &Rewriter{cfg: cfg}
}
func (r *Rewriter) Name() string { return Name }
// Prime records a channel/origin learned elsewhere (e.g. from the KID).
func (r *Rewriter) Prime(channel, originBase string) {
r.mu.Lock()
defer r.mu.Unlock()
if channel != "" {
r.fallbackCh = channel
}
if originBase != "" {
r.fallbackBase = originBase
}
}
// ResolveKID maps a KID back to its encoder channel and origin.
func (r *Rewriter) ResolveKID(kidHex string) (channel, originBase string, ok bool) {
for ch, kid := range r.cfg.KIDs {
if mpd.KIDHex(kid) == kidHex {
return ch, r.cfg.Origins[ch], true
}
}
return "", "", false
}
// Rewrite produces the synthetic MPD. upstreamXML may be empty (expired session):
// the KID fallback then supplies the channel and origin.
func (r *Rewriter) Rewrite(upstreamXML []byte, kidHint string) ([]byte, error) {
r.mu.Lock()
fbCh, fbBase := r.fallbackCh, r.fallbackBase
r.mu.Unlock()
payload, channel, originBase, err := r.rewrite(string(upstreamXML), kidHint, fbCh, fbBase)
if channel != "" || originBase != "" {
r.Prime(channel, originBase)
}
return payload, err
}
func (r *Rewriter) rewrite(xmlText, kidHint, fallbackChannel, fallbackBase string) (payload []byte, channel, originBase string, err error) {
cfg := r.cfg
channel, originBase = r.originFromXML(xmlText)
resolvedKid := mpd.ExtractDefaultKID(xmlText)
if resolvedKid == "" && kidHint != "" {
resolvedKid, _ = mpd.KIDToUUID(kidHint)
}
if (channel == "" || originBase == "") && resolvedKid != "" {
if mapped, base, ok := r.ResolveKID(mpd.KIDHex(resolvedKid)); ok {
if channel == "" {
channel = mapped
}
if originBase == "" {
originBase = base
}
}
}
if channel == "" {
channel = fallbackChannel
}
if originBase == "" {
originBase = fallbackBase
}
if channel != "" && originBase == "" {
originBase = cfg.Origins[channel]
}
if channel != "" && originBase != "" && r.usesEncoderTimeline(channel) {
if resolvedKid == "" {
resolvedKid = cfg.KIDs[channel]
}
if resolvedKid == "" {
return nil, "", "", fmt.Errorf("%s: no KID for synthetic MPD channel %s", Name, channel)
}
payload, err = mpd.BuildSynthetic(cfg.Synthetic, channel, originBase, resolvedKid)
return payload, channel, originBase, err
}
return nil, channel, originBase, fmt.Errorf("%s rewrite: unsupported MPD (no encoder channel); channel=%q", Name, channel)
}
func (r *Rewriter) usesEncoderTimeline(channel string) bool {
ok, _ := regexp.MatchString(r.cfg.EncoderChannelRE, channel)
return ok
}
// originFromXML finds the origin BaseURL and derives the encoder channel from it.
func (r *Rewriter) originFromXML(xmlText string) (channel, base string) {
marker := r.cfg.OriginMarker
if marker == "" {
marker = ".isml"
}
for _, m := range reBaseURL.FindAllStringSubmatch(xmlText, -1) {
text := strings.TrimSpace(m[1])
idx := strings.Index(text, marker)
if idx < 0 {
continue
}
base = text[:idx] + marker + "/"
parts := strings.Split(strings.TrimSuffix(base, "/"), "/")
channel = strings.TrimSuffix(parts[len(parts)-1], marker)
if channel != "" {
return channel, base
}
}
return "", ""
}