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
142
apps/pkg/mpd/smooth.go
Normal file
142
apps/pkg/mpd/smooth.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue