streaming

Media streaming and broadcast systems in Go
Log | Files | Refs | README | LICENSE

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 }