Skip to content

Commit

Permalink
Merge pull request #138 from mjneil/custom
Browse files Browse the repository at this point in the history
Custom Tag Support
  • Loading branch information
leikao authored Aug 9, 2019
2 parents 920643e + b6f2ecc commit 036100c
Show file tree
Hide file tree
Showing 14 changed files with 733 additions and 12 deletions.
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ to the project. They listed below in an alphabetical order:
- Vishal Kumar Tuniki <[email protected]>
- Yevgen Flerko <[email protected]>
- Zac Shenker <[email protected]>

- Matthew Neil [mjneil](https://github.com/mjneil)

If you want to be added to this list (or removed for any reason)
just open an issue about it.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ ways to play HLS or handle playlists over HTTP. So library features are:
* Encryption keys support for use with DRM systems like [Verimatrix](http://verimatrix.com) etc.
* Support for non standard [Google Widevine](http://www.widevine.com) tags.

The library covered by BSD 3-clause license. See [LICENSE](LICENSE) for the full text.
The library covered by BSD 3-clause license. See [LICENSE](LICENSE) for the full text.
Versions 0.8 and below was covered by GPL v3. License was changed from the version 0.9 and upper.

See the list of the library authors at [AUTHORS](AUTHORS) file.
Expand Down Expand Up @@ -81,6 +81,11 @@ You may use API methods to fill structures or create them manually to generate p
fmt.Println(p.Encode().String())
```

Custom Tags
-----------

M3U8 supports parsing and writing of custom tags. You must implement both the `CustomTag` and `CustomDecoder` interface for each custom tag that may be encountered in the playlist. Look at the template files in `example/template/` for examples on parsing custom playlist and segment tags.

Library structure
-----------------

Expand Down
12 changes: 10 additions & 2 deletions example/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,27 @@ import (
"path"

"github.com/grafov/m3u8"
"github.com/grafov/m3u8/example/template"
)

func main() {
GOPATH := os.Getenv("GOPATH")
if GOPATH == "" {
panic("$GOPATH is empty")
}
m3u8File := "github.com/grafov/m3u8/sample-playlists/media-playlist-with-byterange.m3u8"

m3u8File := "github.com/grafov/m3u8/sample-playlists/media-playlist-with-custom-tags.m3u8"
f, err := os.Open(path.Join(GOPATH, "src", m3u8File))
if err != nil {
panic(err)
}
p, listType, err := m3u8.DecodeFrom(bufio.NewReader(f), true)

customTags := []m3u8.CustomDecoder{
&template.CustomPlaylistTag{},
&template.CustomSegmentTag{},
}

p, listType, err := m3u8.DecodeWith(bufio.NewReader(f), true, customTags)
if err != nil {
panic(err)
}
Expand Down
47 changes: 47 additions & 0 deletions example/template/custom-playlist-tag-template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package template

import (
"bytes"
"fmt"
"strconv"

"github.com/grafov/m3u8"
)

// #CUSTOM-PLAYLIST-TAG:<number>

// Implements both CustomTag and CustomDecoder interfaces
type CustomPlaylistTag struct {
Number int
}

// TagName() should return the full indentifier including the leading '#' and trailing ':'
// if the tag also contains a value or attribute list
func (tag *CustomPlaylistTag) TagName() string {
return "#CUSTOM-PLAYLIST-TAG:"
}

// line will be the entire matched line, including the identifier
func (tag *CustomPlaylistTag) Decode(line string) (m3u8.CustomTag, error) {
_, err := fmt.Sscanf(line, "#CUSTOM-PLAYLIST-TAG:%d", &tag.Number)

return tag, err
}

// This is a playlist tag example
func (tag *CustomPlaylistTag) SegmentTag() bool {
return false
}

func (tag *CustomPlaylistTag) Encode() *bytes.Buffer {
buf := new(bytes.Buffer)

buf.WriteString(tag.TagName())
buf.WriteString(strconv.Itoa(tag.Number))

return buf
}

func (tag *CustomPlaylistTag) String() string {
return tag.Encode().String()
}
75 changes: 75 additions & 0 deletions example/template/custom-segment-tag-template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package template

import (
"bytes"
"errors"

"github.com/grafov/m3u8"
)

// #CUSTOM-SEGMENT-TAG:<attribute-list>

// Implements both CustomTag and CustomDecoder interfaces
type CustomSegmentTag struct {
Name string
Jedi bool
}

// TagName() should return the full indentifier including the leading '#' and trailing ':'
// if the tag also contains a value or attribute list
func (tag *CustomSegmentTag) TagName() string {
return "#CUSTOM-SEGMENT-TAG:"
}

// line will be the entire matched line, including the identifier
func (tag *CustomSegmentTag) Decode(line string) (m3u8.CustomTag, error) {
var err error

// Since this is a Segment tag, we want to create a new tag every time it is decoded
// as there can be one for each segment with
newTag := new(CustomSegmentTag)

for k, v := range m3u8.DecodeAttributeList(line[20:]) {
switch k {
case "NAME":
newTag.Name = v
case "JEDI":
if v == "YES" {
newTag.Jedi = true
} else if v == "NO" {
newTag.Jedi = false
} else {
err = errors.New("Valid strings for JEDI attribute are YES and NO.")
}
}
}

return newTag, err
}

// This is a playlist tag example
func (tag *CustomSegmentTag) SegmentTag() bool {
return true
}

func (tag *CustomSegmentTag) Encode() *bytes.Buffer {
buf := new(bytes.Buffer)

if tag.Name != "" {
buf.WriteString(tag.TagName())
buf.WriteString("NAME=\"")
buf.WriteString(tag.Name)
buf.WriteString("\",JEDI=")
if tag.Jedi {
buf.WriteString("YES")
} else {
buf.WriteString("NO")
}
}

return buf
}

func (tag *CustomSegmentTag) String() string {
return tag.Encode().String()
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/grafov/m3u8

go 1.12
112 changes: 105 additions & 7 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ func (p *MasterPlaylist) DecodeFrom(reader io.Reader, strict bool) error {
return p.decode(buf, strict)
}

// WithCustomDecoders adds custom tag decoders to the master playlist for decoding
func (p *MasterPlaylist) WithCustomDecoders(customDecoders []CustomDecoder) Playlist {
// Create the map if it doesn't already exist
if p.Custom == nil {
p.Custom = make(map[string]CustomTag)
}

p.customDecoders = customDecoders

return p
}

// Parse master playlist. Internal function.
func (p *MasterPlaylist) decode(buf *bytes.Buffer, strict bool) error {
var eof bool
Expand Down Expand Up @@ -90,6 +102,18 @@ func (p *MediaPlaylist) DecodeFrom(reader io.Reader, strict bool) error {
return p.decode(buf, strict)
}

// WithCustomDecoders adds custom tag decoders to the media playlist for decoding
func (p *MediaPlaylist) WithCustomDecoders(customDecoders []CustomDecoder) Playlist {
// Create the map if it doesn't already exist
if p.Custom == nil {
p.Custom = make(map[string]CustomTag)
}

p.customDecoders = customDecoders

return p
}

func (p *MediaPlaylist) decode(buf *bytes.Buffer, strict bool) error {
var eof bool
var line string
Expand Down Expand Up @@ -123,7 +147,7 @@ func (p *MediaPlaylist) decode(buf *bytes.Buffer, strict bool) error {
// Decode detects type of playlist and decodes it. It accepts bytes
// buffer as input.
func Decode(data bytes.Buffer, strict bool) (Playlist, ListType, error) {
return decode(&data, strict)
return decode(&data, strict, nil)
}

// DecodeFrom detects type of playlist and decodes it. It accepts data
Expand All @@ -134,12 +158,30 @@ func DecodeFrom(reader io.Reader, strict bool) (Playlist, ListType, error) {
if err != nil {
return nil, 0, err
}
return decode(buf, strict)
return decode(buf, strict, nil)
}

// DecodeWith detects the type of playlist and decodes it. It accepts either bytes.Buffer
// or io.Reader as input. Any custom decoders provided will be used during decoding.
func DecodeWith(input interface{}, strict bool, customDecoders []CustomDecoder) (Playlist, ListType, error) {
switch v := input.(type) {
case bytes.Buffer:
return decode(&v, strict, customDecoders)
case io.Reader:
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(v)
if err != nil {
return nil, 0, err
}
return decode(buf, strict, customDecoders)
default:
return nil, 0, errors.New("input must be bytes.Buffer or io.Reader type")
}
}

// Detect playlist type and decode it. May be used as decoder for both
// master and media playlists.
func decode(buf *bytes.Buffer, strict bool) (Playlist, ListType, error) {
func decode(buf *bytes.Buffer, strict bool, customDecoders []CustomDecoder) (Playlist, ListType, error) {
var eof bool
var line string
var master *MasterPlaylist
Expand All @@ -156,6 +198,13 @@ func decode(buf *bytes.Buffer, strict bool) (Playlist, ListType, error) {
return nil, 0, fmt.Errorf("Create media playlist failed: %s", err)
}

// If we have custom tags to parse
if customDecoders != nil {
media = media.WithCustomDecoders(customDecoders).(*MediaPlaylist)
master = master.WithCustomDecoders(customDecoders).(*MasterPlaylist)
state.custom = make(map[string]CustomTag)
}

for !eof {
if line, err = buf.ReadString('\n'); err == io.EOF {
eof = true
Expand Down Expand Up @@ -202,6 +251,12 @@ func decode(buf *bytes.Buffer, strict bool) (Playlist, ListType, error) {
return nil, state.listType, errors.New("Can't detect playlist type")
}

// DecodeAttributeList turns an attribute list into a key, value map. You should trim
// any characters not part of the attribute list, such as the tag and ':'.
func DecodeAttributeList(line string) map[string]string {
return decodeParamsLine(line)
}

func decodeParamsLine(line string) map[string]string {
out := make(map[string]string)
for _, kv := range reKeyValue.FindAllStringSubmatch(line, -1) {
Expand All @@ -217,6 +272,21 @@ func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line st

line = strings.TrimSpace(line)

// check for custom tags first to allow custom parsing of existing tags
if p.Custom != nil {
for _, v := range p.customDecoders {
if strings.HasPrefix(line, v.TagName()) {
t, err := v.Decode(line)

if strict && err != nil {
return err
}

p.Custom[t.TagName()] = t
}
}
}

switch {
case line == "#EXTM3U": // start tag first
state.m3u = true
Expand Down Expand Up @@ -369,8 +439,8 @@ func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line st
state.variant.HDCPLevel = v
}
}
case strings.HasPrefix(line, "#"): // unknown tags treated as comments
return err
case strings.HasPrefix(line, "#"):
// comments are ignored
}
return err
}
Expand All @@ -380,6 +450,27 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
var err error

line = strings.TrimSpace(line)

// check for custom tags first to allow custom parsing of existing tags
if p.Custom != nil {
for _, v := range p.customDecoders {
if strings.HasPrefix(line, v.TagName()) {
t, err := v.Decode(line)

if strict && err != nil {
return err
}

if v.SegmentTag() {
state.tagCustom = true
state.custom[v.TagName()] = t
} else {
p.Custom[v.TagName()] = t
}
}
}
}

switch {
case !state.tagInf && strings.HasPrefix(line, "#EXTINF:"):
state.tagInf = true
Expand Down Expand Up @@ -463,6 +554,13 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
}
state.tagMap = false
}

// if segment custom tag appeared before EXTINF then it links to this segment
if state.tagCustom {
p.Segments[p.last()].Custom = state.custom
state.custom = make(map[string]CustomTag)
state.tagCustom = false
}
// start tag first
case line == "#EXTM3U":
state.m3u = true
Expand Down Expand Up @@ -717,8 +815,8 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
if err == nil {
state.tagWV = true
}
case strings.HasPrefix(line, "#"): // unknown tags treated as comments
return err
case strings.HasPrefix(line, "#"):
// comments are ignored
}
return err
}
Expand Down
Loading

0 comments on commit 036100c

Please sign in to comment.