parse.go (10623B)
1 package m3u8 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os" 8 "strconv" 9 "strings" 10 "time" 11 ) 12 13 const ( 14 tagHead = tagStart + "M3U" 15 tagVersion = "#EXT-X-VERSION" 16 tagVariant = "#EXT-X-STREAM-INF" 17 tagRendition = "#EXT-X-MEDIA" 18 tagPlaylistType = "#EXT-X-PLAYLIST-TYPE" // RFC 8216, 4.4.3.5 19 tagTargetDuration = "#EXT-X-TARGETDURATION" // RFC 8216, 4.4.3.1 20 tagMediaSequence = "#EXT-X-MEDIA-SEQUENCE" // RFC 8216, 4.3.3.2 21 tagEndList = "#EXT-X-ENDLIST" // RFC 8216, 4.4.3.4 22 tagIndependentSegments = "#EXT-X-INDEPENDENT-SEGMENTS" // RFC 8216, 4.3.5.1 23 tagSessionData = "#EXT-X-SESSION-DATA" // RFC 8216, 4.3.4.4 24 ) 25 26 func Decode(rd io.Reader) (*Playlist, error) { 27 lex := newLexer(rd) 28 go lex.run() 29 it := <-lex.items 30 if it.typ == itemError { 31 return nil, errors.New(it.val) 32 } 33 if it.typ != itemTag || it.val != tagHead { 34 return nil, fmt.Errorf("expected head tag, got %q", it.val) 35 } 36 37 p := &Playlist{} 38 var err error 39 for it := range lex.items { 40 switch it.typ { 41 case itemError: 42 return p, errors.New(it.val) 43 case itemNewline: 44 continue 45 default: 46 if it.typ != itemTag { 47 return p, fmt.Errorf("unexpected %s %q, expected tag", it.typ, it.val) 48 } 49 } 50 51 switch it.val { 52 case tagVersion: 53 it = <-lex.items 54 if p.Version != 0 { 55 return p, fmt.Errorf("parse %s: playlist version already specified", it) 56 } 57 p.Version, err = strconv.Atoi(it.val) 58 if err != nil { 59 return p, fmt.Errorf("parse playlist version: %w", err) 60 } 61 case tagIndependentSegments: 62 p.IndependentSegments = true 63 case tagVariant: 64 variant, err := parseVariant(lex.items) 65 if err != nil { 66 return p, fmt.Errorf("parse variant: %w", err) 67 } 68 p.Variants = append(p.Variants, *variant) 69 case tagRendition: 70 rend, err := parseRendition(lex.items) 71 if err != nil { 72 return p, fmt.Errorf("parse rendition: %w", err) 73 } 74 p.Media = append(p.Media, *rend) 75 case tagPlaylistType: 76 it = <-lex.items 77 typ, err := parsePlaylistType(it) 78 if err != nil { 79 return p, fmt.Errorf("parse playlist type: %w", err) 80 } 81 p.Type = typ 82 83 case tagTargetDuration: 84 it = <-lex.items 85 dur, err := parseTargetDuration(it) 86 if err != nil { 87 return p, fmt.Errorf("parse target duration: %w", err) 88 } 89 p.TargetDuration = dur 90 91 case tagSegmentDuration, tagByteRange, tagKey: 92 segment, err := parseSegment(lex.items, it) 93 if err != nil { 94 return p, fmt.Errorf("parse segment: %w", err) 95 } 96 p.Segments = append(p.Segments, *segment) 97 98 case tagEndList: 99 p.End = true 100 case tagMediaSequence: 101 it = <-lex.items 102 seq, err := strconv.Atoi(it.val) 103 if err != nil { 104 return p, fmt.Errorf("parse media sequence: %w", err) 105 } 106 p.Sequence = seq 107 default: 108 if lex.debug { 109 fmt.Fprintln(os.Stderr, "unknown tag", it) 110 } 111 // throw away whatever is next; we don't support it but also don't want to 112 // return errors while this package is in development. 113 <-lex.items 114 } 115 } 116 return p, nil 117 } 118 119 func parseVariant(items chan item) (*Variant, error) { 120 var v Variant 121 for it := range items { 122 switch it.typ { 123 case itemError: 124 return nil, errors.New(it.val) 125 case itemComma, itemNewline: 126 continue 127 case itemURL: 128 v.URI = it.val 129 return &v, nil 130 default: 131 if it.typ != itemAttrName { 132 return nil, fmt.Errorf("expected %s, got %s", itemAttrName, it.typ) 133 } 134 } 135 attr := it 136 it = <-items 137 if it.typ != itemEquals { 138 return nil, fmt.Errorf("missing equals after %s", attr) 139 } 140 141 switch attr.val { 142 case "PROGRAM-ID", "NAME": 143 // parsing PROGRAM-ID attribute unsupported; removed in HLS version 6 144 // NAME is non-standard, should be set in Rendition. 145 case "BANDWIDTH", "AVERAGE-BANDWIDTH": 146 it = <-items 147 if it.typ != itemNumber { 148 return nil, fmt.Errorf("parse bandwidth attribute: unexpected %s", it) 149 } 150 n, err := strconv.Atoi(it.val) 151 if err != nil { 152 return nil, fmt.Errorf("parse bandwidth: %w", err) 153 } 154 if attr.val == "BANDWIDTH" { 155 v.Bandwidth = n 156 } else { 157 v.AverageBandwidth = n 158 } 159 case "CODECS": 160 it = <-items 161 if it.typ != itemString { 162 return nil, fmt.Errorf("parse codecs attribute: unexpected %s", it) 163 } 164 v.Codecs = strings.Split(strings.Trim(it.val, `"`), ",") 165 case "RESOLUTION": 166 it = <-items 167 res, err := parseResolution(it.val) 168 if err != nil { 169 return nil, fmt.Errorf("parse resolution: %w", err) 170 } 171 v.Resolution = res 172 case "FRAME-RATE": 173 it = <-items 174 if it.typ != itemNumber { 175 return nil, fmt.Errorf("parse frame rate: unexpected %s", it) 176 } 177 n, err := strconv.ParseFloat(it.val, 32) 178 if err != nil { 179 return nil, fmt.Errorf("parse frame rate: %w", err) 180 } 181 v.FrameRate = float32(n) 182 case "HDCP-LEVEL": 183 it = <-items 184 l, err := parseHDCPLevel(it.val) 185 if err != nil { 186 return nil, fmt.Errorf("parse HDCP level: %w", err) 187 } 188 v.HDCP = l 189 case "AUDIO", "VIDEO", "SUBTITLES": 190 name := attr.val 191 it = <-items 192 if it.typ != itemString { 193 return nil, fmt.Errorf("parse %s: unexpected %s", name, it) 194 } 195 it.val = strings.Trim(it.val, `"`) 196 if name == "AUDIO" { 197 v.Audio = it.val 198 } else if name == "VIDEO" { 199 v.Video = it.val 200 } else if name == "SUBTITLES" { 201 v.Subtitles = it.val 202 } 203 case "CLOSED-CAPTIONS": 204 it = <-items 205 if it.typ != itemString { 206 return nil, fmt.Errorf("parse closed-captions: unexpected %s", it) 207 } 208 v.ClosedCaptions = strings.Trim(it.val, `"`) 209 default: 210 return nil, fmt.Errorf("unknown attribute %s", attr.val) 211 } 212 } 213 return nil, fmt.Errorf("no url") 214 } 215 216 func parseResolution(s string) (res [2]int, err error) { 217 x, y, found := strings.Cut(s, "x") 218 if !found { 219 return res, fmt.Errorf("missing x seperator") 220 } 221 res[0], err = strconv.Atoi(x) 222 if err != nil { 223 return res, fmt.Errorf("horizontal pixels: %v", err) 224 } 225 res[1], err = strconv.Atoi(y) 226 if err != nil { 227 return res, fmt.Errorf("vertical pixels: %v", err) 228 } 229 if res[0] < 0 || res[1] < 0 { 230 return res, fmt.Errorf("negative dimensions") 231 } 232 return res, nil 233 } 234 235 func parseHDCPLevel(s string) (HDCPLevel, error) { 236 switch s { 237 case "NONE": 238 return HDCPNone, nil 239 case "TYPE-0": 240 return HDCPType0, nil 241 case "TYPE-1": 242 return HDCPType1, nil 243 } 244 return 0, fmt.Errorf("unknown HDCP level %q", s) 245 } 246 247 func parseRendition(items chan item) (*Rendition, error) { 248 var rend Rendition 249 var err error 250 for it := range items { 251 if it.typ != itemAttrName { 252 return nil, fmt.Errorf("expected attribute name, got %s", it) 253 } 254 attr := it 255 it = <-items 256 if it.typ != itemEquals { 257 return nil, fmt.Errorf("parse %s: expected =, got %s", attr, it) 258 } 259 it = <-items 260 switch attr.val { 261 case "TYPE": 262 rend.Type, err = parseMediaType(it.val) 263 if err != nil { 264 return nil, fmt.Errorf("parse media type: %w", err) 265 } 266 case "URI": 267 rend.URI = strings.Trim(it.val, `"`) 268 case "GROUP-ID": 269 rend.Group = strings.Trim(it.val, `"`) 270 case "LANGUAGE": 271 rend.Language = strings.Trim(it.val, `"`) 272 case "ASSOC-LANGUAGE": 273 rend.AssocLanguage = strings.Trim(it.val, `"`) 274 case "NAME": 275 rend.Name = strings.Trim(it.val, `"`) 276 case "DEFAULT", "AUTOSELECT", "FORCED": 277 b, err := parseBool(it.val) 278 if err != nil { 279 return nil, fmt.Errorf("parse %s: %w", attr, err) 280 } 281 if attr.val == "DEFAULT" { 282 rend.Default = b 283 } else if attr.val == "AUTOSELECT" { 284 rend.AutoSelect = b 285 } else if attr.val == "FORCED" { 286 rend.Forced = b 287 } 288 case "INSTREAM-ID": 289 rend.InstreamID, err = parseCCInfo(it.val) 290 if err != nil { 291 return nil, fmt.Errorf("parse instream-id: %w", err) 292 } 293 case "CHARACTERISTICS": 294 rend.Characteristics = strings.Split(it.val, ",") 295 case "CHANNELS": 296 rend.Channels = strings.Split(strings.Trim(it.val, `"`), "/") 297 default: 298 return nil, fmt.Errorf("unknown rendition attribute %s", attr.val) 299 } 300 it = <-items 301 switch it.typ { 302 case itemError: 303 return nil, fmt.Errorf("next attribute: %s", it.val) 304 case itemComma: 305 continue 306 case itemNewline: 307 return &rend, nil 308 default: 309 return nil, fmt.Errorf("next attribute: expected comma or newline, got %s", it) 310 } 311 } 312 return &rend, nil 313 } 314 315 func parseMediaType(s string) (MediaType, error) { 316 for t := MediaAudio; t <= MediaClosedCaptions; t++ { 317 if t.String() == s { 318 return t, nil 319 } 320 } 321 return 0, fmt.Errorf("unknown media type %s", s) 322 } 323 324 func parseBool(s string) (bool, error) { 325 if s == "YES" { 326 return true, nil 327 } else if s == "NO" { 328 return false, nil 329 } 330 return false, fmt.Errorf("invalid boolean string %s", s) 331 } 332 333 // parseCCInfo parses a CCInfo attribute from s as specified in RFC 8216 section 4.4.6.1. 334 func parseCCInfo(s string) (*CCInfo, error) { 335 // shortest possible is 3 chars, "CC0", "CC1" etc. 336 if len(s) < 3 { 337 return nil, fmt.Errorf("too short") 338 } 339 if s[:2] == "CC" { 340 // MUST have one of the values: "CC1", "CC2", "CC3", "CC4" 341 switch { 342 case len(s) == 3 && s[2] >= '1' && s[2] <= '4': 343 i := int(s[2] - '0') 344 return &CCInfo{i, false}, nil 345 default: 346 return nil, fmt.Errorf("invalid closed caption %s", s) 347 } 348 } 349 // SERVICE00 350 if len(s) < 8 { 351 return nil, fmt.Errorf("invalid keyword %s", s) 352 } 353 if s[:6] != "SERVICE" { 354 return nil, fmt.Errorf("expected keyword %q, got %q", "SERVICE", s[:6]) 355 } else if len(s) > 9 { 356 return nil, fmt.Errorf("service too long") 357 } 358 i, err := strconv.Atoi(s[6:]) 359 if err != nil { 360 return nil, fmt.Errorf("parse service block number: %w", err) 361 } 362 if i < 1 || i > 63 { 363 return nil, fmt.Errorf("invalid service block number %d", i) 364 } 365 return &CCInfo{i, true}, nil 366 } 367 368 func parsePlaylistType(it item) (PlaylistType, error) { 369 if it.typ != itemAttrName { 370 return 0, fmt.Errorf("got %s, want item type %s", it, itemString) 371 } 372 switch it.val { 373 case "EVENT": 374 return PlaylistEvent, nil 375 case "VOD": 376 return PlaylistVOD, nil 377 } 378 return 0, fmt.Errorf("illegal playlist type %q", it.val) 379 } 380 381 func parseTargetDuration(it item) (time.Duration, error) { 382 if it.typ != itemAttrName && it.typ != itemNumber { 383 return 0, fmt.Errorf("got %s: want attribute name or number", it) 384 } 385 i, err := strconv.Atoi(it.val) 386 if err != nil { 387 return 0, err 388 } 389 return time.Duration(i) * time.Second, nil 390 } 391 392 func parseByteRange(s string) (ByteRange, error) { 393 offset, until, found := strings.Cut(s, "@") 394 if !found { 395 n, err := strconv.Atoi(offset) 396 if err != nil { 397 return ByteRange{}, err 398 } 399 return ByteRange{n, 0}, nil 400 } 401 n, err := strconv.Atoi(offset) 402 if err != nil { 403 return ByteRange{}, err 404 } 405 nn, err := strconv.Atoi(until) 406 if err != nil { 407 return ByteRange{}, err 408 } 409 return ByteRange{n, nn}, nil 410 }