diff --git a/reader.go b/reader.go index 7f035ea1..152c875c 100644 --- a/reader.go +++ b/reader.go @@ -299,6 +299,17 @@ func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line st state.variant.Captions = v case "NAME": state.variant.Name = v + case "AVERAGE-BANDWIDTH": + var val int + val, err = strconv.Atoi(v) + if strict && err != nil { + return err + } + state.variant.AverageBandwidth = uint32(val) + case "FRAME-RATE": + if state.variant.FrameRate, err = strconv.ParseFloat(v, 64); strict && err != nil { + return err + } } } case state.tagStreamInf && !strings.HasPrefix(line, "#"): @@ -472,6 +483,11 @@ func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, l p.MediaType = VOD } } + case strings.HasPrefix(line, "#EXT-X-DISCONTINUITY-SEQUENCE:"): + state.listType = MEDIA + if _, err = fmt.Sscanf(line, "#EXT-X-DISCONTINUITY-SEQUENCE:%d", &p.DiscontinuitySeq); strict && err != nil { + return err + } case strings.HasPrefix(line, "#EXT-X-KEY:"): state.listType = MEDIA state.xkey = new(Key) diff --git a/reader_test.go b/reader_test.go index 3d5e3930..8e5da095 100644 --- a/reader_test.go +++ b/reader_test.go @@ -183,6 +183,40 @@ func TestDecodeMasterPlaylistWithIFrameStreamInf(t *testing.T) { } } +func TestDecodeMasterPlaylistWithStreamInfAverageBandwidth(t *testing.T) { + f, err := os.Open("sample-playlists/master-with-stream-inf-1.m3u8") + if err != nil { + t.Fatal(err) + } + p := NewMasterPlaylist() + err = p.DecodeFrom(bufio.NewReader(f), false) + if err != nil { + t.Fatal(err) + } + for _, variant := range p.Variants { + if variant.AverageBandwidth == 0 { + t.Errorf("Empty average bandwidth tag on variant URI: %s", variant.URI) + } + } +} + +func TestDecodeMasterPlaylistWithStreamInfFrameRate(t *testing.T) { + f, err := os.Open("sample-playlists/master-with-stream-inf-1.m3u8") + if err != nil { + t.Fatal(err) + } + p := NewMasterPlaylist() + err = p.DecodeFrom(bufio.NewReader(f), false) + if err != nil { + t.Fatal(err) + } + for _, variant := range p.Variants { + if variant.FrameRate == 0 { + t.Errorf("Empty frame rate tag on variant URI: %s", variant.URI) + } + } +} + func TestDecodeMediaPlaylist(t *testing.T) { f, err := os.Open("sample-playlists/wowza-vod-chunklist.m3u8") if err != nil { @@ -446,6 +480,25 @@ func TestMediaPlaylistWithOATCLSSCTE35Tag(t *testing.T) { } } +func TestDecodeMediaPlaylistWithDiscontinuitySeq(t *testing.T) { + f, err := os.Open("sample-playlists/media-playlist-with-discontinuity-seq.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.DiscontinuitySeq == 0 { + t.Error("Empty discontinuity sequenece tag") + } +} + /*************************** * Code parsing examples * ***************************/ diff --git a/sample-playlists/master-with-stream-inf-1.m3u8 b/sample-playlists/master-with-stream-inf-1.m3u8 new file mode 100644 index 00000000..eb7774ff --- /dev/null +++ b/sample-playlists/master-with-stream-inf-1.m3u8 @@ -0,0 +1,12 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,AVERAGE-BANDWIDTH=300000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 +chunklist-b300000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=600000,AVERAGE-BANDWIDTH=600000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 +chunklist-b600000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850000,AVERAGE-BANDWIDTH=850000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 +chunklist-b850000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,AVERAGE-BANDWIDTH=1000000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 +chunklist-b1000000.m3u8 +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1500000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 +chunklist-b1500000.m3u8 \ No newline at end of file diff --git a/sample-playlists/media-playlist-with-discontinuity-seq.m3u8 b/sample-playlists/media-playlist-with-discontinuity-seq.m3u8 new file mode 100644 index 00000000..b4e89fb3 --- /dev/null +++ b/sample-playlists/media-playlist-with-discontinuity-seq.m3u8 @@ -0,0 +1,14 @@ +#EXTM3U +#EXT-X-TARGETDURATION:10 +#EXT-X-VERSION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-DISCONTINUITY-SEQUENCE:2 +#EXTINF:10.0, +ad0.ts +#EXTINF:8.0, +ad1.ts +#EXT-X-DISCONTINUITY +#EXTINF:10.0, +movieA.ts +#EXTINF:10.0, +movieB.ts \ No newline at end of file diff --git a/structure.go b/structure.go index b442cdec..ab02ecc2 100644 --- a/structure.go +++ b/structure.go @@ -102,25 +102,26 @@ const ( https://priv.example.com/fileSequence2682.ts */ type MediaPlaylist struct { - TargetDuration float64 - SeqNo uint64 // EXT-X-MEDIA-SEQUENCE - Segments []*MediaSegment - Args string // optional arguments placed after URIs (URI?Args) - Iframe bool // EXT-X-I-FRAMES-ONLY - Closed bool // is this VOD (closed) or Live (sliding) playlist? - MediaType MediaType - durationAsInt bool // output durations as integers of floats? - keyformat int - winsize uint // max number of segments displayed in an encoded playlist; need set to zero for VOD playlists - capacity uint // total capacity of slice used for the playlist - head uint // head of FIFO, we add segments to head - tail uint // tail of FIFO, we remove segments from tail - count uint // number of segments added to the playlist - buf bytes.Buffer - ver uint8 - Key *Key // EXT-X-KEY is optional encryption key displayed before any segments (default key for the playlist) - Map *Map // EXT-X-MAP is optional tag specifies how to obtain the Media Initialization Section (default map for the playlist) - WV *WV // Widevine related tags outside of M3U8 specs + TargetDuration float64 + SeqNo uint64 // EXT-X-MEDIA-SEQUENCE + Segments []*MediaSegment + Args string // optional arguments placed after URIs (URI?Args) + Iframe bool // EXT-X-I-FRAMES-ONLY + Closed bool // is this VOD (closed) or Live (sliding) playlist? + MediaType MediaType + DiscontinuitySeq uint64 // EXT-X-DISCONTINUITY-SEQUENCE + durationAsInt bool // output durations as integers of floats? + keyformat int + winsize uint // max number of segments displayed in an encoded playlist; need set to zero for VOD playlists + capacity uint // total capacity of slice used for the playlist + head uint // head of FIFO, we add segments to head + tail uint // tail of FIFO, we remove segments from tail + count uint // number of segments added to the playlist + buf bytes.Buffer + ver uint8 + Key *Key // EXT-X-KEY is optional encryption key displayed before any segments (default key for the playlist) + Map *Map // EXT-X-MAP is optional tag specifies how to obtain the Media Initialization Section (default map for the playlist) + WV *WV // Widevine related tags outside of M3U8 specs } /* @@ -157,17 +158,19 @@ type Variant struct { // This structure represents additional parameters for a variant // used in EXT-X-STREAM-INF and EXT-X-I-FRAME-STREAM-INF type VariantParams struct { - ProgramId uint32 - Bandwidth uint32 - Codecs string - Resolution string - Audio string // EXT-X-STREAM-INF only - Video string - Subtitles string // EXT-X-STREAM-INF only - Captions string // EXT-X-STREAM-INF only - Name string // EXT-X-STREAM-INF only (non standard Wowza/JWPlayer extension to name the variant/quality in UA) - Iframe bool // EXT-X-I-FRAME-STREAM-INF - Alternatives []*Alternative // EXT-X-MEDIA + ProgramId uint32 + Bandwidth uint32 + AverageBandwidth uint32 // EXT-X-STREAM-INF only + Codecs string + Resolution string + Audio string // EXT-X-STREAM-INF only + Video string + Subtitles string // EXT-X-STREAM-INF only + Captions string // EXT-X-STREAM-INF only + Name string // EXT-X-STREAM-INF only (non standard Wowza/JWPlayer extension to name the variant/quality in UA) + FrameRate float64 // EXT-X-STREAM-INF + Iframe bool // EXT-X-I-FRAME-STREAM-INF + Alternatives []*Alternative // EXT-X-MEDIA } // This structure represents EXT-X-MEDIA tag in variants. diff --git a/writer.go b/writer.go index 36f0c8ec..5eb2efa2 100644 --- a/writer.go +++ b/writer.go @@ -174,6 +174,10 @@ func (p *MasterPlaylist) Encode() *bytes.Buffer { p.buf.WriteString(strconv.FormatUint(uint64(pl.ProgramId), 10)) p.buf.WriteString(",BANDWIDTH=") p.buf.WriteString(strconv.FormatUint(uint64(pl.Bandwidth), 10)) + if pl.AverageBandwidth != 0 { + p.buf.WriteString(",AVERAGE-BANDWIDTH=") + p.buf.WriteString(strconv.FormatUint(uint64(pl.Bandwidth), 10)) + } if pl.Codecs != "" { p.buf.WriteString(",CODECS=\"") p.buf.WriteString(pl.Codecs) @@ -213,6 +217,10 @@ func (p *MasterPlaylist) Encode() *bytes.Buffer { p.buf.WriteString(pl.Name) p.buf.WriteRune('"') } + if pl.FrameRate != 0 { + p.buf.WriteString(",FRAME-RATE=") + p.buf.WriteString(strconv.FormatFloat(pl.FrameRate, 'f', 3, 64)) + } p.buf.WriteRune('\n') p.buf.WriteString(pl.URI) if p.Args != "" { @@ -392,6 +400,10 @@ func (p *MediaPlaylist) Encode() *bytes.Buffer { p.buf.WriteString("#EXT-X-TARGETDURATION:") p.buf.WriteString(strconv.FormatInt(int64(math.Ceil(p.TargetDuration)), 10)) // due section 3.4.2 of M3U8 specs EXT-X-TARGETDURATION must be integer p.buf.WriteRune('\n') + if p.DiscontinuitySeq != 0 { + p.buf.WriteString("#EXT-X-DISCONTINUITY-SEQUENCE:") + p.buf.WriteString(strconv.FormatUint(uint64(p.DiscontinuitySeq), 10)) + } if p.Iframe { p.buf.WriteString("#EXT-X-I-FRAMES-ONLY\n") } diff --git a/writer_test.go b/writer_test.go index c737e690..b3b32fec 100644 --- a/writer_test.go +++ b/writer_test.go @@ -844,15 +844,15 @@ func ExampleMasterPlaylist_String() { for i := 0; i < 5; i++ { p.Append(fmt.Sprintf("test%d.ts", i), 5.0, "") } - m.Append("chunklist1.m3u8", p, VariantParams{ProgramId: 123, Bandwidth: 1500000, Resolution: "576x480"}) - m.Append("chunklist2.m3u8", p, VariantParams{ProgramId: 123, Bandwidth: 1500000, Resolution: "576x480"}) + m.Append("chunklist1.m3u8", p, VariantParams{ProgramId: 123, Bandwidth: 1500000, AverageBandwidth: 1500000, Resolution: "576x480", FrameRate: 25.000}) + m.Append("chunklist2.m3u8", p, VariantParams{ProgramId: 123, Bandwidth: 1500000, AverageBandwidth: 1500000, Resolution: "576x480", FrameRate: 25.000}) fmt.Printf("%s", m) // Output: // #EXTM3U // #EXT-X-VERSION:3 - // #EXT-X-STREAM-INF:PROGRAM-ID=123,BANDWIDTH=1500000,RESOLUTION=576x480 + // #EXT-X-STREAM-INF:PROGRAM-ID=123,BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1500000,RESOLUTION=576x480,FRAME-RATE=25.000 // chunklist1.m3u8 - // #EXT-X-STREAM-INF:PROGRAM-ID=123,BANDWIDTH=1500000,RESOLUTION=576x480 + // #EXT-X-STREAM-INF:PROGRAM-ID=123,BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1500000,RESOLUTION=576x480,FRAME-RATE=25.000 // chunklist2.m3u8 }