├── .deepsource.toml ├── .drone.yml ├── .gitignore ├── .travis.yml ├── AUTHORS ├── Gomfile ├── LICENSE ├── M3U8.md ├── README.md ├── doc.go ├── example ├── example.go └── template │ ├── custom-playlist-tag-template.go │ └── custom-segment-tag-template.go ├── go.mod ├── nut.json ├── reader.go ├── reader_test.go ├── sample-playlists ├── master-playlist-with-custom-tags.m3u8 ├── master-with-alternatives-b.m3u8 ├── master-with-alternatives.m3u8 ├── master-with-closed-captions-eq-none.m3u8 ├── master-with-hlsv7.m3u8 ├── master-with-i-frame-stream-inf.m3u8 ├── master-with-independent-segments.m3u8 ├── master-with-multiple-codecs.m3u8 ├── master-with-stream-inf-1.m3u8 ├── master-with-stream-inf-name.m3u8 ├── master.m3u8 ├── media-playlist-large.m3u8 ├── media-playlist-with-byterange.m3u8 ├── media-playlist-with-cue-out-in-without-oatcls.m3u8 ├── media-playlist-with-custom-tags.m3u8 ├── media-playlist-with-discontinuity-seq.m3u8 ├── media-playlist-with-discontinuity.m3u8 ├── media-playlist-with-oatcls-scte35.m3u8 ├── media-playlist-with-program-date-time.m3u8 ├── media-playlist-with-scte35-1.m3u8 ├── media-playlist-with-scte35.m3u8 ├── media-playlist-with-start-time.m3u8 ├── widevine-bitrate.m3u8 ├── widevine-master.m3u8 ├── wowza-master.m3u8 └── wowza-vod-chunklist.m3u8 ├── structure.go ├── structure_test.go ├── writer.go └── writer_test.go /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = [ 4 | "*_test.go" 5 | ] 6 | 7 | exclude_patterns = [ 8 | "vendor/**" 9 | ] 10 | 11 | [[analyzers]] 12 | name = "go" 13 | enabled = true 14 | 15 | [analyzers.meta] 16 | import_path = "github.com/grafov/m3u8" 17 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | name: default 3 | 4 | workspace: 5 | base: /go 6 | path: src/github.com/grafov/m3u8 7 | 8 | steps: 9 | - name: test 10 | image: golang 11 | commands: 12 | - go get 13 | - go test 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | *~ 6 | 7 | # Folders 8 | _obj 9 | _test 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | # Versions of go that are explicitly supported. 4 | go: 5 | - 1.6.3 6 | - 1.7.3 7 | - 1.8.x 8 | - tip 9 | 10 | # Required for coverage. 11 | before_install: 12 | - go get golang.org/x/tools/cmd/cover 13 | - go get github.com/mattn/goveralls 14 | 15 | script: 16 | - go build -a -v ./... 17 | - diff <(gofmt -d .) <("") 18 | - go test -v -covermode=count -coverprofile=coverage.out 19 | - $GOPATH/bin/goveralls -coverprofile=coverage.out -service=travis-ci 20 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | There are many persons contribute their code (including small patches) 2 | to the project. They listed below in an alphabetical order: 3 | 4 | - Alexander I.Grafov 5 | - Andrew Sinclair 6 | - Andrey Chernov 7 | - Bradley Falzon 8 | - Denys Smirnov 9 | - Fabrizio (Misto) Milo 10 | - Hori Ryota 11 | - Jamie Stackhouse 12 | - Julian Cooper 13 | - Kz26 14 | - Lei Gao 15 | - Makombo 16 | - Michael Bow 17 | - Scott Kidder 18 | - Vishal Kumar Tuniki 19 | - Yevgen Flerko 20 | - Zac Shenker 21 | - Matthew Neil [mjneil](https://github.com/mjneil) 22 | 23 | If you want to be added to this list (or removed for any reason) 24 | just open an issue about it. 25 | -------------------------------------------------------------------------------- /Gomfile: -------------------------------------------------------------------------------- 1 | group :development do 2 | gom 'github.com/grafov/m3u8', :goos => [:linux] 3 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2016 Alexander I.Grafov 2 | Copyright (c) 2013-2016 The Project Developers. 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | Redistributions in binary form must reproduce the above copyright notice, this 13 | list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | Neither the name of the author nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /M3U8.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | M3U8 tags cheatsheet 16 | ==================== 17 | 18 | The table above describes tags of M3U8, their occurence in playlists of different types and their support status 19 | in the go-library. 20 | 21 | Legend for playlist types: 22 | 23 | * MAS is master playlist 24 | * MED is media playlist 25 | 26 | 27 | 28 | 29 | | Tag | Occured in | Proto ver | In Go lib since | 30 | |---|---|---|---| 31 | | EXT-X-ALLOW-CACHE | MED | 1 | 0.1 | 32 | | EXT-X-BYTERANGE | MED | 4 | 0.1 | 33 | | EXT-X-DISCONTINUITY | MED | 1 | 0.2 | 34 | | EXT-X-DISCONTINUITY-SEQUENCE | MED | 6 | | 35 | | EXT-X-ENDLIST | MED | 1 | 0.1 | 36 | | EXT-X-I-FRAME-STREAM-INF | MAS | 4 | 0.3 | 37 | | EXT-X-I-FRAMES-ONLY | MED | 4 | 0.3 | 38 | | EXT-X-INDEPENDENT-SEGMENTS | MAS | 6 | | 39 | | EXT-X-KEY | MED | 1 | 0.1 | 40 | | EXT-X-MAP | MED | 5 | 0.3 | 41 | | EXT-X-MEDIA | MAS | 4 | 0.1 | 42 | | EXT-X-MEDIA-SEQUENCE | MED | 1 | 0.1 | 43 | | EXT-X-PLAYLIST-TYPE | MED | 3 | 0.2 | 44 | | EXT-X-PROGRAM-DATE-TIME | MED | 1 | 0.2 | 45 | | EXT-X-SESSION-DATA | MAS | 7 | | 46 | | EXT-X-START | MAS | 6 | | 47 | | EXT-X-STREAM-INF | MAS | 1 | 0.1 | 48 | | EXT-X-TARGETDURATION | MED | 1 | 0.1 | 49 | | EXT-X-VERSION | MAS | 2 | 0.1 | 50 | | EXTINF | MED | 1 | 0.1 | 51 | | EXTM3U | MAS,MED | 1 | 0.1 | 52 | 53 | 54 | 81 | 82 | 83 | IETF drafts notes 84 | ----------------- 85 | 86 | [IETF](http://ietf.org) document currently in Draft status. Different versions of the document introduce changes of HLS protocol playlist formats. Latest version of the HLS protocol is version 7. 87 | 88 | http://tools.ietf.org/html/draft-pantos-http-live-streaming 89 | 90 | * Version 1 of the HLS protocol described in draft00-draft02. 91 | * Version 2 of the HLS protocol described in draft03-draft04. 92 | * Version 3 of the HLS protocol described in draft05-draft06. 93 | * Version 4 of the HLS protocol described in draft07-draft08. 94 | * Version 5 of the HLS protocol described in draft09-draft11. 95 | * Version 6 of the HLS protocol described in draft12-draft13. 96 | * Version 7 of the HLS protocol described in draft14-draft19. 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | M3U8 [![](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#video) 3 | ==== 4 | 5 | Project status 6 | --------------- 7 | 8 | Project support suspended and code moved to read only archive. 9 | https://github.com/grafov/m3u8/issues/225 10 | 11 | About 12 | ----- 13 | 14 | This is the most complete opensource library for parsing and generating of M3U8 playlists 15 | used in HTTP Live Streaming (Apple HLS) for internet video translations. 16 | 17 | M3U8 is simple text format and parsing library for it must be simple too. It does not offer 18 | ways to play HLS or handle playlists over HTTP. So library features are: 19 | 20 | * Support HLS specs up to version 5 of the protocol. 21 | * Parsing and generation of master-playlists and media-playlists. 22 | * Autodetect input streams as master or media playlists. 23 | * Offer structures for keeping playlists metadata. 24 | * Encryption keys support for use with DRM systems like [Verimatrix](http://verimatrix.com) etc. 25 | * Support for non standard [Google Widevine](http://www.widevine.com) tags. 26 | 27 | The library covered by BSD 3-clause license. See [LICENSE](LICENSE) for the full text. 28 | Versions 0.8 and below was covered by GPL v3. License was changed from the version 0.9 and upper. 29 | 30 | See the list of the library authors at [AUTHORS](AUTHORS) file. 31 | 32 | 33 | Install 34 | ------- 35 | 36 | go get github.com/grafov/m3u8 37 | 38 | or get releases from https://github.com/grafov/m3u8/releases 39 | 40 | Documentation [![GoDoc](https://godoc.org/github.com/grafov/m3u8?status.svg)](https://pkg.go.dev/github.com/grafov/m3u8) 41 | ------------- 42 | 43 | Package online documentation (examples included) available at: 44 | 45 | * http://pkg.go.dev/github.com/grafov/m3u8 46 | 47 | Supported by the HLS protocol tags and their library support explained in [M3U8 cheatsheet](M3U8.md). 48 | 49 | Examples 50 | -------- 51 | 52 | Parse playlist: 53 | 54 | ```go 55 | f, err := os.Open("playlist.m3u8") 56 | if err != nil { 57 | panic(err) 58 | } 59 | p, listType, err := m3u8.DecodeFrom(bufio.NewReader(f), true) 60 | if err != nil { 61 | panic(err) 62 | } 63 | switch listType { 64 | case m3u8.MEDIA: 65 | mediapl := p.(*m3u8.MediaPlaylist) 66 | fmt.Printf("%+v\n", mediapl) 67 | case m3u8.MASTER: 68 | masterpl := p.(*m3u8.MasterPlaylist) 69 | fmt.Printf("%+v\n", masterpl) 70 | } 71 | ``` 72 | 73 | Then you get filled with parsed data structures. For master playlists you get ``Master`` struct with slice consists of pointers to ``Variant`` structures (which represent playlists to each bitrate). 74 | For media playlist parser returns ``MediaPlaylist`` structure with slice of ``Segments``. Each segment is of ``MediaSegment`` type. 75 | See ``structure.go`` or full documentation (link below). 76 | 77 | You may use API methods to fill structures or create them manually to generate playlists. Example of media playlist generation: 78 | 79 | ```go 80 | p, e := m3u8.NewMediaPlaylist(3, 10) // with window of size 3 and capacity 10 81 | if e != nil { 82 | panic(fmt.Sprintf("Creating of media playlist failed: %s", e)) 83 | } 84 | for i := 0; i < 5; i++ { 85 | e = p.Append(fmt.Sprintf("test%d.ts", i), 6.0, "") 86 | if e != nil { 87 | panic(fmt.Sprintf("Add segment #%d to a media playlist failed: %s", i, e)) 88 | } 89 | } 90 | fmt.Println(p.Encode().String()) 91 | ``` 92 | 93 | Custom Tags 94 | ----------- 95 | 96 | M3U8 supports parsing and writing of custom tags. You must implement both the `CustomTag` and `CustomDecoder` interface for each custom tag that may be encountered in the playlist. Look at the template files in `example/template/` for examples on parsing custom playlist and segment tags. 97 | 98 | Library structure 99 | ----------------- 100 | 101 | Library has compact code and bundled in three files: 102 | 103 | * `structure.go` — declares all structures related to playlists and their properties 104 | * `reader.go` — playlist parser methods 105 | * `writer.go` — playlist generator methods 106 | 107 | Each file has own test suite placed in `*_test.go` accordingly. 108 | 109 | Related links 110 | ------------- 111 | 112 | * http://en.wikipedia.org/wiki/M3U 113 | * http://en.wikipedia.org/wiki/HTTP_Live_Streaming 114 | * http://gonze.com/playlists/playlist-format-survey.html 115 | 116 | Library usage 117 | ------------- 118 | 119 | This library was successfully used in streaming software developed for company where I worked several 120 | years ago. It was tested then in generating of VOD and Live streams and parsing of Widevine Live streams. 121 | Also the library used in opensource software so you may look at these apps for usage examples: 122 | 123 | * [HLS downloader](https://github.com/kz26/gohls) 124 | * [Another HLS downloader](https://github.com/Makombo/hlsdownloader) 125 | * [HLS utils](https://github.com/archsh/hls-utils) 126 | * [M3U8 reader](https://github.com/jeongmin/m3u8-reader) 127 | 128 | Project status [![Go Report Card](https://goreportcard.com/badge/grafov/m3u8)](https://goreportcard.com/report/grafov/m3u8) 129 | -------------- 130 | 131 | [![Build Status](https://travis-ci.org/grafov/m3u8.png?branch=master)](https://travis-ci.org/grafov/m3u8) [![Build Status](https://cloud.drone.io/api/badges/grafov/m3u8/status.svg)](https://cloud.drone.io/grafov/m3u8) [![Coverage Status](https://coveralls.io/repos/github/grafov/m3u8/badge.svg?branch=master)](https://coveralls.io/github/grafov/m3u8?branch=master) 132 | 133 | [![DeepSource](https://static.deepsource.io/deepsource-badge-light.svg)](https://deepsource.io/gh/grafov/m3u8/?ref=repository-badge) 134 | 135 | Code coverage: https://gocover.io/github.com/grafov/m3u8 136 | 137 | Project maintainers 138 | -------------------- 139 | 140 | Thank to all people who contrubuted to the code and maintain 141 | it. Especially thank to the maintainers who involved in the project 142 | actively in the past and helped to keep code actual: 143 | 144 | * Lei Gao @leikao 145 | * Bradley Falzon @bradleyfalzon 146 | 147 | New maitainers are welcome. 148 | 149 | Alternatives 150 | ------------- 151 | 152 | On the project start in 2013 there was no any other libs in Go for m3u8. Later the alternatives came. Some of them may be more fit current standards. 153 | 154 | * https://github.com/hr8/rosso 155 | 156 | Drop a link in issue if you know other projects. 157 | 158 | FYI M3U8 parsing/generation in other languages 159 | ------------------------------------------ 160 | 161 | * https://github.com/globocom/m3u8 in Python 162 | * https://github.com/zencoder/m3uzi in Ruby 163 | * https://github.com/Jeanvf/M3U8Paser in Objective C 164 | * https://github.com/tedconf/node-m3u8 in Javascript 165 | * http://sourceforge.net/projects/m3u8parser/ in Java 166 | * https://github.com/karlll/erlm3u8 in Erlang 167 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package m3u8 is parser & generator library for Apple HLS. 2 | 3 | /* This is a most complete opensource library for parsing and 4 | generating of M3U8 playlists used in HTTP Live Streaming (Apple 5 | HLS) for internet video translations. 6 | 7 | M3U8 is simple text format and parsing library for it must be simple 8 | too. It did not offer ways to play HLS or handle playlists over 9 | HTTP. Library features are: 10 | 11 | * Support HLS specs up to version 5 of the protocol. 12 | * Parsing and generation of master-playlists and media-playlists. 13 | * Autodetect input streams as master or media playlists. 14 | * Offer structures for keeping playlists metadata. 15 | * Encryption keys support for usage with DRM systems like Verimatrix (http://verimatrix.com) etc. 16 | * Support for non standard Google Widevine (http://www.widevine.com) tags. 17 | 18 | Library coded accordingly with IETF draft 19 | http://tools.ietf.org/html/draft-pantos-http-live-streaming 20 | 21 | Examples of usage may be found in *_test.go files of a package. Also 22 | see below some simple examples. 23 | 24 | Create simple media playlist with sliding window of 3 segments and 25 | maximum of 50 segments. 26 | 27 | p, e := NewMediaPlaylist(3, 50) 28 | if e != nil { 29 | panic(fmt.Sprintf("Create media playlist failed: %s", e)) 30 | } 31 | for i := 0; i < 5; i++ { 32 | e = p.Add(fmt.Sprintf("test%d.ts", i), 5.0) 33 | if e != nil { 34 | panic(fmt.Sprintf("Add segment #%d to a media playlist failed: %s", i, e)) 35 | } 36 | } 37 | fmt.Println(p.Encode(true).String()) 38 | 39 | We add 5 testX.ts segments to playlist then encode it to M3U8 format 40 | and convert to string. 41 | 42 | Next example shows parsing of master playlist: 43 | 44 | f, err := os.Open("sample-playlists/master.m3u8") 45 | if err != nil { 46 | fmt.Println(err) 47 | } 48 | p := NewMasterPlaylist() 49 | err = p.DecodeFrom(bufio.NewReader(f), false) 50 | if err != nil { 51 | fmt.Println(err) 52 | } 53 | 54 | fmt.Printf("Playlist object: %+v\n", p) 55 | 56 | We are open playlist from the file and parse it as master playlist. 57 | */ 58 | 59 | package m3u8 60 | 61 | // Copyright 2013-2019 The Project Developers. 62 | // See the AUTHORS and LICENSE files at the top-level directory of this distribution 63 | // and at https://github.com/grafov/m3u8/ 64 | 65 | // ॐ तारे तुत्तारे तुरे स्व 66 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "path" 8 | 9 | "github.com/grafov/m3u8" 10 | "github.com/grafov/m3u8/example/template" 11 | ) 12 | 13 | func main() { 14 | GOPATH := os.Getenv("GOPATH") 15 | if GOPATH == "" { 16 | panic("$GOPATH is empty") 17 | } 18 | 19 | m3u8File := "github.com/grafov/m3u8/sample-playlists/media-playlist-with-custom-tags.m3u8" 20 | f, err := os.Open(path.Join(GOPATH, "src", m3u8File)) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | customTags := []m3u8.CustomDecoder{ 26 | &template.CustomPlaylistTag{}, 27 | &template.CustomSegmentTag{}, 28 | } 29 | 30 | p, listType, err := m3u8.DecodeWith(bufio.NewReader(f), true, customTags) 31 | if err != nil { 32 | panic(err) 33 | } 34 | switch listType { 35 | case m3u8.MEDIA: 36 | mediapl := p.(*m3u8.MediaPlaylist) 37 | fmt.Printf("%+v\n", mediapl) 38 | case m3u8.MASTER: 39 | masterpl := p.(*m3u8.MasterPlaylist) 40 | fmt.Printf("%+v\n", masterpl) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/template/custom-playlist-tag-template.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/grafov/m3u8" 9 | ) 10 | 11 | // #CUSTOM-PLAYLIST-TAG: 12 | 13 | // CustomPlaylistTag implements both CustomTag and CustomDecoder 14 | // interfaces. 15 | type CustomPlaylistTag struct { 16 | Number int 17 | } 18 | 19 | // TagName should return the full indentifier including the leading 20 | // '#' and trailing ':' if the tag also contains a value or attribute 21 | // list. 22 | func (tag *CustomPlaylistTag) TagName() string { 23 | return "#CUSTOM-PLAYLIST-TAG:" 24 | } 25 | 26 | // Decode decodes the input line. The line will be the entire matched 27 | // line, including the identifier 28 | func (tag *CustomPlaylistTag) Decode(line string) (m3u8.CustomTag, error) { 29 | _, err := fmt.Sscanf(line, "#CUSTOM-PLAYLIST-TAG:%d", &tag.Number) 30 | 31 | return tag, err 32 | } 33 | 34 | // SegmentTag is a playlist tag example. 35 | func (tag *CustomPlaylistTag) SegmentTag() bool { 36 | return false 37 | } 38 | 39 | // Encode formats the structure to the text result. 40 | func (tag *CustomPlaylistTag) Encode() *bytes.Buffer { 41 | buf := new(bytes.Buffer) 42 | 43 | buf.WriteString(tag.TagName()) 44 | buf.WriteString(strconv.Itoa(tag.Number)) 45 | 46 | return buf 47 | } 48 | 49 | // String implements Stringer interface. 50 | func (tag *CustomPlaylistTag) String() string { 51 | return tag.Encode().String() 52 | } 53 | -------------------------------------------------------------------------------- /example/template/custom-segment-tag-template.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | 7 | "github.com/grafov/m3u8" 8 | ) 9 | 10 | // #CUSTOM-SEGMENT-TAG: 11 | 12 | // CustomSegmentTag implements both CustomTag and CustomDecoder 13 | // interfaces. 14 | type CustomSegmentTag struct { 15 | Name string 16 | Jedi bool 17 | } 18 | 19 | // TagName should return the full indentifier including the leading '#' and trailing ':' 20 | // if the tag also contains a value or attribute list 21 | func (tag *CustomSegmentTag) TagName() string { 22 | return "#CUSTOM-SEGMENT-TAG:" 23 | } 24 | 25 | // Decode decodes the input string to the internal structure. The line 26 | // will be the entire matched line, including the identifier. 27 | func (tag *CustomSegmentTag) Decode(line string) (m3u8.CustomTag, error) { 28 | var err error 29 | 30 | // Since this is a Segment tag, we want to create a new tag every time it is decoded 31 | // as there can be one for each segment with 32 | newTag := new(CustomSegmentTag) 33 | 34 | for k, v := range m3u8.DecodeAttributeList(line[20:]) { 35 | switch k { 36 | case "NAME": 37 | newTag.Name = v 38 | case "JEDI": 39 | if v == "YES" { 40 | newTag.Jedi = true 41 | } else if v == "NO" { 42 | newTag.Jedi = false 43 | } else { 44 | err = errors.New("Valid strings for JEDI attribute are YES and NO.") 45 | } 46 | } 47 | } 48 | 49 | return newTag, err 50 | } 51 | 52 | // SegmentTag is a playlist tag example. 53 | func (tag *CustomSegmentTag) SegmentTag() bool { 54 | return true 55 | } 56 | 57 | // Encode encodes the structure to the text result. 58 | func (tag *CustomSegmentTag) Encode() *bytes.Buffer { 59 | buf := new(bytes.Buffer) 60 | 61 | if tag.Name != "" { 62 | buf.WriteString(tag.TagName()) 63 | buf.WriteString("NAME=\"") 64 | buf.WriteString(tag.Name) 65 | buf.WriteString("\",JEDI=") 66 | if tag.Jedi { 67 | buf.WriteString("YES") 68 | } else { 69 | buf.WriteString("NO") 70 | } 71 | } 72 | 73 | return buf 74 | } 75 | 76 | // String implements Stringer interface. 77 | func (tag *CustomSegmentTag) String() string { 78 | return tag.Encode().String() 79 | } 80 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafov/m3u8 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /nut.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "0.1.0", 3 | "Vendor": "grafov", 4 | "Authors": [ 5 | { 6 | "FullName": "Alexander I.Grafov", 7 | "Email": "grafov@gmail.com" 8 | } 9 | ], 10 | "ExtraFiles": [ 11 | "README.md", 12 | "M3U8.md", 13 | "LICENSE", 14 | "TODO.org", 15 | "sample-playlists" 16 | ], 17 | "Homepage": "http://github.com/grafov/m3u8" 18 | } 19 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | /* 4 | Part of M3U8 parser & generator library. 5 | This file defines functions related to playlist parsing. 6 | 7 | Copyright 2013-2019 The Project Developers. 8 | See the AUTHORS and LICENSE files at the top-level directory of this distribution 9 | and at https://github.com/grafov/m3u8/ 10 | 11 | ॐ तारे तुत्तारे तुरे स्व 12 | */ 13 | 14 | import ( 15 | "bytes" 16 | "errors" 17 | "fmt" 18 | "io" 19 | "regexp" 20 | "strconv" 21 | "strings" 22 | "time" 23 | ) 24 | 25 | var reKeyValue = regexp.MustCompile(`([a-zA-Z0-9_-]+)=("[^"]+"|[^",]+)`) 26 | 27 | // TimeParse allows globally apply and/or override Time Parser function. 28 | // Available variants: 29 | // - FullTimeParse - implements full featured ISO/IEC 8601:2004 30 | // - StrictTimeParse - implements only RFC3339 Nanoseconds format 31 | var TimeParse func(value string) (time.Time, error) = FullTimeParse 32 | 33 | // Decode parses a master playlist passed from the buffer. If `strict` 34 | // parameter is true then it returns first syntax error. 35 | func (p *MasterPlaylist) Decode(data bytes.Buffer, strict bool) error { 36 | return p.decode(&data, strict) 37 | } 38 | 39 | // DecodeFrom parses a master playlist passed from the io.Reader 40 | // stream. If `strict` parameter is true then it returns first syntax 41 | // error. 42 | func (p *MasterPlaylist) DecodeFrom(reader io.Reader, strict bool) error { 43 | buf := new(bytes.Buffer) 44 | _, err := buf.ReadFrom(reader) 45 | if err != nil { 46 | return err 47 | } 48 | return p.decode(buf, strict) 49 | } 50 | 51 | // WithCustomDecoders adds custom tag decoders to the master playlist for decoding 52 | func (p *MasterPlaylist) WithCustomDecoders(customDecoders []CustomDecoder) Playlist { 53 | // Create the map if it doesn't already exist 54 | if p.Custom == nil { 55 | p.Custom = make(map[string]CustomTag) 56 | } 57 | 58 | p.customDecoders = customDecoders 59 | 60 | return p 61 | } 62 | 63 | // Parse master playlist. Internal function. 64 | func (p *MasterPlaylist) decode(buf *bytes.Buffer, strict bool) error { 65 | var eof bool 66 | 67 | state := new(decodingState) 68 | 69 | for !eof { 70 | line, err := buf.ReadString('\n') 71 | if err == io.EOF { 72 | eof = true 73 | } else if err != nil { 74 | break 75 | } 76 | err = decodeLineOfMasterPlaylist(p, state, line, strict) 77 | if strict && err != nil { 78 | return err 79 | } 80 | } 81 | 82 | p.attachRenditionsToVariants(state.alternatives) 83 | 84 | if strict && !state.m3u { 85 | return errors.New("#EXTM3U absent") 86 | } 87 | return nil 88 | } 89 | 90 | func (p *MasterPlaylist) attachRenditionsToVariants(alternatives []*Alternative) { 91 | for _, variant := range p.Variants { 92 | for _, alt := range alternatives { 93 | if alt == nil { 94 | continue 95 | } 96 | if variant.Video != "" && alt.Type == "VIDEO" && variant.Video == alt.GroupId { 97 | variant.Alternatives = append(variant.Alternatives, alt) 98 | } 99 | if variant.Audio != "" && alt.Type == "AUDIO" && variant.Audio == alt.GroupId { 100 | variant.Alternatives = append(variant.Alternatives, alt) 101 | } 102 | if variant.Captions != "" && alt.Type == "CLOSED-CAPTIONS" && variant.Captions == alt.GroupId { 103 | variant.Alternatives = append(variant.Alternatives, alt) 104 | } 105 | if variant.Subtitles != "" && alt.Type == "SUBTITLES" && variant.Subtitles == alt.GroupId { 106 | variant.Alternatives = append(variant.Alternatives, alt) 107 | } 108 | } 109 | } 110 | } 111 | 112 | // Decode parses a media playlist passed from the buffer. If `strict` 113 | // parameter is true then return first syntax error. 114 | func (p *MediaPlaylist) Decode(data bytes.Buffer, strict bool) error { 115 | return p.decode(&data, strict) 116 | } 117 | 118 | // DecodeFrom parses a media playlist passed from the io.Reader 119 | // stream. If `strict` parameter is true then it returns first syntax 120 | // error. 121 | func (p *MediaPlaylist) DecodeFrom(reader io.Reader, strict bool) error { 122 | buf := new(bytes.Buffer) 123 | _, err := buf.ReadFrom(reader) 124 | if err != nil { 125 | return err 126 | } 127 | return p.decode(buf, strict) 128 | } 129 | 130 | // WithCustomDecoders adds custom tag decoders to the media playlist for decoding 131 | func (p *MediaPlaylist) WithCustomDecoders(customDecoders []CustomDecoder) Playlist { 132 | // Create the map if it doesn't already exist 133 | if p.Custom == nil { 134 | p.Custom = make(map[string]CustomTag) 135 | } 136 | 137 | p.customDecoders = customDecoders 138 | 139 | return p 140 | } 141 | 142 | func (p *MediaPlaylist) decode(buf *bytes.Buffer, strict bool) error { 143 | var eof bool 144 | var line string 145 | var err error 146 | 147 | state := new(decodingState) 148 | wv := new(WV) 149 | 150 | for !eof { 151 | if line, err = buf.ReadString('\n'); err == io.EOF { 152 | eof = true 153 | } else if err != nil { 154 | break 155 | } 156 | 157 | err = decodeLineOfMediaPlaylist(p, wv, state, line, strict) 158 | if strict && err != nil { 159 | return err 160 | } 161 | 162 | } 163 | if state.tagWV { 164 | p.WV = wv 165 | } 166 | if strict && !state.m3u { 167 | return errors.New("#EXTM3U absent") 168 | } 169 | return nil 170 | } 171 | 172 | // Decode detects type of playlist and decodes it. It accepts bytes 173 | // buffer as input. 174 | func Decode(data bytes.Buffer, strict bool) (Playlist, ListType, error) { 175 | return decode(&data, strict, nil) 176 | } 177 | 178 | // DecodeFrom detects type of playlist and decodes it. It accepts data 179 | // conformed with io.Reader. 180 | func DecodeFrom(reader io.Reader, strict bool) (Playlist, ListType, error) { 181 | buf := new(bytes.Buffer) 182 | _, err := buf.ReadFrom(reader) 183 | if err != nil { 184 | return nil, 0, err 185 | } 186 | return decode(buf, strict, nil) 187 | } 188 | 189 | // DecodeWith detects the type of playlist and decodes it. It accepts either bytes.Buffer 190 | // or io.Reader as input. Any custom decoders provided will be used during decoding. 191 | func DecodeWith(input interface{}, strict bool, customDecoders []CustomDecoder) (Playlist, ListType, error) { 192 | switch v := input.(type) { 193 | case bytes.Buffer: 194 | return decode(&v, strict, customDecoders) 195 | case io.Reader: 196 | buf := new(bytes.Buffer) 197 | _, err := buf.ReadFrom(v) 198 | if err != nil { 199 | return nil, 0, err 200 | } 201 | return decode(buf, strict, customDecoders) 202 | default: 203 | return nil, 0, errors.New("input must be bytes.Buffer or io.Reader type") 204 | } 205 | } 206 | 207 | // Detect playlist type and decode it. May be used as decoder for both 208 | // master and media playlists. 209 | func decode(buf *bytes.Buffer, strict bool, customDecoders []CustomDecoder) (Playlist, ListType, error) { 210 | var eof bool 211 | var line string 212 | var master *MasterPlaylist 213 | var media *MediaPlaylist 214 | var listType ListType 215 | var err error 216 | 217 | state := new(decodingState) 218 | wv := new(WV) 219 | 220 | master = NewMasterPlaylist() 221 | media, err = NewMediaPlaylist(8, 1024) // Winsize for VoD will become 0, capacity auto extends 222 | if err != nil { 223 | return nil, 0, fmt.Errorf("Create media playlist failed: %s", err) 224 | } 225 | 226 | // If we have custom tags to parse 227 | if customDecoders != nil { 228 | media = media.WithCustomDecoders(customDecoders).(*MediaPlaylist) 229 | master = master.WithCustomDecoders(customDecoders).(*MasterPlaylist) 230 | state.custom = make(map[string]CustomTag) 231 | } 232 | 233 | for !eof { 234 | if line, err = buf.ReadString('\n'); err == io.EOF { 235 | eof = true 236 | } else if err != nil { 237 | break 238 | } 239 | 240 | // fixes the issues https://github.com/grafov/m3u8/issues/25 241 | // TODO: the same should be done in decode functions of both Master- and MediaPlaylists 242 | // so some DRYing would be needed. 243 | if len(line) < 1 || line == "\r" { 244 | continue 245 | } 246 | 247 | err = decodeLineOfMasterPlaylist(master, state, line, strict) 248 | master.attachRenditionsToVariants(state.alternatives) 249 | if strict && err != nil { 250 | return master, state.listType, err 251 | } 252 | 253 | err = decodeLineOfMediaPlaylist(media, wv, state, line, strict) 254 | if strict && err != nil { 255 | return media, state.listType, err 256 | } 257 | 258 | } 259 | if state.listType == MEDIA && state.tagWV { 260 | media.WV = wv 261 | } 262 | 263 | if strict && !state.m3u { 264 | return nil, listType, errors.New("#EXTM3U absent") 265 | } 266 | 267 | switch state.listType { 268 | case MASTER: 269 | return master, MASTER, nil 270 | case MEDIA: 271 | if media.Closed || media.MediaType == EVENT { 272 | // VoD and Event's should show the entire playlist 273 | media.SetWinSize(0) 274 | } 275 | return media, MEDIA, nil 276 | } 277 | return nil, state.listType, errors.New("Can't detect playlist type") 278 | } 279 | 280 | // DecodeAttributeList turns an attribute list into a key, value map. You should trim 281 | // any characters not part of the attribute list, such as the tag and ':'. 282 | func DecodeAttributeList(line string) map[string]string { 283 | return decodeParamsLine(line) 284 | } 285 | 286 | func decodeParamsLine(line string) map[string]string { 287 | out := make(map[string]string) 288 | for _, kv := range reKeyValue.FindAllStringSubmatch(line, -1) { 289 | k, v := kv[1], kv[2] 290 | out[k] = strings.Trim(v, ` "`) 291 | } 292 | return out 293 | } 294 | 295 | // Parse one line of master playlist. 296 | func decodeLineOfMasterPlaylist(p *MasterPlaylist, state *decodingState, line string, strict bool) error { 297 | var err error 298 | 299 | line = strings.TrimSpace(line) 300 | 301 | // check for custom tags first to allow custom parsing of existing tags 302 | if p.Custom != nil { 303 | for _, v := range p.customDecoders { 304 | if strings.HasPrefix(line, v.TagName()) { 305 | t, err := v.Decode(line) 306 | 307 | if strict && err != nil { 308 | return err 309 | } 310 | 311 | p.Custom[t.TagName()] = t 312 | } 313 | } 314 | } 315 | 316 | switch { 317 | case line == "#EXTM3U": // start tag first 318 | state.m3u = true 319 | case strings.HasPrefix(line, "#EXT-X-VERSION:"): // version tag 320 | state.listType = MASTER 321 | _, err = fmt.Sscanf(line, "#EXT-X-VERSION:%d", &p.ver) 322 | if strict && err != nil { 323 | return err 324 | } 325 | case line == "#EXT-X-INDEPENDENT-SEGMENTS": 326 | p.SetIndependentSegments(true) 327 | case strings.HasPrefix(line, "#EXT-X-MEDIA:"): 328 | var alt Alternative 329 | state.listType = MASTER 330 | for k, v := range decodeParamsLine(line[13:]) { 331 | switch k { 332 | case "TYPE": 333 | alt.Type = v 334 | case "GROUP-ID": 335 | alt.GroupId = v 336 | case "LANGUAGE": 337 | alt.Language = v 338 | case "NAME": 339 | alt.Name = v 340 | case "DEFAULT": 341 | if strings.ToUpper(v) == "YES" { 342 | alt.Default = true 343 | } else if strings.ToUpper(v) == "NO" { 344 | alt.Default = false 345 | } else if strict { 346 | return errors.New("value must be YES or NO") 347 | } 348 | case "AUTOSELECT": 349 | alt.Autoselect = v 350 | case "FORCED": 351 | alt.Forced = v 352 | case "CHARACTERISTICS": 353 | alt.Characteristics = v 354 | case "SUBTITLES": 355 | alt.Subtitles = v 356 | case "URI": 357 | alt.URI = v 358 | } 359 | } 360 | state.alternatives = append(state.alternatives, &alt) 361 | case !state.tagStreamInf && strings.HasPrefix(line, "#EXT-X-STREAM-INF:"): 362 | state.tagStreamInf = true 363 | state.listType = MASTER 364 | state.variant = new(Variant) 365 | p.Variants = append(p.Variants, state.variant) 366 | for k, v := range decodeParamsLine(line[18:]) { 367 | switch k { 368 | case "PROGRAM-ID": 369 | var val int 370 | val, err = strconv.Atoi(v) 371 | if strict && err != nil { 372 | return err 373 | } 374 | state.variant.ProgramId = uint32(val) 375 | case "BANDWIDTH": 376 | var val int 377 | val, err = strconv.Atoi(v) 378 | if strict && err != nil { 379 | return err 380 | } 381 | state.variant.Bandwidth = uint32(val) 382 | case "CODECS": 383 | state.variant.Codecs = v 384 | case "RESOLUTION": 385 | state.variant.Resolution = v 386 | case "AUDIO": 387 | state.variant.Audio = v 388 | case "VIDEO": 389 | state.variant.Video = v 390 | case "SUBTITLES": 391 | state.variant.Subtitles = v 392 | case "CLOSED-CAPTIONS": 393 | state.variant.Captions = v 394 | case "NAME": 395 | state.variant.Name = v 396 | case "AVERAGE-BANDWIDTH": 397 | var val int 398 | val, err = strconv.Atoi(v) 399 | if strict && err != nil { 400 | return err 401 | } 402 | state.variant.AverageBandwidth = uint32(val) 403 | case "FRAME-RATE": 404 | if state.variant.FrameRate, err = strconv.ParseFloat(v, 64); strict && err != nil { 405 | return err 406 | } 407 | case "VIDEO-RANGE": 408 | state.variant.VideoRange = v 409 | case "HDCP-LEVEL": 410 | state.variant.HDCPLevel = v 411 | } 412 | } 413 | case state.tagStreamInf && !strings.HasPrefix(line, "#"): 414 | state.tagStreamInf = false 415 | state.variant.URI = line 416 | case strings.HasPrefix(line, "#EXT-X-I-FRAME-STREAM-INF:"): 417 | state.listType = MASTER 418 | state.variant = new(Variant) 419 | state.variant.Iframe = true 420 | if len(state.alternatives) > 0 { 421 | state.variant.Alternatives = state.alternatives 422 | state.alternatives = nil 423 | } 424 | p.Variants = append(p.Variants, state.variant) 425 | for k, v := range decodeParamsLine(line[26:]) { 426 | switch k { 427 | case "URI": 428 | state.variant.URI = v 429 | case "PROGRAM-ID": 430 | var val int 431 | val, err = strconv.Atoi(v) 432 | if strict && err != nil { 433 | return err 434 | } 435 | state.variant.ProgramId = uint32(val) 436 | case "BANDWIDTH": 437 | var val int 438 | val, err = strconv.Atoi(v) 439 | if strict && err != nil { 440 | return err 441 | } 442 | state.variant.Bandwidth = uint32(val) 443 | case "CODECS": 444 | state.variant.Codecs = v 445 | case "RESOLUTION": 446 | state.variant.Resolution = v 447 | case "AUDIO": 448 | state.variant.Audio = v 449 | case "VIDEO": 450 | state.variant.Video = v 451 | case "AVERAGE-BANDWIDTH": 452 | var val int 453 | val, err = strconv.Atoi(v) 454 | if strict && err != nil { 455 | return err 456 | } 457 | state.variant.AverageBandwidth = uint32(val) 458 | case "VIDEO-RANGE": 459 | state.variant.VideoRange = v 460 | case "HDCP-LEVEL": 461 | state.variant.HDCPLevel = v 462 | } 463 | } 464 | case strings.HasPrefix(line, "#"): 465 | // comments are ignored 466 | } 467 | return err 468 | } 469 | 470 | // Parse one line of media playlist. 471 | func decodeLineOfMediaPlaylist(p *MediaPlaylist, wv *WV, state *decodingState, line string, strict bool) error { 472 | var err error 473 | 474 | line = strings.TrimSpace(line) 475 | 476 | // check for custom tags first to allow custom parsing of existing tags 477 | if p.Custom != nil { 478 | for _, v := range p.customDecoders { 479 | if strings.HasPrefix(line, v.TagName()) { 480 | t, err := v.Decode(line) 481 | 482 | if strict && err != nil { 483 | return err 484 | } 485 | 486 | if v.SegmentTag() { 487 | state.tagCustom = true 488 | state.custom[v.TagName()] = t 489 | } else { 490 | p.Custom[v.TagName()] = t 491 | } 492 | } 493 | } 494 | } 495 | 496 | switch { 497 | case !state.tagInf && strings.HasPrefix(line, "#EXTINF:"): 498 | state.tagInf = true 499 | state.listType = MEDIA 500 | sepIndex := strings.Index(line, ",") 501 | if sepIndex == -1 { 502 | if strict { 503 | return fmt.Errorf("could not parse: %q", line) 504 | } 505 | sepIndex = len(line) 506 | } 507 | duration := line[8:sepIndex] 508 | if len(duration) > 0 { 509 | if state.duration, err = strconv.ParseFloat(duration, 64); strict && err != nil { 510 | return fmt.Errorf("Duration parsing error: %s", err) 511 | } 512 | } 513 | if len(line) > sepIndex { 514 | state.title = line[sepIndex+1:] 515 | } 516 | case !strings.HasPrefix(line, "#"): 517 | if state.tagInf { 518 | err := p.Append(line, state.duration, state.title) 519 | if err == ErrPlaylistFull { 520 | // Extend playlist by doubling size, reset internal state, try again. 521 | // If the second Append fails, the if err block will handle it. 522 | // Retrying instead of being recursive was chosen as the state maybe 523 | // modified non-idempotently. 524 | p.Segments = append(p.Segments, make([]*MediaSegment, p.Count())...) 525 | p.capacity = uint(len(p.Segments)) 526 | p.tail = p.count 527 | err = p.Append(line, state.duration, state.title) 528 | } 529 | // Check err for first or subsequent Append() 530 | if err != nil { 531 | return err 532 | } 533 | state.tagInf = false 534 | } 535 | if state.tagRange { 536 | if err = p.SetRange(state.limit, state.offset); strict && err != nil { 537 | return err 538 | } 539 | state.tagRange = false 540 | } 541 | if state.tagSCTE35 { 542 | state.tagSCTE35 = false 543 | if err = p.SetSCTE35(state.scte); strict && err != nil { 544 | return err 545 | } 546 | } 547 | if state.tagDiscontinuity { 548 | state.tagDiscontinuity = false 549 | if err = p.SetDiscontinuity(); strict && err != nil { 550 | return err 551 | } 552 | } 553 | if state.tagProgramDateTime && p.Count() > 0 { 554 | state.tagProgramDateTime = false 555 | if err = p.SetProgramDateTime(state.programDateTime); strict && err != nil { 556 | return err 557 | } 558 | } 559 | // If EXT-X-KEY appeared before reference to segment (EXTINF) then it linked to this segment 560 | if state.tagKey { 561 | p.Segments[p.last()].Key = &Key{state.xkey.Method, state.xkey.URI, state.xkey.IV, state.xkey.Keyformat, state.xkey.Keyformatversions} 562 | // First EXT-X-KEY may appeared in the header of the playlist and linked to first segment 563 | // but for convenient playlist generation it also linked as default playlist key 564 | if p.Key == nil { 565 | p.Key = state.xkey 566 | } 567 | state.tagKey = false 568 | } 569 | // If EXT-X-MAP appeared before reference to segment (EXTINF) then it linked to this segment 570 | if state.tagMap { 571 | p.Segments[p.last()].Map = &Map{state.xmap.URI, state.xmap.Limit, state.xmap.Offset} 572 | // First EXT-X-MAP may appeared in the header of the playlist and linked to first segment 573 | // but for convenient playlist generation it also linked as default playlist map 574 | if p.Map == nil { 575 | p.Map = state.xmap 576 | } 577 | state.tagMap = false 578 | } 579 | 580 | // if segment custom tag appeared before EXTINF then it links to this segment 581 | if state.tagCustom { 582 | p.Segments[p.last()].Custom = state.custom 583 | state.custom = make(map[string]CustomTag) 584 | state.tagCustom = false 585 | } 586 | // start tag first 587 | case line == "#EXTM3U": 588 | state.m3u = true 589 | case line == "#EXT-X-ENDLIST": 590 | state.listType = MEDIA 591 | p.Closed = true 592 | case strings.HasPrefix(line, "#EXT-X-VERSION:"): 593 | state.listType = MEDIA 594 | if _, err = fmt.Sscanf(line, "#EXT-X-VERSION:%d", &p.ver); strict && err != nil { 595 | return err 596 | } 597 | case strings.HasPrefix(line, "#EXT-X-TARGETDURATION:"): 598 | state.listType = MEDIA 599 | if _, err = fmt.Sscanf(line, "#EXT-X-TARGETDURATION:%f", &p.TargetDuration); strict && err != nil { 600 | return err 601 | } 602 | case strings.HasPrefix(line, "#EXT-X-MEDIA-SEQUENCE:"): 603 | state.listType = MEDIA 604 | if _, err = fmt.Sscanf(line, "#EXT-X-MEDIA-SEQUENCE:%d", &p.SeqNo); strict && err != nil { 605 | return err 606 | } 607 | case strings.HasPrefix(line, "#EXT-X-PLAYLIST-TYPE:"): 608 | state.listType = MEDIA 609 | var playlistType string 610 | _, err = fmt.Sscanf(line, "#EXT-X-PLAYLIST-TYPE:%s", &playlistType) 611 | if err != nil { 612 | if strict { 613 | return err 614 | } 615 | } else { 616 | switch playlistType { 617 | case "EVENT": 618 | p.MediaType = EVENT 619 | case "VOD": 620 | p.MediaType = VOD 621 | } 622 | } 623 | case strings.HasPrefix(line, "#EXT-X-DISCONTINUITY-SEQUENCE:"): 624 | state.listType = MEDIA 625 | if _, err = fmt.Sscanf(line, "#EXT-X-DISCONTINUITY-SEQUENCE:%d", &p.DiscontinuitySeq); strict && err != nil { 626 | return err 627 | } 628 | case strings.HasPrefix(line, "#EXT-X-START:"): 629 | state.listType = MEDIA 630 | for k, v := range decodeParamsLine(line[13:]) { 631 | switch k { 632 | case "TIME-OFFSET": 633 | st, err := strconv.ParseFloat(v, 64) 634 | if err != nil { 635 | return fmt.Errorf("Invalid TIME-OFFSET: %s: %v", v, err) 636 | } 637 | p.StartTime = st 638 | case "PRECISE": 639 | p.StartTimePrecise = v == "YES" 640 | } 641 | } 642 | case strings.HasPrefix(line, "#EXT-X-KEY:"): 643 | state.listType = MEDIA 644 | state.xkey = new(Key) 645 | for k, v := range decodeParamsLine(line[11:]) { 646 | switch k { 647 | case "METHOD": 648 | state.xkey.Method = v 649 | case "URI": 650 | state.xkey.URI = v 651 | case "IV": 652 | state.xkey.IV = v 653 | case "KEYFORMAT": 654 | state.xkey.Keyformat = v 655 | case "KEYFORMATVERSIONS": 656 | state.xkey.Keyformatversions = v 657 | } 658 | } 659 | state.tagKey = true 660 | case strings.HasPrefix(line, "#EXT-X-MAP:"): 661 | state.listType = MEDIA 662 | state.xmap = new(Map) 663 | for k, v := range decodeParamsLine(line[11:]) { 664 | switch k { 665 | case "URI": 666 | state.xmap.URI = v 667 | case "BYTERANGE": 668 | if _, err = fmt.Sscanf(v, "%d@%d", &state.xmap.Limit, &state.xmap.Offset); strict && err != nil { 669 | return fmt.Errorf("Byterange sub-range length value parsing error: %s", err) 670 | } 671 | } 672 | } 673 | state.tagMap = true 674 | case !state.tagProgramDateTime && strings.HasPrefix(line, "#EXT-X-PROGRAM-DATE-TIME:"): 675 | state.tagProgramDateTime = true 676 | state.listType = MEDIA 677 | if state.programDateTime, err = TimeParse(line[25:]); strict && err != nil { 678 | return err 679 | } 680 | case !state.tagRange && strings.HasPrefix(line, "#EXT-X-BYTERANGE:"): 681 | state.tagRange = true 682 | state.listType = MEDIA 683 | state.offset = 0 684 | params := strings.SplitN(line[17:], "@", 2) 685 | if state.limit, err = strconv.ParseInt(params[0], 10, 64); strict && err != nil { 686 | return fmt.Errorf("Byterange sub-range length value parsing error: %s", err) 687 | } 688 | if len(params) > 1 { 689 | if state.offset, err = strconv.ParseInt(params[1], 10, 64); strict && err != nil { 690 | return fmt.Errorf("Byterange sub-range offset value parsing error: %s", err) 691 | } 692 | } 693 | case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-SCTE35:"): 694 | state.tagSCTE35 = true 695 | state.listType = MEDIA 696 | state.scte = new(SCTE) 697 | state.scte.Syntax = SCTE35_67_2014 698 | for attribute, value := range decodeParamsLine(line[12:]) { 699 | switch attribute { 700 | case "CUE": 701 | state.scte.Cue = value 702 | case "ID": 703 | state.scte.ID = value 704 | case "TIME": 705 | state.scte.Time, _ = strconv.ParseFloat(value, 64) 706 | } 707 | } 708 | case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-OATCLS-SCTE35:"): 709 | // EXT-OATCLS-SCTE35 contains the SCTE35 tag, EXT-X-CUE-OUT contains duration 710 | state.tagSCTE35 = true 711 | state.scte = new(SCTE) 712 | state.scte.Syntax = SCTE35_OATCLS 713 | state.scte.Cue = line[19:] 714 | case state.tagSCTE35 && state.scte.Syntax == SCTE35_OATCLS && strings.HasPrefix(line, "#EXT-X-CUE-OUT:"): 715 | // EXT-OATCLS-SCTE35 contains the SCTE35 tag, EXT-X-CUE-OUT contains duration 716 | state.scte.Time, _ = strconv.ParseFloat(line[15:], 64) 717 | state.scte.CueType = SCTE35Cue_Start 718 | case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-X-CUE-OUT-CONT:"): 719 | state.tagSCTE35 = true 720 | state.scte = new(SCTE) 721 | state.scte.Syntax = SCTE35_OATCLS 722 | state.scte.CueType = SCTE35Cue_Mid 723 | for attribute, value := range decodeParamsLine(line[20:]) { 724 | switch attribute { 725 | case "SCTE35": 726 | state.scte.Cue = value 727 | case "Duration": 728 | state.scte.Time, _ = strconv.ParseFloat(value, 64) 729 | case "ElapsedTime": 730 | state.scte.Elapsed, _ = strconv.ParseFloat(value, 64) 731 | } 732 | } 733 | case !state.tagSCTE35 && strings.HasPrefix(line, "#EXT-X-CUE-OUT"): 734 | state.tagSCTE35 = true 735 | state.scte = new(SCTE) 736 | state.scte.Syntax = SCTE35_OATCLS 737 | state.scte.CueType = SCTE35Cue_Start 738 | lenLine := len(line) 739 | if lenLine > 14 { 740 | state.scte.Time, _ = strconv.ParseFloat(line[15:], 64) 741 | } 742 | case !state.tagSCTE35 && line == "#EXT-X-CUE-IN": 743 | state.tagSCTE35 = true 744 | state.scte = new(SCTE) 745 | state.scte.Syntax = SCTE35_OATCLS 746 | state.scte.CueType = SCTE35Cue_End 747 | case !state.tagDiscontinuity && strings.HasPrefix(line, "#EXT-X-DISCONTINUITY"): 748 | state.tagDiscontinuity = true 749 | state.listType = MEDIA 750 | case strings.HasPrefix(line, "#EXT-X-I-FRAMES-ONLY"): 751 | state.listType = MEDIA 752 | p.Iframe = true 753 | case strings.HasPrefix(line, "#WV-AUDIO-CHANNELS"): 754 | state.listType = MEDIA 755 | if _, err = fmt.Sscanf(line, "#WV-AUDIO-CHANNELS %d", &wv.AudioChannels); strict && err != nil { 756 | return err 757 | } 758 | if err == nil { 759 | state.tagWV = true 760 | } 761 | case strings.HasPrefix(line, "#WV-AUDIO-FORMAT"): 762 | state.listType = MEDIA 763 | if _, err = fmt.Sscanf(line, "#WV-AUDIO-FORMAT %d", &wv.AudioFormat); strict && err != nil { 764 | return err 765 | } 766 | if err == nil { 767 | state.tagWV = true 768 | } 769 | case strings.HasPrefix(line, "#WV-AUDIO-PROFILE-IDC"): 770 | state.listType = MEDIA 771 | if _, err = fmt.Sscanf(line, "#WV-AUDIO-PROFILE-IDC %d", &wv.AudioProfileIDC); strict && err != nil { 772 | return err 773 | } 774 | if err == nil { 775 | state.tagWV = true 776 | } 777 | case strings.HasPrefix(line, "#WV-AUDIO-SAMPLE-SIZE"): 778 | state.listType = MEDIA 779 | if _, err = fmt.Sscanf(line, "#WV-AUDIO-SAMPLE-SIZE %d", &wv.AudioSampleSize); strict && err != nil { 780 | return err 781 | } 782 | if err == nil { 783 | state.tagWV = true 784 | } 785 | case strings.HasPrefix(line, "#WV-AUDIO-SAMPLING-FREQUENCY"): 786 | state.listType = MEDIA 787 | if _, err = fmt.Sscanf(line, "#WV-AUDIO-SAMPLING-FREQUENCY %d", &wv.AudioSamplingFrequency); strict && err != nil { 788 | return err 789 | } 790 | if err == nil { 791 | state.tagWV = true 792 | } 793 | case strings.HasPrefix(line, "#WV-CYPHER-VERSION"): 794 | state.listType = MEDIA 795 | wv.CypherVersion = line[19:] 796 | state.tagWV = true 797 | case strings.HasPrefix(line, "#WV-ECM"): 798 | state.listType = MEDIA 799 | if _, err = fmt.Sscanf(line, "#WV-ECM %s", &wv.ECM); strict && err != nil { 800 | return err 801 | } 802 | if err == nil { 803 | state.tagWV = true 804 | } 805 | case strings.HasPrefix(line, "#WV-VIDEO-FORMAT"): 806 | state.listType = MEDIA 807 | if _, err = fmt.Sscanf(line, "#WV-VIDEO-FORMAT %d", &wv.VideoFormat); strict && err != nil { 808 | return err 809 | } 810 | if err == nil { 811 | state.tagWV = true 812 | } 813 | case strings.HasPrefix(line, "#WV-VIDEO-FRAME-RATE"): 814 | state.listType = MEDIA 815 | if _, err = fmt.Sscanf(line, "#WV-VIDEO-FRAME-RATE %d", &wv.VideoFrameRate); strict && err != nil { 816 | return err 817 | } 818 | if err == nil { 819 | state.tagWV = true 820 | } 821 | case strings.HasPrefix(line, "#WV-VIDEO-LEVEL-IDC"): 822 | state.listType = MEDIA 823 | if _, err = fmt.Sscanf(line, "#WV-VIDEO-LEVEL-IDC %d", &wv.VideoLevelIDC); strict && err != nil { 824 | return err 825 | } 826 | if err == nil { 827 | state.tagWV = true 828 | } 829 | case strings.HasPrefix(line, "#WV-VIDEO-PROFILE-IDC"): 830 | state.listType = MEDIA 831 | if _, err = fmt.Sscanf(line, "#WV-VIDEO-PROFILE-IDC %d", &wv.VideoProfileIDC); strict && err != nil { 832 | return err 833 | } 834 | if err == nil { 835 | state.tagWV = true 836 | } 837 | case strings.HasPrefix(line, "#WV-VIDEO-RESOLUTION"): 838 | state.listType = MEDIA 839 | wv.VideoResolution = line[21:] 840 | state.tagWV = true 841 | case strings.HasPrefix(line, "#WV-VIDEO-SAR"): 842 | state.listType = MEDIA 843 | if _, err = fmt.Sscanf(line, "#WV-VIDEO-SAR %s", &wv.VideoSAR); strict && err != nil { 844 | return err 845 | } 846 | if err == nil { 847 | state.tagWV = true 848 | } 849 | case strings.HasPrefix(line, "#"): 850 | // comments are ignored 851 | } 852 | return err 853 | } 854 | 855 | // StrictTimeParse implements RFC3339 with Nanoseconds accuracy. 856 | func StrictTimeParse(value string) (time.Time, error) { 857 | return time.Parse(DATETIME, value) 858 | } 859 | 860 | // FullTimeParse implements ISO/IEC 8601:2004. 861 | func FullTimeParse(value string) (time.Time, error) { 862 | layouts := []string{ 863 | "2006-01-02T15:04:05.999999999Z0700", 864 | "2006-01-02T15:04:05.999999999Z07:00", 865 | "2006-01-02T15:04:05.999999999Z07", 866 | } 867 | var ( 868 | err error 869 | t time.Time 870 | ) 871 | for _, layout := range layouts { 872 | if t, err = time.Parse(layout, value); err == nil { 873 | return t, nil 874 | } 875 | } 876 | return t, err 877 | } 878 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Playlist parsing tests. 3 | 4 | Copyright 2013-2019 The Project Developers. 5 | See the AUTHORS and LICENSE files at the top-level directory of this distribution 6 | and at https://github.com/grafov/m3u8/ 7 | 8 | ॐ तारे तुत्तारे तुरे स्व 9 | */ 10 | package m3u8 11 | 12 | import ( 13 | "bufio" 14 | "bytes" 15 | "errors" 16 | "fmt" 17 | "os" 18 | "reflect" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestDecodeMasterPlaylist(t *testing.T) { 24 | f, err := os.Open("sample-playlists/master.m3u8") 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | p := NewMasterPlaylist() 29 | err = p.DecodeFrom(bufio.NewReader(f), false) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | // check parsed values 34 | if p.ver != 3 { 35 | t.Errorf("Version of parsed playlist = %d (must = 3)", p.ver) 36 | } 37 | if len(p.Variants) != 5 { 38 | t.Error("Not all variants in master playlist parsed.") 39 | } 40 | // TODO check other values 41 | // fmt.Println(p.Encode().String()) 42 | } 43 | 44 | func TestDecodeMasterPlaylistWithMultipleCodecs(t *testing.T) { 45 | f, err := os.Open("sample-playlists/master-with-multiple-codecs.m3u8") 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | p := NewMasterPlaylist() 50 | err = p.DecodeFrom(bufio.NewReader(f), false) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | // check parsed values 55 | if p.ver != 3 { 56 | t.Errorf("Version of parsed playlist = %d (must = 3)", p.ver) 57 | } 58 | if len(p.Variants) != 5 { 59 | t.Error("Not all variants in master playlist parsed.") 60 | } 61 | for _, v := range p.Variants { 62 | if v.Codecs != "avc1.42c015,mp4a.40.2" { 63 | t.Error("Codec string is wrong") 64 | } 65 | } 66 | // TODO check other values 67 | // fmt.Println(p.Encode().String()) 68 | } 69 | 70 | func TestDecodeMasterPlaylistWithAlternatives(t *testing.T) { 71 | f, err := os.Open("sample-playlists/master-with-alternatives.m3u8") 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | p := NewMasterPlaylist() 76 | err = p.DecodeFrom(bufio.NewReader(f), false) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | // check parsed values 81 | if p.ver != 3 { 82 | t.Errorf("Version of parsed playlist = %d (must = 3)", p.ver) 83 | } 84 | if len(p.Variants) != 4 { 85 | t.Fatal("not all variants in master playlist parsed") 86 | } 87 | // TODO check other values 88 | for i, v := range p.Variants { 89 | if i == 0 && len(v.Alternatives) != 3 { 90 | t.Fatalf("not all alternatives from #EXT-X-MEDIA parsed (has %d but should be 3", len(v.Alternatives)) 91 | } 92 | if i == 1 && len(v.Alternatives) != 3 { 93 | t.Fatalf("not all alternatives from #EXT-X-MEDIA parsed (has %d but should be 3", len(v.Alternatives)) 94 | } 95 | if i == 2 && len(v.Alternatives) != 3 { 96 | t.Fatalf("not all alternatives from #EXT-X-MEDIA parsed (has %d but should be 3", len(v.Alternatives)) 97 | } 98 | if i == 3 && len(v.Alternatives) > 0 { 99 | t.Fatal("should not be alternatives for this variant") 100 | } 101 | } 102 | // fmt.Println(p.Encode().String()) 103 | } 104 | 105 | func TestDecodeMasterPlaylistWithAlternativesB(t *testing.T) { 106 | f, err := os.Open("sample-playlists/master-with-alternatives-b.m3u8") 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | p := NewMasterPlaylist() 111 | err = p.DecodeFrom(bufio.NewReader(f), false) 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | // check parsed values 116 | if p.ver != 3 { 117 | t.Errorf("Version of parsed playlist = %d (must = 3)", p.ver) 118 | } 119 | if len(p.Variants) != 4 { 120 | t.Fatal("not all variants in master playlist parsed") 121 | } 122 | // TODO check other values 123 | for i, v := range p.Variants { 124 | if i == 0 && len(v.Alternatives) != 3 { 125 | t.Fatalf("not all alternatives from #EXT-X-MEDIA parsed (has %d but should be 3", len(v.Alternatives)) 126 | } 127 | if i == 1 && len(v.Alternatives) != 3 { 128 | t.Fatalf("not all alternatives from #EXT-X-MEDIA parsed (has %d but should be 3", len(v.Alternatives)) 129 | } 130 | if i == 2 && len(v.Alternatives) != 3 { 131 | t.Fatalf("not all alternatives from #EXT-X-MEDIA parsed (has %d but should be 3", len(v.Alternatives)) 132 | } 133 | if i == 3 && len(v.Alternatives) > 0 { 134 | t.Fatal("should not be alternatives for this variant") 135 | } 136 | } 137 | // fmt.Println(p.Encode().String()) 138 | } 139 | 140 | func TestDecodeMasterPlaylistWithClosedCaptionEqNone(t *testing.T) { 141 | f, err := os.Open("sample-playlists/master-with-closed-captions-eq-none.m3u8") 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | p := NewMasterPlaylist() 146 | err = p.DecodeFrom(bufio.NewReader(f), false) 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | 151 | if len(p.Variants) != 3 { 152 | t.Fatal("not all variants in master playlist parsed") 153 | } 154 | for _, v := range p.Variants { 155 | if v.Captions != "NONE" { 156 | t.Fatal("variant field for CLOSED-CAPTIONS should be equal to NONE but it equals", v.Captions) 157 | } 158 | } 159 | } 160 | 161 | // Decode a master playlist with Name tag in EXT-X-STREAM-INF 162 | func TestDecodeMasterPlaylistWithStreamInfName(t *testing.T) { 163 | f, err := os.Open("sample-playlists/master-with-stream-inf-name.m3u8") 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | p := NewMasterPlaylist() 168 | err = p.DecodeFrom(bufio.NewReader(f), false) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | for _, variant := range p.Variants { 173 | if variant.Name == "" { 174 | t.Errorf("Empty name tag on variant URI: %s", variant.URI) 175 | } 176 | } 177 | } 178 | 179 | func TestDecodeMediaPlaylistByteRange(t *testing.T) { 180 | f, _ := os.Open("sample-playlists/media-playlist-with-byterange.m3u8") 181 | p, _ := NewMediaPlaylist(3, 3) 182 | _ = p.DecodeFrom(bufio.NewReader(f), true) 183 | expected := []*MediaSegment{ 184 | {URI: "video.ts", Duration: 10, Limit: 75232, SeqId: 0}, 185 | {URI: "video.ts", Duration: 10, Limit: 82112, Offset: 752321, SeqId: 1}, 186 | {URI: "video.ts", Duration: 10, Limit: 69864, SeqId: 2}, 187 | } 188 | for i, seg := range p.Segments { 189 | if !reflect.DeepEqual(*seg, *expected[i]) { 190 | t.Errorf("exp: %+v\ngot: %+v", expected[i], seg) 191 | } 192 | } 193 | } 194 | 195 | // Decode a master playlist with i-frame-stream-inf 196 | func TestDecodeMasterPlaylistWithIFrameStreamInf(t *testing.T) { 197 | f, err := os.Open("sample-playlists/master-with-i-frame-stream-inf.m3u8") 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | p := NewMasterPlaylist() 202 | err = p.DecodeFrom(bufio.NewReader(f), false) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | expected := map[int]*Variant{ 207 | 86000: {URI: "low/iframe.m3u8", VariantParams: VariantParams{Bandwidth: 86000, ProgramId: 1, Codecs: "c1", Resolution: "1x1", Video: "1", Iframe: true}}, 208 | 150000: {URI: "mid/iframe.m3u8", VariantParams: VariantParams{Bandwidth: 150000, ProgramId: 1, Codecs: "c2", Resolution: "2x2", Video: "2", Iframe: true}}, 209 | 550000: {URI: "hi/iframe.m3u8", VariantParams: VariantParams{Bandwidth: 550000, ProgramId: 1, Codecs: "c2", Resolution: "2x2", Video: "2", Iframe: true}}, 210 | } 211 | for _, variant := range p.Variants { 212 | for k, expect := range expected { 213 | if reflect.DeepEqual(variant, expect) { 214 | delete(expected, k) 215 | } 216 | } 217 | } 218 | for _, expect := range expected { 219 | t.Errorf("not found:%+v", expect) 220 | } 221 | } 222 | 223 | func TestDecodeMasterPlaylistWithStreamInfAverageBandwidth(t *testing.T) { 224 | f, err := os.Open("sample-playlists/master-with-stream-inf-1.m3u8") 225 | if err != nil { 226 | t.Fatal(err) 227 | } 228 | p := NewMasterPlaylist() 229 | err = p.DecodeFrom(bufio.NewReader(f), false) 230 | if err != nil { 231 | t.Fatal(err) 232 | } 233 | for _, variant := range p.Variants { 234 | if variant.AverageBandwidth == 0 { 235 | t.Errorf("Empty average bandwidth tag on variant URI: %s", variant.URI) 236 | } 237 | } 238 | } 239 | 240 | func TestDecodeMasterPlaylistWithStreamInfFrameRate(t *testing.T) { 241 | f, err := os.Open("sample-playlists/master-with-stream-inf-1.m3u8") 242 | if err != nil { 243 | t.Fatal(err) 244 | } 245 | p := NewMasterPlaylist() 246 | err = p.DecodeFrom(bufio.NewReader(f), false) 247 | if err != nil { 248 | t.Fatal(err) 249 | } 250 | for _, variant := range p.Variants { 251 | if variant.FrameRate == 0 { 252 | t.Errorf("Empty frame rate tag on variant URI: %s", variant.URI) 253 | } 254 | } 255 | } 256 | 257 | func TestDecodeMasterPlaylistWithIndependentSegments(t *testing.T) { 258 | f, err := os.Open("sample-playlists/master-with-independent-segments.m3u8") 259 | if err != nil { 260 | t.Fatal(err) 261 | } 262 | p := NewMasterPlaylist() 263 | err = p.DecodeFrom(bufio.NewReader(f), false) 264 | if err != nil { 265 | t.Fatal(err) 266 | } 267 | if !p.IndependentSegments() { 268 | t.Error("Expected independent segments to be true") 269 | } 270 | } 271 | 272 | func TestDecodeMasterWithHLSV7(t *testing.T) { 273 | f, err := os.Open("sample-playlists/master-with-hlsv7.m3u8") 274 | if err != nil { 275 | t.Fatal(err) 276 | } 277 | p := NewMasterPlaylist() 278 | err = p.DecodeFrom(bufio.NewReader(f), false) 279 | if err != nil { 280 | t.Fatal(err) 281 | } 282 | var unexpected []*Variant 283 | expected := map[string]VariantParams{ 284 | "sdr_720/prog_index.m3u8": {Bandwidth: 3971374, AverageBandwidth: 2778321, Codecs: "hvc1.2.4.L123.B0", Resolution: "1280x720", Captions: "NONE", VideoRange: "SDR", HDCPLevel: "NONE", FrameRate: 23.976}, 285 | "sdr_1080/prog_index.m3u8": {Bandwidth: 10022043, AverageBandwidth: 6759875, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Captions: "NONE", VideoRange: "SDR", HDCPLevel: "TYPE-0", FrameRate: 23.976}, 286 | "sdr_2160/prog_index.m3u8": {Bandwidth: 28058971, AverageBandwidth: 20985770, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Captions: "NONE", VideoRange: "SDR", HDCPLevel: "TYPE-1", FrameRate: 23.976}, 287 | "dolby_720/prog_index.m3u8": {Bandwidth: 5327059, AverageBandwidth: 3385450, Codecs: "dvh1.05.01", Resolution: "1280x720", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "NONE", FrameRate: 23.976}, 288 | "dolby_1080/prog_index.m3u8": {Bandwidth: 12876596, AverageBandwidth: 7999361, Codecs: "dvh1.05.03", Resolution: "1920x1080", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "TYPE-0", FrameRate: 23.976}, 289 | "dolby_2160/prog_index.m3u8": {Bandwidth: 30041698, AverageBandwidth: 24975091, Codecs: "dvh1.05.06", Resolution: "3840x2160", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "TYPE-1", FrameRate: 23.976}, 290 | "hdr10_720/prog_index.m3u8": {Bandwidth: 5280654, AverageBandwidth: 3320040, Codecs: "hvc1.2.4.L123.B0", Resolution: "1280x720", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "NONE", FrameRate: 23.976}, 291 | "hdr10_1080/prog_index.m3u8": {Bandwidth: 12886714, AverageBandwidth: 7964551, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "TYPE-0", FrameRate: 23.976}, 292 | "hdr10_2160/prog_index.m3u8": {Bandwidth: 29983769, AverageBandwidth: 24833402, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Captions: "NONE", VideoRange: "PQ", HDCPLevel: "TYPE-1", FrameRate: 23.976}, 293 | "sdr_720/iframe_index.m3u8": {Bandwidth: 593626, AverageBandwidth: 248586, Codecs: "hvc1.2.4.L123.B0", Resolution: "1280x720", Iframe: true, VideoRange: "SDR", HDCPLevel: "NONE"}, 294 | "sdr_1080/iframe_index.m3u8": {Bandwidth: 956552, AverageBandwidth: 399790, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Iframe: true, VideoRange: "SDR", HDCPLevel: "TYPE-0"}, 295 | "sdr_2160/iframe_index.m3u8": {Bandwidth: 1941397, AverageBandwidth: 826971, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Iframe: true, VideoRange: "SDR", HDCPLevel: "TYPE-1"}, 296 | "dolby_720/iframe_index.m3u8": {Bandwidth: 573073, AverageBandwidth: 232253, Codecs: "dvh1.05.01", Resolution: "1280x720", Iframe: true, VideoRange: "PQ", HDCPLevel: "NONE"}, 297 | "dolby_1080/iframe_index.m3u8": {Bandwidth: 905037, AverageBandwidth: 365337, Codecs: "dvh1.05.03", Resolution: "1920x1080", Iframe: true, VideoRange: "PQ", HDCPLevel: "TYPE-0"}, 298 | "dolby_2160/iframe_index.m3u8": {Bandwidth: 1893236, AverageBandwidth: 739114, Codecs: "dvh1.05.06", Resolution: "3840x2160", Iframe: true, VideoRange: "PQ", HDCPLevel: "TYPE-1"}, 299 | "hdr10_720/iframe_index.m3u8": {Bandwidth: 572673, AverageBandwidth: 232511, Codecs: "hvc1.2.4.L123.B0", Resolution: "1280x720", Iframe: true, VideoRange: "PQ", HDCPLevel: "NONE"}, 300 | "hdr10_1080/iframe_index.m3u8": {Bandwidth: 905053, AverageBandwidth: 364552, Codecs: "hvc1.2.4.L123.B0", Resolution: "1920x1080", Iframe: true, VideoRange: "PQ", HDCPLevel: "TYPE-0"}, 301 | "hdr10_2160/iframe_index.m3u8": {Bandwidth: 1895477, AverageBandwidth: 739757, Codecs: "hvc1.2.4.L150.B0", Resolution: "3840x2160", Iframe: true, VideoRange: "PQ", HDCPLevel: "TYPE-1"}, 302 | } 303 | for _, variant := range p.Variants { 304 | var found bool 305 | for uri, vp := range expected { 306 | if variant == nil || variant.URI != uri { 307 | continue 308 | } 309 | if reflect.DeepEqual(variant.VariantParams, vp) { 310 | delete(expected, uri) 311 | found = true 312 | } 313 | } 314 | if !found { 315 | unexpected = append(unexpected, variant) 316 | } 317 | } 318 | for uri, expect := range expected { 319 | t.Errorf("not found: uri=%q %+v", uri, expect) 320 | } 321 | for _, unexpect := range unexpected { 322 | t.Errorf("found but not expecting:%+v", unexpect) 323 | } 324 | } 325 | 326 | /**************************** 327 | * Begin Test MediaPlaylist * 328 | ****************************/ 329 | 330 | func TestDecodeMediaPlaylist(t *testing.T) { 331 | f, err := os.Open("sample-playlists/wowza-vod-chunklist.m3u8") 332 | if err != nil { 333 | t.Fatal(err) 334 | } 335 | p, err := NewMediaPlaylist(5, 798) 336 | if err != nil { 337 | t.Fatalf("Create media playlist failed: %s", err) 338 | } 339 | err = p.DecodeFrom(bufio.NewReader(f), true) 340 | if err != nil { 341 | t.Fatal(err) 342 | } 343 | //fmt.Printf("Playlist object: %+v\n", p) 344 | // check parsed values 345 | if p.ver != 3 { 346 | t.Errorf("Version of parsed playlist = %d (must = 3)", p.ver) 347 | } 348 | if p.TargetDuration != 12 { 349 | t.Errorf("TargetDuration of parsed playlist = %f (must = 12.0)", p.TargetDuration) 350 | } 351 | if !p.Closed { 352 | t.Error("This is a closed (VOD) playlist but Close field = false") 353 | } 354 | titles := []string{"Title 1", "Title 2", ""} 355 | for i, s := range p.Segments { 356 | if i > len(titles)-1 { 357 | break 358 | } 359 | if s.Title != titles[i] { 360 | t.Errorf("Segment %v's title = %v (must = %q)", i, s.Title, titles[i]) 361 | } 362 | } 363 | if p.Count() != 522 { 364 | t.Errorf("Excepted segments quantity: 522, got: %v", p.Count()) 365 | } 366 | var seqId, idx uint 367 | for seqId, idx = 1, 0; idx < p.Count(); seqId, idx = seqId+1, idx+1 { 368 | if p.Segments[idx].SeqId != uint64(seqId) { 369 | t.Errorf("Excepted SeqId for %vth segment: %v, got: %v", idx+1, seqId, p.Segments[idx].SeqId) 370 | } 371 | } 372 | // TODO check other values… 373 | //fmt.Println(p.Encode().String()), stream.Name} 374 | } 375 | 376 | func TestDecodeMediaPlaylistExtInfNonStrict2(t *testing.T) { 377 | header := `#EXTM3U 378 | #EXT-X-TARGETDURATION:10 379 | #EXT-X-VERSION:3 380 | #EXT-X-MEDIA-SEQUENCE:0 381 | %s 382 | ` 383 | 384 | tests := []struct { 385 | strict bool 386 | extInf string 387 | wantError bool 388 | wantSegment *MediaSegment 389 | }{ 390 | // strict mode on 391 | {true, "#EXTINF:10.000,", false, &MediaSegment{Duration: 10.0, Title: ""}}, 392 | {true, "#EXTINF:10.000,Title", false, &MediaSegment{Duration: 10.0, Title: "Title"}}, 393 | {true, "#EXTINF:10.000,Title,Track", false, &MediaSegment{Duration: 10.0, Title: "Title,Track"}}, 394 | {true, "#EXTINF:invalid,", true, nil}, 395 | {true, "#EXTINF:10.000", true, nil}, 396 | 397 | // strict mode off 398 | {false, "#EXTINF:10.000,", false, &MediaSegment{Duration: 10.0, Title: ""}}, 399 | {false, "#EXTINF:10.000,Title", false, &MediaSegment{Duration: 10.0, Title: "Title"}}, 400 | {false, "#EXTINF:10.000,Title,Track", false, &MediaSegment{Duration: 10.0, Title: "Title,Track"}}, 401 | {false, "#EXTINF:invalid,", false, &MediaSegment{Duration: 0.0, Title: ""}}, 402 | {false, "#EXTINF:10.000", false, &MediaSegment{Duration: 10.0, Title: ""}}, 403 | } 404 | 405 | for _, test := range tests { 406 | p, err := NewMediaPlaylist(1, 1) 407 | if err != nil { 408 | t.Fatalf("unexpected error: %v", err) 409 | } 410 | reader := bytes.NewBufferString(fmt.Sprintf(header, test.extInf)) 411 | err = p.DecodeFrom(reader, test.strict) 412 | if test.wantError { 413 | if err == nil { 414 | t.Errorf("expected error but have: %v", err) 415 | } 416 | continue 417 | } 418 | if err != nil { 419 | t.Errorf("unexpected error: %v", err) 420 | } 421 | if !reflect.DeepEqual(p.Segments[0], test.wantSegment) { 422 | t.Errorf("\nhave: %+v\nwant: %+v", p.Segments[0], test.wantSegment) 423 | } 424 | } 425 | } 426 | 427 | func TestDecodeMediaPlaylistWithWidevine(t *testing.T) { 428 | f, err := os.Open("sample-playlists/widevine-bitrate.m3u8") 429 | if err != nil { 430 | t.Fatal(err) 431 | } 432 | p, err := NewMediaPlaylist(5, 798) 433 | if err != nil { 434 | t.Fatalf("Create media playlist failed: %s", err) 435 | } 436 | err = p.DecodeFrom(bufio.NewReader(f), true) 437 | if err != nil { 438 | t.Fatal(err) 439 | } 440 | //fmt.Printf("Playlist object: %+v\n", p) 441 | // check parsed values 442 | if p.ver != 2 { 443 | t.Errorf("Version of parsed playlist = %d (must = 2)", p.ver) 444 | } 445 | if p.TargetDuration != 9 { 446 | t.Errorf("TargetDuration of parsed playlist = %f (must = 9.0)", p.TargetDuration) 447 | } 448 | // TODO check other values… 449 | //fmt.Printf("%+v\n", p.Key) 450 | //fmt.Println(p.Encode().String()) 451 | } 452 | 453 | func TestDecodeMasterPlaylistWithAutodetection(t *testing.T) { 454 | f, err := os.Open("sample-playlists/master.m3u8") 455 | if err != nil { 456 | t.Fatal(err) 457 | } 458 | m, listType, err := DecodeFrom(bufio.NewReader(f), false) 459 | if err != nil { 460 | t.Fatal(err) 461 | } 462 | if listType != MASTER { 463 | t.Error("Sample not recognized as master playlist.") 464 | } 465 | mp := m.(*MasterPlaylist) 466 | // fmt.Printf(">%+v\n", mp) 467 | // for _, v := range mp.Variants { 468 | // fmt.Printf(">>%+v +v\n", v) 469 | // } 470 | //fmt.Println("Type below must be MasterPlaylist:") 471 | CheckType(t, mp) 472 | } 473 | 474 | func TestDecodeMediaPlaylistWithAutodetection(t *testing.T) { 475 | f, err := os.Open("sample-playlists/wowza-vod-chunklist.m3u8") 476 | if err != nil { 477 | t.Fatal(err) 478 | } 479 | p, listType, err := DecodeFrom(bufio.NewReader(f), true) 480 | if err != nil { 481 | t.Fatal(err) 482 | } 483 | pp := p.(*MediaPlaylist) 484 | CheckType(t, pp) 485 | if listType != MEDIA { 486 | t.Error("Sample not recognized as media playlist.") 487 | } 488 | // check parsed values 489 | if pp.TargetDuration != 12 { 490 | t.Errorf("TargetDuration of parsed playlist = %f (must = 12.0)", pp.TargetDuration) 491 | } 492 | 493 | if !pp.Closed { 494 | t.Error("This is a closed (VOD) playlist but Close field = false") 495 | } 496 | if pp.winsize != 0 { 497 | t.Errorf("Media window size %v != 0", pp.winsize) 498 | } 499 | // TODO check other values… 500 | // fmt.Println(pp.Encode().String()) 501 | } 502 | 503 | // TestDecodeMediaPlaylistAutoDetectExtend tests a very large playlist auto 504 | // extends to the appropriate size. 505 | func TestDecodeMediaPlaylistAutoDetectExtend(t *testing.T) { 506 | f, err := os.Open("sample-playlists/media-playlist-large.m3u8") 507 | if err != nil { 508 | t.Fatal(err) 509 | } 510 | p, listType, err := DecodeFrom(bufio.NewReader(f), true) 511 | if err != nil { 512 | t.Fatal(err) 513 | } 514 | pp := p.(*MediaPlaylist) 515 | CheckType(t, pp) 516 | if listType != MEDIA { 517 | t.Error("Sample not recognized as media playlist.") 518 | } 519 | var exp uint = 40001 520 | if pp.Count() != exp { 521 | t.Errorf("Media segment count %v != %v", pp.Count(), exp) 522 | } 523 | } 524 | 525 | // Test for FullTimeParse of EXT-X-PROGRAM-DATE-TIME 526 | // We testing ISO/IEC 8601:2004 where we can get time in UTC, UTC with Nanoseconds 527 | // timeZone in formats '±00:00', '±0000', '±00' 528 | // m3u8.FullTimeParse() 529 | func TestFullTimeParse(t *testing.T) { 530 | var timestamps = []struct { 531 | name string 532 | value string 533 | }{ 534 | {"time_in_utc", "2006-01-02T15:04:05Z"}, 535 | {"time_in_utc_nano", "2006-01-02T15:04:05.123456789Z"}, 536 | {"time_with_positive_zone_and_colon", "2006-01-02T15:04:05+01:00"}, 537 | {"time_with_positive_zone_no_colon", "2006-01-02T15:04:05+0100"}, 538 | {"time_with_positive_zone_2digits", "2006-01-02T15:04:05+01"}, 539 | {"time_with_negative_zone_and_colon", "2006-01-02T15:04:05-01:00"}, 540 | {"time_with_negative_zone_no_colon", "2006-01-02T15:04:05-0100"}, 541 | {"time_with_negative_zone_2digits", "2006-01-02T15:04:05-01"}, 542 | } 543 | 544 | var err error 545 | for _, tstamp := range timestamps { 546 | _, err = FullTimeParse(tstamp.value) 547 | if err != nil { 548 | t.Errorf("FullTimeParse Error at %s [%s]: %s", tstamp.name, tstamp.value, err) 549 | } 550 | } 551 | } 552 | 553 | // Test for StrictTimeParse of EXT-X-PROGRAM-DATE-TIME 554 | // We testing Strict format of RFC3339 where we can get time in UTC, UTC with Nanoseconds 555 | // timeZone in formats '±00:00', '±0000', '±00' 556 | // m3u8.StrictTimeParse() 557 | func TestStrictTimeParse(t *testing.T) { 558 | var timestamps = []struct { 559 | name string 560 | value string 561 | }{ 562 | {"time_in_utc", "2006-01-02T15:04:05Z"}, 563 | {"time_in_utc_nano", "2006-01-02T15:04:05.123456789Z"}, 564 | {"time_with_positive_zone_and_colon", "2006-01-02T15:04:05+01:00"}, 565 | {"time_with_negative_zone_and_colon", "2006-01-02T15:04:05-01:00"}, 566 | } 567 | 568 | var err error 569 | for _, tstamp := range timestamps { 570 | _, err = StrictTimeParse(tstamp.value) 571 | if err != nil { 572 | t.Errorf("StrictTimeParse Error at %s [%s]: %s", tstamp.name, tstamp.value, err) 573 | } 574 | } 575 | } 576 | 577 | func TestMediaPlaylistWithOATCLSSCTE35Tag(t *testing.T) { 578 | f, err := os.Open("sample-playlists/media-playlist-with-oatcls-scte35.m3u8") 579 | if err != nil { 580 | t.Fatal(err) 581 | } 582 | p, _, err := DecodeFrom(bufio.NewReader(f), true) 583 | if err != nil { 584 | t.Fatal(err) 585 | } 586 | pp := p.(*MediaPlaylist) 587 | 588 | expect := map[int]*SCTE{ 589 | 0: {Syntax: SCTE35_OATCLS, CueType: SCTE35Cue_Start, Cue: "/DAlAAAAAAAAAP/wFAUAAAABf+/+ANgNkv4AFJlwAAEBAQAA5xULLA==", Time: 15}, 590 | 1: {Syntax: SCTE35_OATCLS, CueType: SCTE35Cue_Mid, Cue: "/DAlAAAAAAAAAP/wFAUAAAABf+/+ANgNkv4AFJlwAAEBAQAA5xULLA==", Time: 15, Elapsed: 8.844}, 591 | 2: {Syntax: SCTE35_OATCLS, CueType: SCTE35Cue_End}, 592 | } 593 | for i := 0; i < int(pp.Count()); i++ { 594 | if !reflect.DeepEqual(pp.Segments[i].SCTE, expect[i]) { 595 | t.Errorf("OATCLS SCTE35 segment %v (uri: %v)\ngot: %#v\nexp: %#v", 596 | i, pp.Segments[i].URI, pp.Segments[i].SCTE, expect[i], 597 | ) 598 | } 599 | } 600 | } 601 | 602 | func TestDecodeMediaPlaylistWithDiscontinuitySeq(t *testing.T) { 603 | f, err := os.Open("sample-playlists/media-playlist-with-discontinuity-seq.m3u8") 604 | if err != nil { 605 | t.Fatal(err) 606 | } 607 | p, listType, err := DecodeFrom(bufio.NewReader(f), true) 608 | if err != nil { 609 | t.Fatal(err) 610 | } 611 | pp := p.(*MediaPlaylist) 612 | CheckType(t, pp) 613 | if listType != MEDIA { 614 | t.Error("Sample not recognized as media playlist.") 615 | } 616 | if pp.DiscontinuitySeq == 0 { 617 | t.Error("Empty discontinuity sequenece tag") 618 | } 619 | if pp.Count() != 4 { 620 | t.Errorf("Excepted segments quantity: 4, got: %v", pp.Count()) 621 | } 622 | if pp.SeqNo != 0 { 623 | t.Errorf("Excepted SeqNo: 0, got: %v", pp.SeqNo) 624 | } 625 | var seqId, idx uint 626 | for seqId, idx = 0, 0; idx < pp.Count(); seqId, idx = seqId+1, idx+1 { 627 | if pp.Segments[idx].SeqId != uint64(seqId) { 628 | t.Errorf("Excepted SeqId for %vth segment: %v, got: %v", idx+1, seqId, pp.Segments[idx].SeqId) 629 | } 630 | } 631 | } 632 | 633 | func TestDecodeMasterPlaylistWithCustomTags(t *testing.T) { 634 | cases := []struct { 635 | src string 636 | customDecoders []CustomDecoder 637 | expectedError error 638 | expectedPlaylistTags []string 639 | }{ 640 | { 641 | src: "sample-playlists/master-playlist-with-custom-tags.m3u8", 642 | customDecoders: nil, 643 | expectedError: nil, 644 | expectedPlaylistTags: nil, 645 | }, 646 | { 647 | src: "sample-playlists/master-playlist-with-custom-tags.m3u8", 648 | customDecoders: []CustomDecoder{ 649 | &MockCustomTag{ 650 | name: "#CUSTOM-PLAYLIST-TAG:", 651 | err: errors.New("Error decoding tag"), 652 | segment: false, 653 | encodedString: "#CUSTOM-PLAYLIST-TAG:42", 654 | }, 655 | }, 656 | expectedError: errors.New("Error decoding tag"), 657 | expectedPlaylistTags: nil, 658 | }, 659 | { 660 | src: "sample-playlists/master-playlist-with-custom-tags.m3u8", 661 | customDecoders: []CustomDecoder{ 662 | &MockCustomTag{ 663 | name: "#CUSTOM-PLAYLIST-TAG:", 664 | err: nil, 665 | segment: false, 666 | encodedString: "#CUSTOM-PLAYLIST-TAG:42", 667 | }, 668 | }, 669 | expectedError: nil, 670 | expectedPlaylistTags: []string{ 671 | "#CUSTOM-PLAYLIST-TAG:", 672 | }, 673 | }, 674 | } 675 | 676 | for _, testCase := range cases { 677 | f, err := os.Open(testCase.src) 678 | 679 | if err != nil { 680 | t.Fatal(err) 681 | } 682 | 683 | p, listType, err := DecodeWith(bufio.NewReader(f), true, testCase.customDecoders) 684 | 685 | if !reflect.DeepEqual(err, testCase.expectedError) { 686 | t.Fatal(err) 687 | } 688 | 689 | if testCase.expectedError != nil { 690 | // No need to make other assertions if we were expecting an error 691 | continue 692 | } 693 | 694 | pp := p.(*MasterPlaylist) 695 | 696 | CheckType(t, pp) 697 | 698 | if listType != MASTER { 699 | t.Error("Sample not recognized as master playlist.") 700 | } 701 | 702 | if len(pp.Custom) != len(testCase.expectedPlaylistTags) { 703 | t.Errorf("Did not parse expected number of custom tags. Got: %d Expected: %d", len(pp.Custom), len(testCase.expectedPlaylistTags)) 704 | } else { 705 | // we have the same count, lets confirm its the right tags 706 | for _, expectedTag := range testCase.expectedPlaylistTags { 707 | if _, ok := pp.Custom[expectedTag]; !ok { 708 | t.Errorf("Did not parse custom tag %s", expectedTag) 709 | } 710 | } 711 | } 712 | } 713 | } 714 | 715 | func TestDecodeMediaPlaylistWithCustomTags(t *testing.T) { 716 | cases := []struct { 717 | src string 718 | customDecoders []CustomDecoder 719 | expectedError error 720 | expectedPlaylistTags []string 721 | expectedSegmentTags []*struct { 722 | index int 723 | names []string 724 | } 725 | }{ 726 | { 727 | src: "sample-playlists/media-playlist-with-custom-tags.m3u8", 728 | customDecoders: nil, 729 | expectedError: nil, 730 | expectedPlaylistTags: nil, 731 | expectedSegmentTags: nil, 732 | }, 733 | { 734 | src: "sample-playlists/media-playlist-with-custom-tags.m3u8", 735 | customDecoders: []CustomDecoder{ 736 | &MockCustomTag{ 737 | name: "#CUSTOM-PLAYLIST-TAG:", 738 | err: errors.New("Error decoding tag"), 739 | segment: false, 740 | encodedString: "#CUSTOM-PLAYLIST-TAG:42", 741 | }, 742 | }, 743 | expectedError: errors.New("Error decoding tag"), 744 | expectedPlaylistTags: nil, 745 | expectedSegmentTags: nil, 746 | }, 747 | { 748 | src: "sample-playlists/media-playlist-with-custom-tags.m3u8", 749 | customDecoders: []CustomDecoder{ 750 | &MockCustomTag{ 751 | name: "#CUSTOM-PLAYLIST-TAG:", 752 | err: nil, 753 | segment: false, 754 | encodedString: "#CUSTOM-PLAYLIST-TAG:42", 755 | }, 756 | &MockCustomTag{ 757 | name: "#CUSTOM-SEGMENT-TAG:", 758 | err: nil, 759 | segment: true, 760 | encodedString: "#CUSTOM-SEGMENT-TAG:NAME=\"Yoda\",JEDI=YES", 761 | }, 762 | &MockCustomTag{ 763 | name: "#CUSTOM-SEGMENT-TAG-B", 764 | err: nil, 765 | segment: true, 766 | encodedString: "#CUSTOM-SEGMENT-TAG-B", 767 | }, 768 | }, 769 | expectedError: nil, 770 | expectedPlaylistTags: []string{ 771 | "#CUSTOM-PLAYLIST-TAG:", 772 | }, 773 | expectedSegmentTags: []*struct { 774 | index int 775 | names []string 776 | }{ 777 | {1, []string{"#CUSTOM-SEGMENT-TAG:"}}, 778 | {2, []string{"#CUSTOM-SEGMENT-TAG:", "#CUSTOM-SEGMENT-TAG-B"}}, 779 | }, 780 | }, 781 | } 782 | 783 | for _, testCase := range cases { 784 | f, err := os.Open(testCase.src) 785 | 786 | if err != nil { 787 | t.Fatal(err) 788 | } 789 | 790 | p, listType, err := DecodeWith(bufio.NewReader(f), true, testCase.customDecoders) 791 | 792 | if !reflect.DeepEqual(err, testCase.expectedError) { 793 | t.Fatal(err) 794 | } 795 | 796 | if testCase.expectedError != nil { 797 | // No need to make other assertions if we were expecting an error 798 | continue 799 | } 800 | 801 | pp := p.(*MediaPlaylist) 802 | 803 | CheckType(t, pp) 804 | 805 | if listType != MEDIA { 806 | t.Error("Sample not recognized as master playlist.") 807 | } 808 | 809 | if len(pp.Custom) != len(testCase.expectedPlaylistTags) { 810 | t.Errorf("Did not parse expected number of custom tags. Got: %d Expected: %d", len(pp.Custom), len(testCase.expectedPlaylistTags)) 811 | } else { 812 | // we have the same count, lets confirm its the right tags 813 | for _, expectedTag := range testCase.expectedPlaylistTags { 814 | if _, ok := pp.Custom[expectedTag]; !ok { 815 | t.Errorf("Did not parse custom tag %s", expectedTag) 816 | } 817 | } 818 | } 819 | 820 | var expectedSegmentTag *struct { 821 | index int 822 | names []string 823 | } 824 | 825 | expectedIndex := 0 826 | 827 | for i := 0; i < int(pp.Count()); i++ { 828 | seg := pp.Segments[i] 829 | if expectedIndex != len(testCase.expectedSegmentTags) { 830 | expectedSegmentTag = testCase.expectedSegmentTags[expectedIndex] 831 | } else { 832 | // we are at the end of the expectedSegmentTags list, the rest of the segments 833 | // should have no custom tags 834 | expectedSegmentTag = nil 835 | } 836 | 837 | if expectedSegmentTag == nil || expectedSegmentTag.index != i { 838 | if len(seg.Custom) != 0 { 839 | t.Errorf("Did not parse expected number of custom tags on Segment %d. Got: %d Expected: %d", i, len(seg.Custom), 0) 840 | } 841 | continue 842 | } 843 | 844 | // We are now checking the segment corresponding to exepectedSegmentTag 845 | // increase our expectedIndex for next iteration 846 | expectedIndex++ 847 | 848 | if len(expectedSegmentTag.names) != len(seg.Custom) { 849 | t.Errorf("Did not parse expected number of custom tags on Segment %d. Got: %d Expected: %d", i, len(seg.Custom), len(expectedSegmentTag.names)) 850 | } else { 851 | // we have the same count, lets confirm its the right tags 852 | for _, expectedTag := range expectedSegmentTag.names { 853 | if _, ok := seg.Custom[expectedTag]; !ok { 854 | t.Errorf("Did not parse customTag %s on Segment %d", expectedTag, i) 855 | } 856 | } 857 | } 858 | } 859 | 860 | if expectedIndex != len(testCase.expectedSegmentTags) { 861 | t.Errorf("Did not parse custom tags on all expected segments. Parsed Segments: %d Expected: %d", expectedIndex, len(testCase.expectedSegmentTags)) 862 | } 863 | } 864 | } 865 | 866 | /*************************** 867 | * Code parsing examples * 868 | ***************************/ 869 | 870 | // Example of parsing a playlist with EXT-X-DISCONTINIUTY tag 871 | // and output it with integer segment durations. 872 | func ExampleMediaPlaylist_DurationAsInt() { 873 | f, _ := os.Open("sample-playlists/media-playlist-with-discontinuity.m3u8") 874 | p, _, _ := DecodeFrom(bufio.NewReader(f), true) 875 | pp := p.(*MediaPlaylist) 876 | pp.DurationAsInt(true) 877 | fmt.Printf("%s", pp) 878 | // Output: 879 | // #EXTM3U 880 | // #EXT-X-VERSION:3 881 | // #EXT-X-MEDIA-SEQUENCE:0 882 | // #EXT-X-TARGETDURATION:10 883 | // #EXTINF:10, 884 | // ad0.ts 885 | // #EXTINF:8, 886 | // ad1.ts 887 | // #EXT-X-DISCONTINUITY 888 | // #EXTINF:10, 889 | // movieA.ts 890 | // #EXTINF:10, 891 | // movieB.ts 892 | } 893 | 894 | func TestMediaPlaylistWithSCTE35Tag(t *testing.T) { 895 | cases := []struct { 896 | playlistLocation string 897 | expectedSCTEIndex int 898 | expectedSCTECue string 899 | expectedSCTEID string 900 | expectedSCTETime float64 901 | }{ 902 | { 903 | "sample-playlists/media-playlist-with-scte35.m3u8", 904 | 2, 905 | "/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==", 906 | "123", 907 | 123.12, 908 | }, 909 | { 910 | "sample-playlists/media-playlist-with-scte35-1.m3u8", 911 | 1, 912 | "/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAA", 913 | "", 914 | 0, 915 | }, 916 | } 917 | for _, c := range cases { 918 | f, _ := os.Open(c.playlistLocation) 919 | playlist, _, _ := DecodeFrom(bufio.NewReader(f), true) 920 | mediaPlaylist := playlist.(*MediaPlaylist) 921 | for index, item := range mediaPlaylist.Segments { 922 | if item == nil { 923 | break 924 | } 925 | if index != c.expectedSCTEIndex && item.SCTE != nil { 926 | t.Error("Not expecting SCTE information on this segment") 927 | } else if index == c.expectedSCTEIndex && item.SCTE == nil { 928 | t.Error("Expecting SCTE information on this segment") 929 | } else if index == c.expectedSCTEIndex && item.SCTE != nil { 930 | if (*item.SCTE).Cue != c.expectedSCTECue { 931 | t.Error("Expected ", c.expectedSCTECue, " got ", (*item.SCTE).Cue) 932 | } else if (*item.SCTE).ID != c.expectedSCTEID { 933 | t.Error("Expected ", c.expectedSCTEID, " got ", (*item.SCTE).ID) 934 | } else if (*item.SCTE).Time != c.expectedSCTETime { 935 | t.Error("Expected ", c.expectedSCTETime, " got ", (*item.SCTE).Time) 936 | } 937 | } 938 | } 939 | } 940 | } 941 | 942 | func TestDecodeMediaPlaylistWithProgramDateTime(t *testing.T) { 943 | f, err := os.Open("sample-playlists/media-playlist-with-program-date-time.m3u8") 944 | if err != nil { 945 | t.Fatal(err) 946 | } 947 | p, listType, err := DecodeFrom(bufio.NewReader(f), true) 948 | if err != nil { 949 | t.Fatal(err) 950 | } 951 | pp := p.(*MediaPlaylist) 952 | CheckType(t, pp) 953 | if listType != MEDIA { 954 | t.Error("Sample not recognized as media playlist.") 955 | } 956 | // check parsed values 957 | if pp.TargetDuration != 15 { 958 | t.Errorf("TargetDuration of parsed playlist = %f (must = 15.0)", pp.TargetDuration) 959 | } 960 | 961 | if !pp.Closed { 962 | t.Error("VOD sample media playlist, closed should be true.") 963 | } 964 | 965 | if pp.SeqNo != 0 { 966 | t.Error("Media sequence defined in sample playlist is 0") 967 | } 968 | 969 | segNames := []string{"20181231/0555e0c371ea801726b92512c331399d_00000000.ts", 970 | "20181231/0555e0c371ea801726b92512c331399d_00000001.ts", 971 | "20181231/0555e0c371ea801726b92512c331399d_00000002.ts", 972 | "20181231/0555e0c371ea801726b92512c331399d_00000003.ts"} 973 | if pp.Count() != uint(len(segNames)) { 974 | t.Errorf("Segments in playlist %d != %d", pp.Count(), len(segNames)) 975 | } 976 | 977 | for idx, name := range segNames { 978 | if pp.Segments[idx].URI != name { 979 | t.Errorf("Segment name mismatch (%d/%d): %s != %s", idx, pp.Count(), pp.Segments[idx].Title, name) 980 | } 981 | } 982 | 983 | // The ProgramDateTime of the 1st segment should be: 2018-12-31T09:47:22+08:00 984 | st, _ := time.Parse(time.RFC3339, "2018-12-31T09:47:22+08:00") 985 | if !pp.Segments[0].ProgramDateTime.Equal(st) { 986 | t.Errorf("The program date time of the 1st segment should be: %v, actual value: %v", 987 | st, pp.Segments[0].ProgramDateTime) 988 | } 989 | } 990 | 991 | func TestDecodeMediaPlaylistStartTime(t *testing.T) { 992 | f, err := os.Open("sample-playlists/media-playlist-with-start-time.m3u8") 993 | if err != nil { 994 | t.Fatal(err) 995 | } 996 | p, listType, err := DecodeFrom(bufio.NewReader(f), true) 997 | if err != nil { 998 | t.Fatal(err) 999 | } 1000 | pp := p.(*MediaPlaylist) 1001 | CheckType(t, pp) 1002 | if listType != MEDIA { 1003 | t.Error("Sample not recognized as media playlist.") 1004 | } 1005 | if pp.StartTime != float64(8.0) { 1006 | t.Errorf("Media segment StartTime != 8: %f", pp.StartTime) 1007 | } 1008 | } 1009 | 1010 | func TestDecodeMediaPlaylistWithCueOutCueIn(t *testing.T) { 1011 | f, err := os.Open("sample-playlists/media-playlist-with-cue-out-in-without-oatcls.m3u8") 1012 | if err != nil { 1013 | t.Fatal(err) 1014 | } 1015 | p, listType, err := DecodeFrom(bufio.NewReader(f), true) 1016 | if err != nil { 1017 | t.Fatal(err) 1018 | } 1019 | pp := p.(*MediaPlaylist) 1020 | CheckType(t, pp) 1021 | if listType != MEDIA { 1022 | t.Error("Sample not recognized as media playlist.") 1023 | } 1024 | 1025 | if pp.Segments[5].SCTE.CueType != SCTE35Cue_Start { 1026 | t.Errorf("EXT-CUE-OUT must result in SCTE35Cue_Start") 1027 | } 1028 | if pp.Segments[5].SCTE.Time != 0 { 1029 | t.Errorf("EXT-CUE-OUT without duration must not have Time set") 1030 | } 1031 | if pp.Segments[9].SCTE.CueType != SCTE35Cue_End { 1032 | t.Errorf("EXT-CUE-IN must result in SCTE35Cue_End") 1033 | } 1034 | if pp.Segments[30].SCTE.CueType != SCTE35Cue_Start { 1035 | t.Errorf("EXT-CUE-OUT must result in SCTE35Cue_Start") 1036 | } 1037 | if pp.Segments[30].SCTE.Time != 180 { 1038 | t.Errorf("EXT-CUE-OUT:180.0 must have time set to 180") 1039 | } 1040 | if pp.Segments[60].SCTE.CueType != SCTE35Cue_End { 1041 | t.Errorf("EXT-CUE-IN must result in SCTE35Cue_End") 1042 | } 1043 | } 1044 | 1045 | /**************** 1046 | * Benchmarks * 1047 | ****************/ 1048 | 1049 | func BenchmarkDecodeMasterPlaylist(b *testing.B) { 1050 | for i := 0; i < b.N; i++ { 1051 | f, err := os.Open("sample-playlists/master.m3u8") 1052 | if err != nil { 1053 | b.Fatal(err) 1054 | } 1055 | p := NewMasterPlaylist() 1056 | if err := p.DecodeFrom(bufio.NewReader(f), false); err != nil { 1057 | b.Fatal(err) 1058 | } 1059 | } 1060 | } 1061 | 1062 | func BenchmarkDecodeMediaPlaylist(b *testing.B) { 1063 | for i := 0; i < b.N; i++ { 1064 | f, err := os.Open("sample-playlists/media-playlist-large.m3u8") 1065 | if err != nil { 1066 | b.Fatal(err) 1067 | } 1068 | p, err := NewMediaPlaylist(50000, 50000) 1069 | if err != nil { 1070 | b.Fatalf("Create media playlist failed: %s", err) 1071 | } 1072 | if err = p.DecodeFrom(bufio.NewReader(f), true); err != nil { 1073 | b.Fatal(err) 1074 | } 1075 | } 1076 | } 1077 | -------------------------------------------------------------------------------- /sample-playlists/master-playlist-with-custom-tags.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #JUST A COMMENT 4 | #CUSTOM-PLAYLIST-TAG:42 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000 6 | chunklist-b300000.m3u8 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=600000 8 | chunklist-b600000.m3u8 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850000 10 | chunklist-b850000.m3u8 11 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000 12 | chunklist-b1000000.m3u8 13 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1500000 14 | chunklist-b1500000.m3u8 15 | -------------------------------------------------------------------------------- /sample-playlists/master-with-alternatives-b.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO="low" 3 | low/main/audio-video.m3u8 4 | #EXT-X-STREAM-INF:BANDWIDTH=2560000,VIDEO="mid" 5 | mid/main/audio-video.m3u8 6 | #EXT-X-STREAM-INF:BANDWIDTH=7680000,VIDEO="hi" 7 | hi/main/audio-video.m3u8 8 | #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" 9 | main/audio-only.m3u8 10 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8" 11 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8" 12 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8" 13 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=YES,URI="mid/main/audio-video.m3u8" 14 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=NO,URI="mid/centerfield/audio-video.m3u8" 15 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8" 16 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8" 17 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8" 18 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8" 19 | -------------------------------------------------------------------------------- /sample-playlists/master-with-alternatives.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8" 3 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8" 4 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8" 5 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO="low" 6 | low/main/audio-video.m3u8 7 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=YES,URI="mid/main/audio-video.m3u8" 8 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=NO,URI="mid/centerfield/audio-video.m3u8" 9 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8" 10 | #EXT-X-STREAM-INF:BANDWIDTH=2560000,VIDEO="mid" 11 | mid/main/audio-video.m3u8 12 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8" 13 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8" 14 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8" 15 | #EXT-X-STREAM-INF:BANDWIDTH=7680000,VIDEO="hi" 16 | hi/main/audio-video.m3u8 17 | #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" 18 | main/audio-only.m3u8 19 | -------------------------------------------------------------------------------- /sample-playlists/master-with-closed-captions-eq-none.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:4 3 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="fra",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="fra",URI="audio_64_fra_rendition.m3u8" 4 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio1",NAME="fra",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="fra",URI="audio_128_fra_rendition.m3u8" 5 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio0",NAME="eng",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="eng",URI="audio_64_eng_rendition.m3u8" 6 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio1",NAME="eng",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="eng",URI="audio_128_eng_rendition.m3u8" 7 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles0",NAME="eng_subtitle",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="eng",URI="subtitle_eng_rendition.m3u8" 8 | #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=1170400,CODECS="avc1",RESOLUTION=320x240,AUDIO="audio0",CLOSED-CAPTIONS=NONE,SUBTITLES="subtitles0" 9 | 1_rendition.m3u8 10 | #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=3630000,CODECS="avc1",RESOLUTION=854x480,AUDIO="audio1",CLOSED-CAPTIONS=NONE,SUBTITLES="subtitles0" 11 | 2_rendition.m3u8 12 | #EXT-X-STREAM-INF:PROGRAM-ID=0,BANDWIDTH=6380000,CODECS="avc1",RESOLUTION=1920x1080,AUDIO="audio1",CLOSED-CAPTIONS=NONE,SUBTITLES="subtitles0" 13 | 3_rendition.m3u8 14 | -------------------------------------------------------------------------------- /sample-playlists/master-with-hlsv7.m3u8: -------------------------------------------------------------------------------- 1 | # https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices/hls_authoring_specification_for_apple_devices_appendices 2 | # 3 | #EXTM3U 4 | #EXT-X-VERSION:7 5 | #EXT-X-INDEPENDENT-SEGMENTS 6 | #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2778321,BANDWIDTH=3971374,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=NONE 7 | sdr_720/prog_index.m3u8 8 | #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6759875,BANDWIDTH=10022043,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-0 9 | sdr_1080/prog_index.m3u8 10 | #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=20985770,BANDWIDTH=28058971,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-1 11 | sdr_2160/prog_index.m3u8 12 | #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3385450,BANDWIDTH=5327059,VIDEO-RANGE=PQ,CODECS="dvh1.05.01",RESOLUTION=1280x720,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=NONE 13 | dolby_720/prog_index.m3u8 14 | #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=7999361,BANDWIDTH=12876596,VIDEO-RANGE=PQ,CODECS="dvh1.05.03",RESOLUTION=1920x1080,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-0 15 | dolby_1080/prog_index.m3u8 16 | #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=24975091,BANDWIDTH=30041698,VIDEO-RANGE=PQ,CODECS="dvh1.05.06",RESOLUTION=3840x2160,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-1 17 | dolby_2160/prog_index.m3u8 18 | #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3320040,BANDWIDTH=5280654,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=NONE 19 | hdr10_720/prog_index.m3u8 20 | #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=7964551,BANDWIDTH=12886714,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-0 21 | hdr10_1080/prog_index.m3u8 22 | #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=24833402,BANDWIDTH=29983769,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-1 23 | hdr10_2160/prog_index.m3u8 24 | #EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=248586,BANDWIDTH=593626,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,HDCP-LEVEL=NONE,URI="sdr_720/iframe_index.m3u8" 25 | #EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=399790,BANDWIDTH=956552,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,HDCP-LEVEL=TYPE-0,URI="sdr_1080/iframe_index.m3u8" 26 | #EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=826971,BANDWIDTH=1941397,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="sdr_2160/iframe_index.m3u8" 27 | #EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=232253,BANDWIDTH=573073,VIDEO-RANGE=PQ,CODECS="dvh1.05.01",RESOLUTION=1280x720,HDCP-LEVEL=NONE,URI="dolby_720/iframe_index.m3u8" 28 | #EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=365337,BANDWIDTH=905037,VIDEO-RANGE=PQ,CODECS="dvh1.05.03",RESOLUTION=1920x1080,HDCP-LEVEL=TYPE-0,URI="dolby_1080/iframe_index.m3u8" 29 | #EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=739114,BANDWIDTH=1893236,VIDEO-RANGE=PQ,CODECS="dvh1.05.06",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="dolby_2160/iframe_index.m3u8" 30 | #EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=232511,BANDWIDTH=572673,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,HDCP-LEVEL=NONE,URI="hdr10_720/iframe_index.m3u8" 31 | #EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=364552,BANDWIDTH=905053,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,HDCP-LEVEL=TYPE-0,URI="hdr10_1080/iframe_index.m3u8" 32 | #EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=739757,BANDWIDTH=1895477,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="hdr10_2160/iframe_index.m3u8" -------------------------------------------------------------------------------- /sample-playlists/master-with-i-frame-stream-inf.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 3 | low/audio-video.m3u8 4 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8",PROGRAM-ID=1,CODECS="c1",RESOLUTION="1x1",VIDEO="1" 5 | #EXT-X-STREAM-INF:BANDWIDTH=2560000 6 | mid/audio-video.m3u8 7 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8",PROGRAM-ID=1,CODECS="c2",RESOLUTION="2x2",VIDEO="2" 8 | #EXT-X-STREAM-INF:BANDWIDTH=7680000 9 | hi/audio-video.m3u8 10 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8",PROGRAM-ID=1,CODECS="c2",RESOLUTION="2x2",VIDEO="2" 11 | #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" 12 | audio-only.m3u8 13 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH="INVALIDBW",URI="hi/iframe.m3u8",PROGRAM-ID=1,CODECS="c2",RESOLUTION="2x2",VIDEO="2" 14 | -------------------------------------------------------------------------------- /sample-playlists/master-with-independent-segments.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-INDEPENDENT-SEGMENTS 4 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1828000,NAME="3 high",RESOLUTION=896x504 5 | chunklist_b1828000_t64NCBoaWdo.m3u8 6 | -------------------------------------------------------------------------------- /sample-playlists/master-with-multiple-codecs.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,CODECS="avc1.42c015,mp4a.40.2" 4 | chunklist-b300000.m3u8 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=600000,CODECS="avc1.42c015,mp4a.40.2" 6 | chunklist-b600000.m3u8 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850000,CODECS="avc1.42c015,mp4a.40.2" 8 | chunklist-b850000.m3u8 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,CODECS="avc1.42c015,mp4a.40.2" 10 | chunklist-b1000000.m3u8 11 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1500000,CODECS="avc1.42c015,mp4a.40.2" 12 | chunklist-b1500000.m3u8 13 | -------------------------------------------------------------------------------- /sample-playlists/master-with-stream-inf-1.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,AVERAGE-BANDWIDTH=300000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 4 | chunklist-b300000.m3u8 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=600000,AVERAGE-BANDWIDTH=600000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 6 | chunklist-b600000.m3u8 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850000,AVERAGE-BANDWIDTH=850000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 8 | chunklist-b850000.m3u8 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000,AVERAGE-BANDWIDTH=1000000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 10 | chunklist-b1000000.m3u8 11 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1500000,AVERAGE-BANDWIDTH=1500000,CODECS="avc1.42c015,mp4a.40.2",FRAME-RATE=25.000 12 | chunklist-b1500000.m3u8 -------------------------------------------------------------------------------- /sample-playlists/master-with-stream-inf-name.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1828000,NAME="3 high",RESOLUTION=896x504 4 | chunklist_b1828000_t64NCBoaWdo.m3u8 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=678000,NAME="2 med",RESOLUTION=512x288 6 | chunklist_b678000_t64MiBtZWQ=.m3u8 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=438000,NAME="1 low",RESOLUTION=384x216 8 | chunklist_b438000_t64MSBsb3c=.m3u8 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=128000,NAME="0 audio" 10 | chunklist_b128000_t64MCBhdWRpbw==.m3u8 11 | -------------------------------------------------------------------------------- /sample-playlists/master.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000 4 | chunklist-b300000.m3u8 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=600000 6 | chunklist-b600000.m3u8 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850000 8 | chunklist-b850000.m3u8 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1000000 10 | chunklist-b1000000.m3u8 11 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1500000 12 | chunklist-b1500000.m3u8 13 | -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-byterange.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-TARGETDURATION:10 3 | #EXT-X-VERSION:4 4 | #EXT-X-MEDIA-SEQUENCE:0 5 | #EXTINF:10.0, 6 | #EXT-X-BYTERANGE:75232@0 7 | video.ts 8 | #EXT-X-BYTERANGE:82112@752321 9 | #EXTINF:10.0, 10 | video.ts 11 | #EXTINF:10.0, 12 | #EXT-X-BYTERANGE:69864 13 | video.ts 14 | -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-cue-out-in-without-oatcls.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:4 3 | #EXT-X-MEDIA-SEQUENCE:275163116 4 | #EXT-X-INDEPENDENT-SEGMENTS 5 | #EXT-X-TARGETDURATION:9 6 | #USP-X-TIMESTAMP-MAP:MPEGTS=7984482175,LOCAL=2022-04-26T13:11:31.333300Z 7 | #EXTINF:6, no desc 8 | 364992-275163141.ts 9 | #EXTINF:5.1666, no desc 10 | 364992-275163142.ts 11 | #EXT-X-CUE-IN 12 | #EXTINF:6.8333, no desc 13 | 364992-275163143.ts 14 | #EXTINF:6, no desc 15 | 364992-275163144.ts 16 | #EXTINF:6, no desc 17 | 364992-275163150.ts 18 | #EXT-X-CUE-OUT 19 | #EXTINF:6, no desc 20 | 364992-275163151.ts 21 | #EXTINF:6, no desc 22 | 364992-275163152.ts 23 | #EXTINF:6, no desc 24 | 364992-275163153.ts 25 | #EXTINF:6, no desc 26 | 364992-275163154.ts 27 | #EXT-X-CUE-IN 28 | #EXTINF:6, no desc 29 | 364992-275163155.ts 30 | #EXTINF:6, no desc 31 | 364992-275163156.ts 32 | #EXTINF:6, no desc 33 | 364992-275163157.ts 34 | #EXTINF:6, no desc 35 | 364992-275163158.ts 36 | #EXTINF:6, no desc 37 | 364992-275163159.ts 38 | #EXTINF:6, no desc 39 | 364992-275163160.ts 40 | #EXTINF:6, no desc 41 | 364992-275163161.ts 42 | #EXTINF:6, no desc 43 | 364992-275163162.ts 44 | #EXTINF:6, no desc 45 | 364992-275163163.ts 46 | #EXTINF:6, no desc 47 | 364992-275163164.ts 48 | #EXTINF:6, no desc 49 | 364992-275163165.ts 50 | #EXTINF:6, no desc 51 | 364992-275163166.ts 52 | #EXTINF:6, no desc 53 | 364992-275163167.ts 54 | #EXTINF:6, no desc 55 | 364992-275163168.ts 56 | #EXTINF:6, no desc 57 | 364992-275163169.ts 58 | #EXTINF:6, no desc 59 | 364992-275163170.ts 60 | #EXTINF:6, no desc 61 | 364992-275163171.ts 62 | #EXTINF:6, no desc 63 | 364992-275163172.ts 64 | #EXTINF:6, no desc 65 | 364992-275163173.ts 66 | #EXTINF:6, no desc 67 | 364992-275163174.ts 68 | #EXTINF:5.2, no desc 69 | 364992-275163175.ts 70 | #EXT-X-CUE-OUT:180 71 | #EXTINF:6.8, no desc 72 | 364992-275163176.ts 73 | #EXTINF:6, no desc 74 | 364992-275163177.ts 75 | #EXTINF:6, no desc 76 | 364992-275163178.ts 77 | #EXTINF:6, no desc 78 | 364992-275163179.ts 79 | #EXTINF:6, no desc 80 | 364992-275163180.ts 81 | #EXTINF:6, no desc 82 | 364992-275163181.ts 83 | #EXTINF:6, no desc 84 | 364992-275163182.ts 85 | #EXTINF:6, no desc 86 | 364992-275163183.ts 87 | #EXTINF:6, no desc 88 | 364992-275163184.ts 89 | #EXTINF:6, no desc 90 | 364992-275163185.ts 91 | #EXTINF:6, no desc 92 | 364992-275163186.ts 93 | #EXTINF:6, no desc 94 | 364992-275163187.ts 95 | #EXTINF:6, no desc 96 | 364992-275163188.ts 97 | #EXTINF:6, no desc 98 | 364992-275163189.ts 99 | #EXTINF:5.2, no desc 100 | 364992-275163190.ts 101 | #EXTINF:6.8, no desc 102 | 364992-275163191.ts 103 | #EXTINF:6, no desc 104 | 364992-275163192.ts 105 | #EXTINF:6, no desc 106 | 364992-275163193.ts 107 | #EXTINF:6, no desc 108 | 364992-275163194.ts 109 | #EXTINF:6, no desc 110 | 364992-275163195.ts 111 | #EXTINF:6, no desc 112 | 364992-275163196.ts 113 | #EXTINF:6, no desc 114 | 364992-275163197.ts 115 | #EXTINF:6, no desc 116 | 364992-275163198.ts 117 | #EXTINF:6, no desc 118 | 364992-275163199.ts 119 | #EXTINF:5.2, no desc 120 | 364992-275163200.ts 121 | #EXTINF:6.8, no desc 122 | 364992-275163201.ts 123 | #EXTINF:6, no desc 124 | 364992-275163202.ts 125 | #EXTINF:6, no desc 126 | 364992-275163203.ts 127 | #EXTINF:6, no desc 128 | 364992-275163204.ts 129 | #EXTINF:5.2333, no desc 130 | 364992-275163205.ts 131 | #EXT-X-CUE-IN 132 | #EXTINF:6.7666, no desc 133 | 364992-275163206.ts 134 | #EXTINF:6, no desc 135 | 364992-275163207.ts 136 | #EXTINF:6, no desc 137 | -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-custom-tags.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-TARGETDURATION:10 3 | #CUSTOM-PLAYLIST-TAG:42 4 | #EXT-X-VERSION:3 5 | #EXT-X-MEDIA-SEQUENCE:0 6 | #JUST A COMMENT 7 | #EXTINF:10.0, 8 | ad0.ts 9 | #CUSTOM-SEGMENT-TAG:NAME="Yoda",JEDI=YES 10 | #EXTINF:8.0, 11 | ad1.ts 12 | #EXT-X-DISCONTINUITY 13 | #CUSTOM-SEGMENT-TAG:NAME="JarJar",JEDI=NO 14 | #CUSTOM-SEGMENT-TAG-B 15 | #EXTINF:10.0, 16 | movieA.ts 17 | #EXTINF:10.0, 18 | movieB.ts 19 | -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-discontinuity-seq.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-TARGETDURATION:10 3 | #EXT-X-VERSION:3 4 | #EXT-X-MEDIA-SEQUENCE:0 5 | #EXT-X-DISCONTINUITY-SEQUENCE:2 6 | #EXTINF:10.0, 7 | ad0.ts 8 | #EXTINF:8.0, 9 | ad1.ts 10 | #EXT-X-DISCONTINUITY 11 | #EXTINF:10.0, 12 | movieA.ts 13 | #EXTINF:10.0, 14 | movieB.ts -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-discontinuity.m3u8: -------------------------------------------------------------------------------- 1 | # https://developer.apple.com/library/ios/technotes/tn2288/_index.html 2 | # 3 | #EXTM3U 4 | #EXT-X-TARGETDURATION:10 5 | #EXT-X-VERSION:3 6 | #EXT-X-MEDIA-SEQUENCE:0 7 | #EXTINF:10.0, 8 | ad0.ts 9 | #EXTINF:8.0, 10 | ad1.ts 11 | #EXT-X-DISCONTINUITY 12 | #EXTINF:10.0, 13 | movieA.ts 14 | #EXTINF:10.0, 15 | movieB.ts -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-oatcls-scte35.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-TARGETDURATION:10 3 | #EXT-X-VERSION:3 4 | #EXT-X-MEDIA-SEQUENCE:0 5 | #EXT-OATCLS-SCTE35:/DAlAAAAAAAAAP/wFAUAAAABf+/+ANgNkv4AFJlwAAEBAQAA5xULLA== 6 | #EXT-X-CUE-OUT:15.000 7 | #EXTINF:8.844, 8 | media0.ts 9 | #EXT-X-CUE-OUT-CONT:ElapsedTime=8.844,Duration=15,SCTE35=/DAlAAAAAAAAAP/wFAUAAAABf+/+ANgNkv4AFJlwAAEBAQAA5xULLA== 10 | #EXTINF:6.156, 11 | media1.ts 12 | #EXT-X-CUE-IN 13 | #EXTINF:3.844, 14 | media2.ts 15 | -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-program-date-time.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-MEDIA-SEQUENCE:0 4 | #EXT-X-ALLOW-CACHE:YES 5 | #EXT-X-PROGRAM-DATE-TIME:2018-12-31T09:47:22+08:00 6 | #EXT-X-TARGETDURATION:15 7 | 8 | 9 | #EXTINF:14.666000, 10 | 20181231/0555e0c371ea801726b92512c331399d_00000000.ts 11 | #EXTINF:13.698000, 12 | 20181231/0555e0c371ea801726b92512c331399d_00000001.ts 13 | #EXTINF:14.668000, 14 | 20181231/0555e0c371ea801726b92512c331399d_00000002.ts 15 | #EXTINF:13.200000, 16 | 20181231/0555e0c371ea801726b92512c331399d_00000003.ts 17 | #EXT-X-ENDLIST 18 | -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-scte35-1.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-TARGETDURATION:10 3 | #EXT-X-VERSION:3 4 | #EXT-X-MEDIA-SEQUENCE:0 5 | #EXTINF:10.000, 6 | media0.ts 7 | #EXT-SCTE35: CUE="/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAA" 8 | #EXTINF:10.000, 9 | media1.ts 10 | #EXTINF:10.000, 11 | media2.ts 12 | -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-scte35.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-TARGETDURATION:10 3 | #EXT-X-VERSION:3 4 | #EXT-X-MEDIA-SEQUENCE:0 5 | #EXTINF:10.000, 6 | media0.ts 7 | #EXTINF:10.000, 8 | media1.ts 9 | #EXT-SCTE35: CUE="/DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==", ID="123", TIME=123.12 10 | #EXTINF:10.000, 11 | media2.ts 12 | -------------------------------------------------------------------------------- /sample-playlists/media-playlist-with-start-time.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-TARGETDURATION:10 3 | #EXT-X-VERSION:3 4 | #EXT-X-MEDIA-SEQUENCE:0 5 | #EXT-X-START:TIME-OFFSET=8.0 6 | #EXTINF:10.0, 7 | ad0.ts 8 | #EXTINF:8.0, 9 | ad1.ts 10 | #EXT-X-DISCONTINUITY 11 | #EXTINF:10.0, 12 | movieA.ts 13 | #EXTINF:10.0, 14 | movieB.ts -------------------------------------------------------------------------------- /sample-playlists/widevine-bitrate.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:2 3 | #EXT-X-ALLOW-CACHE:NO 4 | #EXT-X-KEY:METHOD=AES-128,URI="http://localhost:20001/key?ecm=AAAAAQAAOpgCAAHFYAaVFH6QrFv2wYU1lEaO2L3fGQB1%2FR3oaD9auWtXNAmcVLxgRTvRlHpqHgXX1YY00%2FpdUiOlgONVbViqou2%2FItyDOWc%3D",IV=0X00000000000000000000000000000000 5 | #EXT-X-MEDIA-SEQUENCE:3080 6 | #EXT-X-TARGETDURATION:9 7 | #WV-AUDIO-CHANNELS 2 8 | #WV-AUDIO-FORMAT 1 9 | #WV-AUDIO-PROFILE-IDC 2 10 | #WV-AUDIO-SAMPLE-SIZE 16 11 | #WV-AUDIO-SAMPLING-FREQUENCY 48000 12 | #WV-CYPHER-VERSION Version 5.0.0.4972 AES SVN_500 (Debug) 20111020-03:01:33 13 | #WV-ECM AAAAAQAAOpgCAAHFYAaVFH6QrFv2wYU1lEaO2L3fGQB1/R3oaD9auWtXNAmcVLxgRTvRlHpqHgXX1YY00/pdUiOlgONVbViqou2/ItyDOWc= 14 | #WV-VIDEO-FORMAT 1 15 | #WV-VIDEO-FRAME-RATE 25 16 | #WV-VIDEO-LEVEL-IDC 12 17 | #WV-VIDEO-PROFILE-IDC 66 18 | #WV-VIDEO-RESOLUTION 320 x 192 19 | #WV-VIDEO-SAR 1:1 20 | #EXTINF:6, 21 | 01-3079.ts 22 | #EXTINF:6, 23 | 01-3080.ts 24 | #EXTINF:6, 25 | 01-3081.ts 26 | #EXTINF:8, 27 | 01-3082.ts 28 | #EXTINF:6, 29 | 01-3083.ts 30 | #EXTINF:8, 31 | 01-3084.ts 32 | #EXTINF:6, 33 | 01-3085.ts 34 | #EXTINF:7, 35 | 01-3086.ts 36 | #EXTINF:9, 37 | 01-3087.ts 38 | #EXTINF:7, 39 | 01-3088.ts 40 | -------------------------------------------------------------------------------- /sample-playlists/widevine-master.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:2 3 | #WV-CYPHER-VERSION Version 5.0.0.4972 AES SVN_500 (Debug) 20111020-03:01:33 4 | #EXT-X-STREAM-INF:PROGRAM-ID=1611044116,BANDWIDTH=500000 5 | 01.m3u8 6 | #EXT-X-STREAM-INF:PROGRAM-ID=1611044116,BANDWIDTH=900000 7 | 02.m3u8 8 | #EXT-X-STREAM-INF:PROGRAM-ID=1611044116,BANDWIDTH=1500000 9 | 03.m3u8 10 | -------------------------------------------------------------------------------- /sample-playlists/wowza-master.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000 4 | chunklist-b300000.m3u8?wowzasessionid=1359287668 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=600000 6 | chunklist-b600000.m3u8?wowzasessionid=1359287668 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=850000 8 | chunklist-b850000.m3u8?wowzasessionid=1359287668 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1300000 10 | chunklist-b1300000.m3u8?wowzasessionid=1359287668 11 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2000000 12 | chunklist-b2000000.m3u8?wowzasessionid=1359287668 13 | -------------------------------------------------------------------------------- /sample-playlists/wowza-vod-chunklist.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:12 4 | #EXT-X-MEDIA-SEQUENCE:1 5 | #EXTINF:12.0,Title 1 6 | media-b2000000_1.ts?wowzasessionid=2029972411 7 | #EXTINF:12.0,Title 2 8 | media-b2000000_2.ts?wowzasessionid=2029972411 9 | #EXTINF:12.0, 10 | media-b2000000_3.ts?wowzasessionid=2029972411 11 | #EXTINF:12.0, 12 | media-b2000000_4.ts?wowzasessionid=2029972411 13 | #EXTINF:12.0, 14 | media-b2000000_5.ts?wowzasessionid=2029972411 15 | #EXTINF:12.0, 16 | media-b2000000_6.ts?wowzasessionid=2029972411 17 | #EXTINF:12.0, 18 | media-b2000000_7.ts?wowzasessionid=2029972411 19 | #EXTINF:12.0, 20 | media-b2000000_8.ts?wowzasessionid=2029972411 21 | #EXTINF:12.0, 22 | media-b2000000_9.ts?wowzasessionid=2029972411 23 | #EXTINF:12.0, 24 | media-b2000000_10.ts?wowzasessionid=2029972411 25 | #EXTINF:12.0, 26 | media-b2000000_11.ts?wowzasessionid=2029972411 27 | #EXTINF:12.0, 28 | media-b2000000_12.ts?wowzasessionid=2029972411 29 | #EXTINF:12.0, 30 | media-b2000000_13.ts?wowzasessionid=2029972411 31 | #EXTINF:12.0, 32 | media-b2000000_14.ts?wowzasessionid=2029972411 33 | #EXTINF:12.0, 34 | media-b2000000_15.ts?wowzasessionid=2029972411 35 | #EXTINF:12.0, 36 | media-b2000000_16.ts?wowzasessionid=2029972411 37 | #EXTINF:12.0, 38 | media-b2000000_17.ts?wowzasessionid=2029972411 39 | #EXTINF:12.0, 40 | media-b2000000_18.ts?wowzasessionid=2029972411 41 | #EXTINF:12.0, 42 | media-b2000000_19.ts?wowzasessionid=2029972411 43 | #EXTINF:12.0, 44 | media-b2000000_20.ts?wowzasessionid=2029972411 45 | #EXTINF:12.0, 46 | media-b2000000_21.ts?wowzasessionid=2029972411 47 | #EXTINF:12.0, 48 | media-b2000000_22.ts?wowzasessionid=2029972411 49 | #EXTINF:12.0, 50 | media-b2000000_23.ts?wowzasessionid=2029972411 51 | #EXTINF:12.0, 52 | media-b2000000_24.ts?wowzasessionid=2029972411 53 | #EXTINF:12.0, 54 | media-b2000000_25.ts?wowzasessionid=2029972411 55 | #EXTINF:12.0, 56 | media-b2000000_26.ts?wowzasessionid=2029972411 57 | #EXTINF:12.0, 58 | media-b2000000_27.ts?wowzasessionid=2029972411 59 | #EXTINF:12.0, 60 | media-b2000000_28.ts?wowzasessionid=2029972411 61 | #EXTINF:12.0, 62 | media-b2000000_29.ts?wowzasessionid=2029972411 63 | #EXTINF:12.0, 64 | media-b2000000_30.ts?wowzasessionid=2029972411 65 | #EXTINF:12.0, 66 | media-b2000000_31.ts?wowzasessionid=2029972411 67 | #EXTINF:12.0, 68 | media-b2000000_32.ts?wowzasessionid=2029972411 69 | #EXTINF:12.0, 70 | media-b2000000_33.ts?wowzasessionid=2029972411 71 | #EXTINF:12.0, 72 | media-b2000000_34.ts?wowzasessionid=2029972411 73 | #EXTINF:12.0, 74 | media-b2000000_35.ts?wowzasessionid=2029972411 75 | #EXTINF:12.0, 76 | media-b2000000_36.ts?wowzasessionid=2029972411 77 | #EXTINF:12.0, 78 | media-b2000000_37.ts?wowzasessionid=2029972411 79 | #EXTINF:12.0, 80 | media-b2000000_38.ts?wowzasessionid=2029972411 81 | #EXTINF:12.0, 82 | media-b2000000_39.ts?wowzasessionid=2029972411 83 | #EXTINF:12.0, 84 | media-b2000000_40.ts?wowzasessionid=2029972411 85 | #EXTINF:12.0, 86 | media-b2000000_41.ts?wowzasessionid=2029972411 87 | #EXTINF:12.0, 88 | media-b2000000_42.ts?wowzasessionid=2029972411 89 | #EXTINF:12.0, 90 | media-b2000000_43.ts?wowzasessionid=2029972411 91 | #EXTINF:12.0, 92 | media-b2000000_44.ts?wowzasessionid=2029972411 93 | #EXTINF:12.0, 94 | media-b2000000_45.ts?wowzasessionid=2029972411 95 | #EXTINF:12.0, 96 | media-b2000000_46.ts?wowzasessionid=2029972411 97 | #EXTINF:12.0, 98 | media-b2000000_47.ts?wowzasessionid=2029972411 99 | #EXTINF:12.0, 100 | media-b2000000_48.ts?wowzasessionid=2029972411 101 | #EXTINF:12.0, 102 | media-b2000000_49.ts?wowzasessionid=2029972411 103 | #EXTINF:12.0, 104 | media-b2000000_50.ts?wowzasessionid=2029972411 105 | #EXTINF:12.0, 106 | media-b2000000_51.ts?wowzasessionid=2029972411 107 | #EXTINF:12.0, 108 | media-b2000000_52.ts?wowzasessionid=2029972411 109 | #EXTINF:12.0, 110 | media-b2000000_53.ts?wowzasessionid=2029972411 111 | #EXTINF:12.0, 112 | media-b2000000_54.ts?wowzasessionid=2029972411 113 | #EXTINF:12.0, 114 | media-b2000000_55.ts?wowzasessionid=2029972411 115 | #EXTINF:12.0, 116 | media-b2000000_56.ts?wowzasessionid=2029972411 117 | #EXTINF:12.0, 118 | media-b2000000_57.ts?wowzasessionid=2029972411 119 | #EXTINF:12.0, 120 | media-b2000000_58.ts?wowzasessionid=2029972411 121 | #EXTINF:12.0, 122 | media-b2000000_59.ts?wowzasessionid=2029972411 123 | #EXTINF:12.0, 124 | media-b2000000_60.ts?wowzasessionid=2029972411 125 | #EXTINF:12.0, 126 | media-b2000000_61.ts?wowzasessionid=2029972411 127 | #EXTINF:12.0, 128 | media-b2000000_62.ts?wowzasessionid=2029972411 129 | #EXTINF:12.0, 130 | media-b2000000_63.ts?wowzasessionid=2029972411 131 | #EXTINF:12.0, 132 | media-b2000000_64.ts?wowzasessionid=2029972411 133 | #EXTINF:12.0, 134 | media-b2000000_65.ts?wowzasessionid=2029972411 135 | #EXTINF:12.0, 136 | media-b2000000_66.ts?wowzasessionid=2029972411 137 | #EXTINF:12.0, 138 | media-b2000000_67.ts?wowzasessionid=2029972411 139 | #EXTINF:12.0, 140 | media-b2000000_68.ts?wowzasessionid=2029972411 141 | #EXTINF:12.0, 142 | media-b2000000_69.ts?wowzasessionid=2029972411 143 | #EXTINF:12.0, 144 | media-b2000000_70.ts?wowzasessionid=2029972411 145 | #EXTINF:12.0, 146 | media-b2000000_71.ts?wowzasessionid=2029972411 147 | #EXTINF:12.0, 148 | media-b2000000_72.ts?wowzasessionid=2029972411 149 | #EXTINF:12.0, 150 | media-b2000000_73.ts?wowzasessionid=2029972411 151 | #EXTINF:12.0, 152 | media-b2000000_74.ts?wowzasessionid=2029972411 153 | #EXTINF:12.0, 154 | media-b2000000_75.ts?wowzasessionid=2029972411 155 | #EXTINF:12.0, 156 | media-b2000000_76.ts?wowzasessionid=2029972411 157 | #EXTINF:12.0, 158 | media-b2000000_77.ts?wowzasessionid=2029972411 159 | #EXTINF:12.0, 160 | media-b2000000_78.ts?wowzasessionid=2029972411 161 | #EXTINF:12.0, 162 | media-b2000000_79.ts?wowzasessionid=2029972411 163 | #EXTINF:12.0, 164 | media-b2000000_80.ts?wowzasessionid=2029972411 165 | #EXTINF:12.0, 166 | media-b2000000_81.ts?wowzasessionid=2029972411 167 | #EXTINF:12.0, 168 | media-b2000000_82.ts?wowzasessionid=2029972411 169 | #EXTINF:12.0, 170 | media-b2000000_83.ts?wowzasessionid=2029972411 171 | #EXTINF:12.0, 172 | media-b2000000_84.ts?wowzasessionid=2029972411 173 | #EXTINF:12.0, 174 | media-b2000000_85.ts?wowzasessionid=2029972411 175 | #EXTINF:12.0, 176 | media-b2000000_86.ts?wowzasessionid=2029972411 177 | #EXTINF:12.0, 178 | media-b2000000_87.ts?wowzasessionid=2029972411 179 | #EXTINF:12.0, 180 | media-b2000000_88.ts?wowzasessionid=2029972411 181 | #EXTINF:12.0, 182 | media-b2000000_89.ts?wowzasessionid=2029972411 183 | #EXTINF:12.0, 184 | media-b2000000_90.ts?wowzasessionid=2029972411 185 | #EXTINF:12.0, 186 | media-b2000000_91.ts?wowzasessionid=2029972411 187 | #EXTINF:12.0, 188 | media-b2000000_92.ts?wowzasessionid=2029972411 189 | #EXTINF:12.0, 190 | media-b2000000_93.ts?wowzasessionid=2029972411 191 | #EXTINF:12.0, 192 | media-b2000000_94.ts?wowzasessionid=2029972411 193 | #EXTINF:12.0, 194 | media-b2000000_95.ts?wowzasessionid=2029972411 195 | #EXTINF:12.0, 196 | media-b2000000_96.ts?wowzasessionid=2029972411 197 | #EXTINF:12.0, 198 | media-b2000000_97.ts?wowzasessionid=2029972411 199 | #EXTINF:12.0, 200 | media-b2000000_98.ts?wowzasessionid=2029972411 201 | #EXTINF:12.0, 202 | media-b2000000_99.ts?wowzasessionid=2029972411 203 | #EXTINF:12.0, 204 | media-b2000000_100.ts?wowzasessionid=2029972411 205 | #EXTINF:12.0, 206 | media-b2000000_101.ts?wowzasessionid=2029972411 207 | #EXTINF:12.0, 208 | media-b2000000_102.ts?wowzasessionid=2029972411 209 | #EXTINF:12.0, 210 | media-b2000000_103.ts?wowzasessionid=2029972411 211 | #EXTINF:12.0, 212 | media-b2000000_104.ts?wowzasessionid=2029972411 213 | #EXTINF:12.0, 214 | media-b2000000_105.ts?wowzasessionid=2029972411 215 | #EXTINF:12.0, 216 | media-b2000000_106.ts?wowzasessionid=2029972411 217 | #EXTINF:12.0, 218 | media-b2000000_107.ts?wowzasessionid=2029972411 219 | #EXTINF:12.0, 220 | media-b2000000_108.ts?wowzasessionid=2029972411 221 | #EXTINF:12.0, 222 | media-b2000000_109.ts?wowzasessionid=2029972411 223 | #EXTINF:12.0, 224 | media-b2000000_110.ts?wowzasessionid=2029972411 225 | #EXTINF:12.0, 226 | media-b2000000_111.ts?wowzasessionid=2029972411 227 | #EXTINF:12.0, 228 | media-b2000000_112.ts?wowzasessionid=2029972411 229 | #EXTINF:12.0, 230 | media-b2000000_113.ts?wowzasessionid=2029972411 231 | #EXTINF:12.0, 232 | media-b2000000_114.ts?wowzasessionid=2029972411 233 | #EXTINF:12.0, 234 | media-b2000000_115.ts?wowzasessionid=2029972411 235 | #EXTINF:12.0, 236 | media-b2000000_116.ts?wowzasessionid=2029972411 237 | #EXTINF:12.0, 238 | media-b2000000_117.ts?wowzasessionid=2029972411 239 | #EXTINF:12.0, 240 | media-b2000000_118.ts?wowzasessionid=2029972411 241 | #EXTINF:12.0, 242 | media-b2000000_119.ts?wowzasessionid=2029972411 243 | #EXTINF:12.0, 244 | media-b2000000_120.ts?wowzasessionid=2029972411 245 | #EXTINF:12.0, 246 | media-b2000000_121.ts?wowzasessionid=2029972411 247 | #EXTINF:12.0, 248 | media-b2000000_122.ts?wowzasessionid=2029972411 249 | #EXTINF:12.0, 250 | media-b2000000_123.ts?wowzasessionid=2029972411 251 | #EXTINF:12.0, 252 | media-b2000000_124.ts?wowzasessionid=2029972411 253 | #EXTINF:12.0, 254 | media-b2000000_125.ts?wowzasessionid=2029972411 255 | #EXTINF:12.0, 256 | media-b2000000_126.ts?wowzasessionid=2029972411 257 | #EXTINF:12.0, 258 | media-b2000000_127.ts?wowzasessionid=2029972411 259 | #EXTINF:12.0, 260 | media-b2000000_128.ts?wowzasessionid=2029972411 261 | #EXTINF:12.0, 262 | media-b2000000_129.ts?wowzasessionid=2029972411 263 | #EXTINF:12.0, 264 | media-b2000000_130.ts?wowzasessionid=2029972411 265 | #EXTINF:12.0, 266 | media-b2000000_131.ts?wowzasessionid=2029972411 267 | #EXTINF:12.0, 268 | media-b2000000_132.ts?wowzasessionid=2029972411 269 | #EXTINF:12.0, 270 | media-b2000000_133.ts?wowzasessionid=2029972411 271 | #EXTINF:12.0, 272 | media-b2000000_134.ts?wowzasessionid=2029972411 273 | #EXTINF:12.0, 274 | media-b2000000_135.ts?wowzasessionid=2029972411 275 | #EXTINF:12.0, 276 | media-b2000000_136.ts?wowzasessionid=2029972411 277 | #EXTINF:12.0, 278 | media-b2000000_137.ts?wowzasessionid=2029972411 279 | #EXTINF:12.0, 280 | media-b2000000_138.ts?wowzasessionid=2029972411 281 | #EXTINF:12.0, 282 | media-b2000000_139.ts?wowzasessionid=2029972411 283 | #EXTINF:12.0, 284 | media-b2000000_140.ts?wowzasessionid=2029972411 285 | #EXTINF:12.0, 286 | media-b2000000_141.ts?wowzasessionid=2029972411 287 | #EXTINF:12.0, 288 | media-b2000000_142.ts?wowzasessionid=2029972411 289 | #EXTINF:12.0, 290 | media-b2000000_143.ts?wowzasessionid=2029972411 291 | #EXTINF:12.0, 292 | media-b2000000_144.ts?wowzasessionid=2029972411 293 | #EXTINF:12.0, 294 | media-b2000000_145.ts?wowzasessionid=2029972411 295 | #EXTINF:12.0, 296 | media-b2000000_146.ts?wowzasessionid=2029972411 297 | #EXTINF:12.0, 298 | media-b2000000_147.ts?wowzasessionid=2029972411 299 | #EXTINF:12.0, 300 | media-b2000000_148.ts?wowzasessionid=2029972411 301 | #EXTINF:12.0, 302 | media-b2000000_149.ts?wowzasessionid=2029972411 303 | #EXTINF:12.0, 304 | media-b2000000_150.ts?wowzasessionid=2029972411 305 | #EXTINF:12.0, 306 | media-b2000000_151.ts?wowzasessionid=2029972411 307 | #EXTINF:12.0, 308 | media-b2000000_152.ts?wowzasessionid=2029972411 309 | #EXTINF:12.0, 310 | media-b2000000_153.ts?wowzasessionid=2029972411 311 | #EXTINF:12.0, 312 | media-b2000000_154.ts?wowzasessionid=2029972411 313 | #EXTINF:12.0, 314 | media-b2000000_155.ts?wowzasessionid=2029972411 315 | #EXTINF:12.0, 316 | media-b2000000_156.ts?wowzasessionid=2029972411 317 | #EXTINF:12.0, 318 | media-b2000000_157.ts?wowzasessionid=2029972411 319 | #EXTINF:12.0, 320 | media-b2000000_158.ts?wowzasessionid=2029972411 321 | #EXTINF:12.0, 322 | media-b2000000_159.ts?wowzasessionid=2029972411 323 | #EXTINF:12.0, 324 | media-b2000000_160.ts?wowzasessionid=2029972411 325 | #EXTINF:12.0, 326 | media-b2000000_161.ts?wowzasessionid=2029972411 327 | #EXTINF:12.0, 328 | media-b2000000_162.ts?wowzasessionid=2029972411 329 | #EXTINF:12.0, 330 | media-b2000000_163.ts?wowzasessionid=2029972411 331 | #EXTINF:12.0, 332 | media-b2000000_164.ts?wowzasessionid=2029972411 333 | #EXTINF:12.0, 334 | media-b2000000_165.ts?wowzasessionid=2029972411 335 | #EXTINF:12.0, 336 | media-b2000000_166.ts?wowzasessionid=2029972411 337 | #EXTINF:12.0, 338 | media-b2000000_167.ts?wowzasessionid=2029972411 339 | #EXTINF:12.0, 340 | media-b2000000_168.ts?wowzasessionid=2029972411 341 | #EXTINF:12.0, 342 | media-b2000000_169.ts?wowzasessionid=2029972411 343 | #EXTINF:12.0, 344 | media-b2000000_170.ts?wowzasessionid=2029972411 345 | #EXTINF:12.0, 346 | media-b2000000_171.ts?wowzasessionid=2029972411 347 | #EXTINF:12.0, 348 | media-b2000000_172.ts?wowzasessionid=2029972411 349 | #EXTINF:12.0, 350 | media-b2000000_173.ts?wowzasessionid=2029972411 351 | #EXTINF:12.0, 352 | media-b2000000_174.ts?wowzasessionid=2029972411 353 | #EXTINF:12.0, 354 | media-b2000000_175.ts?wowzasessionid=2029972411 355 | #EXTINF:12.0, 356 | media-b2000000_176.ts?wowzasessionid=2029972411 357 | #EXTINF:12.0, 358 | media-b2000000_177.ts?wowzasessionid=2029972411 359 | #EXTINF:12.0, 360 | media-b2000000_178.ts?wowzasessionid=2029972411 361 | #EXTINF:12.0, 362 | media-b2000000_179.ts?wowzasessionid=2029972411 363 | #EXTINF:12.0, 364 | media-b2000000_180.ts?wowzasessionid=2029972411 365 | #EXTINF:12.0, 366 | media-b2000000_181.ts?wowzasessionid=2029972411 367 | #EXTINF:12.0, 368 | media-b2000000_182.ts?wowzasessionid=2029972411 369 | #EXTINF:12.0, 370 | media-b2000000_183.ts?wowzasessionid=2029972411 371 | #EXTINF:12.0, 372 | media-b2000000_184.ts?wowzasessionid=2029972411 373 | #EXTINF:12.0, 374 | media-b2000000_185.ts?wowzasessionid=2029972411 375 | #EXTINF:12.0, 376 | media-b2000000_186.ts?wowzasessionid=2029972411 377 | #EXTINF:12.0, 378 | media-b2000000_187.ts?wowzasessionid=2029972411 379 | #EXTINF:12.0, 380 | media-b2000000_188.ts?wowzasessionid=2029972411 381 | #EXTINF:12.0, 382 | media-b2000000_189.ts?wowzasessionid=2029972411 383 | #EXTINF:12.0, 384 | media-b2000000_190.ts?wowzasessionid=2029972411 385 | #EXTINF:12.0, 386 | media-b2000000_191.ts?wowzasessionid=2029972411 387 | #EXTINF:12.0, 388 | media-b2000000_192.ts?wowzasessionid=2029972411 389 | #EXTINF:12.0, 390 | media-b2000000_193.ts?wowzasessionid=2029972411 391 | #EXTINF:12.0, 392 | media-b2000000_194.ts?wowzasessionid=2029972411 393 | #EXTINF:12.0, 394 | media-b2000000_195.ts?wowzasessionid=2029972411 395 | #EXTINF:12.0, 396 | media-b2000000_196.ts?wowzasessionid=2029972411 397 | #EXTINF:12.0, 398 | media-b2000000_197.ts?wowzasessionid=2029972411 399 | #EXTINF:12.0, 400 | media-b2000000_198.ts?wowzasessionid=2029972411 401 | #EXTINF:12.0, 402 | media-b2000000_199.ts?wowzasessionid=2029972411 403 | #EXTINF:12.0, 404 | media-b2000000_200.ts?wowzasessionid=2029972411 405 | #EXTINF:12.0, 406 | media-b2000000_201.ts?wowzasessionid=2029972411 407 | #EXTINF:12.0, 408 | media-b2000000_202.ts?wowzasessionid=2029972411 409 | #EXTINF:12.0, 410 | media-b2000000_203.ts?wowzasessionid=2029972411 411 | #EXTINF:12.0, 412 | media-b2000000_204.ts?wowzasessionid=2029972411 413 | #EXTINF:12.0, 414 | media-b2000000_205.ts?wowzasessionid=2029972411 415 | #EXTINF:12.0, 416 | media-b2000000_206.ts?wowzasessionid=2029972411 417 | #EXTINF:12.0, 418 | media-b2000000_207.ts?wowzasessionid=2029972411 419 | #EXTINF:12.0, 420 | media-b2000000_208.ts?wowzasessionid=2029972411 421 | #EXTINF:12.0, 422 | media-b2000000_209.ts?wowzasessionid=2029972411 423 | #EXTINF:12.0, 424 | media-b2000000_210.ts?wowzasessionid=2029972411 425 | #EXTINF:12.0, 426 | media-b2000000_211.ts?wowzasessionid=2029972411 427 | #EXTINF:12.0, 428 | media-b2000000_212.ts?wowzasessionid=2029972411 429 | #EXTINF:12.0, 430 | media-b2000000_213.ts?wowzasessionid=2029972411 431 | #EXTINF:12.0, 432 | media-b2000000_214.ts?wowzasessionid=2029972411 433 | #EXTINF:12.0, 434 | media-b2000000_215.ts?wowzasessionid=2029972411 435 | #EXTINF:12.0, 436 | media-b2000000_216.ts?wowzasessionid=2029972411 437 | #EXTINF:12.0, 438 | media-b2000000_217.ts?wowzasessionid=2029972411 439 | #EXTINF:12.0, 440 | media-b2000000_218.ts?wowzasessionid=2029972411 441 | #EXTINF:12.0, 442 | media-b2000000_219.ts?wowzasessionid=2029972411 443 | #EXTINF:12.0, 444 | media-b2000000_220.ts?wowzasessionid=2029972411 445 | #EXTINF:12.0, 446 | media-b2000000_221.ts?wowzasessionid=2029972411 447 | #EXTINF:12.0, 448 | media-b2000000_222.ts?wowzasessionid=2029972411 449 | #EXTINF:12.0, 450 | media-b2000000_223.ts?wowzasessionid=2029972411 451 | #EXTINF:12.0, 452 | media-b2000000_224.ts?wowzasessionid=2029972411 453 | #EXTINF:12.0, 454 | media-b2000000_225.ts?wowzasessionid=2029972411 455 | #EXTINF:12.0, 456 | media-b2000000_226.ts?wowzasessionid=2029972411 457 | #EXTINF:12.0, 458 | media-b2000000_227.ts?wowzasessionid=2029972411 459 | #EXTINF:12.0, 460 | media-b2000000_228.ts?wowzasessionid=2029972411 461 | #EXTINF:12.0, 462 | media-b2000000_229.ts?wowzasessionid=2029972411 463 | #EXTINF:12.0, 464 | media-b2000000_230.ts?wowzasessionid=2029972411 465 | #EXTINF:12.0, 466 | media-b2000000_231.ts?wowzasessionid=2029972411 467 | #EXTINF:12.0, 468 | media-b2000000_232.ts?wowzasessionid=2029972411 469 | #EXTINF:12.0, 470 | media-b2000000_233.ts?wowzasessionid=2029972411 471 | #EXTINF:12.0, 472 | media-b2000000_234.ts?wowzasessionid=2029972411 473 | #EXTINF:12.0, 474 | media-b2000000_235.ts?wowzasessionid=2029972411 475 | #EXTINF:12.0, 476 | media-b2000000_236.ts?wowzasessionid=2029972411 477 | #EXTINF:12.0, 478 | media-b2000000_237.ts?wowzasessionid=2029972411 479 | #EXTINF:12.0, 480 | media-b2000000_238.ts?wowzasessionid=2029972411 481 | #EXTINF:12.0, 482 | media-b2000000_239.ts?wowzasessionid=2029972411 483 | #EXTINF:12.0, 484 | media-b2000000_240.ts?wowzasessionid=2029972411 485 | #EXTINF:12.0, 486 | media-b2000000_241.ts?wowzasessionid=2029972411 487 | #EXTINF:12.0, 488 | media-b2000000_242.ts?wowzasessionid=2029972411 489 | #EXTINF:12.0, 490 | media-b2000000_243.ts?wowzasessionid=2029972411 491 | #EXTINF:12.0, 492 | media-b2000000_244.ts?wowzasessionid=2029972411 493 | #EXTINF:12.0, 494 | media-b2000000_245.ts?wowzasessionid=2029972411 495 | #EXTINF:12.0, 496 | media-b2000000_246.ts?wowzasessionid=2029972411 497 | #EXTINF:12.0, 498 | media-b2000000_247.ts?wowzasessionid=2029972411 499 | #EXTINF:12.0, 500 | media-b2000000_248.ts?wowzasessionid=2029972411 501 | #EXTINF:12.0, 502 | media-b2000000_249.ts?wowzasessionid=2029972411 503 | #EXTINF:12.0, 504 | media-b2000000_250.ts?wowzasessionid=2029972411 505 | #EXTINF:12.0, 506 | media-b2000000_251.ts?wowzasessionid=2029972411 507 | #EXTINF:12.0, 508 | media-b2000000_252.ts?wowzasessionid=2029972411 509 | #EXTINF:12.0, 510 | media-b2000000_253.ts?wowzasessionid=2029972411 511 | #EXTINF:12.0, 512 | media-b2000000_254.ts?wowzasessionid=2029972411 513 | #EXTINF:12.0, 514 | media-b2000000_255.ts?wowzasessionid=2029972411 515 | #EXTINF:12.0, 516 | media-b2000000_256.ts?wowzasessionid=2029972411 517 | #EXTINF:12.0, 518 | media-b2000000_257.ts?wowzasessionid=2029972411 519 | #EXTINF:12.0, 520 | media-b2000000_258.ts?wowzasessionid=2029972411 521 | #EXTINF:12.0, 522 | media-b2000000_259.ts?wowzasessionid=2029972411 523 | #EXTINF:12.0, 524 | media-b2000000_260.ts?wowzasessionid=2029972411 525 | #EXTINF:12.0, 526 | media-b2000000_261.ts?wowzasessionid=2029972411 527 | #EXTINF:12.0, 528 | media-b2000000_262.ts?wowzasessionid=2029972411 529 | #EXTINF:12.0, 530 | media-b2000000_263.ts?wowzasessionid=2029972411 531 | #EXTINF:12.0, 532 | media-b2000000_264.ts?wowzasessionid=2029972411 533 | #EXTINF:12.0, 534 | media-b2000000_265.ts?wowzasessionid=2029972411 535 | #EXTINF:12.0, 536 | media-b2000000_266.ts?wowzasessionid=2029972411 537 | #EXTINF:12.0, 538 | media-b2000000_267.ts?wowzasessionid=2029972411 539 | #EXTINF:12.0, 540 | media-b2000000_268.ts?wowzasessionid=2029972411 541 | #EXTINF:12.0, 542 | media-b2000000_269.ts?wowzasessionid=2029972411 543 | #EXTINF:12.0, 544 | media-b2000000_270.ts?wowzasessionid=2029972411 545 | #EXTINF:12.0, 546 | media-b2000000_271.ts?wowzasessionid=2029972411 547 | #EXTINF:12.0, 548 | media-b2000000_272.ts?wowzasessionid=2029972411 549 | #EXTINF:12.0, 550 | media-b2000000_273.ts?wowzasessionid=2029972411 551 | #EXTINF:12.0, 552 | media-b2000000_274.ts?wowzasessionid=2029972411 553 | #EXTINF:12.0, 554 | media-b2000000_275.ts?wowzasessionid=2029972411 555 | #EXTINF:12.0, 556 | media-b2000000_276.ts?wowzasessionid=2029972411 557 | #EXTINF:12.0, 558 | media-b2000000_277.ts?wowzasessionid=2029972411 559 | #EXTINF:12.0, 560 | media-b2000000_278.ts?wowzasessionid=2029972411 561 | #EXTINF:12.0, 562 | media-b2000000_279.ts?wowzasessionid=2029972411 563 | #EXTINF:12.0, 564 | media-b2000000_280.ts?wowzasessionid=2029972411 565 | #EXTINF:12.0, 566 | media-b2000000_281.ts?wowzasessionid=2029972411 567 | #EXTINF:12.0, 568 | media-b2000000_282.ts?wowzasessionid=2029972411 569 | #EXTINF:12.0, 570 | media-b2000000_283.ts?wowzasessionid=2029972411 571 | #EXTINF:12.0, 572 | media-b2000000_284.ts?wowzasessionid=2029972411 573 | #EXTINF:12.0, 574 | media-b2000000_285.ts?wowzasessionid=2029972411 575 | #EXTINF:12.0, 576 | media-b2000000_286.ts?wowzasessionid=2029972411 577 | #EXTINF:12.0, 578 | media-b2000000_287.ts?wowzasessionid=2029972411 579 | #EXTINF:12.0, 580 | media-b2000000_288.ts?wowzasessionid=2029972411 581 | #EXTINF:12.0, 582 | media-b2000000_289.ts?wowzasessionid=2029972411 583 | #EXTINF:12.0, 584 | media-b2000000_290.ts?wowzasessionid=2029972411 585 | #EXTINF:12.0, 586 | media-b2000000_291.ts?wowzasessionid=2029972411 587 | #EXTINF:12.0, 588 | media-b2000000_292.ts?wowzasessionid=2029972411 589 | #EXTINF:12.0, 590 | media-b2000000_293.ts?wowzasessionid=2029972411 591 | #EXTINF:12.0, 592 | media-b2000000_294.ts?wowzasessionid=2029972411 593 | #EXTINF:12.0, 594 | media-b2000000_295.ts?wowzasessionid=2029972411 595 | #EXTINF:12.0, 596 | media-b2000000_296.ts?wowzasessionid=2029972411 597 | #EXTINF:12.0, 598 | media-b2000000_297.ts?wowzasessionid=2029972411 599 | #EXTINF:12.0, 600 | media-b2000000_298.ts?wowzasessionid=2029972411 601 | #EXTINF:12.0, 602 | media-b2000000_299.ts?wowzasessionid=2029972411 603 | #EXTINF:12.0, 604 | media-b2000000_300.ts?wowzasessionid=2029972411 605 | #EXTINF:12.0, 606 | media-b2000000_301.ts?wowzasessionid=2029972411 607 | #EXTINF:12.0, 608 | media-b2000000_302.ts?wowzasessionid=2029972411 609 | #EXTINF:12.0, 610 | media-b2000000_303.ts?wowzasessionid=2029972411 611 | #EXTINF:12.0, 612 | media-b2000000_304.ts?wowzasessionid=2029972411 613 | #EXTINF:12.0, 614 | media-b2000000_305.ts?wowzasessionid=2029972411 615 | #EXTINF:12.0, 616 | media-b2000000_306.ts?wowzasessionid=2029972411 617 | #EXTINF:12.0, 618 | media-b2000000_307.ts?wowzasessionid=2029972411 619 | #EXTINF:12.0, 620 | media-b2000000_308.ts?wowzasessionid=2029972411 621 | #EXTINF:12.0, 622 | media-b2000000_309.ts?wowzasessionid=2029972411 623 | #EXTINF:12.0, 624 | media-b2000000_310.ts?wowzasessionid=2029972411 625 | #EXTINF:12.0, 626 | media-b2000000_311.ts?wowzasessionid=2029972411 627 | #EXTINF:12.0, 628 | media-b2000000_312.ts?wowzasessionid=2029972411 629 | #EXTINF:12.0, 630 | media-b2000000_313.ts?wowzasessionid=2029972411 631 | #EXTINF:12.0, 632 | media-b2000000_314.ts?wowzasessionid=2029972411 633 | #EXTINF:12.0, 634 | media-b2000000_315.ts?wowzasessionid=2029972411 635 | #EXTINF:12.0, 636 | media-b2000000_316.ts?wowzasessionid=2029972411 637 | #EXTINF:12.0, 638 | media-b2000000_317.ts?wowzasessionid=2029972411 639 | #EXTINF:12.0, 640 | media-b2000000_318.ts?wowzasessionid=2029972411 641 | #EXTINF:12.0, 642 | media-b2000000_319.ts?wowzasessionid=2029972411 643 | #EXTINF:12.0, 644 | media-b2000000_320.ts?wowzasessionid=2029972411 645 | #EXTINF:12.0, 646 | media-b2000000_321.ts?wowzasessionid=2029972411 647 | #EXTINF:12.0, 648 | media-b2000000_322.ts?wowzasessionid=2029972411 649 | #EXTINF:12.0, 650 | media-b2000000_323.ts?wowzasessionid=2029972411 651 | #EXTINF:12.0, 652 | media-b2000000_324.ts?wowzasessionid=2029972411 653 | #EXTINF:12.0, 654 | media-b2000000_325.ts?wowzasessionid=2029972411 655 | #EXTINF:12.0, 656 | media-b2000000_326.ts?wowzasessionid=2029972411 657 | #EXTINF:12.0, 658 | media-b2000000_327.ts?wowzasessionid=2029972411 659 | #EXTINF:12.0, 660 | media-b2000000_328.ts?wowzasessionid=2029972411 661 | #EXTINF:12.0, 662 | media-b2000000_329.ts?wowzasessionid=2029972411 663 | #EXTINF:12.0, 664 | media-b2000000_330.ts?wowzasessionid=2029972411 665 | #EXTINF:12.0, 666 | media-b2000000_331.ts?wowzasessionid=2029972411 667 | #EXTINF:12.0, 668 | media-b2000000_332.ts?wowzasessionid=2029972411 669 | #EXTINF:12.0, 670 | media-b2000000_333.ts?wowzasessionid=2029972411 671 | #EXTINF:12.0, 672 | media-b2000000_334.ts?wowzasessionid=2029972411 673 | #EXTINF:12.0, 674 | media-b2000000_335.ts?wowzasessionid=2029972411 675 | #EXTINF:12.0, 676 | media-b2000000_336.ts?wowzasessionid=2029972411 677 | #EXTINF:12.0, 678 | media-b2000000_337.ts?wowzasessionid=2029972411 679 | #EXTINF:12.0, 680 | media-b2000000_338.ts?wowzasessionid=2029972411 681 | #EXTINF:12.0, 682 | media-b2000000_339.ts?wowzasessionid=2029972411 683 | #EXTINF:12.0, 684 | media-b2000000_340.ts?wowzasessionid=2029972411 685 | #EXTINF:12.0, 686 | media-b2000000_341.ts?wowzasessionid=2029972411 687 | #EXTINF:12.0, 688 | media-b2000000_342.ts?wowzasessionid=2029972411 689 | #EXTINF:12.0, 690 | media-b2000000_343.ts?wowzasessionid=2029972411 691 | #EXTINF:12.0, 692 | media-b2000000_344.ts?wowzasessionid=2029972411 693 | #EXTINF:12.0, 694 | media-b2000000_345.ts?wowzasessionid=2029972411 695 | #EXTINF:12.0, 696 | media-b2000000_346.ts?wowzasessionid=2029972411 697 | #EXTINF:12.0, 698 | media-b2000000_347.ts?wowzasessionid=2029972411 699 | #EXTINF:12.0, 700 | media-b2000000_348.ts?wowzasessionid=2029972411 701 | #EXTINF:12.0, 702 | media-b2000000_349.ts?wowzasessionid=2029972411 703 | #EXTINF:12.0, 704 | media-b2000000_350.ts?wowzasessionid=2029972411 705 | #EXTINF:12.0, 706 | media-b2000000_351.ts?wowzasessionid=2029972411 707 | #EXTINF:12.0, 708 | media-b2000000_352.ts?wowzasessionid=2029972411 709 | #EXTINF:12.0, 710 | media-b2000000_353.ts?wowzasessionid=2029972411 711 | #EXTINF:12.0, 712 | media-b2000000_354.ts?wowzasessionid=2029972411 713 | #EXTINF:12.0, 714 | media-b2000000_355.ts?wowzasessionid=2029972411 715 | #EXTINF:12.0, 716 | media-b2000000_356.ts?wowzasessionid=2029972411 717 | #EXTINF:12.0, 718 | media-b2000000_357.ts?wowzasessionid=2029972411 719 | #EXTINF:12.0, 720 | media-b2000000_358.ts?wowzasessionid=2029972411 721 | #EXTINF:12.0, 722 | media-b2000000_359.ts?wowzasessionid=2029972411 723 | #EXTINF:12.0, 724 | media-b2000000_360.ts?wowzasessionid=2029972411 725 | #EXTINF:12.0, 726 | media-b2000000_361.ts?wowzasessionid=2029972411 727 | #EXTINF:12.0, 728 | media-b2000000_362.ts?wowzasessionid=2029972411 729 | #EXTINF:12.0, 730 | media-b2000000_363.ts?wowzasessionid=2029972411 731 | #EXTINF:12.0, 732 | media-b2000000_364.ts?wowzasessionid=2029972411 733 | #EXTINF:12.0, 734 | media-b2000000_365.ts?wowzasessionid=2029972411 735 | #EXTINF:12.0, 736 | media-b2000000_366.ts?wowzasessionid=2029972411 737 | #EXTINF:12.0, 738 | media-b2000000_367.ts?wowzasessionid=2029972411 739 | #EXTINF:12.0, 740 | media-b2000000_368.ts?wowzasessionid=2029972411 741 | #EXTINF:12.0, 742 | media-b2000000_369.ts?wowzasessionid=2029972411 743 | #EXTINF:12.0, 744 | media-b2000000_370.ts?wowzasessionid=2029972411 745 | #EXTINF:12.0, 746 | media-b2000000_371.ts?wowzasessionid=2029972411 747 | #EXTINF:12.0, 748 | media-b2000000_372.ts?wowzasessionid=2029972411 749 | #EXTINF:12.0, 750 | media-b2000000_373.ts?wowzasessionid=2029972411 751 | #EXTINF:12.0, 752 | media-b2000000_374.ts?wowzasessionid=2029972411 753 | #EXTINF:12.0, 754 | media-b2000000_375.ts?wowzasessionid=2029972411 755 | #EXTINF:12.0, 756 | media-b2000000_376.ts?wowzasessionid=2029972411 757 | #EXTINF:12.0, 758 | media-b2000000_377.ts?wowzasessionid=2029972411 759 | #EXTINF:12.0, 760 | media-b2000000_378.ts?wowzasessionid=2029972411 761 | #EXTINF:12.0, 762 | media-b2000000_379.ts?wowzasessionid=2029972411 763 | #EXTINF:12.0, 764 | media-b2000000_380.ts?wowzasessionid=2029972411 765 | #EXTINF:12.0, 766 | media-b2000000_381.ts?wowzasessionid=2029972411 767 | #EXTINF:12.0, 768 | media-b2000000_382.ts?wowzasessionid=2029972411 769 | #EXTINF:12.0, 770 | media-b2000000_383.ts?wowzasessionid=2029972411 771 | #EXTINF:12.0, 772 | media-b2000000_384.ts?wowzasessionid=2029972411 773 | #EXTINF:12.0, 774 | media-b2000000_385.ts?wowzasessionid=2029972411 775 | #EXTINF:12.0, 776 | media-b2000000_386.ts?wowzasessionid=2029972411 777 | #EXTINF:12.0, 778 | media-b2000000_387.ts?wowzasessionid=2029972411 779 | #EXTINF:12.0, 780 | media-b2000000_388.ts?wowzasessionid=2029972411 781 | #EXTINF:12.0, 782 | media-b2000000_389.ts?wowzasessionid=2029972411 783 | #EXTINF:12.0, 784 | media-b2000000_390.ts?wowzasessionid=2029972411 785 | #EXTINF:12.0, 786 | media-b2000000_391.ts?wowzasessionid=2029972411 787 | #EXTINF:12.0, 788 | media-b2000000_392.ts?wowzasessionid=2029972411 789 | #EXTINF:12.0, 790 | media-b2000000_393.ts?wowzasessionid=2029972411 791 | #EXTINF:12.0, 792 | media-b2000000_394.ts?wowzasessionid=2029972411 793 | #EXTINF:12.0, 794 | media-b2000000_395.ts?wowzasessionid=2029972411 795 | #EXTINF:12.0, 796 | media-b2000000_396.ts?wowzasessionid=2029972411 797 | #EXTINF:12.0, 798 | media-b2000000_397.ts?wowzasessionid=2029972411 799 | #EXTINF:12.0, 800 | media-b2000000_398.ts?wowzasessionid=2029972411 801 | #EXTINF:12.0, 802 | media-b2000000_399.ts?wowzasessionid=2029972411 803 | #EXTINF:12.0, 804 | media-b2000000_400.ts?wowzasessionid=2029972411 805 | #EXTINF:12.0, 806 | media-b2000000_401.ts?wowzasessionid=2029972411 807 | #EXTINF:12.0, 808 | media-b2000000_402.ts?wowzasessionid=2029972411 809 | #EXTINF:12.0, 810 | media-b2000000_403.ts?wowzasessionid=2029972411 811 | #EXTINF:12.0, 812 | media-b2000000_404.ts?wowzasessionid=2029972411 813 | #EXTINF:12.0, 814 | media-b2000000_405.ts?wowzasessionid=2029972411 815 | #EXTINF:12.0, 816 | media-b2000000_406.ts?wowzasessionid=2029972411 817 | #EXTINF:12.0, 818 | media-b2000000_407.ts?wowzasessionid=2029972411 819 | #EXTINF:12.0, 820 | media-b2000000_408.ts?wowzasessionid=2029972411 821 | #EXTINF:12.0, 822 | media-b2000000_409.ts?wowzasessionid=2029972411 823 | #EXTINF:12.0, 824 | media-b2000000_410.ts?wowzasessionid=2029972411 825 | #EXTINF:12.0, 826 | media-b2000000_411.ts?wowzasessionid=2029972411 827 | #EXTINF:12.0, 828 | media-b2000000_412.ts?wowzasessionid=2029972411 829 | #EXTINF:12.0, 830 | media-b2000000_413.ts?wowzasessionid=2029972411 831 | #EXTINF:12.0, 832 | media-b2000000_414.ts?wowzasessionid=2029972411 833 | #EXTINF:12.0, 834 | media-b2000000_415.ts?wowzasessionid=2029972411 835 | #EXTINF:12.0, 836 | media-b2000000_416.ts?wowzasessionid=2029972411 837 | #EXTINF:12.0, 838 | media-b2000000_417.ts?wowzasessionid=2029972411 839 | #EXTINF:12.0, 840 | media-b2000000_418.ts?wowzasessionid=2029972411 841 | #EXTINF:12.0, 842 | media-b2000000_419.ts?wowzasessionid=2029972411 843 | #EXTINF:12.0, 844 | media-b2000000_420.ts?wowzasessionid=2029972411 845 | #EXTINF:12.0, 846 | media-b2000000_421.ts?wowzasessionid=2029972411 847 | #EXTINF:12.0, 848 | media-b2000000_422.ts?wowzasessionid=2029972411 849 | #EXTINF:12.0, 850 | media-b2000000_423.ts?wowzasessionid=2029972411 851 | #EXTINF:12.0, 852 | media-b2000000_424.ts?wowzasessionid=2029972411 853 | #EXTINF:12.0, 854 | media-b2000000_425.ts?wowzasessionid=2029972411 855 | #EXTINF:12.0, 856 | media-b2000000_426.ts?wowzasessionid=2029972411 857 | #EXTINF:12.0, 858 | media-b2000000_427.ts?wowzasessionid=2029972411 859 | #EXTINF:12.0, 860 | media-b2000000_428.ts?wowzasessionid=2029972411 861 | #EXTINF:12.0, 862 | media-b2000000_429.ts?wowzasessionid=2029972411 863 | #EXTINF:12.0, 864 | media-b2000000_430.ts?wowzasessionid=2029972411 865 | #EXTINF:12.0, 866 | media-b2000000_431.ts?wowzasessionid=2029972411 867 | #EXTINF:12.0, 868 | media-b2000000_432.ts?wowzasessionid=2029972411 869 | #EXTINF:12.0, 870 | media-b2000000_433.ts?wowzasessionid=2029972411 871 | #EXTINF:12.0, 872 | media-b2000000_434.ts?wowzasessionid=2029972411 873 | #EXTINF:12.0, 874 | media-b2000000_435.ts?wowzasessionid=2029972411 875 | #EXTINF:12.0, 876 | media-b2000000_436.ts?wowzasessionid=2029972411 877 | #EXTINF:12.0, 878 | media-b2000000_437.ts?wowzasessionid=2029972411 879 | #EXTINF:12.0, 880 | media-b2000000_438.ts?wowzasessionid=2029972411 881 | #EXTINF:12.0, 882 | media-b2000000_439.ts?wowzasessionid=2029972411 883 | #EXTINF:12.0, 884 | media-b2000000_440.ts?wowzasessionid=2029972411 885 | #EXTINF:12.0, 886 | media-b2000000_441.ts?wowzasessionid=2029972411 887 | #EXTINF:12.0, 888 | media-b2000000_442.ts?wowzasessionid=2029972411 889 | #EXTINF:12.0, 890 | media-b2000000_443.ts?wowzasessionid=2029972411 891 | #EXTINF:12.0, 892 | media-b2000000_444.ts?wowzasessionid=2029972411 893 | #EXTINF:12.0, 894 | media-b2000000_445.ts?wowzasessionid=2029972411 895 | #EXTINF:12.0, 896 | media-b2000000_446.ts?wowzasessionid=2029972411 897 | #EXTINF:12.0, 898 | media-b2000000_447.ts?wowzasessionid=2029972411 899 | #EXTINF:12.0, 900 | media-b2000000_448.ts?wowzasessionid=2029972411 901 | #EXTINF:12.0, 902 | media-b2000000_449.ts?wowzasessionid=2029972411 903 | #EXTINF:12.0, 904 | media-b2000000_450.ts?wowzasessionid=2029972411 905 | #EXTINF:12.0, 906 | media-b2000000_451.ts?wowzasessionid=2029972411 907 | #EXTINF:12.0, 908 | media-b2000000_452.ts?wowzasessionid=2029972411 909 | #EXTINF:12.0, 910 | media-b2000000_453.ts?wowzasessionid=2029972411 911 | #EXTINF:12.0, 912 | media-b2000000_454.ts?wowzasessionid=2029972411 913 | #EXTINF:12.0, 914 | media-b2000000_455.ts?wowzasessionid=2029972411 915 | #EXTINF:12.0, 916 | media-b2000000_456.ts?wowzasessionid=2029972411 917 | #EXTINF:12.0, 918 | media-b2000000_457.ts?wowzasessionid=2029972411 919 | #EXTINF:12.0, 920 | media-b2000000_458.ts?wowzasessionid=2029972411 921 | #EXTINF:12.0, 922 | media-b2000000_459.ts?wowzasessionid=2029972411 923 | #EXTINF:12.0, 924 | media-b2000000_460.ts?wowzasessionid=2029972411 925 | #EXTINF:12.0, 926 | media-b2000000_461.ts?wowzasessionid=2029972411 927 | #EXTINF:12.0, 928 | media-b2000000_462.ts?wowzasessionid=2029972411 929 | #EXTINF:12.0, 930 | media-b2000000_463.ts?wowzasessionid=2029972411 931 | #EXTINF:12.0, 932 | media-b2000000_464.ts?wowzasessionid=2029972411 933 | #EXTINF:12.0, 934 | media-b2000000_465.ts?wowzasessionid=2029972411 935 | #EXTINF:12.0, 936 | media-b2000000_466.ts?wowzasessionid=2029972411 937 | #EXTINF:12.0, 938 | media-b2000000_467.ts?wowzasessionid=2029972411 939 | #EXTINF:12.0, 940 | media-b2000000_468.ts?wowzasessionid=2029972411 941 | #EXTINF:12.0, 942 | media-b2000000_469.ts?wowzasessionid=2029972411 943 | #EXTINF:12.0, 944 | media-b2000000_470.ts?wowzasessionid=2029972411 945 | #EXTINF:12.0, 946 | media-b2000000_471.ts?wowzasessionid=2029972411 947 | #EXTINF:12.0, 948 | media-b2000000_472.ts?wowzasessionid=2029972411 949 | #EXTINF:12.0, 950 | media-b2000000_473.ts?wowzasessionid=2029972411 951 | #EXTINF:12.0, 952 | media-b2000000_474.ts?wowzasessionid=2029972411 953 | #EXTINF:12.0, 954 | media-b2000000_475.ts?wowzasessionid=2029972411 955 | #EXTINF:12.0, 956 | media-b2000000_476.ts?wowzasessionid=2029972411 957 | #EXTINF:12.0, 958 | media-b2000000_477.ts?wowzasessionid=2029972411 959 | #EXTINF:12.0, 960 | media-b2000000_478.ts?wowzasessionid=2029972411 961 | #EXTINF:12.0, 962 | media-b2000000_479.ts?wowzasessionid=2029972411 963 | #EXTINF:12.0, 964 | media-b2000000_480.ts?wowzasessionid=2029972411 965 | #EXTINF:12.0, 966 | media-b2000000_481.ts?wowzasessionid=2029972411 967 | #EXTINF:12.0, 968 | media-b2000000_482.ts?wowzasessionid=2029972411 969 | #EXTINF:12.0, 970 | media-b2000000_483.ts?wowzasessionid=2029972411 971 | #EXTINF:12.0, 972 | media-b2000000_484.ts?wowzasessionid=2029972411 973 | #EXTINF:12.0, 974 | media-b2000000_485.ts?wowzasessionid=2029972411 975 | #EXTINF:12.0, 976 | media-b2000000_486.ts?wowzasessionid=2029972411 977 | #EXTINF:12.0, 978 | media-b2000000_487.ts?wowzasessionid=2029972411 979 | #EXTINF:12.0, 980 | media-b2000000_488.ts?wowzasessionid=2029972411 981 | #EXTINF:12.0, 982 | media-b2000000_489.ts?wowzasessionid=2029972411 983 | #EXTINF:12.0, 984 | media-b2000000_490.ts?wowzasessionid=2029972411 985 | #EXTINF:12.0, 986 | media-b2000000_491.ts?wowzasessionid=2029972411 987 | #EXTINF:12.0, 988 | media-b2000000_492.ts?wowzasessionid=2029972411 989 | #EXTINF:12.0, 990 | media-b2000000_493.ts?wowzasessionid=2029972411 991 | #EXTINF:12.0, 992 | media-b2000000_494.ts?wowzasessionid=2029972411 993 | #EXTINF:12.0, 994 | media-b2000000_495.ts?wowzasessionid=2029972411 995 | #EXTINF:12.0, 996 | media-b2000000_496.ts?wowzasessionid=2029972411 997 | #EXTINF:12.0, 998 | media-b2000000_497.ts?wowzasessionid=2029972411 999 | #EXTINF:12.0, 1000 | media-b2000000_498.ts?wowzasessionid=2029972411 1001 | #EXTINF:12.0, 1002 | media-b2000000_499.ts?wowzasessionid=2029972411 1003 | #EXTINF:12.0, 1004 | media-b2000000_500.ts?wowzasessionid=2029972411 1005 | #EXTINF:12.0, 1006 | media-b2000000_501.ts?wowzasessionid=2029972411 1007 | #EXTINF:12.0, 1008 | media-b2000000_502.ts?wowzasessionid=2029972411 1009 | #EXTINF:12.0, 1010 | media-b2000000_503.ts?wowzasessionid=2029972411 1011 | #EXTINF:12.0, 1012 | media-b2000000_504.ts?wowzasessionid=2029972411 1013 | #EXTINF:12.0, 1014 | media-b2000000_505.ts?wowzasessionid=2029972411 1015 | #EXTINF:12.0, 1016 | media-b2000000_506.ts?wowzasessionid=2029972411 1017 | #EXTINF:12.0, 1018 | media-b2000000_507.ts?wowzasessionid=2029972411 1019 | #EXTINF:12.0, 1020 | media-b2000000_508.ts?wowzasessionid=2029972411 1021 | #EXTINF:12.0, 1022 | media-b2000000_509.ts?wowzasessionid=2029972411 1023 | #EXTINF:12.0, 1024 | media-b2000000_510.ts?wowzasessionid=2029972411 1025 | #EXTINF:12.0, 1026 | media-b2000000_511.ts?wowzasessionid=2029972411 1027 | #EXTINF:12.0, 1028 | media-b2000000_512.ts?wowzasessionid=2029972411 1029 | #EXTINF:12.0, 1030 | media-b2000000_513.ts?wowzasessionid=2029972411 1031 | #EXTINF:12.0, 1032 | media-b2000000_514.ts?wowzasessionid=2029972411 1033 | #EXTINF:12.0, 1034 | media-b2000000_515.ts?wowzasessionid=2029972411 1035 | #EXTINF:12.0, 1036 | media-b2000000_516.ts?wowzasessionid=2029972411 1037 | #EXTINF:12.0, 1038 | media-b2000000_517.ts?wowzasessionid=2029972411 1039 | #EXTINF:12.0, 1040 | media-b2000000_518.ts?wowzasessionid=2029972411 1041 | #EXTINF:12.0, 1042 | media-b2000000_519.ts?wowzasessionid=2029972411 1043 | #EXTINF:12.0, 1044 | media-b2000000_520.ts?wowzasessionid=2029972411 1045 | #EXTINF:12.0, 1046 | media-b2000000_521.ts?wowzasessionid=2029972411 1047 | #EXTINF:7.2, 1048 | media-b2000000_522.ts?wowzasessionid=2029972411 1049 | #EXT-X-ENDLIST 1050 | -------------------------------------------------------------------------------- /structure.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | /* 4 | Part of M3U8 parser & generator library. 5 | This file defines data structures related to package. 6 | 7 | Copyright 2013-2017 The Project Developers. 8 | See the AUTHORS and LICENSE files at the top-level directory of this distribution 9 | and at https://github.com/grafov/m3u8/ 10 | 11 | ॐ तारे तुत्तारे तुरे स्व 12 | */ 13 | 14 | import ( 15 | "bytes" 16 | "io" 17 | "time" 18 | ) 19 | 20 | const ( 21 | /* 22 | Compatibility rules described in section 7: 23 | Clients and servers MUST implement protocol version 2 or higher to use: 24 | o The IV attribute of the EXT-X-KEY tag. 25 | Clients and servers MUST implement protocol version 3 or higher to use: 26 | o Floating-point EXTINF duration values. 27 | Clients and servers MUST implement protocol version 4 or higher to use: 28 | o The EXT-X-BYTERANGE tag. 29 | o The EXT-X-I-FRAME-STREAM-INF tag. 30 | o The EXT-X-I-FRAMES-ONLY tag. 31 | o The EXT-X-MEDIA tag. 32 | o The AUDIO and VIDEO attributes of the EXT-X-STREAM-INF tag. 33 | */ 34 | minver = uint8(3) 35 | 36 | // DATETIME represents format of the timestamps in encoded 37 | // playlists. Format for EXT-X-PROGRAM-DATE-TIME defined in 38 | // section 3.4.5 39 | DATETIME = time.RFC3339Nano 40 | ) 41 | 42 | // ListType is type of the playlist. 43 | type ListType uint 44 | 45 | const ( 46 | // use 0 for not defined type 47 | MASTER ListType = iota + 1 48 | MEDIA 49 | ) 50 | 51 | // MediaType is the type for EXT-X-PLAYLIST-TYPE tag 52 | type MediaType uint 53 | 54 | const ( 55 | // use 0 for not defined type 56 | EVENT MediaType = iota + 1 57 | VOD 58 | ) 59 | 60 | // SCTE35Syntax defines the format of the SCTE-35 cue points which do not use 61 | // the draft-pantos-http-live-streaming-19 EXT-X-DATERANGE tag and instead 62 | // have their own custom tags 63 | type SCTE35Syntax uint 64 | 65 | const ( 66 | // SCTE35_67_2014 will be the default due to backwards compatibility reasons. 67 | SCTE35_67_2014 SCTE35Syntax = iota // SCTE35_67_2014 defined in http://www.scte.org/documents/pdf/standards/SCTE%2067%202014.pdf 68 | SCTE35_OATCLS // SCTE35_OATCLS is a non-standard but common format 69 | ) 70 | 71 | // SCTE35CueType defines the type of cue point, used by readers and writers to 72 | // write a different syntax 73 | type SCTE35CueType uint 74 | 75 | const ( 76 | SCTE35Cue_Start SCTE35CueType = iota // SCTE35Cue_Start indicates an out cue point 77 | SCTE35Cue_Mid // SCTE35Cue_Mid indicates a segment between start and end cue points 78 | SCTE35Cue_End // SCTE35Cue_End indicates an in cue point 79 | ) 80 | 81 | // MediaPlaylist structure represents a single bitrate playlist aka 82 | // media playlist. It related to both a simple media playlists and a 83 | // sliding window media playlists. URI lines in the Playlist point to 84 | // media segments. 85 | // 86 | // Simple Media Playlist file sample: 87 | // 88 | // #EXTM3U 89 | // #EXT-X-VERSION:3 90 | // #EXT-X-TARGETDURATION:5220 91 | // #EXTINF:5219.2, 92 | // http://media.example.com/entire.ts 93 | // #EXT-X-ENDLIST 94 | // 95 | // Sample of Sliding Window Media Playlist, using HTTPS: 96 | // 97 | // #EXTM3U 98 | // #EXT-X-VERSION:3 99 | // #EXT-X-TARGETDURATION:8 100 | // #EXT-X-MEDIA-SEQUENCE:2680 101 | // 102 | // #EXTINF:7.975, 103 | // https://priv.example.com/fileSequence2680.ts 104 | // #EXTINF:7.941, 105 | // https://priv.example.com/fileSequence2681.ts 106 | // #EXTINF:7.975, 107 | // https://priv.example.com/fileSequence2682.ts 108 | type MediaPlaylist struct { 109 | TargetDuration float64 110 | SeqNo uint64 // EXT-X-MEDIA-SEQUENCE 111 | Segments []*MediaSegment 112 | Args string // optional arguments placed after URIs (URI?Args) 113 | Iframe bool // EXT-X-I-FRAMES-ONLY 114 | Closed bool // is this VOD (closed) or Live (sliding) playlist? 115 | MediaType MediaType 116 | DiscontinuitySeq uint64 // EXT-X-DISCONTINUITY-SEQUENCE 117 | StartTime float64 118 | StartTimePrecise bool 119 | durationAsInt bool // output durations as integers of floats? 120 | keyformat int 121 | winsize uint // max number of segments displayed in an encoded playlist; need set to zero for VOD playlists 122 | capacity uint // total capacity of slice used for the playlist 123 | head uint // head of FIFO, we add segments to head 124 | tail uint // tail of FIFO, we remove segments from tail 125 | count uint // number of segments added to the playlist 126 | buf bytes.Buffer 127 | ver uint8 128 | Key *Key // EXT-X-KEY is optional encryption key displayed before any segments (default key for the playlist) 129 | Map *Map // EXT-X-MAP is optional tag specifies how to obtain the Media Initialization Section (default map for the playlist) 130 | WV *WV // Widevine related tags outside of M3U8 specs 131 | Custom map[string]CustomTag 132 | customDecoders []CustomDecoder 133 | } 134 | 135 | // MasterPlaylist structure represents a master playlist which 136 | // combines media playlists for multiple bitrates. URI lines in the 137 | // playlist identify media playlists. Sample of Master Playlist file: 138 | // 139 | // #EXTM3U 140 | // #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000 141 | // http://example.com/low.m3u8 142 | // #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2560000 143 | // http://example.com/mid.m3u8 144 | // #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000 145 | // http://example.com/hi.m3u8 146 | // #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=65000,CODECS="mp4a.40.5" 147 | // http://example.com/audio-only.m3u8 148 | type MasterPlaylist struct { 149 | Variants []*Variant 150 | Args string // optional arguments placed after URI (URI?Args) 151 | CypherVersion string // non-standard tag for Widevine (see also WV struct) 152 | buf bytes.Buffer 153 | ver uint8 154 | independentSegments bool 155 | Custom map[string]CustomTag 156 | customDecoders []CustomDecoder 157 | } 158 | 159 | // Variant structure represents variants for master playlist. 160 | // Variants included in a master playlist and point to media 161 | // playlists. 162 | type Variant struct { 163 | URI string 164 | Chunklist *MediaPlaylist 165 | VariantParams 166 | } 167 | 168 | // VariantParams structure represents additional parameters for a 169 | // variant used in EXT-X-STREAM-INF and EXT-X-I-FRAME-STREAM-INF 170 | type VariantParams struct { 171 | ProgramId uint32 172 | Bandwidth uint32 173 | AverageBandwidth uint32 // EXT-X-STREAM-INF only 174 | Codecs string 175 | Resolution string 176 | Audio string // EXT-X-STREAM-INF only 177 | Video string 178 | Subtitles string // EXT-X-STREAM-INF only 179 | Captions string // EXT-X-STREAM-INF only 180 | Name string // EXT-X-STREAM-INF only (non standard Wowza/JWPlayer extension to name the variant/quality in UA) 181 | Iframe bool // EXT-X-I-FRAME-STREAM-INF 182 | VideoRange string 183 | HDCPLevel string 184 | FrameRate float64 // EXT-X-STREAM-INF 185 | Alternatives []*Alternative // EXT-X-MEDIA 186 | } 187 | 188 | // Alternative structure represents EXT-X-MEDIA tag in variants. 189 | type Alternative struct { 190 | GroupId string 191 | URI string 192 | Type string 193 | Language string 194 | Name string 195 | Default bool 196 | Autoselect string 197 | Forced string 198 | Characteristics string 199 | Subtitles string 200 | } 201 | 202 | // MediaSegment structure represents a media segment included in a 203 | // media playlist. Media segment may be encrypted. Widevine supports 204 | // own tags for encryption metadata. 205 | type MediaSegment struct { 206 | SeqId uint64 207 | Title string // optional second parameter for EXTINF tag 208 | URI string 209 | 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 210 | Limit int64 // EXT-X-BYTERANGE is length in bytes for the file under URI 211 | Offset int64 // EXT-X-BYTERANGE [@o] is offset from the start of the file under URI 212 | Key *Key // EXT-X-KEY displayed before the segment and means changing of encryption key (in theory each segment may have own key) 213 | Map *Map // EXT-X-MAP displayed before the segment 214 | 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) 215 | SCTE *SCTE // SCTE-35 used for Ad signaling in HLS 216 | ProgramDateTime time.Time // EXT-X-PROGRAM-DATE-TIME tag associates the first sample of a media segment with an absolute date and/or time 217 | Custom map[string]CustomTag 218 | } 219 | 220 | // SCTE holds custom, non EXT-X-DATERANGE, SCTE-35 tags 221 | type SCTE struct { 222 | Syntax SCTE35Syntax // Syntax defines the format of the SCTE-35 cue tag 223 | CueType SCTE35CueType // CueType defines whether the cue is a start, mid, end (if applicable) 224 | Cue string 225 | ID string 226 | Time float64 227 | Elapsed float64 228 | } 229 | 230 | // Key structure represents information about stream encryption. 231 | // 232 | // Realizes EXT-X-KEY tag. 233 | type Key struct { 234 | Method string 235 | URI string 236 | IV string 237 | Keyformat string 238 | Keyformatversions string 239 | } 240 | 241 | // Map structure represents specifies how to obtain the Media 242 | // Initialization Section required to parse the applicable 243 | // Media Segments. 244 | // 245 | // It applied to every Media Segment that appears after it in the 246 | // Playlist until the next EXT-X-MAP tag or until the end of the 247 | // playlist. 248 | // 249 | // Realizes EXT-MAP tag. 250 | type Map struct { 251 | URI string 252 | Limit int64 // is length in bytes for the file under URI 253 | Offset int64 // [@o] is offset from the start of the file under URI 254 | } 255 | 256 | // WV structure represents metadata for Google Widevine playlists. 257 | // This format not described in IETF draft but provied by Widevine Live Packager as 258 | // additional tags with #WV-prefix. 259 | type WV struct { 260 | AudioChannels uint 261 | AudioFormat uint 262 | AudioProfileIDC uint 263 | AudioSampleSize uint 264 | AudioSamplingFrequency uint 265 | CypherVersion string 266 | ECM string 267 | VideoFormat uint 268 | VideoFrameRate uint 269 | VideoLevelIDC uint 270 | VideoProfileIDC uint 271 | VideoResolution string 272 | VideoSAR string 273 | } 274 | 275 | // Playlist interface applied to various playlist types. 276 | type Playlist interface { 277 | Encode() *bytes.Buffer 278 | Decode(bytes.Buffer, bool) error 279 | DecodeFrom(reader io.Reader, strict bool) error 280 | WithCustomDecoders([]CustomDecoder) Playlist 281 | String() string 282 | } 283 | 284 | // CustomDecoder interface for decoding custom and unsupported tags 285 | type CustomDecoder interface { 286 | // TagName should return the full indentifier including the leading '#' as well as the 287 | // trailing ':' if the tag also contains a value or attribute list 288 | TagName() string 289 | // Decode parses a line from the playlist and returns the CustomTag representation 290 | Decode(line string) (CustomTag, error) 291 | // SegmentTag should return true if this CustomDecoder should apply per segment. 292 | // Should returns false if it a MediaPlaylist header tag. 293 | // This value is ignored for MasterPlaylists. 294 | SegmentTag() bool 295 | } 296 | 297 | // CustomTag interface for encoding custom and unsupported tags 298 | type CustomTag interface { 299 | // TagName should return the full indentifier including the leading '#' as well as the 300 | // trailing ':' if the tag also contains a value or attribute list 301 | TagName() string 302 | // Encode should return the complete tag string as a *bytes.Buffer. This will 303 | // be used by Playlist.Decode to write the tag to the m3u8. 304 | // Return nil to not write anything to the m3u8. 305 | Encode() *bytes.Buffer 306 | // String should return the encoded tag as a string. 307 | String() string 308 | } 309 | 310 | // Internal structure for decoding a line of input stream with a list type detection 311 | type decodingState struct { 312 | listType ListType 313 | m3u bool 314 | tagWV bool 315 | tagStreamInf bool 316 | tagInf bool 317 | tagSCTE35 bool 318 | tagRange bool 319 | tagDiscontinuity bool 320 | tagProgramDateTime bool 321 | tagKey bool 322 | tagMap bool 323 | tagCustom bool 324 | programDateTime time.Time 325 | limit int64 326 | offset int64 327 | duration float64 328 | title string 329 | variant *Variant 330 | alternatives []*Alternative 331 | xkey *Key 332 | xmap *Map 333 | scte *SCTE 334 | custom map[string]CustomTag 335 | } 336 | -------------------------------------------------------------------------------- /structure_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Playlist structures tests. 3 | 4 | Copyright 2013-2017 The Project Developers. 5 | See the AUTHORS and LICENSE files at the top-level directory of this distribution 6 | and at https://github.com/grafov/m3u8/ 7 | 8 | ॐ तारे तुत्तारे तुरे स्व 9 | */ 10 | package m3u8 11 | 12 | import ( 13 | "bytes" 14 | "testing" 15 | ) 16 | 17 | func CheckType(t *testing.T, p Playlist) { 18 | t.Logf("%T implements Playlist interface OK\n", p) 19 | } 20 | 21 | // Create new media playlist. 22 | func TestNewMediaPlaylist(t *testing.T) { 23 | _, e := NewMediaPlaylist(1, 2) 24 | if e != nil { 25 | t.Fatalf("Create media playlist failed: %s", e) 26 | } 27 | } 28 | 29 | type MockCustomTag struct { 30 | name string 31 | err error 32 | segment bool 33 | encodedString string 34 | } 35 | 36 | func (t *MockCustomTag) TagName() string { 37 | return t.name 38 | } 39 | 40 | func (t *MockCustomTag) Decode(line string) (CustomTag, error) { 41 | return t, t.err 42 | } 43 | 44 | func (t *MockCustomTag) Encode() *bytes.Buffer { 45 | if t.encodedString == "" { 46 | return nil 47 | } 48 | 49 | buf := new(bytes.Buffer) 50 | 51 | buf.WriteString(t.encodedString) 52 | 53 | return buf 54 | } 55 | 56 | func (t *MockCustomTag) String() string { 57 | return t.encodedString 58 | } 59 | 60 | func (t *MockCustomTag) SegmentTag() bool { 61 | return t.segment 62 | } 63 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | /* 4 | Part of M3U8 parser & generator library. 5 | This file defines functions related to playlist generation. 6 | 7 | Copyright 2013-2019 The Project Developers. 8 | See the AUTHORS and LICENSE files at the top-level directory of this distribution 9 | and at https://github.com/grafov/m3u8/ 10 | 11 | ॐ तारे तुत्तारे तुरे स्व 12 | */ 13 | 14 | import ( 15 | "bytes" 16 | "errors" 17 | "fmt" 18 | "math" 19 | "strconv" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | // ErrPlaylistFull declares the playlist error. 25 | var ErrPlaylistFull = errors.New("playlist is full") 26 | 27 | // Set version of the playlist accordingly with section 7 28 | func version(ver *uint8, newver uint8) { 29 | if *ver < newver { 30 | *ver = newver 31 | } 32 | } 33 | 34 | func strver(ver uint8) string { 35 | return strconv.FormatUint(uint64(ver), 10) 36 | } 37 | 38 | // NewMasterPlaylist creates a new empty master playlist. Master 39 | // playlist consists of variants. 40 | func NewMasterPlaylist() *MasterPlaylist { 41 | p := new(MasterPlaylist) 42 | p.ver = minver 43 | return p 44 | } 45 | 46 | // Append appends a variant to master playlist. This operation does 47 | // reset playlist cache. 48 | func (p *MasterPlaylist) Append(uri string, chunklist *MediaPlaylist, params VariantParams) { 49 | v := new(Variant) 50 | v.URI = uri 51 | v.Chunklist = chunklist 52 | v.VariantParams = params 53 | p.Variants = append(p.Variants, v) 54 | if len(v.Alternatives) > 0 { 55 | // From section 7: 56 | // The EXT-X-MEDIA tag and the AUDIO, VIDEO and SUBTITLES attributes of 57 | // the EXT-X-STREAM-INF tag are backward compatible to protocol version 58 | // 1, but playback on older clients may not be desirable. A server MAY 59 | // consider indicating a EXT-X-VERSION of 4 or higher in the Master 60 | // Playlist but is not required to do so. 61 | version(&p.ver, 4) // so it is optional and in theory may be set to ver.1 62 | // but more tests required 63 | } 64 | p.buf.Reset() 65 | } 66 | 67 | // ResetCache resetes the playlist' cache. 68 | func (p *MasterPlaylist) ResetCache() { 69 | p.buf.Reset() 70 | } 71 | 72 | // Encode generates the output in M3U8 format. 73 | func (p *MasterPlaylist) Encode() *bytes.Buffer { 74 | if p.buf.Len() > 0 { 75 | return &p.buf 76 | } 77 | 78 | p.buf.WriteString("#EXTM3U\n#EXT-X-VERSION:") 79 | p.buf.WriteString(strver(p.ver)) 80 | p.buf.WriteRune('\n') 81 | 82 | if p.IndependentSegments() { 83 | p.buf.WriteString("#EXT-X-INDEPENDENT-SEGMENTS\n") 84 | } 85 | 86 | // Write any custom master tags 87 | if p.Custom != nil { 88 | for _, v := range p.Custom { 89 | if customBuf := v.Encode(); customBuf != nil { 90 | p.buf.WriteString(customBuf.String()) 91 | p.buf.WriteRune('\n') 92 | } 93 | } 94 | } 95 | 96 | altsWritten := make(map[string]bool) 97 | 98 | for _, pl := range p.Variants { 99 | if pl.Alternatives != nil { 100 | for _, alt := range pl.Alternatives { 101 | // Make sure that we only write out an alternative once 102 | altKey := fmt.Sprintf("%s-%s-%s-%s", alt.Type, alt.GroupId, alt.Name, alt.Language) 103 | if altsWritten[altKey] { 104 | continue 105 | } 106 | altsWritten[altKey] = true 107 | 108 | p.buf.WriteString("#EXT-X-MEDIA:") 109 | if alt.Type != "" { 110 | p.buf.WriteString("TYPE=") // Type should not be quoted 111 | p.buf.WriteString(alt.Type) 112 | } 113 | if alt.GroupId != "" { 114 | p.buf.WriteString(",GROUP-ID=\"") 115 | p.buf.WriteString(alt.GroupId) 116 | p.buf.WriteRune('"') 117 | } 118 | if alt.Name != "" { 119 | p.buf.WriteString(",NAME=\"") 120 | p.buf.WriteString(alt.Name) 121 | p.buf.WriteRune('"') 122 | } 123 | p.buf.WriteString(",DEFAULT=") 124 | if alt.Default { 125 | p.buf.WriteString("YES") 126 | } else { 127 | p.buf.WriteString("NO") 128 | } 129 | if alt.Autoselect != "" { 130 | p.buf.WriteString(",AUTOSELECT=") 131 | p.buf.WriteString(alt.Autoselect) 132 | } 133 | if alt.Language != "" { 134 | p.buf.WriteString(",LANGUAGE=\"") 135 | p.buf.WriteString(alt.Language) 136 | p.buf.WriteRune('"') 137 | } 138 | if alt.Forced != "" { 139 | p.buf.WriteString(",FORCED=\"") 140 | p.buf.WriteString(alt.Forced) 141 | p.buf.WriteRune('"') 142 | } 143 | if alt.Characteristics != "" { 144 | p.buf.WriteString(",CHARACTERISTICS=\"") 145 | p.buf.WriteString(alt.Characteristics) 146 | p.buf.WriteRune('"') 147 | } 148 | if alt.Subtitles != "" { 149 | p.buf.WriteString(",SUBTITLES=\"") 150 | p.buf.WriteString(alt.Subtitles) 151 | p.buf.WriteRune('"') 152 | } 153 | if alt.URI != "" { 154 | p.buf.WriteString(",URI=\"") 155 | p.buf.WriteString(alt.URI) 156 | p.buf.WriteRune('"') 157 | } 158 | p.buf.WriteRune('\n') 159 | } 160 | } 161 | if pl.Iframe { 162 | p.buf.WriteString("#EXT-X-I-FRAME-STREAM-INF:PROGRAM-ID=") 163 | p.buf.WriteString(strconv.FormatUint(uint64(pl.ProgramId), 10)) 164 | p.buf.WriteString(",BANDWIDTH=") 165 | p.buf.WriteString(strconv.FormatUint(uint64(pl.Bandwidth), 10)) 166 | if pl.AverageBandwidth != 0 { 167 | p.buf.WriteString(",AVERAGE-BANDWIDTH=") 168 | p.buf.WriteString(strconv.FormatUint(uint64(pl.AverageBandwidth), 10)) 169 | } 170 | if pl.Codecs != "" { 171 | p.buf.WriteString(",CODECS=\"") 172 | p.buf.WriteString(pl.Codecs) 173 | p.buf.WriteRune('"') 174 | } 175 | if pl.Resolution != "" { 176 | p.buf.WriteString(",RESOLUTION=") // Resolution should not be quoted 177 | p.buf.WriteString(pl.Resolution) 178 | } 179 | if pl.Video != "" { 180 | p.buf.WriteString(",VIDEO=\"") 181 | p.buf.WriteString(pl.Video) 182 | p.buf.WriteRune('"') 183 | } 184 | if pl.VideoRange != "" { 185 | p.buf.WriteString(",VIDEO-RANGE=") 186 | p.buf.WriteString(pl.VideoRange) 187 | } 188 | if pl.HDCPLevel != "" { 189 | p.buf.WriteString(",HDCP-LEVEL=") 190 | p.buf.WriteString(pl.HDCPLevel) 191 | } 192 | if pl.URI != "" { 193 | p.buf.WriteString(",URI=\"") 194 | p.buf.WriteString(pl.URI) 195 | p.buf.WriteRune('"') 196 | } 197 | p.buf.WriteRune('\n') 198 | } else { 199 | p.buf.WriteString("#EXT-X-STREAM-INF:PROGRAM-ID=") 200 | p.buf.WriteString(strconv.FormatUint(uint64(pl.ProgramId), 10)) 201 | p.buf.WriteString(",BANDWIDTH=") 202 | p.buf.WriteString(strconv.FormatUint(uint64(pl.Bandwidth), 10)) 203 | if pl.AverageBandwidth != 0 { 204 | p.buf.WriteString(",AVERAGE-BANDWIDTH=") 205 | p.buf.WriteString(strconv.FormatUint(uint64(pl.AverageBandwidth), 10)) 206 | } 207 | if pl.Codecs != "" { 208 | p.buf.WriteString(",CODECS=\"") 209 | p.buf.WriteString(pl.Codecs) 210 | p.buf.WriteRune('"') 211 | } 212 | if pl.Resolution != "" { 213 | p.buf.WriteString(",RESOLUTION=") // Resolution should not be quoted 214 | p.buf.WriteString(pl.Resolution) 215 | } 216 | if pl.Audio != "" { 217 | p.buf.WriteString(",AUDIO=\"") 218 | p.buf.WriteString(pl.Audio) 219 | p.buf.WriteRune('"') 220 | } 221 | if pl.Video != "" { 222 | p.buf.WriteString(",VIDEO=\"") 223 | p.buf.WriteString(pl.Video) 224 | p.buf.WriteRune('"') 225 | } 226 | if pl.Captions != "" { 227 | p.buf.WriteString(",CLOSED-CAPTIONS=") 228 | if pl.Captions == "NONE" { 229 | p.buf.WriteString(pl.Captions) // CC should not be quoted when eq NONE 230 | } else { 231 | p.buf.WriteRune('"') 232 | p.buf.WriteString(pl.Captions) 233 | p.buf.WriteRune('"') 234 | } 235 | } 236 | if pl.Subtitles != "" { 237 | p.buf.WriteString(",SUBTITLES=\"") 238 | p.buf.WriteString(pl.Subtitles) 239 | p.buf.WriteRune('"') 240 | } 241 | if pl.Name != "" { 242 | p.buf.WriteString(",NAME=\"") 243 | p.buf.WriteString(pl.Name) 244 | p.buf.WriteRune('"') 245 | } 246 | if pl.FrameRate != 0 { 247 | p.buf.WriteString(",FRAME-RATE=") 248 | p.buf.WriteString(strconv.FormatFloat(pl.FrameRate, 'f', 3, 64)) 249 | } 250 | if pl.VideoRange != "" { 251 | p.buf.WriteString(",VIDEO-RANGE=") 252 | p.buf.WriteString(pl.VideoRange) 253 | } 254 | if pl.HDCPLevel != "" { 255 | p.buf.WriteString(",HDCP-LEVEL=") 256 | p.buf.WriteString(pl.HDCPLevel) 257 | } 258 | 259 | p.buf.WriteRune('\n') 260 | p.buf.WriteString(pl.URI) 261 | if p.Args != "" { 262 | if strings.Contains(pl.URI, "?") { 263 | p.buf.WriteRune('&') 264 | } else { 265 | p.buf.WriteRune('?') 266 | } 267 | p.buf.WriteString(p.Args) 268 | } 269 | p.buf.WriteRune('\n') 270 | } 271 | } 272 | 273 | return &p.buf 274 | } 275 | 276 | // SetCustomTag sets the provided tag on the master playlist for its TagName 277 | func (p *MasterPlaylist) SetCustomTag(tag CustomTag) { 278 | if p.Custom == nil { 279 | p.Custom = make(map[string]CustomTag) 280 | } 281 | 282 | p.Custom[tag.TagName()] = tag 283 | } 284 | 285 | // Version returns the current playlist version number 286 | func (p *MasterPlaylist) Version() uint8 { 287 | return p.ver 288 | } 289 | 290 | // SetVersion sets the playlist version number, note the version maybe changed 291 | // automatically by other Set methods. 292 | func (p *MasterPlaylist) SetVersion(ver uint8) { 293 | p.ver = ver 294 | } 295 | 296 | // IndependentSegments returns true if all media samples in a segment can be 297 | // decoded without information from other buf. 298 | func (p *MasterPlaylist) IndependentSegments() bool { 299 | return p.independentSegments 300 | } 301 | 302 | // SetIndependentSegments sets whether all media samples in a segment can be 303 | // decoded without information from other buf. 304 | func (p *MasterPlaylist) SetIndependentSegments(b bool) { 305 | p.independentSegments = b 306 | } 307 | 308 | // String here for compatibility with Stringer interface. For example 309 | // fmt.Printf("%s", sampleMediaList) will encode playist and print its 310 | // string representation. 311 | func (p *MasterPlaylist) String() string { 312 | return p.Encode().String() 313 | } 314 | 315 | // NewMediaPlaylist creates a new media playlist structure. Winsize 316 | // defines how much items will displayed on playlist generation. 317 | // Capacity is total size of a playlist. 318 | func NewMediaPlaylist(winsize uint, capacity uint) (*MediaPlaylist, error) { 319 | p := new(MediaPlaylist) 320 | p.ver = minver 321 | p.capacity = capacity 322 | if err := p.SetWinSize(winsize); err != nil { 323 | return nil, err 324 | } 325 | p.Segments = make([]*MediaSegment, capacity) 326 | return p, nil 327 | } 328 | 329 | // last returns the previously written segment's index 330 | func (p *MediaPlaylist) last() uint { 331 | if p.tail == 0 { 332 | return p.capacity - 1 333 | } 334 | return p.tail - 1 335 | } 336 | 337 | // Remove current segment from the head of chunk slice form a media playlist. Useful for sliding playlists. 338 | // This operation does reset playlist cache. 339 | func (p *MediaPlaylist) Remove() (err error) { 340 | if p.count == 0 { 341 | return errors.New("playlist is empty") 342 | } 343 | p.head = (p.head + 1) % p.capacity 344 | p.count-- 345 | if !p.Closed { 346 | p.SeqNo++ 347 | } 348 | p.buf.Reset() 349 | return nil 350 | } 351 | 352 | // Append general chunk to the tail of chunk slice for a media playlist. 353 | // This operation does reset playlist cache. 354 | func (p *MediaPlaylist) Append(uri string, duration float64, title string) error { 355 | seg := new(MediaSegment) 356 | seg.URI = uri 357 | seg.Duration = duration 358 | seg.Title = title 359 | return p.AppendSegment(seg) 360 | } 361 | 362 | // AppendSegment appends a MediaSegment to the tail of chunk slice for 363 | // a media playlist. This operation does reset playlist cache. 364 | func (p *MediaPlaylist) AppendSegment(seg *MediaSegment) error { 365 | if p.head == p.tail && p.count > 0 { 366 | return ErrPlaylistFull 367 | } 368 | seg.SeqId = p.SeqNo 369 | if p.count > 0 { 370 | seg.SeqId = p.Segments[(p.capacity+p.tail-1)%p.capacity].SeqId + 1 371 | } 372 | p.Segments[p.tail] = seg 373 | p.tail = (p.tail + 1) % p.capacity 374 | p.count++ 375 | if p.TargetDuration < seg.Duration { 376 | p.TargetDuration = math.Ceil(seg.Duration) 377 | } 378 | p.buf.Reset() 379 | return nil 380 | } 381 | 382 | // Slide combines two operations: firstly it removes one chunk from 383 | // the head of chunk slice and move pointer to next chunk. Secondly it 384 | // appends one chunk to the tail of chunk slice. Useful for sliding 385 | // playlists. This operation does reset cache. 386 | func (p *MediaPlaylist) Slide(uri string, duration float64, title string) { 387 | if !p.Closed && p.count >= p.winsize { 388 | p.Remove() 389 | } 390 | p.Append(uri, duration, title) 391 | } 392 | 393 | // ResetCache resets playlist cache. Next called Encode() will 394 | // regenerate playlist from the chunk slice. 395 | func (p *MediaPlaylist) ResetCache() { 396 | p.buf.Reset() 397 | } 398 | 399 | // Encode generates output in M3U8 format. Marshal `winsize` elements 400 | // from bottom of the `buf` queue. 401 | func (p *MediaPlaylist) Encode() *bytes.Buffer { 402 | if p.buf.Len() > 0 { 403 | return &p.buf 404 | } 405 | 406 | p.buf.WriteString("#EXTM3U\n#EXT-X-VERSION:") 407 | p.buf.WriteString(strver(p.ver)) 408 | p.buf.WriteRune('\n') 409 | 410 | // Write any custom master tags 411 | if p.Custom != nil { 412 | for _, v := range p.Custom { 413 | if customBuf := v.Encode(); customBuf != nil { 414 | p.buf.WriteString(customBuf.String()) 415 | p.buf.WriteRune('\n') 416 | } 417 | } 418 | } 419 | 420 | // default key (workaround for Widevine) 421 | if p.Key != nil { 422 | p.buf.WriteString("#EXT-X-KEY:") 423 | p.buf.WriteString("METHOD=") 424 | p.buf.WriteString(p.Key.Method) 425 | if p.Key.Method != "NONE" { 426 | p.buf.WriteString(",URI=\"") 427 | p.buf.WriteString(p.Key.URI) 428 | p.buf.WriteRune('"') 429 | if p.Key.IV != "" { 430 | p.buf.WriteString(",IV=") 431 | p.buf.WriteString(p.Key.IV) 432 | } 433 | if p.Key.Keyformat != "" { 434 | p.buf.WriteString(",KEYFORMAT=\"") 435 | p.buf.WriteString(p.Key.Keyformat) 436 | p.buf.WriteRune('"') 437 | } 438 | if p.Key.Keyformatversions != "" { 439 | p.buf.WriteString(",KEYFORMATVERSIONS=\"") 440 | p.buf.WriteString(p.Key.Keyformatversions) 441 | p.buf.WriteRune('"') 442 | } 443 | } 444 | p.buf.WriteRune('\n') 445 | } 446 | if p.Map != nil { 447 | p.buf.WriteString("#EXT-X-MAP:") 448 | p.buf.WriteString("URI=\"") 449 | p.buf.WriteString(p.Map.URI) 450 | p.buf.WriteRune('"') 451 | if p.Map.Limit > 0 { 452 | p.buf.WriteString(",BYTERANGE=") 453 | p.buf.WriteString(strconv.FormatInt(p.Map.Limit, 10)) 454 | p.buf.WriteRune('@') 455 | p.buf.WriteString(strconv.FormatInt(p.Map.Offset, 10)) 456 | } 457 | p.buf.WriteRune('\n') 458 | } 459 | if p.MediaType > 0 { 460 | p.buf.WriteString("#EXT-X-PLAYLIST-TYPE:") 461 | switch p.MediaType { 462 | case EVENT: 463 | p.buf.WriteString("EVENT\n") 464 | p.buf.WriteString("#EXT-X-ALLOW-CACHE:NO\n") 465 | case VOD: 466 | p.buf.WriteString("VOD\n") 467 | } 468 | } 469 | p.buf.WriteString("#EXT-X-MEDIA-SEQUENCE:") 470 | p.buf.WriteString(strconv.FormatUint(p.SeqNo, 10)) 471 | p.buf.WriteRune('\n') 472 | p.buf.WriteString("#EXT-X-TARGETDURATION:") 473 | 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 474 | p.buf.WriteRune('\n') 475 | if p.StartTime > 0.0 { 476 | p.buf.WriteString("#EXT-X-START:TIME-OFFSET=") 477 | p.buf.WriteString(strconv.FormatFloat(p.StartTime, 'f', -1, 64)) 478 | if p.StartTimePrecise { 479 | p.buf.WriteString(",PRECISE=YES") 480 | } 481 | p.buf.WriteRune('\n') 482 | } 483 | if p.DiscontinuitySeq != 0 { 484 | p.buf.WriteString("#EXT-X-DISCONTINUITY-SEQUENCE:") 485 | p.buf.WriteString(strconv.FormatUint(uint64(p.DiscontinuitySeq), 10)) 486 | p.buf.WriteRune('\n') 487 | } 488 | if p.Iframe { 489 | p.buf.WriteString("#EXT-X-I-FRAMES-ONLY\n") 490 | } 491 | // Widevine tags 492 | if p.WV != nil { 493 | if p.WV.AudioChannels != 0 { 494 | p.buf.WriteString("#WV-AUDIO-CHANNELS ") 495 | p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioChannels), 10)) 496 | p.buf.WriteRune('\n') 497 | } 498 | if p.WV.AudioFormat != 0 { 499 | p.buf.WriteString("#WV-AUDIO-FORMAT ") 500 | p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioFormat), 10)) 501 | p.buf.WriteRune('\n') 502 | } 503 | if p.WV.AudioProfileIDC != 0 { 504 | p.buf.WriteString("#WV-AUDIO-PROFILE-IDC ") 505 | p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioProfileIDC), 10)) 506 | p.buf.WriteRune('\n') 507 | } 508 | if p.WV.AudioSampleSize != 0 { 509 | p.buf.WriteString("#WV-AUDIO-SAMPLE-SIZE ") 510 | p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioSampleSize), 10)) 511 | p.buf.WriteRune('\n') 512 | } 513 | if p.WV.AudioSamplingFrequency != 0 { 514 | p.buf.WriteString("#WV-AUDIO-SAMPLING-FREQUENCY ") 515 | p.buf.WriteString(strconv.FormatUint(uint64(p.WV.AudioSamplingFrequency), 10)) 516 | p.buf.WriteRune('\n') 517 | } 518 | if p.WV.CypherVersion != "" { 519 | p.buf.WriteString("#WV-CYPHER-VERSION ") 520 | p.buf.WriteString(p.WV.CypherVersion) 521 | p.buf.WriteRune('\n') 522 | } 523 | if p.WV.ECM != "" { 524 | p.buf.WriteString("#WV-ECM ") 525 | p.buf.WriteString(p.WV.ECM) 526 | p.buf.WriteRune('\n') 527 | } 528 | if p.WV.VideoFormat != 0 { 529 | p.buf.WriteString("#WV-VIDEO-FORMAT ") 530 | p.buf.WriteString(strconv.FormatUint(uint64(p.WV.VideoFormat), 10)) 531 | p.buf.WriteRune('\n') 532 | } 533 | if p.WV.VideoFrameRate != 0 { 534 | p.buf.WriteString("#WV-VIDEO-FRAME-RATE ") 535 | p.buf.WriteString(strconv.FormatUint(uint64(p.WV.VideoFrameRate), 10)) 536 | p.buf.WriteRune('\n') 537 | } 538 | if p.WV.VideoLevelIDC != 0 { 539 | p.buf.WriteString("#WV-VIDEO-LEVEL-IDC") 540 | p.buf.WriteString(strconv.FormatUint(uint64(p.WV.VideoLevelIDC), 10)) 541 | p.buf.WriteRune('\n') 542 | } 543 | if p.WV.VideoProfileIDC != 0 { 544 | p.buf.WriteString("#WV-VIDEO-PROFILE-IDC ") 545 | p.buf.WriteString(strconv.FormatUint(uint64(p.WV.VideoProfileIDC), 10)) 546 | p.buf.WriteRune('\n') 547 | } 548 | if p.WV.VideoResolution != "" { 549 | p.buf.WriteString("#WV-VIDEO-RESOLUTION ") 550 | p.buf.WriteString(p.WV.VideoResolution) 551 | p.buf.WriteRune('\n') 552 | } 553 | if p.WV.VideoSAR != "" { 554 | p.buf.WriteString("#WV-VIDEO-SAR ") 555 | p.buf.WriteString(p.WV.VideoSAR) 556 | p.buf.WriteRune('\n') 557 | } 558 | } 559 | 560 | var ( 561 | seg *MediaSegment 562 | durationCache = make(map[float64]string) 563 | ) 564 | 565 | head := p.head 566 | count := p.count 567 | for i := uint(0); (i < p.winsize || p.winsize == 0) && count > 0; count-- { 568 | seg = p.Segments[head] 569 | head = (head + 1) % p.capacity 570 | if seg == nil { // protection from badly filled chunklists 571 | continue 572 | } 573 | if p.winsize > 0 { // skip for VOD playlists, where winsize = 0 574 | i++ 575 | } 576 | if seg.SCTE != nil { 577 | switch seg.SCTE.Syntax { 578 | case SCTE35_67_2014: 579 | p.buf.WriteString("#EXT-SCTE35:") 580 | p.buf.WriteString("CUE=\"") 581 | p.buf.WriteString(seg.SCTE.Cue) 582 | p.buf.WriteRune('"') 583 | if seg.SCTE.ID != "" { 584 | p.buf.WriteString(",ID=\"") 585 | p.buf.WriteString(seg.SCTE.ID) 586 | p.buf.WriteRune('"') 587 | } 588 | if seg.SCTE.Time != 0 { 589 | p.buf.WriteString(",TIME=") 590 | p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64)) 591 | } 592 | p.buf.WriteRune('\n') 593 | case SCTE35_OATCLS: 594 | switch seg.SCTE.CueType { 595 | case SCTE35Cue_Start: 596 | if seg.SCTE.Cue != "" { 597 | p.buf.WriteString("#EXT-OATCLS-SCTE35:") 598 | p.buf.WriteString(seg.SCTE.Cue) 599 | p.buf.WriteRune('\n') 600 | } 601 | p.buf.WriteString("#EXT-X-CUE-OUT:") 602 | p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64)) 603 | p.buf.WriteRune('\n') 604 | case SCTE35Cue_Mid: 605 | p.buf.WriteString("#EXT-X-CUE-OUT-CONT:") 606 | p.buf.WriteString("ElapsedTime=") 607 | p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Elapsed, 'f', -1, 64)) 608 | p.buf.WriteString(",Duration=") 609 | p.buf.WriteString(strconv.FormatFloat(seg.SCTE.Time, 'f', -1, 64)) 610 | p.buf.WriteString(",SCTE35=") 611 | p.buf.WriteString(seg.SCTE.Cue) 612 | p.buf.WriteRune('\n') 613 | case SCTE35Cue_End: 614 | p.buf.WriteString("#EXT-X-CUE-IN") 615 | p.buf.WriteRune('\n') 616 | } 617 | } 618 | } 619 | // check for key change 620 | if seg.Key != nil && p.Key != seg.Key { 621 | p.buf.WriteString("#EXT-X-KEY:") 622 | p.buf.WriteString("METHOD=") 623 | p.buf.WriteString(seg.Key.Method) 624 | if seg.Key.Method != "NONE" { 625 | p.buf.WriteString(",URI=\"") 626 | p.buf.WriteString(seg.Key.URI) 627 | p.buf.WriteRune('"') 628 | if seg.Key.IV != "" { 629 | p.buf.WriteString(",IV=") 630 | p.buf.WriteString(seg.Key.IV) 631 | } 632 | if seg.Key.Keyformat != "" { 633 | p.buf.WriteString(",KEYFORMAT=\"") 634 | p.buf.WriteString(seg.Key.Keyformat) 635 | p.buf.WriteRune('"') 636 | } 637 | if seg.Key.Keyformatversions != "" { 638 | p.buf.WriteString(",KEYFORMATVERSIONS=\"") 639 | p.buf.WriteString(seg.Key.Keyformatversions) 640 | p.buf.WriteRune('"') 641 | } 642 | } 643 | p.buf.WriteRune('\n') 644 | } 645 | if seg.Discontinuity { 646 | p.buf.WriteString("#EXT-X-DISCONTINUITY\n") 647 | } 648 | // ignore segment Map if default playlist Map is present 649 | if p.Map == nil && seg.Map != nil { 650 | p.buf.WriteString("#EXT-X-MAP:") 651 | p.buf.WriteString("URI=\"") 652 | p.buf.WriteString(seg.Map.URI) 653 | p.buf.WriteRune('"') 654 | if seg.Map.Limit > 0 { 655 | p.buf.WriteString(",BYTERANGE=") 656 | p.buf.WriteString(strconv.FormatInt(seg.Map.Limit, 10)) 657 | p.buf.WriteRune('@') 658 | p.buf.WriteString(strconv.FormatInt(seg.Map.Offset, 10)) 659 | } 660 | p.buf.WriteRune('\n') 661 | } 662 | if !seg.ProgramDateTime.IsZero() { 663 | p.buf.WriteString("#EXT-X-PROGRAM-DATE-TIME:") 664 | p.buf.WriteString(seg.ProgramDateTime.Format(DATETIME)) 665 | p.buf.WriteRune('\n') 666 | } 667 | if seg.Limit > 0 { 668 | p.buf.WriteString("#EXT-X-BYTERANGE:") 669 | p.buf.WriteString(strconv.FormatInt(seg.Limit, 10)) 670 | p.buf.WriteRune('@') 671 | p.buf.WriteString(strconv.FormatInt(seg.Offset, 10)) 672 | p.buf.WriteRune('\n') 673 | } 674 | 675 | // Add Custom Segment Tags here 676 | if seg.Custom != nil { 677 | for _, v := range seg.Custom { 678 | if customBuf := v.Encode(); customBuf != nil { 679 | p.buf.WriteString(customBuf.String()) 680 | p.buf.WriteRune('\n') 681 | } 682 | } 683 | } 684 | 685 | p.buf.WriteString("#EXTINF:") 686 | if str, ok := durationCache[seg.Duration]; ok { 687 | p.buf.WriteString(str) 688 | } else { 689 | if p.durationAsInt { 690 | // Old Android players has problems with non integer Duration. 691 | durationCache[seg.Duration] = strconv.FormatInt(int64(math.Ceil(seg.Duration)), 10) 692 | } else { 693 | // Wowza Mediaserver and some others prefer floats. 694 | durationCache[seg.Duration] = strconv.FormatFloat(seg.Duration, 'f', 3, 32) 695 | } 696 | p.buf.WriteString(durationCache[seg.Duration]) 697 | } 698 | p.buf.WriteRune(',') 699 | p.buf.WriteString(seg.Title) 700 | p.buf.WriteRune('\n') 701 | p.buf.WriteString(seg.URI) 702 | if p.Args != "" { 703 | p.buf.WriteRune('?') 704 | p.buf.WriteString(p.Args) 705 | } 706 | p.buf.WriteRune('\n') 707 | } 708 | if p.Closed { 709 | p.buf.WriteString("#EXT-X-ENDLIST\n") 710 | } 711 | return &p.buf 712 | } 713 | 714 | // String here for compatibility with Stringer interface For example 715 | // fmt.Printf("%s", sampleMediaList) will encode playist and print its 716 | // string representation. 717 | func (p *MediaPlaylist) String() string { 718 | return p.Encode().String() 719 | } 720 | 721 | // DurationAsInt represents the duration as the integer in encoded playlist. 722 | func (p *MediaPlaylist) DurationAsInt(yes bool) { 723 | if yes { 724 | // duration must be integers if protocol version is less than 3 725 | version(&p.ver, 3) 726 | } 727 | p.durationAsInt = yes 728 | } 729 | 730 | // Count tells us the number of items that are currently in the media 731 | // playlist. 732 | func (p *MediaPlaylist) Count() uint { 733 | return p.count 734 | } 735 | 736 | // Close sliding playlist and make them fixed. 737 | func (p *MediaPlaylist) Close() { 738 | if p.buf.Len() > 0 { 739 | p.buf.WriteString("#EXT-X-ENDLIST\n") 740 | } 741 | p.Closed = true 742 | } 743 | 744 | // SetDefaultKey sets encryption key appeared once in header of the 745 | // playlist (pointer to MediaPlaylist.Key). It useful when keys not 746 | // changed during playback. Set tag for the whole list. 747 | func (p *MediaPlaylist) SetDefaultKey(method, uri, iv, keyformat, keyformatversions string) error { 748 | // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it 749 | // contains: 750 | // - The KEYFORMAT and KEYFORMATVERSIONS attributes of the EXT-X-KEY tag. 751 | if keyformat != "" || keyformatversions != "" { 752 | version(&p.ver, 5) 753 | } 754 | p.Key = &Key{method, uri, iv, keyformat, keyformatversions} 755 | 756 | return nil 757 | } 758 | 759 | // SetDefaultMap sets default Media Initialization Section values for 760 | // playlist (pointer to MediaPlaylist.Map). Set EXT-X-MAP tag for the 761 | // whole playlist. 762 | func (p *MediaPlaylist) SetDefaultMap(uri string, limit, offset int64) { 763 | version(&p.ver, 5) // due section 4 764 | p.Map = &Map{uri, limit, offset} 765 | } 766 | 767 | // SetIframeOnly marks medialist as consists of only I-frames (Intra 768 | // frames). Set tag for the whole list. 769 | func (p *MediaPlaylist) SetIframeOnly() { 770 | version(&p.ver, 4) // due section 4.3.3 771 | p.Iframe = true 772 | } 773 | 774 | // SetKey sets encryption key for the current segment of media playlist 775 | // (pointer to Segment.Key). 776 | func (p *MediaPlaylist) SetKey(method, uri, iv, keyformat, keyformatversions string) error { 777 | if p.count == 0 { 778 | return errors.New("playlist is empty") 779 | } 780 | 781 | // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it 782 | // contains: 783 | // - The KEYFORMAT and KEYFORMATVERSIONS attributes of the EXT-X-KEY tag. 784 | if keyformat != "" || keyformatversions != "" { 785 | version(&p.ver, 5) 786 | } 787 | 788 | p.Segments[p.last()].Key = &Key{method, uri, iv, keyformat, keyformatversions} 789 | return nil 790 | } 791 | 792 | // SetMap sets map for the current segment of media playlist (pointer 793 | // to Segment.Map). 794 | func (p *MediaPlaylist) SetMap(uri string, limit, offset int64) error { 795 | if p.count == 0 { 796 | return errors.New("playlist is empty") 797 | } 798 | version(&p.ver, 5) // due section 4 799 | p.Segments[p.last()].Map = &Map{uri, limit, offset} 800 | return nil 801 | } 802 | 803 | // SetRange sets limit and offset for the current media segment 804 | // (EXT-X-BYTERANGE support for protocol version 4). 805 | func (p *MediaPlaylist) SetRange(limit, offset int64) error { 806 | if p.count == 0 { 807 | return errors.New("playlist is empty") 808 | } 809 | version(&p.ver, 4) // due section 3.4.1 810 | p.Segments[p.last()].Limit = limit 811 | p.Segments[p.last()].Offset = offset 812 | return nil 813 | } 814 | 815 | // SetSCTE sets the SCTE cue format for the current media segment. 816 | // 817 | // Deprecated: Use SetSCTE35 instead. 818 | func (p *MediaPlaylist) SetSCTE(cue string, id string, time float64) error { 819 | return p.SetSCTE35(&SCTE{Syntax: SCTE35_67_2014, Cue: cue, ID: id, Time: time}) 820 | } 821 | 822 | // SetSCTE35 sets the SCTE cue format for the current media segment 823 | func (p *MediaPlaylist) SetSCTE35(scte35 *SCTE) error { 824 | if p.count == 0 { 825 | return errors.New("playlist is empty") 826 | } 827 | p.Segments[p.last()].SCTE = scte35 828 | return nil 829 | } 830 | 831 | // SetDiscontinuity sets discontinuity flag for the current media 832 | // segment. EXT-X-DISCONTINUITY indicates an encoding discontinuity 833 | // between the media segment that follows it and the one that preceded 834 | // it (i.e. file format, number and type of tracks, encoding 835 | // parameters, encoding sequence, timestamp sequence). 836 | func (p *MediaPlaylist) SetDiscontinuity() error { 837 | if p.count == 0 { 838 | return errors.New("playlist is empty") 839 | } 840 | p.Segments[p.last()].Discontinuity = true 841 | return nil 842 | } 843 | 844 | // SetProgramDateTime sets program date and time for the current media 845 | // segment. EXT-X-PROGRAM-DATE-TIME tag associates the first sample of 846 | // a media segment with an absolute date and/or time. It applies only 847 | // to the current media segment. Date/time format is 848 | // YYYY-MM-DDThh:mm:ssZ (ISO8601) and includes time zone. 849 | func (p *MediaPlaylist) SetProgramDateTime(value time.Time) error { 850 | if p.count == 0 { 851 | return errors.New("playlist is empty") 852 | } 853 | p.Segments[p.last()].ProgramDateTime = value 854 | return nil 855 | } 856 | 857 | // SetCustomTag sets the provided tag on the media playlist for its 858 | // TagName. 859 | func (p *MediaPlaylist) SetCustomTag(tag CustomTag) { 860 | if p.Custom == nil { 861 | p.Custom = make(map[string]CustomTag) 862 | } 863 | 864 | p.Custom[tag.TagName()] = tag 865 | } 866 | 867 | // SetCustomSegmentTag sets the provided tag on the current media 868 | // segment for its TagName. 869 | func (p *MediaPlaylist) SetCustomSegmentTag(tag CustomTag) error { 870 | if p.count == 0 { 871 | return errors.New("playlist is empty") 872 | } 873 | 874 | last := p.Segments[p.last()] 875 | 876 | if last.Custom == nil { 877 | last.Custom = make(map[string]CustomTag) 878 | } 879 | 880 | last.Custom[tag.TagName()] = tag 881 | 882 | return nil 883 | } 884 | 885 | // Version returns the current playlist version number 886 | func (p *MediaPlaylist) Version() uint8 { 887 | return p.ver 888 | } 889 | 890 | // SetVersion sets the playlist version number, note the version maybe changed 891 | // automatically by other Set methods. 892 | func (p *MediaPlaylist) SetVersion(ver uint8) { 893 | p.ver = ver 894 | } 895 | 896 | // WinSize returns the playlist's window size. 897 | func (p *MediaPlaylist) WinSize() uint { 898 | return p.winsize 899 | } 900 | 901 | // SetWinSize overwrites the playlist's window size. 902 | func (p *MediaPlaylist) SetWinSize(winsize uint) error { 903 | if winsize > p.capacity { 904 | return errors.New("capacity must be greater than winsize or equal") 905 | } 906 | p.winsize = winsize 907 | return nil 908 | } 909 | 910 | // GetAllSegments could get all segments currently added to 911 | // playlist. 912 | func (p *MediaPlaylist) GetAllSegments() []*MediaSegment { 913 | if p.count == 0 { 914 | return nil 915 | } 916 | buf := make([]*MediaSegment, 0, p.count) 917 | if p.head < p.tail { 918 | for i := p.head; i < p.tail; i++ { 919 | buf = append(buf, p.Segments[i]) 920 | } 921 | return buf 922 | } 923 | for i := uint(0); i < p.tail; i++ { 924 | buf = append(buf, p.Segments[i]) 925 | } 926 | for i := p.head; i < p.capacity; i++ { 927 | buf = append(buf, p.Segments[i]) 928 | } 929 | return buf 930 | } 931 | --------------------------------------------------------------------------------