Skip to content

Commit

Permalink
Merge pull request #8 from edgeware/ESB3021-130-date-range-scte35-sup…
Browse files Browse the repository at this point in the history
…port

ESB3021-130: DATERANGE scte35 support
  • Loading branch information
NhanNguyen700 authored Feb 20, 2024
2 parents 7ed1217 + cc957c1 commit 4ad1c6a
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 40 deletions.
68 changes: 66 additions & 2 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ package m3u8

import (
"bytes"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
Expand All @@ -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`
Expand Down Expand Up @@ -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
Expand Down
65 changes: 60 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 @@ -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 {
Expand Down
21 changes: 21 additions & 0 deletions sample-playlists/media-playlist-with-scte35-daterange.m3u8
Original file line number Diff line number Diff line change
@@ -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
72 changes: 39 additions & 33 deletions structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ package m3u8

import (
"bytes"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"math"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 4ad1c6a

Please sign in to comment.