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(``) ) // 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 timeline of one StreamIndex into // absolute MSS tick times. streamName matches Name="" or Type="". func ParseMSSStreamTimes(manifestXML, streamName string) []int64 { reBlock := regexp.MustCompile(`(?is)]*\bName="` + regexp.QuoteMeta(streamName) + `"[^>]*>(.*?)`) m := reBlock.FindStringSubmatch(manifestXML) if m == nil { reBlock = regexp.MustCompile(`(?is)]*\bType="` + regexp.QuoteMeta(streamName) + `"[^>]*>(.*?)`) 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 }