Skip to content
This repository has been archived by the owner on Dec 8, 2024. It is now read-only.

support #EXTINF tag with attributes on strict mode #208

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
57 changes: 52 additions & 5 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import (
"strconv"
"strings"
"time"
"unicode"
)

var reKeyValue = regexp.MustCompile(`([a-zA-Z0-9_-]+)=("[^"]+"|[^",]+)`)

// TimeParse allows globally apply and/or override Time Parser function.
// Available variants:
// * FullTimeParse - implements full featured ISO/IEC 8601:2004
// * StrictTimeParse - implements only RFC3339 Nanoseconds format
// - FullTimeParse - implements full featured ISO/IEC 8601:2004
// - StrictTimeParse - implements only RFC3339 Nanoseconds format
var TimeParse func(value string) (time.Time, error) = FullTimeParse

// Decode parses a master playlist passed from the buffer. If `strict`
Expand Down Expand Up @@ -482,18 +483,59 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
}
sepIndex = len(line)
}
duration := line[8:sepIndex]
durationRe := regexp.MustCompile(`^(-?\d+\.?\d*)`)
if len(durationRe.FindStringSubmatch(line[8:sepIndex])) != 2 {
return errors.New("Duration parsing error: duration not found")
}
duration := durationRe.FindStringSubmatch(line[8:sepIndex])[1]
if len(duration) > 0 {
if state.duration, err = strconv.ParseFloat(duration, 64); strict && err != nil {
return fmt.Errorf("Duration parsing error: %s", err)
}
}

if len(line) > sepIndex {
state.title = line[sepIndex+1:]
}

if strict {
durationLength := len(durationRe.FindStringSubmatch(line[8:sepIndex])[1])
titleLength := len(line[sepIndex+1:])
if len(line) > durationLength+titleLength+9 {
state.tagAttribute = true
lastQuote := rune(0)
formatter := func(c rune) bool {
switch {
case c == lastQuote:
lastQuote = rune(0)
return false
case lastQuote != rune(0):
return false
case unicode.In(c, unicode.Quotation_Mark):
lastQuote = c
return false
default:
return unicode.IsSpace(c)
}
}
attributes := strings.FieldsFunc(line[9+durationLength:sepIndex], formatter)
state.attribute = make(map[string]string)
if len(attributes) <= 0 {
return errors.New("EXTINF attributes parsing error: attributes not found")
}
for _, attribute := range attributes {
state.attribute[strings.Split(attribute, "=")[0]] = strings.Trim(strings.Split(attribute, "=")[1], "\"")
}
}
}

case !strings.HasPrefix(line, "#"):
if state.tagInf {
err := p.Append(line, state.duration, state.title)
if state.tagAttribute {
err = p.AppendWithAttributes(line, state.duration, state.title, state.attribute)
} else {
err = p.Append(line, state.duration, state.title)
}
if err == ErrPlaylistFull {
// Extend playlist by doubling size, reset internal state, try again.
// If the second Append fails, the if err block will handle it.
Expand All @@ -502,7 +544,11 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
p.Segments = append(p.Segments, make([]*MediaSegment, p.Count())...)
p.capacity = uint(len(p.Segments))
p.tail = p.count
err = p.Append(line, state.duration, state.title)
if state.tagAttribute {
err = p.AppendWithAttributes(line, state.duration, state.title, state.attribute)
} else {
err = p.Append(line, state.duration, state.title)
}
}
// Check err for first or subsequent Append()
if err != nil {
Expand Down Expand Up @@ -561,6 +607,7 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
state.custom = make(map[string]CustomTag)
state.tagCustom = false
}

// start tag first
case line == "#EXTM3U":
state.m3u = true
Expand Down
29 changes: 24 additions & 5 deletions reader_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/*
Playlist parsing tests.
Playlist parsing tests.

Copyright 2013-2019 The Project Developers.
See the AUTHORS and LICENSE files at the top-level directory of this distribution
and at https://github.com/grafov/m3u8/
Copyright 2013-2019 The Project Developers.
See the AUTHORS and LICENSE files at the top-level directory of this distribution
and at https://github.com/grafov/m3u8/

ॐ तारे तुत्तारे तुरे स्व
ॐ तारे तुत्तारे तुरे स्व
*/
package m3u8

Expand Down Expand Up @@ -972,6 +972,25 @@ func TestDecodeMediaPlaylistStartTime(t *testing.T) {
}
}

func TestDecodeMediaPlaylistAttributes(t *testing.T) {
f, err := os.Open("sample-playlists/media-playlist-with-attributes.m3u8")
if err != nil {
t.Fatal(err)
}
p, listType, err := DecodeFrom(bufio.NewReader(f), true)
if err != nil {
t.Fatal(err)
}
pp := p.(*MediaPlaylist)
CheckType(t, pp)
if listType != MEDIA {
t.Error("Sample not recognized as media playlist.")
}
if _, ok := pp.Segments[0].Attritube["group-title"]; !ok {
t.Error("Sample attributes not unpacked")
}
}

func TestDecodeMediaPlaylistWithCueOutCueIn(t *testing.T) {
f, err := os.Open("sample-playlists/media-playlist-with-cue-out-in-without-oatcls.m3u8")
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions sample-playlists/media-playlist-with-attributes.m3u8
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:10
#EXTINF:-1 tvg-id="id0" tvg-name="Channel Name 0" tvg-logo="logo0.png" group-title="Channel Group 0",Channel 0
channel0.ts
69 changes: 36 additions & 33 deletions structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,26 +85,26 @@ const (
//
// Simple Media Playlist file sample:
//
// #EXTM3U
// #EXT-X-VERSION:3
// #EXT-X-TARGETDURATION:5220
// #EXTINF:5219.2,
// http://media.example.com/entire.ts
// #EXT-X-ENDLIST
// #EXTM3U
// #EXT-X-VERSION:3
// #EXT-X-TARGETDURATION:5220
// #EXTINF:5219.2,
// http://media.example.com/entire.ts
// #EXT-X-ENDLIST
//
// Sample of Sliding Window Media Playlist, using HTTPS:
//
// #EXTM3U
// #EXT-X-VERSION:3
// #EXT-X-TARGETDURATION:8
// #EXT-X-MEDIA-SEQUENCE:2680
// #EXTM3U
// #EXT-X-VERSION:3
// #EXT-X-TARGETDURATION:8
// #EXT-X-MEDIA-SEQUENCE:2680
//
// #EXTINF:7.975,
// https://priv.example.com/fileSequence2680.ts
// #EXTINF:7.941,
// https://priv.example.com/fileSequence2681.ts
// #EXTINF:7.975,
// https://priv.example.com/fileSequence2682.ts
// #EXTINF:7.975,
// https://priv.example.com/fileSequence2680.ts
// #EXTINF:7.941,
// https://priv.example.com/fileSequence2681.ts
// #EXTINF:7.975,
// https://priv.example.com/fileSequence2682.ts
type MediaPlaylist struct {
TargetDuration float64
SeqNo uint64 // EXT-X-MEDIA-SEQUENCE
Expand Down Expand Up @@ -136,15 +136,15 @@ type MediaPlaylist struct {
// combines media playlists for multiple bitrates. URI lines in the
// playlist identify media playlists. Sample of Master Playlist file:
//
// #EXTM3U
// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000
// http://example.com/low.m3u8
// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2560000
// http://example.com/mid.m3u8
// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000
// http://example.com/hi.m3u8
// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=65000,CODECS="mp4a.40.5"
// http://example.com/audio-only.m3u8
// #EXTM3U
// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000
// http://example.com/low.m3u8
// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2560000
// http://example.com/mid.m3u8
// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000
// http://example.com/hi.m3u8
// #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=65000,CODECS="mp4a.40.5"
// http://example.com/audio-only.m3u8
type MasterPlaylist struct {
Variants []*Variant
Args string // optional arguments placed after URI (URI?Args)
Expand Down Expand Up @@ -206,14 +206,15 @@ type MediaSegment struct {
SeqId uint64
Title string // optional second parameter for EXTINF tag
URI string
Duration float64 // first parameter for EXTINF tag; duration must be integers if protocol version is less than 3 but we are always keep them float
Limit int64 // EXT-X-BYTERANGE <n> is length in bytes for the file under URI
Offset int64 // EXT-X-BYTERANGE [@o] is offset from the start of the file under URI
Key *Key // EXT-X-KEY displayed before the segment and means changing of encryption key (in theory each segment may have own key)
Map *Map // EXT-X-MAP displayed before the segment
Discontinuity bool // EXT-X-DISCONTINUITY indicates an encoding discontinuity between the media segment that follows it and the one that preceded it (i.e. file format, number and type of tracks, encoding parameters, encoding sequence, timestamp sequence)
SCTE *SCTE // SCTE-35 used for Ad signaling in HLS
ProgramDateTime time.Time // EXT-X-PROGRAM-DATE-TIME tag associates the first sample of a media segment with an absolute date and/or time
Duration float64 // first parameter for EXTINF tag; duration must be integers if protocol version is less than 3 but we are always keep them float
Attritube map[string]string // unpack from EXTINF tag; usually exist in IPTV media list
Limit int64 // EXT-X-BYTERANGE <n> is length in bytes for the file under URI
Offset int64 // EXT-X-BYTERANGE [@o] is offset from the start of the file under URI
Key *Key // EXT-X-KEY displayed before the segment and means changing of encryption key (in theory each segment may have own key)
Map *Map // EXT-X-MAP displayed before the segment
Discontinuity bool // EXT-X-DISCONTINUITY indicates an encoding discontinuity between the media segment that follows it and the one that preceded it (i.e. file format, number and type of tracks, encoding parameters, encoding sequence, timestamp sequence)
SCTE *SCTE // SCTE-35 used for Ad signaling in HLS
ProgramDateTime time.Time // EXT-X-PROGRAM-DATE-TIME tag associates the first sample of a media segment with an absolute date and/or time
Custom map[string]CustomTag
}

Expand Down Expand Up @@ -314,6 +315,7 @@ type decodingState struct {
tagWV bool
tagStreamInf bool
tagInf bool
tagAttribute bool
tagSCTE35 bool
tagRange bool
tagDiscontinuity bool
Expand All @@ -325,6 +327,7 @@ type decodingState struct {
limit int64
offset int64
duration float64
attribute map[string]string
title string
variant *Variant
alternatives []*Alternative
Expand Down
17 changes: 17 additions & 0 deletions writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,17 @@ func (p *MediaPlaylist) Append(uri string, duration float64, title string) error
return p.AppendSegment(seg)
}

// AppendWithAttributes append general chunk to the tail of chunk slice for a media playlist with attributes.
// This operation does reset playlist cache.
func (p *MediaPlaylist) AppendWithAttributes(uri string, duration float64, title string, attributes map[string]string) error {
seg := new(MediaSegment)
seg.URI = uri
seg.Duration = duration
seg.Title = title
seg.Attritube = attributes
return p.AppendSegment(seg)
}

// AppendSegment appends a MediaSegment to the tail of chunk slice for
// a media playlist. This operation does reset playlist cache.
func (p *MediaPlaylist) AppendSegment(seg *MediaSegment) error {
Expand Down Expand Up @@ -695,6 +706,12 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer {
}
p.buf.WriteString(durationCache[seg.Duration])
}
if len(seg.Attritube) > 0 {
for key, value := range seg.Attritube {
p.buf.WriteString(" ")
p.buf.WriteString(fmt.Sprintf("%s=\"%s\"", key, value))
}
}
p.buf.WriteRune(',')
p.buf.WriteString(seg.Title)
p.buf.WriteRune('\n')
Expand Down
22 changes: 22 additions & 0 deletions writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,28 @@ func TestEncodeMediaPlaylistWithDefaultMap(t *testing.T) {
}
}

// Create new media playlist
// Add media attributes
// Add segment with attributes
func TestEncodeMediaPlaylistWithAttributes(t *testing.T) {
p, e := NewMediaPlaylist(1, 1)
if e != nil {
t.Fatalf("Create media playlist failed: %s", e)
}

attribute := make(map[string]string)
attribute["tvg-id"] = "id0"
attribute["tvg-name"] = "Channel Name 0"
attribute["tvg-logo"] = "logo0.png"
attribute["group-title"] = "Channel Group 0"

e = p.AppendWithAttributes("channel0.ts", -1, "Channel 0", attribute)
if e != nil {
t.Fatalf("Add 1st segment to a media playlist failed: %s", e)
}
//fmt.Println(p.Encode().String())
}

// Create new media playlist
// Add custom playlist tag
// Add segment with custom tag
Expand Down