Null-DRM-Official/apps/pkg/mpd/smooth.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

142 lines
3.9 KiB
Go

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
}