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
280
apps/pkg/mpd/synthetic.go
Normal file
280
apps/pkg/mpd/synthetic.go
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
package mpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SyntheticConfig describes a live encoder's DASH grid well enough to publish a
|
||||
// synthetic manifest for it. Every value a provider could differ on lives here;
|
||||
// nothing in this package names a provider.
|
||||
type SyntheticConfig struct {
|
||||
// Timescales.
|
||||
MSSTimescale int64 `yaml:"mss_timescale"`
|
||||
VideoTimescale int64 `yaml:"video_timescale"`
|
||||
AudioTimescale int64 `yaml:"audio_timescale"`
|
||||
|
||||
// Smooth Streaming stream names used to learn the segment grid.
|
||||
ManifestPath string `yaml:"manifest_path"`
|
||||
VideoName string `yaml:"video_name"`
|
||||
AudioName string `yaml:"audio_name"`
|
||||
|
||||
// Segment path appended to the origin base for DASH segments.
|
||||
SegmentPathSuffix string `yaml:"segment_path_suffix"`
|
||||
|
||||
// Representation attributes.
|
||||
VideoBandwidth int `yaml:"video_bandwidth"`
|
||||
AudioBandwidth int `yaml:"audio_bandwidth"`
|
||||
VideoCodecs string `yaml:"video_codecs"`
|
||||
AudioCodecs string `yaml:"audio_codecs"`
|
||||
Width int `yaml:"width"`
|
||||
Height int `yaml:"height"`
|
||||
AudioSamplingRate int `yaml:"audio_sampling_rate"`
|
||||
AudioChannels int `yaml:"audio_channels"`
|
||||
Lang string `yaml:"lang"`
|
||||
|
||||
// Segment naming. "{channel}" is substituted; empty means derive as
|
||||
// <channel>-<stream name>=<bandwidth>[-$Time$].<ext>.
|
||||
VideoInitTemplate string `yaml:"video_init_template"`
|
||||
VideoMediaTemplate string `yaml:"video_media_template"`
|
||||
AudioInitTemplate string `yaml:"audio_init_template"`
|
||||
AudioMediaTemplate string `yaml:"audio_media_template"`
|
||||
SegmentExt string `yaml:"segment_ext"`
|
||||
|
||||
// Timeline shape.
|
||||
LiveEdgeOffset float64 `yaml:"live_edge_offset_s"`
|
||||
WindowSegments int64 `yaml:"window_segments"`
|
||||
FallbackSegmentDuration int64 `yaml:"fallback_segment_duration"`
|
||||
|
||||
// MPD-level durations, as ISO-8601 strings.
|
||||
MinUpdatePeriod string `yaml:"min_update_period"`
|
||||
MinBufferTime string `yaml:"min_buffer_time"`
|
||||
TimeShiftBufferDepth string `yaml:"time_shift_buffer_depth"`
|
||||
MaxSegmentDuration string `yaml:"max_segment_duration"`
|
||||
}
|
||||
|
||||
// WithDefaults fills anything left zero with the common Smooth-to-DASH values.
|
||||
// These are container mechanics, not provider identity, so defaulting is safe.
|
||||
func (c SyntheticConfig) WithDefaults() SyntheticConfig {
|
||||
if c.MSSTimescale == 0 {
|
||||
c.MSSTimescale = DefaultMSSTimescale
|
||||
}
|
||||
if c.VideoTimescale == 0 {
|
||||
c.VideoTimescale = 600
|
||||
}
|
||||
if c.AudioTimescale == 0 {
|
||||
c.AudioTimescale = 48_000
|
||||
}
|
||||
if c.ManifestPath == "" {
|
||||
c.ManifestPath = "Manifest"
|
||||
}
|
||||
if c.VideoName == "" {
|
||||
c.VideoName = "video"
|
||||
}
|
||||
if c.AudioName == "" {
|
||||
c.AudioName = "audio_128k"
|
||||
}
|
||||
if c.SegmentPathSuffix == "" {
|
||||
c.SegmentPathSuffix = "dash/"
|
||||
}
|
||||
if c.VideoBandwidth == 0 {
|
||||
c.VideoBandwidth = 6_000_000
|
||||
}
|
||||
if c.AudioBandwidth == 0 {
|
||||
c.AudioBandwidth = 128_000
|
||||
}
|
||||
if c.VideoCodecs == "" {
|
||||
c.VideoCodecs = "avc1.640028"
|
||||
}
|
||||
if c.AudioCodecs == "" {
|
||||
c.AudioCodecs = "mp4a.40.2"
|
||||
}
|
||||
if c.Width == 0 {
|
||||
c.Width = 1920
|
||||
}
|
||||
if c.Height == 0 {
|
||||
c.Height = 1080
|
||||
}
|
||||
if c.AudioSamplingRate == 0 {
|
||||
c.AudioSamplingRate = 48_000
|
||||
}
|
||||
if c.AudioChannels == 0 {
|
||||
c.AudioChannels = 2
|
||||
}
|
||||
if c.Lang == "" {
|
||||
c.Lang = "en"
|
||||
}
|
||||
if c.SegmentExt == "" {
|
||||
c.SegmentExt = "dash"
|
||||
}
|
||||
if c.LiveEdgeOffset == 0 {
|
||||
c.LiveEdgeOffset = 12
|
||||
}
|
||||
if c.WindowSegments == 0 {
|
||||
c.WindowSegments = 6
|
||||
}
|
||||
if c.FallbackSegmentDuration == 0 {
|
||||
c.FallbackSegmentDuration = 38_400_000
|
||||
}
|
||||
if c.MinUpdatePeriod == "" {
|
||||
c.MinUpdatePeriod = "PT2S"
|
||||
}
|
||||
if c.MinBufferTime == "" {
|
||||
c.MinBufferTime = "PT8S"
|
||||
}
|
||||
if c.TimeShiftBufferDepth == "" {
|
||||
c.TimeShiftBufferDepth = "PT1M"
|
||||
}
|
||||
if c.MaxSegmentDuration == "" {
|
||||
c.MaxSegmentDuration = "PT4S"
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// GridSpec is the LearnGrid view of this config, exported so a provider module can
|
||||
// learn the same encoder grid the builder uses.
|
||||
func (c SyntheticConfig) GridSpec() GridSpec {
|
||||
return GridSpec{
|
||||
ManifestPath: c.ManifestPath,
|
||||
VideoName: c.VideoName,
|
||||
AudioName: c.AudioName,
|
||||
VideoTimescale: c.VideoTimescale,
|
||||
AudioTimescale: c.AudioTimescale,
|
||||
MSSTimescale: c.MSSTimescale,
|
||||
FallbackSegmentDuration: c.FallbackSegmentDuration,
|
||||
}
|
||||
}
|
||||
|
||||
func (c SyntheticConfig) segName(tmpl, channel, stream string, bandwidth int, withTime bool) string {
|
||||
if tmpl != "" {
|
||||
return strings.ReplaceAll(tmpl, "{channel}", channel)
|
||||
}
|
||||
if withTime {
|
||||
return fmt.Sprintf("%s-%s=%d-$Time$.%s", channel, stream, bandwidth, c.SegmentExt)
|
||||
}
|
||||
return fmt.Sprintf("%s-%s=%d.%s", channel, stream, bandwidth, c.SegmentExt)
|
||||
}
|
||||
|
||||
type anchor struct {
|
||||
V, A int64
|
||||
}
|
||||
|
||||
var (
|
||||
anchorMu sync.Mutex
|
||||
anchors = map[string]*anchor{}
|
||||
)
|
||||
|
||||
const syntheticTemplate = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:cenc="urn:mpeg:cenc:2013"
|
||||
profiles="urn:mpeg:dash:profile:isoff-live:2011" type="dynamic"
|
||||
availabilityStartTime="1970-01-01T00:00:00Z" publishTime="%s"
|
||||
minimumUpdatePeriod="%s" minBufferTime="%s" timeShiftBufferDepth="%s"
|
||||
maxSegmentDuration="%s">
|
||||
<BaseURL>%s</BaseURL>
|
||||
<Period id="1" start="PT0S">
|
||||
<AdaptationSet id="1" contentType="video" mimeType="video/mp4" lang="%s">
|
||||
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="%s"/>
|
||||
<Representation id="r0" bandwidth="%d" codecs="%s" width="%d" height="%d">
|
||||
<SegmentTemplate timescale="%d" initialization="%s" media="%s" startNumber="1">
|
||||
<SegmentTimeline><S t="%d" d="%d" r="%d"/></SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet id="2" contentType="audio" mimeType="audio/mp4" lang="%s">
|
||||
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="%s"/>
|
||||
<Representation id="r1" bandwidth="%d" codecs="%s" audioSamplingRate="%d">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="%d"/>
|
||||
<SegmentTemplate timescale="%d" initialization="%s" media="%s" startNumber="1">
|
||||
<SegmentTimeline><S t="%d" d="%d" r="%d"/></SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
</Period>
|
||||
</MPD>
|
||||
`
|
||||
|
||||
// BuildSynthetic publishes a dynamic MPD on the encoder's own segment grid, so a
|
||||
// downloader keeps fetching live segments even once the upstream (ad-stitched)
|
||||
// manifest session has expired.
|
||||
func BuildSynthetic(cfg SyntheticConfig, channel, originBase, kid string) ([]byte, error) {
|
||||
cfg = cfg.WithDefaults()
|
||||
if !strings.HasSuffix(originBase, "/") {
|
||||
originBase += "/"
|
||||
}
|
||||
segBase := originBase + cfg.SegmentPathSuffix
|
||||
kidU, err := KIDToUUID(kid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grid, err := LearnGrid(channel, originBase, cfg.GridSpec())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Float seconds so sub-second phase stays close to the encoder.
|
||||
now := float64(time.Now().UTC().UnixNano())/1e9 - cfg.LiveEdgeOffset
|
||||
align := func(ts, dur, mod int64) int64 {
|
||||
raw := int64(now * float64(ts))
|
||||
return raw - (raw-mod)%dur
|
||||
}
|
||||
vt := align(cfg.VideoTimescale, grid.VDur, grid.VMod)
|
||||
at := align(cfg.AudioTimescale, grid.ADur, grid.AMod)
|
||||
maxCount := cfg.WindowSegments
|
||||
|
||||
anchorMu.Lock()
|
||||
a := anchors[channel]
|
||||
if a == nil {
|
||||
a = &anchor{
|
||||
V: vt - (maxCount-1)*grid.VDur,
|
||||
A: at - (maxCount-1)*grid.ADur,
|
||||
}
|
||||
anchors[channel] = a
|
||||
}
|
||||
vStart, aStart := a.V, a.A
|
||||
if vt > vStart+(maxCount-1)*grid.VDur {
|
||||
vStart = vt - (maxCount-1)*grid.VDur
|
||||
aStart = at - (maxCount-1)*grid.ADur
|
||||
a.V, a.A = vStart, aStart
|
||||
} else if vt < vStart {
|
||||
vStart = vt - (maxCount-1)*grid.VDur
|
||||
aStart = at - (maxCount-1)*grid.ADur
|
||||
a.V, a.A = vStart, aStart
|
||||
}
|
||||
anchorMu.Unlock()
|
||||
|
||||
vCount := (vt-vStart)/grid.VDur + 1
|
||||
aCount := (at-aStart)/grid.ADur + 1
|
||||
if vCount < 1 {
|
||||
vCount = 1
|
||||
}
|
||||
if aCount < 1 {
|
||||
aCount = 1
|
||||
}
|
||||
if vCount > maxCount {
|
||||
vStart = vt - (maxCount-1)*grid.VDur
|
||||
aStart = at - (maxCount-1)*grid.ADur
|
||||
vCount, aCount = maxCount, maxCount
|
||||
anchorMu.Lock()
|
||||
anchors[channel] = &anchor{V: vStart, A: aStart}
|
||||
anchorMu.Unlock()
|
||||
}
|
||||
|
||||
published := time.Now().UTC().Format("2006-01-02T15:04:05.000000Z")
|
||||
vInit := cfg.segName(cfg.VideoInitTemplate, channel, cfg.VideoName, cfg.VideoBandwidth, false)
|
||||
vMedia := cfg.segName(cfg.VideoMediaTemplate, channel, cfg.VideoName, cfg.VideoBandwidth, true)
|
||||
aInit := cfg.segName(cfg.AudioInitTemplate, channel, cfg.AudioName, cfg.AudioBandwidth, false)
|
||||
aMedia := cfg.segName(cfg.AudioMediaTemplate, channel, cfg.AudioName, cfg.AudioBandwidth, true)
|
||||
|
||||
out := fmt.Sprintf(syntheticTemplate,
|
||||
published, cfg.MinUpdatePeriod, cfg.MinBufferTime, cfg.TimeShiftBufferDepth, cfg.MaxSegmentDuration,
|
||||
XMLEscape(segBase),
|
||||
cfg.Lang, kidU,
|
||||
cfg.VideoBandwidth, cfg.VideoCodecs, cfg.Width, cfg.Height,
|
||||
cfg.VideoTimescale, vInit, vMedia, vStart, grid.VDur, vCount-1,
|
||||
cfg.Lang, kidU,
|
||||
cfg.AudioBandwidth, cfg.AudioCodecs, cfg.AudioSamplingRate, cfg.AudioChannels,
|
||||
cfg.AudioTimescale, aInit, aMedia, aStart, grid.ADur, aCount-1,
|
||||
)
|
||||
return []byte(out), nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue