commit 77cd9c1576a8929f7aed8cc9f81a8efca01130fd
parent 7c2872aa194394d29015373350eb6bafec4ab2ec
Author: Oliver Lowe <o@olowe.co>
Date: Fri, 17 May 2024 12:59:56 +1000
m3u8: implement parsing, byte ranges, endlist
With tests and new documentation for the corresponding struct fields.
Diffstat:
6 files changed, 88 insertions(+), 13 deletions(-)
diff --git a/m3u8/lex.go b/m3u8/lex.go
@@ -1,4 +1,4 @@
-package m3u82
+package m3u8
import (
"bufio"
@@ -217,6 +217,8 @@ func lexAttrs(l *lexer) stateFn {
return lexAttrs(l)
case r == '.':
return lexAttrValue(l)
+ case r == '@':
+ return lexAttrValue(l)
default:
return l.errorf("illegal character %q in attribute name", r)
}
@@ -230,6 +232,9 @@ func lexAttrValue(l *lexer) stateFn {
return lexNumber(l)
case '"':
return lexQString(l)
+ case '@':
+ // we're lexing a byte range, e.g. 69@420
+ return lexRawString(l)
}
if isTagNameChar(r) {
return lexRawString(l)
diff --git a/m3u8/lex_test.go b/m3u8/lex_test.go
@@ -1,4 +1,4 @@
-package m3u82
+package m3u8
import (
"os"
diff --git a/m3u8/m3u8.go b/m3u8/m3u8.go
@@ -1,6 +1,6 @@
// Package m3u8 implements reading and writing of m3u8 playlists
// used in HTTP Live Streaming (HLS) as specified in RFC 8216.
-package m3u82
+package m3u8
import (
"fmt"
@@ -33,15 +33,22 @@ type Playlist struct {
}
type Segment struct {
- URI string
+ URI string
// Duration of this specific segment from the #EXTINF tag.
- Duration time.Duration
- Range ByteRange
+ Duration time.Duration
+ // Indicates this segment holds a subset of the segment point to by URI.
+ // Range is the length of the subsegment from from the #EXT-X-BYTERANGE tag.
+ Range ByteRange
+ // If true, the preceding segment and the following segment
+ // are discontinuous. For example, this segment is part of a
+ // commercial break.
Discontinuity bool
- Key *Key
- Map *Map
- DateTime time.Time
- DateRange *DateRange
+ // Holds information on how to decrypt this segment.
+ // If nil, the segment is not encrypted.
+ Key *Key
+ Map *Map
+ DateTime time.Time
+ DateRange *DateRange
}
// A Key specifies how to decrypt encrypted playlist segments.
diff --git a/m3u8/parse.go b/m3u8/parse.go
@@ -1,4 +1,4 @@
-package m3u82
+package m3u8
import (
"errors"
@@ -16,6 +16,7 @@ const (
tagRendition = "#EXT-X-MEDIA"
tagPlaylistType = "#EXT-X-PLAYLIST-TYPE" // RFC 8216, 4.4.3.5
tagTargetDuration = "#EXT-X-TARGETDURATION" // RFC 8216, 4.4.3.1
+ tagEndList = "#EXT-X-ENDLIST" // RFC 8216, 4.4.3.4
)
func ParsePlaylist(rd io.Reader) (*Playlist, error) {
@@ -77,6 +78,8 @@ func ParsePlaylist(rd io.Reader) (*Playlist, error) {
return p, fmt.Errorf("parse segment: %w", err)
}
p.Segments = append(p.Segments, *segment)
+ case tagEndList:
+ p.End = true
}
}
}
@@ -347,3 +350,23 @@ func parseTargetDuration(it item) (time.Duration, error) {
}
return time.Duration(i) * time.Second, nil
}
+
+func parseByteRange(s string) (ByteRange, error) {
+ offset, until, found := strings.Cut(s, "@")
+ if !found {
+ n, err := strconv.Atoi(offset)
+ if err != nil {
+ return ByteRange{}, err
+ }
+ return ByteRange{n, 0}, nil
+ }
+ n, err := strconv.Atoi(offset)
+ if err != nil {
+ return ByteRange{}, err
+ }
+ nn, err := strconv.Atoi(until)
+ if err != nil {
+ return ByteRange{}, err
+ }
+ return ByteRange{n, nn}, nil
+}
diff --git a/m3u8/parse_test.go b/m3u8/parse_test.go
@@ -1,4 +1,4 @@
-package m3u82
+package m3u8
import (
"fmt"
@@ -31,3 +31,29 @@ func TestParseDuration(t *testing.T) {
t.Errorf("parseSegmentDuration(%s) = %s, want %s", it, dur, want)
}
}
+
+func TestParseByteRange(t *testing.T) {
+ var tests = []struct {
+ in string
+ want ByteRange
+ valid bool
+ }{
+ {"27@46", ByteRange{27, 46}, true},
+ {"69", ByteRange{69}, true},
+ {"732@", ByteRange{0, 0}, false},
+ {"@", ByteRange{0, 0}, false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.in, func(t *testing.T) {
+ r, err := parseByteRange(tt.in)
+ if err != nil && tt.valid {
+ t.Fatalf("parseByteRange(%s): %v", tt.in, err)
+ } else if err == nil && !tt.valid {
+ t.Fatalf("parseByteRange(%s): nil error on invalid byte range", tt.in)
+ }
+ if r != tt.want {
+ t.Errorf("parseByteRange(%s) = %v, want %v", tt.in, r, tt.want)
+ }
+ })
+ }
+}
diff --git a/m3u8/segment.go b/m3u8/segment.go
@@ -1,4 +1,4 @@
-package m3u82
+package m3u8
import (
"errors"
@@ -54,8 +54,22 @@ func parseSegment(items chan item, leading item) (*Segment, error) {
return nil, fmt.Errorf("parse segment duration: %w", err)
}
seg.Duration = dur
+ case tagByteRange:
+ it = <-items
+ if it.typ != itemString {
+ return nil, fmt.Errorf("parse byte range: got %s, want item type string", it)
+ }
+ r, err := parseByteRange(it.val)
+ if err != nil {
+ return nil, fmt.Errorf("parse byte range: %w", err)
+ }
+ seg.Range = r
case tagDiscontinuity:
seg.Discontinuity = true
+ case tagKey:
+ return nil, fmt.Errorf("parsing %s unsupported", it)
+ default:
+ return nil, fmt.Errorf("parsing %s unsupported", it)
}
}
}