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
146
apps/modules/rte/mpd.go
Normal file
146
apps/modules/rte/mpd.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
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 "", ""
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue