diff --git a/.deepsource.toml b/.deepsource.toml index f5332a56..ce616498 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -13,4 +13,4 @@ name = "go" enabled = true [analyzers.meta] - import_path = "github.com/grafov/m3u8" + import_path = "github.com/jwplayer/m3u8" diff --git a/.drone.yml b/.drone.yml index 34fe10d9..021d06f6 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,7 +3,7 @@ name: default workspace: base: /go - path: src/github.com/grafov/m3u8 + path: src/github.com/jwplayer/m3u8 steps: - name: test diff --git a/example/example.go b/example/example.go index 074efff4..7efae581 100644 --- a/example/example.go +++ b/example/example.go @@ -6,8 +6,8 @@ import ( "os" "path" - "github.com/grafov/m3u8" - "github.com/grafov/m3u8/example/template" + "github.com/jwplayer/m3u8" + "github.com/jwplayer/m3u8/example/template" ) func main() { @@ -16,7 +16,7 @@ func main() { panic("$GOPATH is empty") } - m3u8File := "github.com/grafov/m3u8/sample-playlists/media-playlist-with-custom-tags.m3u8" + m3u8File := "github.com/jwplayer/m3u8/sample-playlists/media-playlist-with-custom-tags.m3u8" f, err := os.Open(path.Join(GOPATH, "src", m3u8File)) if err != nil { panic(err) diff --git a/example/template/custom-playlist-tag-template.go b/example/template/custom-playlist-tag-template.go index e1769b99..5d72b5ba 100644 --- a/example/template/custom-playlist-tag-template.go +++ b/example/template/custom-playlist-tag-template.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - "github.com/grafov/m3u8" + "github.com/jwplayer/m3u8" ) // #CUSTOM-PLAYLIST-TAG: diff --git a/example/template/custom-segment-tag-template.go b/example/template/custom-segment-tag-template.go index abe6ef49..59e4f799 100644 --- a/example/template/custom-segment-tag-template.go +++ b/example/template/custom-segment-tag-template.go @@ -4,7 +4,7 @@ import ( "bytes" "errors" - "github.com/grafov/m3u8" + "github.com/jwplayer/m3u8" ) // #CUSTOM-SEGMENT-TAG: diff --git a/go.mod b/go.mod index 0e2a4148..232a5877 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/grafov/m3u8 +module github.com/jwplayer/m3u8 go 1.12 diff --git a/reader.go b/reader.go index b19324eb..8e4ff03a 100644 --- a/reader.go +++ b/reader.go @@ -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` @@ -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) @@ -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 @@ -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 @@ -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) diff --git a/reader_test.go b/reader_test.go index 8d60b16c..5ac13130 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,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 { @@ -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 * ****************/ diff --git a/sample-playlists/media-playlist-with-cue-out-in-without-oatcls.m3u8 b/sample-playlists/media-playlist-with-cue-out-in-without-oatcls.m3u8 new file mode 100644 index 00000000..569072ef --- /dev/null +++ b/sample-playlists/media-playlist-with-cue-out-in-without-oatcls.m3u8 @@ -0,0 +1,136 @@ +#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-MEDIA-SEQUENCE:275163116 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-TARGETDURATION:9 +#USP-X-TIMESTAMP-MAP:MPEGTS=7984482175,LOCAL=2022-04-26T13:11:31.333300Z +#EXTINF:6, no desc +364992-275163141.ts +#EXTINF:5.1666, no desc +364992-275163142.ts +#EXT-X-CUE-IN +#EXTINF:6.8333, no desc +364992-275163143.ts +#EXTINF:6, no desc +364992-275163144.ts +#EXTINF:6, no desc +364992-275163150.ts +#EXT-X-CUE-OUT +#EXTINF:6, no desc +364992-275163151.ts +#EXTINF:6, no desc +364992-275163152.ts +#EXTINF:6, no desc +364992-275163153.ts +#EXTINF:6, no desc +364992-275163154.ts +#EXT-X-CUE-IN +#EXTINF:6, no desc +364992-275163155.ts +#EXTINF:6, no desc +364992-275163156.ts +#EXTINF:6, no desc +364992-275163157.ts +#EXTINF:6, no desc +364992-275163158.ts +#EXTINF:6, no desc +364992-275163159.ts +#EXTINF:6, no desc +364992-275163160.ts +#EXTINF:6, no desc +364992-275163161.ts +#EXTINF:6, no desc +364992-275163162.ts +#EXTINF:6, no desc +364992-275163163.ts +#EXTINF:6, no desc +364992-275163164.ts +#EXTINF:6, no desc +364992-275163165.ts +#EXTINF:6, no desc +364992-275163166.ts +#EXTINF:6, no desc +364992-275163167.ts +#EXTINF:6, no desc +364992-275163168.ts +#EXTINF:6, no desc +364992-275163169.ts +#EXTINF:6, no desc +364992-275163170.ts +#EXTINF:6, no desc +364992-275163171.ts +#EXTINF:6, no desc +364992-275163172.ts +#EXTINF:6, no desc +364992-275163173.ts +#EXTINF:6, no desc +364992-275163174.ts +#EXTINF:5.2, no desc +364992-275163175.ts +#EXT-X-CUE-OUT:180 +#EXTINF:6.8, no desc +364992-275163176.ts +#EXTINF:6, no desc +364992-275163177.ts +#EXTINF:6, no desc +364992-275163178.ts +#EXTINF:6, no desc +364992-275163179.ts +#EXTINF:6, no desc +364992-275163180.ts +#EXTINF:6, no desc +364992-275163181.ts +#EXTINF:6, no desc +364992-275163182.ts +#EXTINF:6, no desc +364992-275163183.ts +#EXTINF:6, no desc +364992-275163184.ts +#EXTINF:6, no desc +364992-275163185.ts +#EXTINF:6, no desc +364992-275163186.ts +#EXTINF:6, no desc +364992-275163187.ts +#EXTINF:6, no desc +364992-275163188.ts +#EXTINF:6, no desc +364992-275163189.ts +#EXTINF:5.2, no desc +364992-275163190.ts +#EXTINF:6.8, no desc +364992-275163191.ts +#EXTINF:6, no desc +364992-275163192.ts +#EXTINF:6, no desc +364992-275163193.ts +#EXTINF:6, no desc +364992-275163194.ts +#EXTINF:6, no desc +364992-275163195.ts +#EXTINF:6, no desc +364992-275163196.ts +#EXTINF:6, no desc +364992-275163197.ts +#EXTINF:6, no desc +364992-275163198.ts +#EXTINF:6, no desc +364992-275163199.ts +#EXTINF:5.2, no desc +364992-275163200.ts +#EXTINF:6.8, no desc +364992-275163201.ts +#EXTINF:6, no desc +364992-275163202.ts +#EXTINF:6, no desc +364992-275163203.ts +#EXTINF:6, no desc +364992-275163204.ts +#EXTINF:5.2333, no desc +364992-275163205.ts +#EXT-X-CUE-IN +#EXTINF:6.7666, no desc +364992-275163206.ts +#EXTINF:6, no desc +364992-275163207.ts +#EXTINF:6, no desc diff --git a/sample-playlists/media-playlist-with-daterange.m3u8 b/sample-playlists/media-playlist-with-daterange.m3u8 new file mode 100644 index 00000000..004a1d8a --- /dev/null +++ b/sample-playlists/media-playlist-with-daterange.m3u8 @@ -0,0 +1,29 @@ +#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-MEDIA-SEQUENCE:275162928 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-TARGETDURATION:9 +#USP-X-TIMESTAMP-MAP:MPEGTS=7882962175,LOCAL=2022-04-26T12:00:00.000000Z +#EXT-X-PROGRAM-DATE-TIME:2022-04-26T12:00:00.000000Z +#EXTINF:6, no desc +live-video=364992-275163016.ts +#EXT-X-DATERANGE:ID="1",START-DATE="2022-04-26T12:00:00.000000Z",DURATION=26.0,SCTE35-CMD=0xFC304A000000000BB800FFF00506FFCD4CC4900034023243554549FFFFFFFF7FFF0002631D50011E30303034304D4130303030303030303030383554303432373232303530302101003B1B1B19 +#EXT-X-DATERANGE:ID="2",START-DATE="2022-04-26T12:00:00.000000Z",PLANNED-DURATION=26.0,SCTE35-OUT=0xFC304A000000000BB80000000506FFCD4CC4900034023243554549FFFFFFFF7FFF0000E969E8011E30303034304D413030303030303030303038355430343237323230343330220103AF904DEA +#EXT-X-PROGRAM-DATE-TIME:2022-04-26T13:01:40.733333Z +#EXTINF:8.6, no desc +live-video=364992-275163018.ts +#EXTINF:6, no desc +live-video=364992-275163019.ts +#EXT-X-DATERANGE:ID="3",START-DATE="2022-04-26T12:00:00.000000Z",DURATION=26.0,SCTE35-CMD=0xFC304A000000000BB80000000506FFCD5A80300034023243554549FFFFFFFF7FFF0000E969E8011E30303034304D41303030303030303030303835543034323732323034333001010349323975 +#EXTINF:6, no desc +live-video=364992-275163020.ts +#EXTINF:5.4, no desc +live-video=364992-275163045.ts +#EXT-X-DATERANGE:ID="4",START-DATE="2022-04-26T12:00:00.000000Z",END-DATE="2022-04-26T12:00:26.000000Z",DURATION=26.0,SCTE35-IN=0xFC304A000000000BB80000000506FFCE363A300034023243554549FFFFFFFF7FFF0000E969E8011E30303034304D4130303030303030303030383554303432373232303433302301031D34C4FF +#EXT-X-DATERANGE:ID="5",START-DATE="2022-04-26T12:00:26.000000Z",DURATION=416.766666,SCTE35-CMD=0xFC304A000000000BB800FFF00506FFCE363A300034023243554549FFFFFFFF7FFF00023C5788011E30303034304D4130303030303030303030383554303432373232303433302001002A4FFFBD +#EXT-X-PROGRAM-DATE-TIME:2022-04-26T12:00:26.000000Z +#EXTINF:6.6, no desc +live-video=364992-275163046.ts +#EXT-X-DATERANGE:ID="5",START-DATE="2022-04-26T12:00:32.600000Z",DURATION=416.766667,SCTE35-CMD=0xFC304A000000000BB800FFF00506FFCE36AF600034023243554549FFFFFFFF7FFF00023C5788011E30303034304D41303030303030303030303835543034323732323034333001010036D92B0C +#EXTINF:6, no desc +live-video=364992-275163047.ts diff --git a/structure.go b/structure.go index eb5d01a2..7d9546c8 100644 --- a/structure.go +++ b/structure.go @@ -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 @@ -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) @@ -197,6 +197,7 @@ type Alternative struct { Forced string Characteristics string Subtitles string + InstreamId string } // MediaSegment structure represents a media segment included in a @@ -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 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 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 } @@ -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. @@ -332,4 +349,5 @@ type decodingState struct { xmap *Map scte *SCTE custom map[string]CustomTag + daterange []*DateRange } diff --git a/structure_test.go b/structure_test.go index 4ab7d36a..319176db 100644 --- a/structure_test.go +++ b/structure_test.go @@ -1,11 +1,11 @@ /* - Playlist structures tests. +Playlist structures tests. - Copyright 2013-2017 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-2017 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 diff --git a/writer.go b/writer.go index dec3690c..ce43095b 100644 --- a/writer.go +++ b/writer.go @@ -155,6 +155,11 @@ func (p *MasterPlaylist) Encode() *bytes.Buffer { p.buf.WriteString(alt.URI) p.buf.WriteRune('"') } + if alt.InstreamId != "" { + p.buf.WriteString(",INSTREAM-ID=\"") + p.buf.WriteString(alt.InstreamId) + p.buf.WriteRune('"') + } p.buf.WriteRune('\n') } } @@ -593,9 +598,11 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer { case SCTE35_OATCLS: switch seg.SCTE.CueType { case SCTE35Cue_Start: - p.buf.WriteString("#EXT-OATCLS-SCTE35:") - p.buf.WriteString(seg.SCTE.Cue) - p.buf.WriteRune('\n') + if seg.SCTE.Cue != "" { + p.buf.WriteString("#EXT-OATCLS-SCTE35:") + p.buf.WriteString(seg.SCTE.Cue) + p.buf.WriteRune('\n') + } p.buf.WriteString("#EXT-X-CUE-OUT:") p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64)) p.buf.WriteRune('\n') @@ -640,6 +647,62 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer { } p.buf.WriteRune('\n') } + if len(seg.DateRange) > 0 { + for _, dr := range seg.DateRange { + p.buf.WriteString("#EXT-X-DATERANGE:") + p.buf.WriteString("ID=\"") + p.buf.WriteString(dr.ID) + p.buf.WriteRune('"') + if dr.Class != "" { + p.buf.WriteString(",CLASS=\"") + p.buf.WriteString(dr.Class) + p.buf.WriteRune('"') + } + if !dr.StartDate.IsZero() { + p.buf.WriteString(",START-DATE=\"") + p.buf.WriteString(dr.StartDate.Format(DATETIME)) + p.buf.WriteRune('"') + } + if !dr.EndDate.IsZero() { + p.buf.WriteString(",END-DATE=\"") + p.buf.WriteString(dr.EndDate.Format(DATETIME)) + p.buf.WriteRune('"') + } + if dr.Duration > 0 { + p.buf.WriteString(",DURATION=") + p.buf.WriteString(strconv.FormatFloat(dr.Duration, 'f', -1, 64)) + } + if dr.PlannedDuration > 0 { + p.buf.WriteString(",PLANNED-DURATION=") + p.buf.WriteString(strconv.FormatFloat(dr.PlannedDuration, 'f', -1, 64)) + } + if dr.SCTE35Cmd != "" { + p.buf.WriteString(",SCTE35-CMD=") + p.buf.WriteString(dr.SCTE35Cmd) + } + if dr.SCTE35In != "" { + p.buf.WriteString(",SCTE35-IN=") + p.buf.WriteString(dr.SCTE35In) + } + if dr.SCTE35Out != "" { + p.buf.WriteString(",SCTE35-OUT=") + p.buf.WriteString(dr.SCTE35Out) + } + if dr.EndOnNext != "" { + p.buf.WriteString(",END-ON-NEXT=\"") + p.buf.WriteString(dr.EndOnNext) + p.buf.WriteRune('"') + } + for k, v := range dr.X { + p.buf.WriteString(",") + p.buf.WriteString(k) + p.buf.WriteString("=\"") + p.buf.WriteString(v) + p.buf.WriteRune('"') + } + p.buf.WriteString("\n") + } + } if seg.Discontinuity { p.buf.WriteString("#EXT-X-DISCONTINUITY\n") } @@ -826,6 +889,32 @@ func (p *MediaPlaylist) SetSCTE35(scte35 *SCTE) error { return nil } +// SetDateRange sets DateRange to the current media segment +func (p *MediaPlaylist) SetDateRange(drs []*DateRange) error { + if p.count == 0 { + return errors.New("playlist is empty") + } + for _, dr := range drs { + if dr.ID == "" { + return errors.New("DateRange ID") + } + } + p.Segments[p.last()].DateRange = drs + return nil +} + +// AppendDateRange appends DateRange to the current media segment +func (p *MediaPlaylist) AppendDateRange(dr *DateRange) error { + if p.count == 0 { + return errors.New("playlist is empty") + } + if dr.ID == "" { + return errors.New("DateRange ID") + } + p.Segments[p.last()].DateRange = append(p.Segments[p.last()].DateRange, dr) + return nil +} + // SetDiscontinuity sets discontinuity flag for the current media // segment. EXT-X-DISCONTINUITY indicates an encoding discontinuity // between the media segment that follows it and the one that preceded diff --git a/writer_test.go b/writer_test.go index b4281331..fa2544fb 100644 --- a/writer_test.go +++ b/writer_test.go @@ -1,11 +1,11 @@ /* - Package m3u8. Playlist generation tests. +Package m3u8. Playlist generation 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 @@ -947,6 +947,45 @@ func TestMasterSetVersion(t *testing.T) { } } +func TestEncodeMediaPlaylistDateRangeTags(t *testing.T) { + p, err := NewMediaPlaylist(1, 1) + if err != nil { + t.Fatalf("Create media playlist failed: %s", err) + } + + st := time.Unix(1640995200, 222222000).UTC() + et := st.Add(time.Second * 4).UTC() + + dr := &DateRange{ + ID: "123", + StartDate: st, + EndDate: et, + Duration: 24.2, + PlannedDuration: 24.2, + SCTE35Out: "0xFC304A0000000", + SCTE35In: "0xFC304A0000000", + SCTE35Cmd: "0xFC304A0000000", + } + + err = p.Append("test01.ts", 5.0, "") + if err != nil { + t.Fatalf("Add 1st segment to a media playlist failed: %s", err) + } + + err = p.AppendDateRange(dr) + if err != nil { + t.Fatalf("Append DateRange to segment failed: %s", err) + } + + encoded := p.Encode().String() + expectedStrings := []string{"#EXT-X-DATERANGE:ID=\"123\",START-DATE=\"2022-01-01T00:00:00.222222Z\",END-DATE=\"2022-01-01T00:00:04.222222Z\",DURATION=24.2,PLANNED-DURATION=24.2,SCTE35-CMD=0xFC304A0000000,SCTE35-IN=0xFC304A0000000,SCTE35-OUT=0xFC304A0000000"} + for _, expected := range expectedStrings { + if !strings.Contains(encoded, expected) { + t.Fatalf("Media playlist does not contain daterange tag: %s\nMedia Playlist:\n%v", expected, encoded) + } + } +} + /****************************** * Code generation examples * ******************************/