Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve playlist parser #186

Merged
merged 1 commit into from
Oct 2, 2024
Merged
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: 2 additions & 2 deletions client_primary_downloader.go
Original file line number Diff line number Diff line change
@@ -222,8 +222,8 @@ func (d *clientPrimaryDownloader) run(ctx context.Context) error {
return fmt.Errorf("audio playlist with id \"%s\" not found", leadingPlaylist.Audio)
}

if audioPlaylist.URI != "" {
u, err = clientAbsoluteURL(d.primaryPlaylistURL, audioPlaylist.URI)
if audioPlaylist.URI != nil {
u, err = clientAbsoluteURL(d.primaryPlaylistURL, *audioPlaylist.URI)
if err != nil {
return err
}
2 changes: 1 addition & 1 deletion muxer_stream.go
Original file line number Diff line number Diff line change
@@ -159,7 +159,7 @@
r := &playlist.MultivariantRendition{
Type: playlist.MultivariantRenditionTypeAudio,
GroupID: "audio",
URI: uri,
URI: &uri,

Check warning on line 162 in muxer_stream.go

Codecov / codecov/patch

muxer_stream.go#L162

Added line #L162 was not covered by tests
}
pl.Renditions = append(pl.Renditions, r)
}
39 changes: 21 additions & 18 deletions pkg/playlist/media.go
Original file line number Diff line number Diff line change
@@ -42,57 +42,57 @@ const (

// Media is a media playlist.
type Media struct {
// #EXT-X-VERSION
// EXT-X-VERSION
// required
Version int

// #EXT-X-INDEPENDENT-SEGMENTS
// EXT-X-INDEPENDENT-SEGMENTS
IndependentSegments bool

// #EXT-X-START
// EXT-X-START
Start *MediaStart

// #EXT-X-ALLOWCACHE
// EXT-X-ALLOWCACHE
// removed since v7
AllowCache *bool

// #EXT-X-TARGETDURATION
// EXT-X-TARGETDURATION
// required
TargetDuration int

// #EXT-X-SERVER-CONTROL
// EXT-X-SERVER-CONTROL
ServerControl *MediaServerControl

// #EXT-X-PART-INF
// EXT-X-PART-INF
PartInf *MediaPartInf

// #EXT-X-MEDIA-SEQUENCE
// EXT-X-MEDIA-SEQUENCE
// required
MediaSequence int

// #EXT-X-DISCONTINUITY-SEQUENCE
// EXT-X-DISCONTINUITY-SEQUENCE
DiscontinuitySequence *int

// #EXT-X-PLAYLIST-TYPE
// EXT-X-PLAYLIST-TYPE
PlaylistType *MediaPlaylistType

// #EXT-X-MAP
// EXT-X-MAP
Map *MediaMap

// #EXT-X-SKIP
// EXT-X-SKIP
Skip *MediaSkip

// segments
// at least one is required
Segments []*MediaSegment

// #EXT-X-PART
// EXT-X-PART
Parts []*MediaPart

// #EXT-X-PRELOAD-HINT
// EXT-X-PRELOAD-HINT
PreloadHint *MediaPreloadHint

// #EXT-X-ENDLIST
// EXT-X-ENDLIST
Endlist bool
}

@@ -233,6 +233,12 @@ func (m *Media) Unmarshal(buf []byte) error {
return err
}

case line == "#EXT-X-DISCONTINUITY":
curSegment.Discontinuity = true

case line == "#EXT-X-GAP":
curSegment.Gap = true

case strings.HasPrefix(line, "#EXT-X-PROGRAM-DATE-TIME:"):
line = line[len("#EXT-X-PROGRAM-DATE-TIME:"):]

@@ -244,9 +250,6 @@ func (m *Media) Unmarshal(buf []byte) error {

curSegment.DateTime = &tmp

case line == "#EXT-X-GAP":
curSegment.Gap = true

case strings.HasPrefix(line, "#EXT-X-BITRATE:"):
line = line[len("#EXT-X-BITRATE:"):]

29 changes: 18 additions & 11 deletions pkg/playlist/media_segment.go
Original file line number Diff line number Diff line change
@@ -10,29 +10,32 @@ import (

// MediaSegment is a segment of a media playlist.
type MediaSegment struct {
// #EXTINF
// EXTINF
// required
Duration time.Duration
Title string

// segment URI.
// URI.
// required
URI string

// #EXT-X-PROGRAM-DATE-TIME
DateTime *time.Time // optional
// EXT-X-DISCONTINUITY
Discontinuity bool

// #EXT-X-GAP
Gap bool // optional
// EXT-X-GAP
Gap bool

// #EXT-X-BITRATE
// EXT-X-PROGRAM-DATE-TIME
DateTime *time.Time

// EXT-X-BITRATE
Bitrate *int

// #EXT-X-BYTERANGE
// EXT-X-BYTERANGE
ByteRangeLength *uint64
ByteRangeStart *uint64

// #EXT-X-PART
// EXT-X-PART
Parts []*MediaPart
}

@@ -50,14 +53,18 @@ func (s MediaSegment) validate() error {
func (s MediaSegment) marshal() string {
ret := ""

if s.DateTime != nil {
ret += "#EXT-X-PROGRAM-DATE-TIME:" + s.DateTime.Format(timeRFC3339Millis) + "\n"
if s.Discontinuity {
ret += "#EXT-X-DISCONTINUITY\n"
}

if s.Gap {
ret += "#EXT-X-GAP\n"
}

if s.DateTime != nil {
ret += "#EXT-X-PROGRAM-DATE-TIME:" + s.DateTime.Format(timeRFC3339Millis) + "\n"
}

if s.Bitrate != nil {
ret += "#EXT-X-BITRATE:" + strconv.FormatInt(int64(*s.Bitrate), 10) + "\n"
}
11 changes: 7 additions & 4 deletions pkg/playlist/media_test.go
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ var casesMedia = []struct {
"#EXT-X-PROGRAM-DATE-TIME:2014-08-25T00:00:00Z\n" +
"#EXTINF:2.00000,\n" +
"seg1.mp4\n" +
"#EXT-X-DISCONTINUITY\n" +
"#EXT-X-PROGRAM-DATE-TIME:2014-08-25T00:00:00Z\n" +
"#EXT-X-BITRATE:14213213\n" +
"#EXT-X-PART:DURATION=1.50000,URI=\"part1.mp4\",INDEPENDENT=YES\n" +
@@ -60,6 +61,7 @@ var casesMedia = []struct {
"#EXT-X-PROGRAM-DATE-TIME:2014-08-25T00:00:00Z\n" +
"#EXTINF:2.00000,\n" +
"seg1.mp4\n" +
"#EXT-X-DISCONTINUITY\n" +
"#EXT-X-PROGRAM-DATE-TIME:2014-08-25T00:00:00Z\n" +
"#EXT-X-BITRATE:14213213\n" +
"#EXT-X-PART:DURATION=1.50000,URI=\"part1.mp4\",INDEPENDENT=YES\n" +
@@ -101,10 +103,11 @@ var casesMedia = []struct {
URI: "seg1.mp4",
},
{
DateTime: timePtr(time.Date(2014, 8, 25, 0, 0, 0, 0, time.UTC)),
Bitrate: intPtr(14213213),
Duration: 3 * time.Second,
URI: "seg2.mp4",
DateTime: timePtr(time.Date(2014, 8, 25, 0, 0, 0, 0, time.UTC)),
Bitrate: intPtr(14213213),
Duration: 3 * time.Second,
URI: "seg2.mp4",
Discontinuity: true,
Parts: []*MediaPart{
{
Duration: 1500 * time.Millisecond,
10 changes: 5 additions & 5 deletions pkg/playlist/multivariant.go
Original file line number Diff line number Diff line change
@@ -10,21 +10,21 @@ import (

// Multivariant is a multivariant playlist.
type Multivariant struct {
// #EXT-X-VERSION
// EXT-X-VERSION
// required
Version int

// #EXT-X-INDEPENDENT-SEGMENTS
// EXT-X-INDEPENDENT-SEGMENTS
IndependentSegments bool

// #EXT-X-START
// EXT-X-START
Start *MultivariantStart

// #EXT-X-STREAM-INF
// EXT-X-STREAM-INF
// at least one is required
Variants []*MultivariantVariant

// #EXT-X-MEDIA
// EXT-X-MEDIA
Renditions []*MultivariantRendition
}

80 changes: 44 additions & 36 deletions pkg/playlist/multivariant_rendition.go
Original file line number Diff line number Diff line change
@@ -27,32 +27,33 @@
// required
GroupID string

// URI
// required for all types except CLOSED-CAPTIONS
URI string

// INSTREAM-ID
// required for CLOSED-CAPTIONS
InstreamID string

// NAME
// required
Name string

// LANGUAGE
Language string

// DEFAULT
Default bool

// AUTOSELECT
Autoselect bool

// DEFAULT
Default bool

// FORCED
Forced *bool
Forced bool

// CHANNELS
Channels string
// for AUDIO only
Channels *string

// URI
// must not be present for CLOSED-CAPTIONS
URI *string

// INSTREAM-ID
// for CLOSED-CAPTIONS only
InStreamID *string
}

func (t *MultivariantRendition) unmarshal(v string) error {
@@ -89,17 +90,19 @@
t.Autoselect = (val == "YES")

case "FORCED":
v := (val == "YES")
t.Forced = &v
t.Forced = (val == "YES")

case "CHANNELS":
t.Channels = val
v := val
t.Channels = &v

case "URI":
t.URI = val
v := val
t.URI = &v

case "INSTREAM-ID":
t.InstreamID = val
v := val
t.InStreamID = &v
}
}

@@ -117,12 +120,12 @@
// type is SUBTITLES, but OPTIONAL if the media type is VIDEO or AUDIO.
switch t.Type {
case MultivariantRenditionTypeClosedCaptions:
if t.URI != "" {
if t.URI != nil {
return fmt.Errorf("URI is forbidden for type CLOSED-CAPTIONS")
}

case MultivariantRenditionTypeSubtitles:
if t.URI == "" {
if t.URI == nil {
return fmt.Errorf("URI is required for type SUBTITLES")
}

@@ -132,11 +135,17 @@
// This attribute is REQUIRED if the TYPE attribute is CLOSED-CAPTIONS
// For all other TYPE values, the INSTREAM-ID MUST NOT be specified.
if t.Type == MultivariantRenditionTypeClosedCaptions {
if t.InstreamID == "" {
if t.InStreamID == nil {
return fmt.Errorf("missing INSTREAM-ID")
}
} else if t.InstreamID != "" {
return fmt.Errorf("INSTREAM-ID is forbidden with type %s", t.Type)
} else if t.InStreamID != nil {
return fmt.Errorf("INSTREAM-ID is forbidden for type %s", t.Type)
}

// The CHANNELS attribute MUST NOT be present unless the TYPE is
// AUDIO.
if t.Channels != nil && t.Type != MultivariantRenditionTypeAudio {
return fmt.Errorf("CHANNELS is forbidden for type %s", t.Type)

Check warning on line 148 in pkg/playlist/multivariant_rendition.go

Codecov / codecov/patch

pkg/playlist/multivariant_rendition.go#L148

Added line #L148 was not covered by tests
}

return nil
@@ -153,29 +162,28 @@
ret += ",NAME=\"" + t.Name + "\""
}

if t.Autoselect {
ret += ",AUTOSELECT=YES"
}

if t.Default {
ret += ",DEFAULT=YES"
}

if t.Autoselect {
ret += ",AUTOSELECT=YES"
if t.Forced {
ret += ",FORCED=YES"

Check warning on line 174 in pkg/playlist/multivariant_rendition.go

Codecov / codecov/patch

pkg/playlist/multivariant_rendition.go#L174

Added line #L174 was not covered by tests
}

if t.Forced != nil {
ret += ",FORCED="
if *t.Forced {
ret += "YES"
} else {
ret += "NO"
}
if t.Channels != nil {
ret += ",CHANNELS=\"" + *t.Channels + "\""
}

if t.Channels != "" {
ret += ",CHANNELS=\"" + t.Channels + "\""
if t.URI != nil {
ret += ",URI=\"" + *t.URI + "\""
}

if t.URI != "" {
ret += ",URI=\"" + t.URI + "\""
if t.InStreamID != nil {
ret += ",INSTREAM-ID=\"" + *t.InStreamID + "\""
}

ret += "\n"
52 changes: 28 additions & 24 deletions pkg/playlist/multivariant_test.go
Original file line number Diff line number Diff line change
@@ -32,6 +32,10 @@ func timePtr(v time.Time) *time.Time {
return &v
}

func stringPtr(v string) *string {
return &v
}

var casesMultivariant = []struct {
name string
input string
@@ -53,18 +57,18 @@ var casesMultivariant = []struct {
"stream2.m3u8\n" +
"\n" +
"#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"english\"" +
",DEFAULT=YES,AUTOSELECT=YES,CHANNELS=\"2\",URI=\"audio.m3u8\"\n" +
",AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"audio.m3u8\"\n" +
"#EXT-X-MEDIA:TYPE=\"SUBTITLES\",GROUP-ID=\"sub1\",LANGUAGE=\"en\",NAME=\"english\"" +
",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,URI=\"sub.m3u8\"\n",
",AUTOSELECT=YES,DEFAULT=YES,FORCED=NO,URI=\"sub.m3u8\"\n",
"#EXTM3U\n" +
"#EXT-X-VERSION:9\n" +
"#EXT-X-INDEPENDENT-SEGMENTS\n" +
"#EXT-X-START:TIME-OFFSET=15.00000\n" +
"\n" +
"#EXT-X-MEDIA:TYPE=\"AUDIO\",GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"english\"" +
",DEFAULT=YES,AUTOSELECT=YES,CHANNELS=\"2\",URI=\"audio.m3u8\"\n" +
",AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"audio.m3u8\"\n" +
"#EXT-X-MEDIA:TYPE=\"SUBTITLES\",GROUP-ID=\"sub1\",LANGUAGE=\"en\",NAME=\"english\"" +
",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,URI=\"sub.m3u8\"\n" +
",AUTOSELECT=YES,DEFAULT=YES,URI=\"sub.m3u8\"\n" +
"\n" +
"#EXT-X-STREAM-INF:BANDWIDTH=155000,AVERAGE-BANDWIDTH=120000,CODECS=\"avc1.42c028,mp4a.40.2\"" +
",RESOLUTION=1280x720,FRAME-RATE=24.000,AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n" +
@@ -107,23 +111,23 @@ var casesMultivariant = []struct {
Renditions: []*MultivariantRendition{
{
Type: MultivariantRenditionTypeAudio,
URI: "audio.m3u8",
URI: stringPtr("audio.m3u8"),
GroupID: "aud1",
Language: "en",
Name: "english",
Autoselect: true,
Default: true,
Channels: "2",
Channels: stringPtr("2"),
},
{
Type: MultivariantRenditionTypeSubtitles,
URI: "sub.m3u8",
URI: stringPtr("sub.m3u8"),
GroupID: "sub1",
Language: "en",
Name: "english",
Autoselect: true,
Default: true,
Forced: boolPtr(false),
Forced: false,
},
},
},
@@ -204,11 +208,11 @@ v2/prog_index.m3u8
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE="AUDIO",GROUP-ID="aud1",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="a1/prog_index.m3u8"
#EXT-X-MEDIA:TYPE="AUDIO",GROUP-ID="aud2",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="6",URI="a2/prog_index.m3u8"
#EXT-X-MEDIA:TYPE="AUDIO",GROUP-ID="aud3",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="6",URI="a3/prog_index.m3u8"
#EXT-X-MEDIA:TYPE="CLOSED-CAPTIONS",GROUP-ID="cc1",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES
#EXT-X-MEDIA:TYPE="SUBTITLES",GROUP-ID="sub1",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,URI="s1/en/prog_index.m3u8"
#EXT-X-MEDIA:TYPE="AUDIO",GROUP-ID="aud1",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="a1/prog_index.m3u8"
#EXT-X-MEDIA:TYPE="AUDIO",GROUP-ID="aud2",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="6",URI="a2/prog_index.m3u8"
#EXT-X-MEDIA:TYPE="AUDIO",GROUP-ID="aud3",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="6",URI="a3/prog_index.m3u8"
#EXT-X-MEDIA:TYPE="CLOSED-CAPTIONS",GROUP-ID="cc1",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,INSTREAM-ID="CC1"
#EXT-X-MEDIA:TYPE="SUBTITLES",GROUP-ID="sub1",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="s1/en/prog_index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=2177116,AVERAGE-BANDWIDTH=2168183,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v5/prog_index.m3u8
@@ -603,33 +607,33 @@ v2/prog_index.m3u8
Renditions: []*MultivariantRendition{
{
Type: MultivariantRenditionTypeAudio,
URI: "a1/prog_index.m3u8",
URI: stringPtr("a1/prog_index.m3u8"),
GroupID: "aud1",
Language: "en",
Name: "English",
Default: true,
Autoselect: true,
Channels: "2",
Channels: stringPtr("2"),
},
{
Type: MultivariantRenditionTypeAudio,
URI: "a2/prog_index.m3u8",
URI: stringPtr("a2/prog_index.m3u8"),
GroupID: "aud2",
Language: "en",
Name: "English",
Default: true,
Autoselect: true,
Channels: "6",
Channels: stringPtr("6"),
},
{
Type: MultivariantRenditionTypeAudio,
URI: "a3/prog_index.m3u8",
URI: stringPtr("a3/prog_index.m3u8"),
GroupID: "aud3",
Language: "en",
Name: "English",
Default: true,
Autoselect: true,
Channels: "6",
Channels: stringPtr("6"),
},
{
Type: MultivariantRenditionTypeClosedCaptions,
@@ -638,17 +642,17 @@ v2/prog_index.m3u8
Name: "English",
Default: true,
Autoselect: true,
InstreamID: "CC1",
InStreamID: stringPtr("CC1"),
},
{
Type: MultivariantRenditionTypeSubtitles,
URI: "s1/en/prog_index.m3u8",
URI: stringPtr("s1/en/prog_index.m3u8"),
GroupID: "sub1",
Language: "en",
Name: "English",
Default: true,
Autoselect: true,
Forced: boolPtr(false),
Forced: false,
},
},
},
@@ -795,13 +799,13 @@ QualityLevels(5977913)/Manifest(video,format=m3u8-aapl)
{
Type: MultivariantRenditionTypeAudio,
GroupID: "audio",
URI: "QualityLevels(125615)/Manifest(AAC_und_ch2_128kbps,format=m3u8-aapl)",
URI: stringPtr("QualityLevels(125615)/Manifest(AAC_und_ch2_128kbps,format=m3u8-aapl)"),
Name: "AAC_und_ch2_128kbps",
},
{
Type: MultivariantRenditionTypeAudio,
GroupID: "audio",
URI: "QualityLevels(53620)/Manifest(AAC_und_ch2_56kbps,format=m3u8-aapl)",
URI: stringPtr("QualityLevels(53620)/Manifest(AAC_und_ch2_56kbps,format=m3u8-aapl)"),
Name: "AAC_und_ch2_56kbps",
Default: true,
},