Skip to content

Commit

Permalink
added EXT-X-DATERANGE support and INSTREAM-ID (attribute) to EXT-X-MEDIA
Browse files Browse the repository at this point in the history
  • Loading branch information
rogerpales committed Feb 14, 2023
1 parent 6acf45f commit 336fe69
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 43 deletions.
54 changes: 52 additions & 2 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ 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 @@ -329,6 +329,8 @@ func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line st
alt.Subtitles = v
case "URI":
alt.URI = v
case "INSTREAM-ID":
alt.InstreamId = v
}
}
state.alternatives = append(state.alternatives, &alt)
Expand Down Expand Up @@ -561,6 +563,10 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
state.custom = make(map[string]CustomTag)
state.tagCustom = false
}
if len(state.daterange) > 0 {
p.SetDateRange(state.daterange)
state.daterange = []*DateRange{}
}
// start tag first
case line == "#EXTM3U":
state.m3u = true
Expand Down Expand Up @@ -655,6 +661,41 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
if state.programDateTime, err = TimeParse(line[25:]); strict && err != nil {
return err
}
case strings.HasPrefix(line, "#EXT-X-DATERANGE:"):
dr := new(DateRange)
for k, v := range decodeParamsLine(line[17:]) {
switch k {
case "ID":
dr.ID = v
case "CLASS":
dr.Class = v
case "START-DATE":
dr.StartDate, _ = time.Parse(DATETIME, v)
case "END-DATE":
dr.EndDate, _ = time.Parse(DATETIME, v)
case "DURATION":
dr.Duration, _ = strconv.ParseFloat(v, 64)
case "PLANNED-DURATION":
dr.PlannedDuration, _ = strconv.ParseFloat(v, 64)
case "SCTE35-CMD":
dr.SCTE35Cmd = v
case "SCTE35-OUT":
dr.SCTE35Out = v
case "SCTE35-IN":
dr.SCTE35In = v
case "END-ON-NEXT":
dr.EndOnNext = v
default:
if strings.HasPrefix(k, "X-") {
dr.X[k] = v
} else {
if strict {
return fmt.Errorf("unrecognized EXT-X-DATERANGE attribte: %s", k)
}
}
}
}
state.daterange = append(state.daterange, dr)
case !state.tagRange && strings.HasPrefix(line, "#EXT-X-BYTERANGE:"):
state.tagRange = true
state.listType = MEDIA
Expand Down Expand Up @@ -708,6 +749,15 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l
state.scte.Elapsed, _ = strconv.ParseFloat(value, 64)
}
}
case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-X-CUE-OUT"):
state.tagSCTE35 = true
state.scte = new(SCTE)
state.scte.Syntax = SCTE35_OATCLS
state.scte.CueType = SCTE35Cue_Start
lenLine := len(line)
if lenLine > 14 {
state.scte.Time, _ = strconv.ParseFloat(line[15:], 64)
}
case !state.tagSCTE35 && line == "#EXT-X-CUE-IN":
state.tagSCTE35 = true
state.scte = new(SCTE)
Expand Down
86 changes: 86 additions & 0 deletions reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,57 @@ func TestMediaPlaylistWithOATCLSSCTE35Tag(t *testing.T) {
}
}

func TestMediaPlaylistWithDATERANAGETags(t *testing.T) {
f, err := os.Open("sample-playlists/media-playlist-with-daterange.m3u8")
if err != nil {
t.Fatal(err)
}
p, _, err := DecodeFrom(bufio.NewReader(f), true)
if err != nil {
t.Fatal(err)
}
pp := p.(*MediaPlaylist)

d1, _ := time.Parse(time.RFC3339, "2022-04-26T12:00:00.000000Z")
d2, _ := time.Parse(time.RFC3339, "2022-04-26T12:00:26.000000Z")
expect := map[int][]*DateRange{
1: {
{ID: "1", StartDate: d1, Duration: 26.0, SCTE35Cmd: "0xFC304A000000000BB800FFF00506FFCD4CC4900034023243554549FFFFFFFF7FFF0002631D50011E30303034304D4130303030303030303030383554303432373232303530302101003B1B1B19"},
{ID: "2", StartDate: d1, PlannedDuration: 26.0, SCTE35Out: "0xFC304A000000000BB80000000506FFCD4CC4900034023243554549FFFFFFFF7FFF0000E969E8011E30303034304D413030303030303030303038355430343237323230343330220103AF904DEA"},
},
3: {
{ID: "3", StartDate: d1, Duration: 26.0, SCTE35Cmd: "0xFC304A000000000BB80000000506FFCD5A80300034023243554549FFFFFFFF7FFF0000E969E8011E30303034304D41303030303030303030303835543034323732323034333001010349323975"},
},
5: {
{ID: "4", StartDate: d1, EndDate: d2, Duration: 26.0, SCTE35In: "0xFC304A000000000BB80000000506FFCE363A300034023243554549FFFFFFFF7FFF0000E969E8011E30303034304D4130303030303030303030383554303432373232303433302301031D34C4FF"},
{ID: "5", StartDate: d2, Duration: 416.766666, SCTE35Cmd: "0xFC304A000000000BB800FFF00506FFCE363A300034023243554549FFFFFFFF7FFF00023C5788011E30303034304D4130303030303030303030383554303432373232303433302001002A4FFFBD"},
},
}

for i, v := range expect {
for j, dr := range pp.Segments[i].DateRange {
if v[j].ID != dr.ID {
t.Errorf("daterange comparison error ID %s != %s", v[j].ID, dr.ID)
}
if v[j].Duration != dr.Duration {
t.Errorf("daterange comparison error Duration %f != %f", v[j].Duration, dr.Duration)
}
if v[j].PlannedDuration != dr.PlannedDuration {
t.Errorf("daterange comparison error PlannedDuration %f != %f", v[j].PlannedDuration, dr.PlannedDuration)
}
if v[j].SCTE35Cmd != dr.SCTE35Cmd {
t.Errorf("daterange comparison error SCTE35Cmd %s != %s", v[j].SCTE35Cmd, dr.SCTE35Cmd)
}
if v[j].SCTE35Out != dr.SCTE35Out {
t.Errorf("daterange comparison error SCTE35Out %s != %s", v[j].SCTE35Out, dr.SCTE35Out)
}
if v[j].SCTE35In != dr.SCTE35In {
t.Errorf("daterange comparison error SCTE35In %s != %s", v[j].SCTE35In, dr.SCTE35In)
}
}
}
}

func TestDecodeMediaPlaylistWithDiscontinuitySeq(t *testing.T) {
f, err := os.Open("sample-playlists/media-playlist-with-discontinuity-seq.m3u8")
if err != nil {
Expand Down Expand Up @@ -972,6 +1023,41 @@ func TestDecodeMediaPlaylistStartTime(t *testing.T) {
}
}

func TestDecodeMediaPlaylistWithCueOutCueIn(t *testing.T) {
f, err := os.Open("sample-playlists/media-playlist-with-cue-out-in-without-oatcls.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 pp.Segments[5].SCTE.CueType != SCTE35Cue_Start {
t.Errorf("EXT-CUE-OUT must result in SCTE35Cue_Start")
}
if pp.Segments[5].SCTE.Time != 0 {
t.Errorf("EXT-CUE-OUT without duration must not have Time set")
}
if pp.Segments[9].SCTE.CueType != SCTE35Cue_End {
t.Errorf("EXT-CUE-IN must result in SCTE35Cue_End")
}
if pp.Segments[30].SCTE.CueType != SCTE35Cue_Start {
t.Errorf("EXT-CUE-OUT must result in SCTE35Cue_Start")
}
if pp.Segments[30].SCTE.Time != 180 {
t.Errorf("EXT-CUE-OUT:180.0 must have time set to 180")
}
if pp.Segments[60].SCTE.CueType != SCTE35Cue_End {
t.Errorf("EXT-CUE-IN must result in SCTE35Cue_End")
}
}

/****************
* Benchmarks *
****************/
Expand Down
84 changes: 51 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 @@ -197,6 +197,7 @@ type Alternative struct {
Forced string
Characteristics string
Subtitles string
InstreamId string
}

// MediaSegment structure represents a media segment included in a
Expand All @@ -206,14 +207,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
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)
DateRange []*DateRange // EXT-X-DATERANGE tags
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 All @@ -227,6 +229,21 @@ type SCTE struct {
Elapsed float64
}

// DateRange holds the EXT-X-DATERANGE attributes specified in 4.3.2.7 https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming
type DateRange struct {
ID string
Class string
StartDate time.Time
EndDate time.Time
Duration float64
PlannedDuration float64
X map[string]string // X-" prefixed client-defined attributes
SCTE35Cmd string
SCTE35In string
SCTE35Out string
EndOnNext string
}

// Key structure represents information about stream encryption.
//
// Realizes EXT-X-KEY tag.
Expand Down Expand Up @@ -332,4 +349,5 @@ type decodingState struct {
xmap *Map
scte *SCTE
custom map[string]CustomTag
daterange []*DateRange
}
Loading

0 comments on commit 336fe69

Please sign in to comment.