Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion m3u8/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ func lexAttrs(l *lexer) stateFn {

func lexAttrValue(l *lexer) stateFn {
r := l.next()
if r == '@' {
r = l.next()
}
switch r {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ':':
return lexNumber(l)
Expand Down Expand Up @@ -295,7 +298,6 @@ Loop:
return lexAttrs(l)
}


func lexQString(l *lexer) stateFn {
for {
r := l.next()
Expand Down
1 change: 1 addition & 0 deletions m3u8/m3u8.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Playlist struct {
// Master playlist
Media []Rendition
Variants []Variant
Iframes []IFrameInfo
SessionData []SessionData
SessionKey *Key
}
Expand Down
22 changes: 21 additions & 1 deletion m3u8/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,13 @@ type Variant struct {
Video string
Subtitles string
ClosedCaptions string // May be NoClosedCaptions to explicitly signal no rendition.

// Outdate Vesrsion up to 6
ProgramID *int
}

func (v Variant) String() string {
// variantAttributes return list of marshaled attributes
func variantAttributes(v Variant) []string {
var attrs []string
attrs = append(attrs, fmt.Sprintf("BANDWIDTH=%d", v.Bandwidth))
if v.AverageBandwidth > 0 {
Expand Down Expand Up @@ -214,6 +218,14 @@ func (v Variant) String() string {
if v.ClosedCaptions != "" && v.ClosedCaptions != NoClosedCaptions {
attrs = append(attrs, fmt.Sprintf("CLOSED-CAPTIONS=%q", v.ClosedCaptions))
}
if v.ProgramID != nil {
attrs = append(attrs, fmt.Sprintf("PROGRAM-ID=%d", *v.ProgramID))
}
return attrs
}

func (v Variant) String() string {
attrs := variantAttributes(v)
return fmt.Sprintf("%s:%s\n%s", tagVariant, strings.Join(attrs, ","), v.URI)
}

Expand Down Expand Up @@ -250,6 +262,14 @@ func (l HDCPLevel) String() string {
// - ClosedCaptions
type IFrameInfo Variant

func (iv IFrameInfo) String() string {
attrs := variantAttributes(Variant(iv))
if iv.URI != "" {
attrs = append(attrs, fmt.Sprintf("URI=%q", iv.URI))
}
return fmt.Sprintf("%s:%s\n", tagIFrame, strings.Join(attrs, ","))
}

// SessionData represents the EXT-X-SESSION-DATA tag.
type SessionData struct {
ID string // This attribute is REQUIRED
Expand Down
42 changes: 39 additions & 3 deletions m3u8/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
tagEndList = "#EXT-X-ENDLIST" // RFC 8216, 4.4.3.4
tagIndependentSegments = "#EXT-X-INDEPENDENT-SEGMENTS" // RFC 8216, 4.3.5.1
tagSessionData = "#EXT-X-SESSION-DATA" // RFC 8216, 4.3.4.4
tagIFrame = "#EXT-X-I-FRAME-STREAM-INF" // RFC 8216, 4.3.4.3
)

func Decode(rd io.Reader) (*Playlist, error) {
Expand Down Expand Up @@ -66,6 +67,14 @@ func Decode(rd io.Reader) (*Playlist, error) {
return p, fmt.Errorf("parse variant: %w", err)
}
p.Variants = append(p.Variants, *variant)

case tagIFrame:
variant, err := parseVariant(lex.items)
if err != nil {
return p, fmt.Errorf("parse i-frame variant: %w", err)
}
p.Iframes = append(p.Iframes, IFrameInfo(*variant))

case tagRendition:
rend, err := parseRendition(lex.items)
if err != nil {
Expand All @@ -88,7 +97,7 @@ func Decode(rd io.Reader) (*Playlist, error) {
}
p.TargetDuration = dur

case tagSegmentDuration, tagByteRange, tagKey:
case tagSegmentDuration, tagByteRange, tagKey, tagMap:
segment, err := parseSegment(lex.items, it)
if err != nil {
return p, fmt.Errorf("parse segment: %w", err)
Expand Down Expand Up @@ -122,7 +131,12 @@ func parseVariant(items chan item) (*Variant, error) {
switch it.typ {
case itemError:
return nil, errors.New(it.val)
case itemComma, itemNewline:
case itemComma:
continue
case itemNewline:
if v.URI != "" {
return &v, nil
}
continue
case itemURL:
v.URI = it.val
Expand All @@ -139,9 +153,23 @@ func parseVariant(items chan item) (*Variant, error) {
}

switch attr.val {
case "PROGRAM-ID", "NAME":
case "PROGRAM-ID":
// parsing PROGRAM-ID attribute unsupported; removed in HLS version 6
// NAME is non-standard, should be set in Rendition.
it = <-items
if it.typ != itemNumber {
return nil, fmt.Errorf("parse program-id: unexpected %s", it)
}
n, err := strconv.Atoi(it.val)
if err != nil {
return nil, fmt.Errorf("parse program-id: %w", err)
}
v.ProgramID = &n

case "NAME":
// just skip
<-items

case "BANDWIDTH", "AVERAGE-BANDWIDTH":
it = <-items
if it.typ != itemNumber {
Expand Down Expand Up @@ -205,7 +233,15 @@ func parseVariant(items chan item) (*Variant, error) {
if it.typ != itemString {
return nil, fmt.Errorf("parse closed-captions: unexpected %s", it)
}

v.ClosedCaptions = strings.Trim(it.val, `"`)
case "URI":
it = <-items
if it.typ != itemString {
return nil, fmt.Errorf("parse URI: unexpected %s", it)
}
v.URI = strings.Trim(it.val, `"`)

default:
return nil, fmt.Errorf("unknown attribute %s", attr.val)
}
Expand Down
11 changes: 7 additions & 4 deletions m3u8/segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ func parseSegment(items chan item, leading item) (*Segment, error) {
it = <-segItems
seg.Title = it.val


case tagByteRange:
it = <-segItems
r, err := parseByteRange(it.val)
bbValue := strings.Trim(it.val, `"`)
r, err := parseByteRange(bbValue)
if err != nil {
return nil, fmt.Errorf("parse byte range: %w", err)
}
Expand Down Expand Up @@ -219,6 +219,8 @@ func parseMap(items chan item) (Map, error) {
switch it.typ {
case itemError:
return mmap, errors.New(it.val)
case itemComma:
continue
case itemNewline:
return mmap, nil
}
Expand All @@ -236,7 +238,8 @@ func parseMap(items chan item) (Map, error) {
case "URI":
mmap.URI = strings.Trim(it.val, `"`)
case "BYTERANGE":
r, err := parseByteRange(it.val)
bbValue := strings.Trim(it.val, `"`)
r, err := parseByteRange(bbValue)
if err != nil {
return Map{}, fmt.Errorf("parse byte range: %w", err)
}
Expand Down Expand Up @@ -300,7 +303,7 @@ func (seg *Segment) MarshalText() ([]byte, error) {
// we do .03f for the same precision as test-streams.mux.dev.
durTag := fmt.Sprintf("%s:%.03f", tagSegmentDuration, float32(us)/1e6)
if seg.Title != "" {
durTag += ","+seg.Title
durTag += "," + seg.Title
}
tags = append(tags, durTag)
tags = append(tags, seg.URI)
Expand Down
16 changes: 16 additions & 0 deletions m3u8/write.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ func Encode(w io.Writer, p *Playlist) error {
}
}

for i, v := range p.Iframes {
if _, err := writeIframeVariant(w, &v); err != nil {
return fmt.Errorf("write i-frame variant %d: %w", i, err)
}
}

for i, sd := range p.SessionData {
if _, err := writeSessionData(w, sd); err != nil {
return fmt.Errorf("write session data %d: %w", i, err)
Expand All @@ -64,6 +70,16 @@ func writeVariant(w io.Writer, v *Variant) (n int, err error) {
return fmt.Fprintln(w, v)
}

func writeIframeVariant(w io.Writer, v *IFrameInfo) (n int, err error) {
if v.Bandwidth <= 0 {
return 0, fmt.Errorf("invalid bandwidth %d: must be larger than zero", v.Bandwidth)
}
if v.URI == "" {
return 0, fmt.Errorf("empty URI")
}
return fmt.Fprintln(w, v)
}

func writeDateRange(w io.Writer, dr *DateRange) error {
if dr.ID == "" {
return fmt.Errorf("empty ID")
Expand Down
37 changes: 37 additions & 0 deletions m3u8/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,40 @@ small.m3u8`,
})
}
}

func TestWrite(t *testing.T) {
var cases = []struct {
name string
v IFrameInfo
want string
valid bool
}{
{
"simple",
IFrameInfo{
URI: "0_hd_hls/0_hd_1000_IFRAME.m3u8",
Bandwidth: 41666,
},
`#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=41666,URI="0_hd_hls/0_hd_1000_IFRAME.m3u8"`,
true,
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
_, err := writeIframeVariant(buf, &tt.v)
if err != nil && tt.valid {
t.Fatalf("non-nil error for valid i-frame variant %v: %v", tt.v, err)
} else if !tt.valid && err == nil {
t.Fatalf("nil error for invalid i-frame variant %v: %v", tt.v, err)
}
got := strings.TrimSpace(buf.String()) // trim newline
if got != tt.want {
t.Errorf("unexpected i-frame variant text")
t.Logf("got: %s", got)
t.Logf("want: %s", tt.want)
}
})
}
}