Skip to content

Commit 268d753

Browse files
committed
Add DecodePlaylist function and Playlist interface
1 parent 2c95b62 commit 268d753

File tree

6 files changed

+228
-2
lines changed

6 files changed

+228
-2
lines changed

Diff for: examples/decode/decode.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
m3u8 "github.com/abema/go-simple-m3u8"
8+
)
9+
10+
const sampleData = `#EXTM3U
11+
#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000
12+
http://example.com/low.m3u8
13+
#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000
14+
http://example.com/mid.m3u8
15+
#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000
16+
http://example.com/hi.m3u8
17+
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
18+
http://example.com/audio-only.m3u8
19+
`
20+
21+
func main() {
22+
playlist, err := m3u8.DecodePlaylist(strings.NewReader(sampleData))
23+
if err != nil {
24+
panic(err)
25+
}
26+
fmt.Println("Type:", playlist.Type())
27+
fmt.Println("Tags:", len(playlist.Master().Tags))
28+
for name, values := range playlist.Master().Tags {
29+
fmt.Printf(" %s: %d\n", name, len(values))
30+
}
31+
fmt.Println("Streams:")
32+
for i, stream := range playlist.Master().Streams {
33+
fmt.Printf(" %d:\n", i)
34+
height, width, _ := stream.Attributes.Resolution()
35+
fmt.Println(" Height:", height)
36+
fmt.Println(" Width:", width)
37+
fmt.Println(" URI:", stream.URI)
38+
}
39+
}
40+
41+
/* Output:
42+
Type: master
43+
Tags: 1
44+
EXTM3U: 1
45+
Streams:
46+
0:
47+
Height: 0
48+
Width: 0
49+
URI: http://example.com/low.m3u8
50+
1:
51+
Height: 0
52+
Width: 0
53+
URI: http://example.com/mid.m3u8
54+
2:
55+
Height: 0
56+
Width: 0
57+
URI: http://example.com/hi.m3u8
58+
3:
59+
Height: 0
60+
Width: 0
61+
URI: http://example.com/audio-only.m3u8
62+
*/

Diff for: master_playlist.go

+17
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,20 @@ func (playlist *MasterPlaylist) Encode(w io.Writer) error {
133133
}
134134
return nil
135135
}
136+
137+
// Type returns the type of the playlist.
138+
func (playlist *MasterPlaylist) Type() PlaylistType {
139+
return PlaylistTypeMaster
140+
}
141+
142+
// Master returns the master playlist.
143+
// If the playlist is not a master playlist, it returns nil.
144+
func (playlist *MasterPlaylist) Master() *MasterPlaylist {
145+
return playlist
146+
}
147+
148+
// Media returns the media playlist.
149+
// If the playlist is not a media playlist, it returns nil.
150+
func (playlist *MasterPlaylist) Media() *MediaPlaylist {
151+
return nil
152+
}

Diff for: media_playlist.go

+17
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,20 @@ func (playlist *MediaPlaylist) Encode(w io.Writer) error {
121121
}
122122
return nil
123123
}
124+
125+
// Type returns the type of the playlist.
126+
func (playlist *MediaPlaylist) Type() PlaylistType {
127+
return PlaylistTypeMedia
128+
}
129+
130+
// Master returns the master playlist.
131+
// If the playlist is not a master playlist, it returns nil.
132+
func (playlist *MediaPlaylist) Master() *MasterPlaylist {
133+
return nil
134+
}
135+
136+
// Media returns the media playlist.
137+
// If the playlist is not a media playlist, it returns nil.
138+
func (playlist *MediaPlaylist) Media() *MediaPlaylist {
139+
return playlist
140+
}

Diff for: playlist.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package m3u8
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"io"
7+
)
8+
9+
// PlaylistType represents the type of playlist.
10+
type PlaylistType string
11+
12+
const (
13+
// PlaylistTypeMaster represents a master playlist.
14+
PlaylistTypeMaster PlaylistType = "master"
15+
// PlaylistTypeMedia represents a media playlist.
16+
PlaylistTypeMedia PlaylistType = "media"
17+
)
18+
19+
type Playlist interface {
20+
// Encode encodes the playlist to io.Writer.
21+
Encode(w io.Writer) error
22+
23+
// Type returns the type of the playlist.
24+
Type() PlaylistType
25+
26+
// Master returns the master playlist.
27+
// If the playlist is not a master playlist, it returns nil.
28+
Master() *MasterPlaylist
29+
30+
// Media returns the media playlist.
31+
// If the playlist is not a media playlist, it returns nil.
32+
Media() *MediaPlaylist
33+
}
34+
35+
// DecodePlaylist detects the type of playlist and decodes it from io.Reader.
36+
func DecodePlaylist(r io.Reader) (Playlist, error) {
37+
data, err := io.ReadAll(r)
38+
if err != nil {
39+
return nil, err
40+
}
41+
br := bytes.NewReader(data)
42+
43+
var masterPlaylistTagCount int
44+
var mediaPlaylistTagCount int
45+
scanner := bufio.NewScanner(br)
46+
for scanner.Scan() {
47+
line := scanner.Text()
48+
if line == "" {
49+
continue
50+
}
51+
tagName := TagName(line)
52+
if isMasterPlaylistTag(tagName) {
53+
masterPlaylistTagCount++
54+
} else if isMediaPlaylistTag(tagName) || IsSegmentTagName(tagName) {
55+
mediaPlaylistTagCount++
56+
}
57+
}
58+
if err := scanner.Err(); err != nil {
59+
return nil, err
60+
}
61+
62+
br.Seek(0, io.SeekStart)
63+
if masterPlaylistTagCount >= mediaPlaylistTagCount {
64+
return DecodeMasterPlaylist(br)
65+
} else {
66+
return DecodeMediaPlaylist(br)
67+
}
68+
}

Diff for: playlist_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package m3u8
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestDecodePlaylist(t *testing.T) {
13+
for idx, testData := range []struct {
14+
input string
15+
output string
16+
playlistType PlaylistType
17+
}{
18+
{input: sampleMaster01Input, output: sampleMaster01Output, playlistType: PlaylistTypeMaster},
19+
{input: sampleMaster02Input, output: sampleMaster02Output, playlistType: PlaylistTypeMaster},
20+
{input: sampleMedia01Input, output: sampleMedia01Output, playlistType: PlaylistTypeMedia},
21+
{input: sampleMedia02Input, output: sampleMedia02Output, playlistType: PlaylistTypeMedia},
22+
} {
23+
t.Run(fmt.Sprintf("%d", idx), func(t *testing.T) {
24+
r := bytes.NewReader([]byte(testData.input))
25+
playlist, err := DecodePlaylist(r)
26+
require.NoError(t, err)
27+
require.Equal(t, playlist.Type(), testData.playlistType)
28+
require.Equal(t, playlist.Master() != nil, testData.playlistType == PlaylistTypeMaster)
29+
require.Equal(t, playlist.Media() != nil, testData.playlistType == PlaylistTypeMedia)
30+
w := bytes.NewBuffer(nil)
31+
require.NoError(t, playlist.Encode(w))
32+
assert.Equal(t, testData.output, w.String())
33+
})
34+
}
35+
}

Diff for: tags.go

+29-2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,23 @@ func getTagOrder(name string) int {
102102
return math.MaxInt
103103
}
104104

105+
var masterPlaylistTagSet = map[string]struct{}{
106+
TagExtXMedia: {},
107+
TagExtXStreamInf: {},
108+
TagExtXIFrameStreamInf: {},
109+
TagExtXSessionData: {},
110+
TagExtXSessionKey: {},
111+
}
112+
113+
var mediaPlaylistTagSet = map[string]struct{}{
114+
TagExtXTargetDuration: {},
115+
TagExtXPlaylistType: {},
116+
TagExtXIFramesOnly: {},
117+
TagExtXMediaSequence: {},
118+
TagExtXDiscontinuitySequence: {},
119+
TagExtXEndlist: {},
120+
}
121+
105122
var segmentTagSet = map[string]struct{}{
106123
TagExtInf: {},
107124
TagExtXByteRange: {},
@@ -143,10 +160,20 @@ func AttributeString(line string) string {
143160
return line[idx+1:]
144161
}
145162

163+
func isMasterPlaylistTag(name string) bool {
164+
_, ok := masterPlaylistTagSet[name]
165+
return ok
166+
}
167+
168+
func isMediaPlaylistTag(name string) bool {
169+
_, ok := mediaPlaylistTagSet[name]
170+
return ok
171+
}
172+
146173
// IsSegmentTagName returns true if the name is a segment tag name.
147174
func IsSegmentTagName(name string) bool {
148-
_, t := segmentTagSet[name]
149-
return t
175+
_, ok := segmentTagSet[name]
176+
return ok
150177
}
151178

152179
// Attributes represents a set of attributes of a tag.

0 commit comments

Comments
 (0)