diff --git a/reader.go b/reader.go index c99798fa..41b4c181 100644 --- a/reader.go +++ b/reader.go @@ -13,6 +13,8 @@ package m3u8 import ( "bytes" + "encoding/base64" + "encoding/hex" "errors" "fmt" "io" @@ -26,8 +28,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` @@ -721,6 +723,68 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l state.scte = new(SCTE) state.scte.Syntax = SCTE35_OATCLS state.scte.CueType = SCTE35Cue_End + case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-X-DATERANGE:"): + tagSCTE35 := false + scte := new(SCTE) + scte.Syntax = SCTE35_DATERANGE + for attribute, value := range decodeParamsLine(line[12:]) { + switch attribute { + case "SCTE35-CMD": + tagSCTE35 = true + buf, err := hex.DecodeString(strings.TrimLeft(value, "0x")) + if err != nil { + return err + } + scte.Cue = base64.StdEncoding.EncodeToString(buf) + scte.CueType = SCTE35Cue_Cmd + case "ID": + scte.ID = value + case "START-DATE": + startDate, err := time.Parse(DATETIME, value) + if err != nil { + return err + } + scte.StartDate = &startDate + case "END-DATE": + endDate, err := time.Parse(DATETIME, value) + if err != nil { + return err + } + scte.EndDate = &endDate + case "DURATION": + duration, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + scte.Duration = &duration + case "PLANNED-DURATION": + plannedDuration, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + scte.PlannedDuration = &plannedDuration + case "SCTE35-OUT": + tagSCTE35 = true + scte.CueType = SCTE35Cue_Start + buf, err := hex.DecodeString(strings.TrimLeft(value, "0x")) + if err != nil { + return err + } + scte.Cue = base64.StdEncoding.EncodeToString(buf) + case "SCTE35-IN": + scte.CueType = SCTE35Cue_End + buf, err := hex.DecodeString(strings.TrimLeft(value, "0x")) + if err != nil { + return err + } + tagSCTE35 = true + scte.Cue = base64.StdEncoding.EncodeToString(buf) + } + } + if tagSCTE35 { + state.tagSCTE35 = true + state.scte = scte + } case !state.tagDiscontinuity && strings.HasPrefix(line, "#EXT-X-DISCONTINUITY"): state.tagDiscontinuity = true state.listType = MEDIA diff --git a/reader_test.go b/reader_test.go index 8bfed863..681f9350 100644 --- a/reader_test.go +++ b/reader_test.go @@ -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 @@ -564,6 +564,61 @@ func TestMediaPlaylistWithOATCLSSCTE35Tag(t *testing.T) { } } +func TestMediaPlaylistWithDateRangeSCTE35Tag(t *testing.T) { + f, err := os.Open("sample-playlists/media-playlist-with-scte35-daterange.m3u8") + if err != nil { + t.Fatal(err) + } + p, _, err := DecodeFrom(bufio.NewReader(f), true) + if err != nil { + t.Fatal(err) + } + pp := p.(*MediaPlaylist) + startDateOut, _ := time.Parse(DATETIME, "2014-03-05T11:15:00Z") + startDateIn, _ := time.Parse(DATETIME, "2014-03-05T11:16:00Z") + + ptr := func(f float64) *float64 { + return &f + } + + expect := map[int]*SCTE{ + 0: { + Syntax: SCTE35_DATERANGE, + CueType: SCTE35Cue_Start, + Cue: "/AAvAAAAAAD/AA==", + StartDate: &startDateOut, + Duration: ptr(60), + PlannedDuration: ptr(60), + ID: "splice-6FFFFFF0", + }, + 1: { + Syntax: SCTE35_DATERANGE, + CueType: SCTE35Cue_End, + Cue: "/AAvAAAAAAD/EA==", + StartDate: &startDateIn, + Duration: ptr(60), + PlannedDuration: ptr(60), + ID: "splice-6FFFFFF0", + }, + } + + actual := make([]*SCTE, 0, 2) + for i := 0; i < int(pp.Count()); i++ { + if pp.Segments[i].SCTE != nil { + actual = append(actual, pp.Segments[i].SCTE) + } + } + + for i := 0; i < len(expect); i++ { + if !reflect.DeepEqual(actual[i], expect[i]) { + t.Errorf("DATERANGE SCTE35 segment %v \ngot: %#v\nexp: %#v", + i, actual[i], expect[i], + ) + } + } + +} + func TestDecodeMediaPlaylistWithDiscontinuitySeq(t *testing.T) { f, err := os.Open("sample-playlists/media-playlist-with-discontinuity-seq.m3u8") if err != nil { diff --git a/sample-playlists/media-playlist-with-scte35-daterange.m3u8 b/sample-playlists/media-playlist-with-scte35-daterange.m3u8 new file mode 100644 index 00000000..c108cae2 --- /dev/null +++ b/sample-playlists/media-playlist-with-scte35-daterange.m3u8 @@ -0,0 +1,21 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:10 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:10, +fileSequence0.ts +#EXTINF:10, +fileSequence1.ts +#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00Z",PLANNED-DURATION=60,DURATION=60,SCTE35-OUT=0xFC002F0000000000FF00 +#EXTINF:10, +fileSequence2.ts +#EXTINF:10, +fileSequence3.ts +#EXTINF:10, +fileSequence4.ts +#EXTINF:10, +fileSequence5.ts +#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:16:00Z",PLANNED-DURATION=60,DURATION=60,SCTE35-IN=0xFC002F0000000000FF10 +#EXTINF:10, +fileSequence6.ts +#EXT-X-ENDLIST \ No newline at end of file diff --git a/structure.go b/structure.go index 35f3df51..b35ef14b 100644 --- a/structure.go +++ b/structure.go @@ -64,8 +64,9 @@ type SCTE35Syntax uint const ( // SCTE35_67_2014 will be the default due to backwards compatibility reasons. - SCTE35_67_2014 SCTE35Syntax = iota // SCTE35_67_2014 defined in http://www.scte.org/documents/pdf/standards/SCTE%2067%202014.pdf - SCTE35_OATCLS // SCTE35_OATCLS is a non-standard but common format + SCTE35_67_2014 SCTE35Syntax = iota // SCTE35_67_2014 defined in http://www.scte.org/documents/pdf/standards/SCTE%2067%202014.pdf + SCTE35_OATCLS // SCTE35_OATCLS is a non-standard but common format + SCTE35_DATERANGE // SCTE35_DATERANGE is standard format for HLS ) // SCTE35CueType defines the type of cue point, used by readers and writers to @@ -76,6 +77,7 @@ const ( SCTE35Cue_Start SCTE35CueType = iota // SCTE35Cue_Start indicates an out cue point SCTE35Cue_Mid // SCTE35Cue_Mid indicates a segment between start and end cue points SCTE35Cue_End // SCTE35Cue_End indicates an in cue point + SCTE35Cue_Cmd // Not in, out, or mid. Indicates a command for splice, like splice_null, splice_schedule, etc. ) // MediaPlaylist structure represents a single bitrate playlist aka @@ -85,26 +87,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 @@ -136,15 +138,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) @@ -221,12 +223,16 @@ type MediaSegment struct { // SCTE holds custom, non EXT-X-DATERANGE, SCTE-35 tags type SCTE struct { - Syntax SCTE35Syntax // Syntax defines the format of the SCTE-35 cue tag - CueType SCTE35CueType // CueType defines whether the cue is a start, mid, end (if applicable) - Cue string - ID string - Time float64 - Elapsed float64 + Syntax SCTE35Syntax // Syntax defines the format of the SCTE-35 cue tag + CueType SCTE35CueType // CueType defines whether the cue is a start, mid, end (if applicable) + Cue string + ID string + Time float64 + Elapsed float64 + PlannedDuration *float64 + Duration *float64 + StartDate *time.Time + EndDate *time.Time } // Key structure represents information about stream encryption. diff --git a/writer.go b/writer.go index 348631b2..ee0f08e3 100644 --- a/writer.go +++ b/writer.go @@ -13,6 +13,8 @@ package m3u8 import ( "bytes" + "encoding/base64" + "encoding/hex" "errors" "fmt" "math" @@ -618,6 +620,43 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer { p.buf.WriteString("#EXT-X-CUE-IN") p.buf.WriteRune('\n') } + case SCTE35_DATERANGE: + p.buf.WriteString("#EXT-X-DATERANGE:") + p.buf.WriteString("ID=\"") + p.buf.WriteString(seg.SCTE.ID) + p.buf.WriteString("\"") + if seg.SCTE.StartDate != nil { + p.buf.WriteString(",START-DATE=\"") + p.buf.WriteString(seg.SCTE.StartDate.Format(DATETIME)) + p.buf.WriteString("\"") + } + if seg.SCTE.EndDate != nil { + p.buf.WriteString(",END-DATE=\"") + p.buf.WriteString(seg.SCTE.EndDate.Format(DATETIME)) + p.buf.WriteString("\"") + } + if seg.SCTE.Duration != nil { + p.buf.WriteString(",DURATION=") + p.buf.WriteString(strconv.FormatFloat(*seg.SCTE.Duration, 'f', -1, 64)) + } + if seg.SCTE.PlannedDuration != nil { + p.buf.WriteString(",PLANNED-DURATION=") + p.buf.WriteString(strconv.FormatFloat(*seg.SCTE.PlannedDuration, 'f', -1, 64)) + } + + switch seg.SCTE.CueType { + case SCTE35Cue_Start: + p.buf.WriteString(",SCTE35-OUT=0x") + case SCTE35Cue_End: + p.buf.WriteString(",SCTE35-IN=0x") + case SCTE35Cue_Cmd: + p.buf.WriteString(",SCTE35-CMD=0x") + } + cue, _ := base64.StdEncoding.DecodeString(seg.SCTE.Cue) + fmt.Println("NHANDEBUG1", seg.SCTE.Cue) + cueHex := hex.EncodeToString(cue) + p.buf.WriteString(cueHex) + p.buf.WriteRune('\n') } } // check for key change