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 // -=[-$Time$].. 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 = ` %s ` // 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 }