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.
142 lines
3.9 KiB
Go
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
|
|
}
|