github.com/puellanivis/breton@v0.2.16/lib/net/dash/manifest.go (about) 1 // Package dash provides an abstraction to accessing DASH streams. 2 package dash 3 4 import ( 5 "context" 6 "io" 7 "strings" 8 "time" 9 10 "github.com/puellanivis/breton/lib/files" 11 "github.com/puellanivis/breton/lib/glog" 12 "github.com/puellanivis/breton/lib/net/dash/mpd" 13 14 "github.com/pkg/errors" 15 ) 16 17 type adaptation struct { 18 // indexes into the period and adaptions 19 pid string 20 aid uint 21 22 base string 23 24 // template strings 25 init, media string 26 startNum uint 27 28 reps []*mpd.Representation 29 } 30 31 // A Manifest holds the essential identifying information about a DASH manifest it is used to generate Streams. 32 type Manifest struct { 33 base string 34 manifest string 35 36 metrics *metricsPack 37 38 dynamic bool 39 adaptations map[string]*adaptation 40 41 m *cachedMPD 42 } 43 44 // IsDynamic returns true if the Manifest describes a dynamic stream. 45 func (m *Manifest) IsDynamic() bool { 46 return m.dynamic 47 } 48 49 func updateBase(ctx context.Context, baseURL []*mpd.BaseURL) context.Context { 50 for _, url := range baseURL { 51 if url.CDATA == "" { 52 continue 53 } 54 55 base := url.CDATA 56 57 ctx, err := files.WithRoot(ctx, base) 58 if err != nil { 59 glog.Fatal("omg, root failed", err) 60 continue 61 } 62 63 return ctx 64 } 65 66 return ctx 67 } 68 69 // New returns a Manifest constructed from the given URL. 70 func New(ctx context.Context, manifest string) (*Manifest, error) { 71 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 72 defer cancel() 73 74 m, err := readMPD(ctx, manifest) 75 if err != nil { 76 return nil, err 77 } 78 79 url := urlLabel.WithValue(manifest) 80 81 idx := strings.LastIndexByte(manifest, '/') 82 fqManifest := manifest 83 base, manifest := manifest[:idx+1], manifest[idx+1:] 84 85 ctx, err = files.WithRoot(ctx, base) 86 if err != nil { 87 return nil, err 88 } 89 90 ctx = updateBase(ctx, m.BaseURL) 91 92 adaptations := make(map[string]*adaptation) 93 94 for _, p := range m.Period { 95 pid := p.Id 96 97 ctx := updateBase(ctx, p.BaseURL) 98 99 for _, as := range p.AdaptationSet { 100 ctx := updateBase(ctx, as.BaseURL) 101 102 if root, ok := files.GetRoot(ctx); ok { 103 base = root 104 } 105 106 a := &adaptation{ 107 pid: pid, 108 aid: as.Id, 109 base: base, 110 } 111 112 if tmpl := as.SegmentTemplate; tmpl != nil { 113 a.init = tmpl.Initialization 114 a.media = tmpl.Media 115 } 116 117 for _, r := range as.Representation { 118 if as.MimeType == "" { 119 as.MimeType = r.MimeType 120 } 121 122 a.reps = append(a.reps, r) 123 } 124 125 if as.MimeType == "" { 126 continue 127 } 128 129 if _, ok := adaptations[as.MimeType]; ok { 130 continue 131 } 132 133 adaptations[as.MimeType] = a 134 } 135 } 136 137 minTime, err := m.MinimumUpdatePeriod.Duration() 138 if err != nil { 139 return nil, err 140 } 141 142 // I can’t imagine using a minimum time of less than one millisecond. 143 if minTime < time.Millisecond { 144 minTime = time.Millisecond 145 } 146 147 return &Manifest{ 148 base: base, 149 manifest: manifest, 150 dynamic: m.Type == "dynamic", 151 adaptations: adaptations, 152 m: newMPD(fqManifest, minTime), 153 154 metrics: baseMetrics.WithLabels(url), 155 }, nil 156 } 157 158 // MinimumUpdatePeriod returns the shortest period within with a Manifest’s 159 // information is to update. 160 func (m *Manifest) MinimumUpdatePeriod() time.Duration { 161 return m.m.expTime 162 } 163 164 // Stream returns a Stream object that specifies a specific series of segments within the DASH manifest. 165 func (m *Manifest) Stream(w io.Writer, mimeType string, picker Picker) (*Stream, error) { 166 if picker == nil { 167 picker = PickFirst() 168 } 169 170 as, ok := m.adaptations[mimeType] 171 if !ok { 172 return nil, errors.New("mime-type not available") 173 } 174 175 var picked *mpd.Representation 176 177 for _, rep := range as.reps { 178 // I don’t know how this could end up being here, 179 // but let’s discard it regardless. 180 if rep == nil { 181 continue 182 } 183 184 picked = picker(rep) 185 } 186 187 if picked == nil { 188 return nil, errors.New("no suitable representations found") 189 } 190 191 if glog.V(1) { 192 glog.Infof("chose %s with bandwidth: %d", mimeType, picked.Bandwidth) 193 if picked.Height > 0 && picked.Width > 0 { 194 glog.Infof("chose %s with %d×%d", mimeType, picked.Height, picked.Width) 195 } 196 } 197 198 return &Stream{ 199 w: w, 200 m: m, 201 202 metrics: m.metrics.WithLabels(typeLabel.WithValue(mimeType)), 203 204 pid: as.pid, 205 aid: as.aid, 206 207 dynamic: m.dynamic, 208 base: as.base, 209 init: as.init, 210 media: as.media, 211 212 bw: picked.Bandwidth, 213 repID: string(picked.Id), 214 }, nil 215 }