├── .github └── workflows │ ├── dependency-review.yml │ └── main.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── m3u8 ├── byteRange.go ├── codecs.go ├── common.go ├── dateRangeItem.go ├── discontinuityItem.go ├── encryptable.go ├── errors.go ├── keyItem.go ├── mapItem.go ├── mediaItem.go ├── playbackStart.go ├── playlist.go ├── playlistItem.go ├── reader.go ├── resolution.go ├── segmentItem.go ├── sessionDataItem.go ├── sessionKeyItem.go ├── tags.go ├── timeItem.go └── writer.go └── test ├── byteRange_test.go ├── common.go ├── common_test.go ├── dateRangeItem_test.go ├── discontinuityItem_test.go ├── fixtures ├── dateRangeScte35.m3u8 ├── encrypted.m3u8 ├── iframes.m3u8 ├── mapPlaylist.m3u8 ├── master.m3u8 ├── masterIframes.m3u8 ├── playlist-live.m3u8 ├── playlist.m3u8 ├── playlistWithComments.m3u8 ├── sessionData.m3u8 ├── timestampPlaylist.m3u8 ├── variantAngles.m3u8 └── variantAudio.m3u8 ├── keyItem_test.go ├── mapItem_test.go ├── mediaItem_test.go ├── playbackStart_test.go ├── playlistItem_test.go ├── playlist_test.go ├── reader_test.go ├── segmentItem_test.go ├── sessionDataItem_test.go ├── sessionKeyItem_test.go ├── timeItem_test.go └── writer_test.go /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v2 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the "master" branch 8 | push: 9 | branches: [ "master" ] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | # The type of runner that the job will run on 19 | runs-on: ubuntu-latest 20 | 21 | # Steps represent a sequence of tasks that will be executed as part of the job 22 | steps: 23 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 24 | - uses: actions/checkout@v3 25 | 26 | - uses: go-semantic-release/action@v1 27 | with: 28 | github-token: ${{ secrets.GITHUB_TOKEN }} 29 | changelog-file: CHANGELOG.md 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.13" 4 | 5 | before_install: 6 | - go get -t -v ./... 7 | 8 | script: 9 | - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./test/... 10 | 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tan Quang Ngo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/etherlabsio/go-m3u8.svg?branch=master)](https://travis-ci.org/etherlabsio/go-m3u8) 2 | [![codecov](https://codecov.io/gh/etherlabsio/go-m3u8/branch/master/graph/badge.svg)](https://codecov.io/gh/etherlabsio/go-m3u8) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/etherlabsio/go-m3u8)](https://goreportcard.com/report/github.com/etherlabsio/go-m3u8) 4 | [![GoDoc](https://godoc.org/github.com/etherlabsio/go-m3u8/m3u8?status.svg)](https://godoc.org/github.com/etherlabsio/go-m3u8/m3u8) 5 | 6 | # go-m3u8 7 | This is an actively maintained fork of go module https://github.com/quangngotan95/go-m3u8, initally intended for internal use only and later made public. 8 | 9 | Golang package for m3u8 (ported m3u8 gem https://github.com/sethdeckard/m3u8) 10 | 11 | `go-m3u8` provides easy generation and parsing of m3u8 playlists defined in the HTTP Live Streaming (HLS) Internet Draft published by Apple. 12 | * The library completely implements version 20 of the HLS Internet Draft. 13 | * Provides parsing of an m3u8 playlist into an object model from any File, io.Reader or string. 14 | * Provides ability to write playlist to a string via String() 15 | * Distinction between a master and media playlist is handled automatically (single Playlist class). 16 | * Optionally, the library can automatically generate the audio/video codecs string used in the CODEC attribute based on specified H.264, AAC, or MP3 options (such as Profile/Level). 17 | 18 | ## Installation 19 | `go get github.com/etherlabsio/go-m3u8` 20 | 21 | ## Usage (creating playlists) 22 | Create a master playlist and child playlists for adaptive bitrate streaming: 23 | ```go 24 | import ( 25 | "github.com/etherlabsio/go-m3u8/m3u8" 26 | "github.com/AlekSi/pointer" 27 | ) 28 | 29 | playlist := m3u8.NewPlaylist() 30 | ``` 31 | Create a new playlist item: 32 | ```go 33 | item := &m3u8.PlaylistItem{ 34 | Width: pointer.ToInt(1920), 35 | Height: pointer.ToInt(1080), 36 | Profile: pointer.ToString("high"), 37 | Level: pointer.ToString("4.1"), 38 | AudioCodec: pointer.ToString("aac-lc"), 39 | Bandwidth: 540, 40 | URI: "test.url", 41 | } 42 | playlist.AppendItem(item) 43 | ``` 44 | Add alternate audio, camera angles, closed captions and subtitles by creating MediaItem instances and adding them to the Playlist: 45 | ```go 46 | item := &m3u8.MediaItem{ 47 | Type: "AUDIO", 48 | GroupID: "audio-lo", 49 | Name: "Francais", 50 | Language: pointer.ToString("fre"), 51 | AssocLanguage: pointer.ToString("spoken"), 52 | AutoSelect: pointer.ToBool(true), 53 | Default: pointer.ToBool(false), 54 | Forced: pointer.ToBool(true), 55 | URI: pointer.ToString("frelo/prog_index.m3u8"), 56 | } 57 | playlist.AppendItem(item) 58 | ``` 59 | Create a standard playlist and add MPEG-TS segments via SegmentItem. You can also specify options for this type of playlist, however these options are ignored if playlist becomes a master playlist (anything but segments added): 60 | ```go 61 | playlist := &m3u8.Playlist{ 62 | Target: 12, 63 | Sequence: 1, 64 | Version: pointer.ToInt(1), 65 | Cache: pointer.ToBool(false), 66 | Items: []m3u8.Item{ 67 | &m3u8.SegmentItem{ 68 | Duration: 11, 69 | Segment: "test.ts", 70 | }, 71 | }, 72 | } 73 | ``` 74 | You can also access the playlist as a string: 75 | ```go 76 | var str string 77 | str = playlist.String() 78 | ... 79 | fmt.Print(playlist) 80 | ``` 81 | Alternatively you can set codecs rather than having it generated automatically: 82 | ```go 83 | item := &m3u8.PlaylistItem{ 84 | Width: pointer.ToInt(1920), 85 | Height: pointer.ToInt(1080), 86 | Codecs: pointer.ToString("avc1.66.30,mp4a.40.2"), 87 | Bandwidth: 540, 88 | URI: "test.url", 89 | } 90 | ``` 91 | 92 | ## Usage (parsing playlists) 93 | Parse from file 94 | ```go 95 | playlist, err := m3u8.ReadFile("path/to/file") 96 | ``` 97 | Read from string 98 | ```go 99 | playlist, err := m3u8.ReadString(string) 100 | ``` 101 | Read from generic `io.Reader` 102 | ```go 103 | playlist, err := m3u8.Read(reader) 104 | ``` 105 | 106 | Access items in playlist: 107 | ```go 108 | gore> playlist.Items[0] 109 | (*m3u8.SessionKeyItem)#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" 110 | gore> playlist.Items[1] 111 | (*m3u8.PlaybackStart)#EXT-X-START:TIME-OFFSET=20.2 112 | ``` 113 | 114 | ## Misc 115 | Codecs: 116 | * Values for audio_codec (codec name): aac-lc, he-aac, mp3 117 | * Values for profile (H.264 Profile): baseline, main, high. 118 | * Values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1. 119 | 120 | Not all Levels and Profiles can be combined and validation is not currently implemented, consult H.264 documentation for further details. 121 | 122 | ## Contributing 123 | 1. Fork it https://github.com/etherlabsio/go-m3u8/fork 124 | 2. Create your feature branch `git checkout -b my-new-feature` 125 | 3. Run tests `go test ./test/...`, make sure they all pass and new features are covered 126 | 4. Commit your changes `git commit -am "Add new features"` 127 | 5. Push to the branch `git push origin my-new-feature` 128 | 6. Create a new Pull Request 129 | 130 | ## License 131 | MIT License - See [LICENSE](https://github.com/etherlabsio/go-m3u8/blob/master/LICENSE) for details 132 | 133 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fetherlabsio%2Fgo-m3u8.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fetherlabsio%2Fgo-m3u8?ref=badge_large) 134 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/etherlabsio/go-m3u8 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/AlekSi/pointer v1.0.0 7 | github.com/stretchr/testify v1.3.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlekSi/pointer v1.0.0 h1:KWCWzsvFxNLcmM5XmiqHsGTTsuwZMsLFwWF9Y+//bNE= 2 | github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 9 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 10 | -------------------------------------------------------------------------------- /m3u8/byteRange.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // ByteRange represents sub range of a resource 10 | type ByteRange struct { 11 | Length *int 12 | Start *int 13 | } 14 | 15 | // NewByteRange parses a text line in playlist file and returns a *ByteRange 16 | func NewByteRange(text string) (*ByteRange, error) { 17 | if text == "" { 18 | return nil, nil 19 | } 20 | 21 | values := strings.Split(text, "@") 22 | 23 | lengthValue, err := strconv.Atoi(values[0]) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | br := ByteRange{Length: &lengthValue} 29 | 30 | if len(values) >= 2 { 31 | startValue, err := strconv.Atoi(values[1]) 32 | if err != nil { 33 | return &br, err 34 | } 35 | br.Start = &startValue 36 | } 37 | 38 | return &br, nil 39 | } 40 | 41 | func (br *ByteRange) String() string { 42 | if br.Start == nil { 43 | return fmt.Sprintf("%d", *br.Length) 44 | } 45 | 46 | return fmt.Sprintf("%d@%d", *br.Length, *br.Start) 47 | } 48 | -------------------------------------------------------------------------------- /m3u8/codecs.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import "strings" 4 | 5 | var ( 6 | // AudioCodecMap maps audio codec to representation 7 | AudioCodecMap = map[string]string{ 8 | "aac-lc": "mp4a.40.2", 9 | "he-aac": "mp4a.40.5", 10 | "mp3": "mp4a.40.34", 11 | } 12 | 13 | // BaselineCodecMap maps baseline profile with levels to representation 14 | BaselineCodecMap = map[string]string{ 15 | "3.0": "avc1.66.30", 16 | "3.1": "avc1.42001f", 17 | } 18 | 19 | // MainCodecMap maps main profile with levels to representation 20 | MainCodecMap = map[string]string{ 21 | "3.0": "avc1.77.30", 22 | "3.1": "avc1.4d001f", 23 | "4.0": "avc1.4d0028", 24 | "4.1": "avc1.4d0029", 25 | } 26 | 27 | // HighCodecMap maps high profile with levels to representation 28 | HighCodecMap = map[string]string{ 29 | "3.0": "avc1.64001e", 30 | "3.1": "avc1.64001f", 31 | "3.2": "avc1.640020", 32 | "4.0": "avc1.640028", 33 | "4.1": "avc1.640029", 34 | "4.2": "avc1.64002a", 35 | "5.0": "avc1.640032", 36 | "5.1": "avc1.640033", 37 | "5.2": "avc1.640034", 38 | } 39 | ) 40 | 41 | func audioCodec(codec *string) *string { 42 | if codec == nil { 43 | return nil 44 | } 45 | 46 | key := strings.ToLower(*codec) 47 | value, ok := AudioCodecMap[key] 48 | 49 | if !ok { 50 | return nil 51 | } 52 | 53 | return &value 54 | } 55 | 56 | func videoCodec(profile *string, level *string) *string { 57 | if profile == nil || level == nil { 58 | return nil 59 | } 60 | 61 | var value string 62 | var ok bool 63 | 64 | switch *profile { 65 | case "baseline": 66 | value, ok = BaselineCodecMap[*level] 67 | case "main": 68 | value, ok = MainCodecMap[*level] 69 | case "high": 70 | value, ok = HighCodecMap[*level] 71 | } 72 | 73 | if !ok { 74 | return nil 75 | } 76 | 77 | return &value 78 | } 79 | -------------------------------------------------------------------------------- /m3u8/common.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | quotedFormatString = `%s="%v"` 11 | formatString = `%s=%v` 12 | frameRateFormatString = `%s=%.3f` 13 | ) 14 | 15 | var ( 16 | parseRegex = regexp.MustCompile(`([A-z0-9-]+)\s*=\s*("[^"]*"|[^,]*)`) 17 | ) 18 | 19 | // ParseAttributes parses a text line in playlist and returns an attributes map 20 | func ParseAttributes(text string) map[string]string { 21 | res := make(map[string]string) 22 | value := strings.Replace(text, "\n", "", -1) 23 | matches := parseRegex.FindAllStringSubmatch(value, -1) 24 | for _, match := range matches { 25 | if len(match) >= 3 { 26 | key := match[1] 27 | value := strings.Replace(match[2], `"`, "", -1) 28 | res[key] = value 29 | } 30 | } 31 | 32 | return res 33 | } 34 | 35 | func parseFloat(attributes map[string]string, key string) (*float64, error) { 36 | stringValue, ok := attributes[key] 37 | if !ok { 38 | return nil, nil 39 | } 40 | 41 | value, err := strconv.ParseFloat(stringValue, 64) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &value, nil 47 | } 48 | 49 | func parseInt(attributes map[string]string, key string) (*int, error) { 50 | stringValue, ok := attributes[key] 51 | if !ok { 52 | return nil, nil 53 | } 54 | 55 | int64Value, err := strconv.ParseInt(stringValue, 0, 0) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | value := int(int64Value) 61 | 62 | return &value, nil 63 | } 64 | 65 | func parseYesNo(attributes map[string]string, key string) *bool { 66 | stringValue, ok := attributes[key] 67 | 68 | if !ok { 69 | return nil 70 | } 71 | 72 | val := false 73 | 74 | if stringValue == YesValue { 75 | val = true 76 | } 77 | 78 | return &val 79 | } 80 | 81 | func formatYesNo(value bool) string { 82 | if value { 83 | return YesValue 84 | } 85 | 86 | return NoValue 87 | } 88 | 89 | func attributeExists(key string, attributes map[string]string) bool { 90 | _, ok := attributes[key] 91 | return ok 92 | } 93 | 94 | func pointerTo(attributes map[string]string, key string) *string { 95 | value, ok := attributes[key] 96 | 97 | if !ok { 98 | return nil 99 | } 100 | 101 | return &value 102 | } 103 | -------------------------------------------------------------------------------- /m3u8/dateRangeItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // DateRangeItem represents a #EXT-X-DATERANGE tag 10 | type DateRangeItem struct { 11 | ID string 12 | Class *string 13 | StartDate string 14 | EndDate *string 15 | Duration *float64 16 | PlannedDuration *float64 17 | Scte35Cmd *string 18 | Scte35Out *string 19 | Scte35In *string 20 | EndOnNext bool 21 | ClientAttributes map[string]string 22 | } 23 | 24 | // NewDateRangeItem parses a text line in playlist and returns a *DateRangeItem 25 | func NewDateRangeItem(text string) (*DateRangeItem, error) { 26 | attributes := ParseAttributes(text) 27 | duration, err := parseFloat(attributes, DurationTag) 28 | if err != nil { 29 | return nil, err 30 | } 31 | plannedDuartion, err := parseFloat(attributes, PlannedDurationTag) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return &DateRangeItem{ 37 | ID: attributes[IDTag], 38 | Class: pointerTo(attributes, ClassTag), 39 | StartDate: attributes[StartDateTag], 40 | EndDate: pointerTo(attributes, EndDateTag), 41 | Duration: duration, 42 | PlannedDuration: plannedDuartion, 43 | Scte35Cmd: pointerTo(attributes, Scte35CmdTag), 44 | Scte35Out: pointerTo(attributes, Scte35OutTag), 45 | Scte35In: pointerTo(attributes, Scte35InTag), 46 | EndOnNext: attributeExists(EndOnNextTag, attributes), 47 | ClientAttributes: parseClientAttributes(attributes), 48 | }, nil 49 | } 50 | 51 | func (dri *DateRangeItem) String() string { 52 | var slice []string 53 | 54 | slice = append(slice, fmt.Sprintf(quotedFormatString, IDTag, dri.ID)) 55 | if dri.Class != nil { 56 | slice = append(slice, fmt.Sprintf(quotedFormatString, ClassTag, *dri.Class)) 57 | } 58 | slice = append(slice, fmt.Sprintf(quotedFormatString, StartDateTag, dri.StartDate)) 59 | if dri.EndDate != nil { 60 | slice = append(slice, fmt.Sprintf(quotedFormatString, EndDateTag, *dri.EndDate)) 61 | } 62 | if dri.Duration != nil { 63 | slice = append(slice, fmt.Sprintf(formatString, DurationTag, *dri.Duration)) 64 | } 65 | if dri.PlannedDuration != nil { 66 | slice = append(slice, fmt.Sprintf(formatString, PlannedDurationTag, *dri.PlannedDuration)) 67 | } 68 | clientAttributes := formatClientAttributes(dri.ClientAttributes) 69 | slice = append(slice, clientAttributes...) 70 | 71 | if dri.Scte35Cmd != nil { 72 | slice = append(slice, fmt.Sprintf(formatString, Scte35CmdTag, *dri.Scte35Cmd)) 73 | } 74 | if dri.Scte35Out != nil { 75 | slice = append(slice, fmt.Sprintf(formatString, Scte35OutTag, *dri.Scte35Out)) 76 | } 77 | if dri.Scte35In != nil { 78 | slice = append(slice, fmt.Sprintf(formatString, Scte35InTag, *dri.Scte35In)) 79 | } 80 | if dri.EndOnNext { 81 | slice = append(slice, fmt.Sprintf(`%s=YES`, EndOnNextTag)) 82 | } 83 | 84 | return fmt.Sprintf("%s:%s", DateRangeItemTag, strings.Join(slice, ",")) 85 | } 86 | 87 | func parseClientAttributes(attributes map[string]string) map[string]string { 88 | result := make(map[string]string) 89 | hasCA := false 90 | 91 | for key, value := range attributes { 92 | if strings.HasPrefix(key, "X-") { 93 | result[key] = value 94 | hasCA = true 95 | } 96 | } 97 | 98 | if hasCA { 99 | return result 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func formatClientAttributes(ca map[string]string) []string { 106 | if ca == nil { 107 | return nil 108 | } 109 | 110 | var slice []string 111 | 112 | for key, value := range ca { 113 | formatString := `%s=%s` 114 | _, err := strconv.ParseFloat(value, 64) 115 | if err != nil { 116 | formatString = `%s="%s"` 117 | } 118 | slice = append(slice, fmt.Sprintf(formatString, key, value)) 119 | } 120 | 121 | return slice 122 | } 123 | -------------------------------------------------------------------------------- /m3u8/discontinuityItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import "fmt" 4 | 5 | // DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a 6 | // discontinuity between the SegmentItems that proceed and follow it. 7 | type DiscontinuityItem struct{} 8 | 9 | // NewDiscontinuityItem returns a *DiscontinuityItem 10 | func NewDiscontinuityItem() (*DiscontinuityItem, error) { 11 | return &DiscontinuityItem{}, nil 12 | } 13 | 14 | func (di *DiscontinuityItem) String() string { 15 | return fmt.Sprintf("%s\n", DiscontinuityItemTag) 16 | } 17 | -------------------------------------------------------------------------------- /m3u8/encryptable.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Encryptable is common representation for KeyItem and SessionKeyItem 9 | type Encryptable struct { 10 | Method string 11 | URI *string 12 | IV *string 13 | KeyFormat *string 14 | KeyFormatVersions *string 15 | } 16 | 17 | // NewEncryptable takes an attributes map and returns an *Encryptable 18 | func NewEncryptable(attributes map[string]string) *Encryptable { 19 | return &Encryptable{ 20 | Method: attributes[MethodTag], 21 | URI: pointerTo(attributes, URITag), 22 | IV: pointerTo(attributes, IVTag), 23 | KeyFormat: pointerTo(attributes, KeyFormatTag), 24 | KeyFormatVersions: pointerTo(attributes, KeyFormatVersionsTag), 25 | } 26 | } 27 | 28 | func (e *Encryptable) String() string { 29 | var slice []string 30 | 31 | slice = append(slice, fmt.Sprintf(formatString, MethodTag, e.Method)) 32 | if e.URI != nil { 33 | slice = append(slice, fmt.Sprintf(quotedFormatString, URITag, *e.URI)) 34 | } 35 | if e.IV != nil { 36 | slice = append(slice, fmt.Sprintf(formatString, IVTag, *e.IV)) 37 | } 38 | if e.KeyFormat != nil { 39 | slice = append(slice, fmt.Sprintf(quotedFormatString, KeyFormatTag, *e.KeyFormat)) 40 | } 41 | if e.KeyFormatVersions != nil { 42 | slice = append(slice, fmt.Sprintf(quotedFormatString, KeyFormatVersionsTag, *e.KeyFormatVersions)) 43 | } 44 | 45 | return strings.Join(slice, ",") 46 | } 47 | -------------------------------------------------------------------------------- /m3u8/errors.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrPlaylistInvalid represents playlist error when playlist does not start with #EXTM3U 7 | ErrPlaylistInvalid = errors.New("invalid playlist, must start with #EXTM3U") 8 | 9 | // ErrPlaylistInvalidType represents playlist error when it's mixed between master and media playlist 10 | ErrPlaylistInvalidType = errors.New("invalid playlist, mixed master and media") 11 | 12 | // ErrResolutionInvalid represents error when a resolution is invalid 13 | ErrResolutionInvalid = errors.New("invalid resolution") 14 | 15 | // ErrBandwidthMissing represents error when a segment does not have bandwidth 16 | ErrBandwidthMissing = errors.New("missing bandwidth") 17 | 18 | // ErrBandwidthInvalid represents error when a bandwidth is invalid 19 | ErrBandwidthInvalid = errors.New("invalid bandwidth") 20 | 21 | // ErrSegmentItemInvalid represents error when a segment item is invalid 22 | ErrSegmentItemInvalid = errors.New("invalid segment item") 23 | 24 | // ErrPlaylistItemInvalid represents error when a playlist item is invalid 25 | ErrPlaylistItemInvalid = errors.New("invalid playlist item") 26 | ) 27 | -------------------------------------------------------------------------------- /m3u8/keyItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import "fmt" 4 | 5 | // KeyItem represents a set of EXT-X-KEY attributes 6 | type KeyItem struct { 7 | Encryptable *Encryptable 8 | } 9 | 10 | // NewKeyItem parses a text line and returns a *KeyItem 11 | func NewKeyItem(text string) (*KeyItem, error) { 12 | attributes := ParseAttributes(text) 13 | return &KeyItem{ 14 | Encryptable: NewEncryptable(attributes), 15 | }, nil 16 | } 17 | 18 | func (ki *KeyItem) String() string { 19 | return fmt.Sprintf("%s:%v", KeyItemTag, ki.Encryptable.String()) 20 | } 21 | -------------------------------------------------------------------------------- /m3u8/mapItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import "fmt" 4 | 5 | // MapItem represents a EXT-X-MAP tag which specifies how to obtain the Media 6 | // Initialization Section 7 | type MapItem struct { 8 | URI string 9 | ByteRange *ByteRange 10 | } 11 | 12 | // NewMapItem parses a text line and returns a *MapItem 13 | func NewMapItem(text string) (*MapItem, error) { 14 | attributes := ParseAttributes(text) 15 | 16 | br, err := NewByteRange(attributes[ByteRangeTag]) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return &MapItem{ 22 | URI: attributes[URITag], 23 | ByteRange: br, 24 | }, nil 25 | } 26 | 27 | func (mi *MapItem) String() string { 28 | if mi.ByteRange == nil { 29 | return fmt.Sprintf(`%s:%s="%s"`, MapItemTag, URITag, mi.URI) 30 | } 31 | 32 | return fmt.Sprintf(`%s:%s="%s",%s="%v"`, MapItemTag, URITag, mi.URI, ByteRangeTag, mi.ByteRange) 33 | } 34 | -------------------------------------------------------------------------------- /m3u8/mediaItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // MediaItem represents a set of EXT-X-MEDIA attributes 9 | type MediaItem struct { 10 | Type string 11 | GroupID string 12 | Name string 13 | Language *string 14 | AssocLanguage *string 15 | AutoSelect *bool 16 | Default *bool 17 | Forced *bool 18 | URI *string 19 | InStreamID *string 20 | Characteristics *string 21 | Channels *string 22 | StableRenditionId *string 23 | } 24 | 25 | // NewMediaItem parses a text line and returns a *MediaItem 26 | func NewMediaItem(text string) (*MediaItem, error) { 27 | attributes := ParseAttributes(text) 28 | 29 | return &MediaItem{ 30 | Type: attributes[TypeTag], 31 | GroupID: attributes[GroupIDTag], 32 | Name: attributes[NameTag], 33 | Language: pointerTo(attributes, LanguageTag), 34 | AssocLanguage: pointerTo(attributes, AssocLanguageTag), 35 | AutoSelect: parseYesNo(attributes, AutoSelectTag), 36 | Default: parseYesNo(attributes, DefaultTag), 37 | Forced: parseYesNo(attributes, ForcedTag), 38 | URI: pointerTo(attributes, URITag), 39 | InStreamID: pointerTo(attributes, InStreamIDTag), 40 | Characteristics: pointerTo(attributes, CharacteristicsTag), 41 | Channels: pointerTo(attributes, ChannelsTag), 42 | StableRenditionId: pointerTo(attributes, StableRenditionIDTag), 43 | }, nil 44 | } 45 | 46 | func (mi *MediaItem) String() string { 47 | slice := []string{ 48 | fmt.Sprintf(formatString, TypeTag, mi.Type), 49 | fmt.Sprintf(quotedFormatString, GroupIDTag, mi.GroupID), 50 | } 51 | 52 | if mi.Language != nil { 53 | slice = append(slice, fmt.Sprintf(quotedFormatString, LanguageTag, *mi.Language)) 54 | } 55 | if mi.AssocLanguage != nil { 56 | slice = append(slice, fmt.Sprintf(quotedFormatString, AssocLanguageTag, *mi.AssocLanguage)) 57 | } 58 | slice = append(slice, fmt.Sprintf(quotedFormatString, NameTag, mi.Name)) 59 | if mi.AutoSelect != nil { 60 | slice = append(slice, fmt.Sprintf(formatString, AutoSelectTag, formatYesNo(*mi.AutoSelect))) 61 | } 62 | if mi.Default != nil { 63 | slice = append(slice, fmt.Sprintf(formatString, DefaultTag, formatYesNo(*mi.Default))) 64 | } 65 | if mi.URI != nil { 66 | slice = append(slice, fmt.Sprintf(quotedFormatString, URITag, *mi.URI)) 67 | } 68 | if mi.Forced != nil { 69 | slice = append(slice, fmt.Sprintf(formatString, ForcedTag, formatYesNo(*mi.Forced))) 70 | } 71 | if mi.InStreamID != nil { 72 | slice = append(slice, fmt.Sprintf(quotedFormatString, InStreamIDTag, *mi.InStreamID)) 73 | } 74 | if mi.Characteristics != nil { 75 | slice = append(slice, fmt.Sprintf(quotedFormatString, CharacteristicsTag, *mi.Characteristics)) 76 | } 77 | if mi.Channels != nil { 78 | slice = append(slice, fmt.Sprintf(quotedFormatString, ChannelsTag, *mi.Channels)) 79 | } 80 | if mi.StableRenditionId != nil { 81 | slice = append(slice, fmt.Sprintf(quotedFormatString, StableRenditionIDTag, *mi.StableRenditionId)) 82 | } 83 | 84 | return fmt.Sprintf("%s:%s", MediaItemTag, strings.Join(slice, ",")) 85 | } 86 | -------------------------------------------------------------------------------- /m3u8/playbackStart.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // PlaybackStart represents a #EXT-X-START tag and attributes 10 | type PlaybackStart struct { 11 | TimeOffset float64 12 | Precise *bool 13 | } 14 | 15 | // NewPlaybackStart parses a text line and returns a *PlaybackStart 16 | func NewPlaybackStart(text string) (*PlaybackStart, error) { 17 | attributes := ParseAttributes(text) 18 | 19 | timeOffset, err := strconv.ParseFloat(attributes[TimeOffsetTag], 64) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return &PlaybackStart{ 25 | TimeOffset: timeOffset, 26 | Precise: parseYesNo(attributes, PreciseTag), 27 | }, nil 28 | } 29 | 30 | func (ps *PlaybackStart) String() string { 31 | slice := []string{fmt.Sprintf(formatString, TimeOffsetTag, ps.TimeOffset)} 32 | if ps.Precise != nil { 33 | slice = append(slice, fmt.Sprintf(formatString, PreciseTag, formatYesNo(*ps.Precise))) 34 | } 35 | 36 | return fmt.Sprintf(`%s:%s`, PlaybackStartTag, strings.Join(slice, ",")) 37 | } 38 | -------------------------------------------------------------------------------- /m3u8/playlist.go: -------------------------------------------------------------------------------- 1 | // Package m3u8 provides utilities for parsing and generating m3u8 playlists 2 | package m3u8 3 | 4 | // Item represents an item in a playlist 5 | type Item interface { 6 | String() string 7 | } 8 | 9 | // Playlist represents an m3u8 playlist, it can be a master playlist or a set 10 | // of media segments 11 | type Playlist struct { 12 | Items []Item 13 | Version *int 14 | Cache *bool 15 | Target int 16 | Sequence int 17 | DiscontinuitySequence *int 18 | Type *string 19 | IFramesOnly bool 20 | IndependentSegments bool 21 | Live bool 22 | Master *bool 23 | } 24 | 25 | func (pl *Playlist) String() string { 26 | s, err := Write(pl) 27 | if err != nil { 28 | return "" 29 | } 30 | 31 | return s 32 | } 33 | 34 | // NewPlaylist returns a playlist with default target 10 35 | func NewPlaylist() *Playlist { 36 | return &Playlist{ 37 | Target: 10, 38 | Live: true, 39 | } 40 | } 41 | 42 | // NewPlaylistWithItems returns a playlist with a list of items 43 | func NewPlaylistWithItems(items []Item) *Playlist { 44 | return &Playlist{ 45 | Target: 10, 46 | Items: items, 47 | } 48 | } 49 | 50 | // AppendItem appends an item to the playlist 51 | func (pl *Playlist) AppendItem(item Item) { 52 | pl.Items = append(pl.Items, item) 53 | } 54 | 55 | // IsLive checks if playlist is live (not vod) 56 | func (pl *Playlist) IsLive() bool { 57 | if pl.IsMaster() { 58 | return false 59 | } 60 | 61 | return pl.Live 62 | } 63 | 64 | // IsMaster checks if a playlist is a master playlist 65 | func (pl *Playlist) IsMaster() bool { 66 | if pl.Master != nil { 67 | return *pl.Master 68 | } 69 | 70 | plSize := pl.PlaylistSize() 71 | smSize := pl.SegmentSize() 72 | if plSize <= 0 && smSize <= 0 { 73 | return false 74 | } 75 | 76 | return plSize > 0 77 | } 78 | 79 | // PlaylistSize returns number of playlist items in a playlist 80 | func (pl *Playlist) PlaylistSize() int { 81 | result := 0 82 | 83 | for _, item := range pl.Items { 84 | if _, ok := item.(*PlaylistItem); ok { 85 | result++ 86 | } 87 | } 88 | 89 | return result 90 | } 91 | 92 | // Playlists returns list of segment items in a playlist 93 | func (pl *Playlist) Playlists() []*PlaylistItem { 94 | var p []*PlaylistItem 95 | for _, i := range pl.Items { 96 | if pi, ok := i.(*PlaylistItem); ok { 97 | p = append(p, pi) 98 | } 99 | } 100 | return p 101 | } 102 | 103 | // SegmentSize returns number of segment items in a playlist 104 | func (pl *Playlist) SegmentSize() int { 105 | result := 0 106 | 107 | for _, item := range pl.Items { 108 | if _, ok := (item).(*SegmentItem); ok { 109 | result++ 110 | } 111 | } 112 | 113 | return result 114 | } 115 | 116 | // Segments returns list of segment items in a playlist 117 | func (pl *Playlist) Segments() []*SegmentItem { 118 | var s []*SegmentItem 119 | for _, i := range pl.Items { 120 | if si, ok := i.(*SegmentItem); ok { 121 | s = append(s, si) 122 | } 123 | } 124 | return s 125 | } 126 | 127 | // ItemSize returns number of items in a playlist 128 | func (pl *Playlist) ItemSize() int { 129 | return len(pl.Items) 130 | } 131 | 132 | // IsValid checks if a playlist is valid or not 133 | func (pl *Playlist) IsValid() bool { 134 | return !(pl.PlaylistSize() > 0 && pl.SegmentSize() > 0) 135 | } 136 | 137 | // Duration returns duration of a media playlist 138 | func (pl *Playlist) Duration() float64 { 139 | duration := 0.0 140 | 141 | for _, item := range pl.Items { 142 | if segmentItem, ok := item.(*SegmentItem); ok { 143 | duration += segmentItem.Duration 144 | } 145 | } 146 | 147 | return duration 148 | } 149 | -------------------------------------------------------------------------------- /m3u8/playlistItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // PlaylistItem represents a set of EXT-X-STREAM-INF or 10 | // EXT-X-I-FRAME-STREAM-INF attributes 11 | type PlaylistItem struct { 12 | Bandwidth int 13 | URI string 14 | IFrame bool 15 | 16 | Name *string 17 | Width *int 18 | Height *int 19 | AverageBandwidth *int 20 | ProgramID *string 21 | Codecs *string 22 | AudioCodec *string 23 | Profile *string 24 | Level *string 25 | Video *string 26 | Audio *string 27 | Subtitles *string 28 | ClosedCaptions *string 29 | FrameRate *float64 30 | HDCPLevel *string 31 | Resolution *Resolution 32 | StableVariantID *string 33 | } 34 | 35 | // NewPlaylistItem parses a text line and returns a *PlaylistItem 36 | func NewPlaylistItem(text string) (*PlaylistItem, error) { 37 | attributes := ParseAttributes(text) 38 | 39 | resolution, err := parseResolution(attributes, ResolutionTag) 40 | if err != nil { 41 | return nil, err 42 | } 43 | var width, height *int 44 | if resolution != nil { 45 | width = &resolution.Width 46 | height = &resolution.Height 47 | } 48 | 49 | averageBandwidth, err := parseInt(attributes, AverageBandwidthTag) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | frameRate, err := parseFloat(attributes, FrameRateTag) 55 | if err != nil { 56 | return nil, err 57 | } 58 | if frameRate != nil && *frameRate <= 0 { 59 | frameRate = nil 60 | } 61 | 62 | bandwidth, err := parseBandwidth(attributes, BandwidthTag) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | return &PlaylistItem{ 68 | ProgramID: pointerTo(attributes, ProgramIDTag), 69 | Codecs: pointerTo(attributes, CodecsTag), 70 | Width: width, 71 | Height: height, 72 | Bandwidth: bandwidth, 73 | AverageBandwidth: averageBandwidth, 74 | FrameRate: frameRate, 75 | Video: pointerTo(attributes, VideoTag), 76 | Audio: pointerTo(attributes, AudioTag), 77 | URI: attributes[URITag], 78 | Subtitles: pointerTo(attributes, SubtitlesTag), 79 | ClosedCaptions: pointerTo(attributes, ClosedCaptionsTag), 80 | Name: pointerTo(attributes, NameTag), 81 | HDCPLevel: pointerTo(attributes, HDCPLevelTag), 82 | Resolution: resolution, 83 | StableVariantID: pointerTo(attributes, StableVariantIDTag), 84 | }, nil 85 | } 86 | 87 | func (pi *PlaylistItem) String() string { 88 | var slice []string 89 | // Check resolution 90 | if pi.Resolution == nil && pi.Width != nil && pi.Height != nil { 91 | r := &Resolution{ 92 | Width: *pi.Width, 93 | Height: *pi.Height, 94 | } 95 | pi.Resolution = r 96 | } 97 | if pi.ProgramID != nil { 98 | slice = append(slice, fmt.Sprintf(formatString, ProgramIDTag, *pi.ProgramID)) 99 | } 100 | if pi.Resolution != nil { 101 | slice = append(slice, fmt.Sprintf(formatString, ResolutionTag, pi.Resolution.String())) 102 | } 103 | codecs := formatCodecs(pi) 104 | if codecs != nil { 105 | slice = append(slice, fmt.Sprintf(quotedFormatString, CodecsTag, *codecs)) 106 | } 107 | slice = append(slice, fmt.Sprintf(formatString, BandwidthTag, pi.Bandwidth)) 108 | if pi.AverageBandwidth != nil { 109 | slice = append(slice, fmt.Sprintf(formatString, AverageBandwidthTag, *pi.AverageBandwidth)) 110 | } 111 | if pi.FrameRate != nil { 112 | slice = append(slice, fmt.Sprintf(frameRateFormatString, FrameRateTag, *pi.FrameRate)) 113 | } 114 | if pi.HDCPLevel != nil { 115 | slice = append(slice, fmt.Sprintf(formatString, HDCPLevelTag, *pi.HDCPLevel)) 116 | } 117 | if pi.Audio != nil { 118 | slice = append(slice, fmt.Sprintf(quotedFormatString, AudioTag, *pi.Audio)) 119 | } 120 | if pi.Video != nil { 121 | slice = append(slice, fmt.Sprintf(quotedFormatString, VideoTag, *pi.Video)) 122 | } 123 | if pi.Subtitles != nil { 124 | slice = append(slice, fmt.Sprintf(quotedFormatString, SubtitlesTag, *pi.Subtitles)) 125 | } 126 | if pi.ClosedCaptions != nil { 127 | cc := *pi.ClosedCaptions 128 | fs := quotedFormatString 129 | if cc == NoneValue { 130 | fs = formatString 131 | } 132 | slice = append(slice, fmt.Sprintf(fs, ClosedCaptionsTag, cc)) 133 | } 134 | if pi.Name != nil { 135 | slice = append(slice, fmt.Sprintf(quotedFormatString, NameTag, *pi.Name)) 136 | } 137 | if pi.StableVariantID != nil { 138 | slice = append(slice, fmt.Sprintf(quotedFormatString, StableVariantIDTag, *pi.StableVariantID)) 139 | } 140 | 141 | attributesString := strings.Join(slice, ",") 142 | 143 | if pi.IFrame { 144 | return fmt.Sprintf(`%s:%s,%s="%s"`, PlaylistIframeTag, attributesString, URITag, pi.URI) 145 | } 146 | 147 | return fmt.Sprintf("%s:%s\n%s", PlaylistItemTag, attributesString, pi.URI) 148 | } 149 | 150 | // CodecsString returns the string representation of codecs for a playlist item 151 | func (pi *PlaylistItem) CodecsString() string { 152 | codecsPtr := formatCodecs(pi) 153 | if codecsPtr == nil { 154 | return "" 155 | } 156 | 157 | return *codecsPtr 158 | } 159 | 160 | func formatCodecs(pi *PlaylistItem) *string { 161 | if pi.Codecs != nil { 162 | return pi.Codecs 163 | } 164 | 165 | videoCodecPtr := videoCodec(pi.Profile, pi.Level) 166 | // profile or level were specified but not recognized any codecs 167 | if !(pi.Profile == nil && pi.Level == nil) && videoCodecPtr == nil { 168 | return nil 169 | } 170 | 171 | audioCodecPtr := audioCodec(pi.AudioCodec) 172 | // audio codec was specified but not recognized 173 | if !(pi.AudioCodec == nil) && audioCodecPtr == nil { 174 | return nil 175 | } 176 | 177 | var slice []string 178 | if videoCodecPtr != nil { 179 | slice = append(slice, *videoCodecPtr) 180 | } 181 | if audioCodecPtr != nil { 182 | slice = append(slice, *audioCodecPtr) 183 | } 184 | 185 | if len(slice) <= 0 { 186 | return nil 187 | } 188 | 189 | value := strings.Join(slice, ",") 190 | return &value 191 | } 192 | 193 | func parseBandwidth(attributes map[string]string, key string) (int, error) { 194 | bw, ok := attributes[key] 195 | if !ok { 196 | return 0, ErrBandwidthMissing 197 | } 198 | 199 | bandwidth, err := strconv.ParseInt(bw, 0, 0) 200 | if err != nil { 201 | return 0, ErrBandwidthInvalid 202 | } 203 | 204 | return int(bandwidth), nil 205 | } 206 | 207 | func parseResolution(attributes map[string]string, key string) (*Resolution, error) { 208 | resolution, ok := attributes[key] 209 | if !ok { 210 | return nil, nil 211 | } 212 | 213 | return NewResolution(resolution) 214 | } 215 | -------------------------------------------------------------------------------- /m3u8/reader.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "strings" 9 | ) 10 | 11 | type state struct { 12 | open bool 13 | currentItem Item 14 | master bool 15 | } 16 | 17 | // ReadString parses a text string and returns a playlist 18 | func ReadString(text string) (*Playlist, error) { 19 | return Read(strings.NewReader(text)) 20 | } 21 | 22 | // ReadFile reads text from a file and returns a playlist 23 | func ReadFile(path string) (*Playlist, error) { 24 | f, err := ioutil.ReadFile(path) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return Read(bytes.NewReader(f)) 29 | } 30 | 31 | // Read reads text from an io.Reader and returns a playlist 32 | func Read(reader io.Reader) (*Playlist, error) { 33 | var buf bytes.Buffer 34 | _, err := buf.ReadFrom(reader) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | pl := NewPlaylist() 40 | st := &state{} 41 | eof := false 42 | header := true 43 | 44 | for !eof { 45 | line, err := buf.ReadString('\n') 46 | if err == io.EOF { 47 | eof = true 48 | } else if err != nil { 49 | return nil, err 50 | } 51 | 52 | value := strings.TrimSpace(line) 53 | if header && value != HeaderTag { 54 | return nil, ErrPlaylistInvalid 55 | } 56 | 57 | if err := parseLine(value, pl, st); err != nil { 58 | return nil, err 59 | } 60 | 61 | header = false 62 | } 63 | 64 | return pl, nil 65 | } 66 | 67 | func parseLine(line string, pl *Playlist, st *state) error { 68 | var err error 69 | switch { 70 | // basic tags 71 | case matchTag(line, VersionTag): 72 | pl.Version, err = parseIntPtr(line, VersionTag) 73 | // media segment tags 74 | case matchTag(line, SegmentItemTag): 75 | st.currentItem, err = NewSegmentItem(line) 76 | st.master = false 77 | st.open = true 78 | case matchTag(line, DiscontinuityItemTag): 79 | st.master = false 80 | st.open = false 81 | item, err := NewDiscontinuityItem() 82 | if err != nil { 83 | return parseError(line, err) 84 | } 85 | pl.Items = append(pl.Items, item) 86 | case matchTag(line, ByteRangeItemTag): 87 | value := strings.Replace(line, ByteRangeItemTag+":", "", -1) 88 | value = strings.Replace(value, "\n", "", -1) 89 | br, err := NewByteRange(value) 90 | if err != nil { 91 | return parseError(line, err) 92 | } 93 | mit, ok := st.currentItem.(*MapItem) 94 | if ok { 95 | mit.ByteRange = br 96 | st.currentItem = mit 97 | } else { 98 | sit, ok := st.currentItem.(*SegmentItem) 99 | if ok { 100 | sit.ByteRange = br 101 | st.currentItem = sit 102 | } 103 | } 104 | 105 | case matchTag(line, KeyItemTag): 106 | item, err := NewKeyItem(line) 107 | if err != nil { 108 | return parseError(line, err) 109 | } 110 | pl.Items = append(pl.Items, item) 111 | case matchTag(line, MapItemTag): 112 | item, err := NewMapItem(line) 113 | if err != nil { 114 | return parseError(line, err) 115 | } 116 | pl.Items = append(pl.Items, item) 117 | case matchTag(line, TimeItemTag): 118 | pdt, err := NewTimeItem(line) 119 | if err != nil { 120 | return parseError(line, err) 121 | } 122 | if st.open { 123 | item, ok := st.currentItem.(*SegmentItem) 124 | if !ok { 125 | return parseError(line, ErrSegmentItemInvalid) 126 | } 127 | item.ProgramDateTime = pdt 128 | } else { 129 | pl.Items = append(pl.Items, pdt) 130 | } 131 | case matchTag(line, DateRangeItemTag): 132 | dri, err := NewDateRangeItem(line) 133 | if err != nil { 134 | return parseError(line, err) 135 | } 136 | pl.Items = append(pl.Items, dri) 137 | 138 | // media playlist tags 139 | case matchTag(line, MediaSequenceTag): 140 | pl.Sequence, err = parseIntValue(line, MediaSequenceTag) 141 | case matchTag(line, DiscontinuitySequenceTag): 142 | pl.DiscontinuitySequence, err = parseIntPtr(line, DiscontinuitySequenceTag) 143 | case matchTag(line, CacheTag): 144 | ptr := parseYesNoPtr(line, CacheTag) 145 | pl.Cache = ptr 146 | case matchTag(line, TargetDurationTag): 147 | pl.Target, err = parseIntValue(line, TargetDurationTag) 148 | case matchTag(line, IFramesOnlyTag): 149 | pl.IFramesOnly = true 150 | case matchTag(line, PlaylistTypeTag): 151 | pl.Type = parseStringPtr(line, PlaylistTypeTag) 152 | 153 | // master playlist tags 154 | case matchTag(line, MediaItemTag): 155 | st.open = false 156 | mi, err := NewMediaItem(line) 157 | if err != nil { 158 | return parseError(line, err) 159 | } 160 | pl.Items = append(pl.Items, mi) 161 | case matchTag(line, SessionDataItemTag): 162 | sdi, err := NewSessionDataItem(line) 163 | if err != nil { 164 | return parseError(line, err) 165 | } 166 | pl.Items = append(pl.Items, sdi) 167 | case matchTag(line, SessionKeyItemTag): 168 | ski, err := NewSessionKeyItem(line) 169 | if err != nil { 170 | return parseError(line, err) 171 | } 172 | pl.Items = append(pl.Items, ski) 173 | case matchTag(line, PlaylistItemTag): 174 | st.master = true 175 | st.open = true 176 | pi, err := NewPlaylistItem(line) 177 | if err != nil { 178 | return parseError(line, err) 179 | } 180 | st.currentItem = pi 181 | case matchTag(line, PlaylistIframeTag): 182 | st.master = true 183 | st.open = false 184 | pi, err := NewPlaylistItem(line) 185 | if err != nil { 186 | return parseError(line, err) 187 | } 188 | pi.IFrame = true 189 | pl.Items = append(pl.Items, pi) 190 | st.currentItem = pi 191 | 192 | // universal tags 193 | case matchTag(line, PlaybackStartTag): 194 | ps, err := NewPlaybackStart(line) 195 | if err != nil { 196 | return parseError(line, err) 197 | } 198 | pl.Items = append(pl.Items, ps) 199 | case matchTag(line, IndependentSegmentsTag): 200 | pl.IndependentSegments = true 201 | case matchTag(line, FooterTag): 202 | pl.Live = false 203 | default: 204 | if st.currentItem != nil && st.open { 205 | return parseNextLine(line, pl, st) 206 | } 207 | } 208 | 209 | return parseError(line, err) 210 | } 211 | 212 | func parseNextLine(line string, pl *Playlist, st *state) error { 213 | value := strings.Replace(line, "\n", "", -1) 214 | value = strings.Replace(value, "\r", "", -1) 215 | if st.master { 216 | // PlaylistItem 217 | it, ok := st.currentItem.(*PlaylistItem) 218 | if !ok { 219 | return parseError(line, ErrPlaylistItemInvalid) 220 | } 221 | it.URI = value 222 | pl.Items = append(pl.Items, it) 223 | } else { 224 | // SegmentItem 225 | it, ok := st.currentItem.(*SegmentItem) 226 | if !ok { 227 | return parseError(line, ErrSegmentItemInvalid) 228 | } 229 | it.Segment = value 230 | pl.Items = append(pl.Items, it) 231 | } 232 | 233 | st.open = false 234 | 235 | return nil 236 | } 237 | 238 | func matchTag(line, tag string) bool { 239 | return strings.HasPrefix(line, tag) && !strings.HasPrefix(line, tag+"-") 240 | } 241 | 242 | func parseIntValue(line string, tag string) (int, error) { 243 | var v int 244 | _, err := fmt.Sscanf(line, tag+":%d", &v) 245 | return v, err 246 | } 247 | 248 | func parseIntPtr(line string, tag string) (*int, error) { 249 | var ptr int 250 | _, err := fmt.Sscanf(line, tag+":%d", &ptr) 251 | return &ptr, err 252 | } 253 | 254 | func parseStringPtr(line string, tag string) *string { 255 | value := strings.Replace(line, tag+":", "", -1) 256 | if value == "" { 257 | return nil 258 | } 259 | return &value 260 | } 261 | 262 | func parseYesNoPtr(line string, tag string) *bool { 263 | value := strings.Replace(line, tag+":", "", -1) 264 | var b bool 265 | if value == YesValue { 266 | b = true 267 | } else { 268 | b = false 269 | } 270 | 271 | return &b 272 | } 273 | 274 | func parseError(line string, err error) error { 275 | if err == nil { 276 | return nil 277 | } 278 | return fmt.Errorf("error: %v when parsing playlist error for line: %s", err, line) 279 | } 280 | -------------------------------------------------------------------------------- /m3u8/resolution.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Resolution represents a resolution for a playlist item, e.g: 1920x1080 10 | type Resolution struct { 11 | Width int 12 | Height int 13 | } 14 | 15 | func (r *Resolution) String() string { 16 | if r == nil { 17 | return "" 18 | } 19 | 20 | return fmt.Sprintf("%dx%d", r.Width, r.Height) 21 | } 22 | 23 | // NewResolution parses a string and returns a *Resolution 24 | func NewResolution(text string) (*Resolution, error) { 25 | values := strings.Split(text, "x") 26 | if len(values) <= 1 { 27 | return nil, ErrResolutionInvalid 28 | } 29 | 30 | width, err := strconv.ParseInt(values[0], 0, 0) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | height, err := strconv.ParseInt(values[1], 0, 0) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return &Resolution{ 41 | Width: int(width), 42 | Height: int(height), 43 | }, nil 44 | } 45 | -------------------------------------------------------------------------------- /m3u8/segmentItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // SegmentItem represents EXTINF attributes with the URI that follows, 10 | // optionally allowing an EXT-X-BYTERANGE tag to be set. 11 | type SegmentItem struct { 12 | Duration float64 13 | Segment string 14 | Comment *string 15 | ProgramDateTime *TimeItem 16 | ByteRange *ByteRange 17 | } 18 | 19 | // NewSegmentItem parses a text line and returns a *SegmentItem 20 | func NewSegmentItem(text string) (*SegmentItem, error) { 21 | var si SegmentItem 22 | line := strings.Replace(text, SegmentItemTag+":", "", -1) 23 | line = strings.Replace(line, "\n", "", -1) 24 | values := strings.Split(line, ",") 25 | d, err := strconv.ParseFloat(values[0], 64) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | si.Duration = d 31 | if len(values) > 1 && values[1] != "" { 32 | si.Comment = &values[1] 33 | } 34 | 35 | return &si, nil 36 | } 37 | 38 | func (si *SegmentItem) String() string { 39 | date := "" 40 | if si.ProgramDateTime != nil { 41 | date = fmt.Sprintf("%v\n", si.ProgramDateTime) 42 | } 43 | byteRange := "" 44 | if si.ByteRange != nil { 45 | byteRange = fmt.Sprintf("\n%s:%v", ByteRangeItemTag, si.ByteRange.String()) 46 | } 47 | 48 | comment := "" 49 | if si.Comment != nil { 50 | comment = *si.Comment 51 | } 52 | 53 | return fmt.Sprintf("%s:%v,%s%s\n%s%s", SegmentItemTag, si.Duration, comment, byteRange, date, si.Segment) 54 | } 55 | -------------------------------------------------------------------------------- /m3u8/sessionDataItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // SessionDataItem represents a set of EXT-X-SESSION-DATA attributes 9 | type SessionDataItem struct { 10 | DataID string 11 | Value *string 12 | URI *string 13 | Language *string 14 | } 15 | 16 | // NewSessionDataItem parses a text line and returns a *SessionDataItem 17 | func NewSessionDataItem(text string) (*SessionDataItem, error) { 18 | attributes := ParseAttributes(text) 19 | 20 | return &SessionDataItem{ 21 | DataID: attributes[DataIDTag], 22 | Value: pointerTo(attributes, ValueTag), 23 | URI: pointerTo(attributes, URITag), 24 | Language: pointerTo(attributes, LanguageTag), 25 | }, nil 26 | } 27 | 28 | func (sdi *SessionDataItem) String() string { 29 | slice := []string{fmt.Sprintf(quotedFormatString, DataIDTag, sdi.DataID)} 30 | 31 | if sdi.Value != nil { 32 | slice = append(slice, fmt.Sprintf(quotedFormatString, ValueTag, *sdi.Value)) 33 | } 34 | if sdi.URI != nil { 35 | slice = append(slice, fmt.Sprintf(quotedFormatString, URITag, *sdi.URI)) 36 | } 37 | if sdi.Language != nil { 38 | slice = append(slice, fmt.Sprintf(quotedFormatString, LanguageTag, *sdi.Language)) 39 | } 40 | 41 | return fmt.Sprintf(`%s:%s`, SessionDataItemTag, strings.Join(slice, ",")) 42 | } 43 | -------------------------------------------------------------------------------- /m3u8/sessionKeyItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import "fmt" 4 | 5 | // SessionKeyItem represents a set of EXT-X-SESSION-KEY attributes 6 | type SessionKeyItem struct { 7 | Encryptable *Encryptable 8 | } 9 | 10 | // NewSessionKeyItem parses a text line and returns a *SessionKeyItem 11 | func NewSessionKeyItem(text string) (*SessionKeyItem, error) { 12 | attributes := ParseAttributes(text) 13 | return &SessionKeyItem{ 14 | Encryptable: NewEncryptable(attributes), 15 | }, nil 16 | } 17 | 18 | func (ski *SessionKeyItem) String() string { 19 | return fmt.Sprintf("%s:%v", SessionKeyItemTag, ski.Encryptable.String()) 20 | } 21 | -------------------------------------------------------------------------------- /m3u8/tags.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | const ( 4 | // Item tags 5 | 6 | SessionKeyItemTag = `#EXT-X-SESSION-KEY` 7 | KeyItemTag = `#EXT-X-KEY` 8 | DiscontinuityItemTag = `#EXT-X-DISCONTINUITY` 9 | TimeItemTag = `#EXT-X-PROGRAM-DATE-TIME` 10 | DateRangeItemTag = `#EXT-X-DATERANGE` 11 | MapItemTag = `#EXT-X-MAP` 12 | SessionDataItemTag = `#EXT-X-SESSION-DATA` 13 | SegmentItemTag = `#EXTINF` 14 | ByteRangeItemTag = `#EXT-X-BYTERANGE` 15 | PlaybackStartTag = `#EXT-X-START` 16 | MediaItemTag = `#EXT-X-MEDIA` 17 | PlaylistItemTag = `#EXT-X-STREAM-INF` 18 | PlaylistIframeTag = `#EXT-X-I-FRAME-STREAM-INF` 19 | 20 | // Playlist tags 21 | 22 | HeaderTag = `#EXTM3U` 23 | FooterTag = `#EXT-X-ENDLIST` 24 | TargetDurationTag = `#EXT-X-TARGETDURATION` 25 | CacheTag = `#EXT-X-ALLOW-CACHE` 26 | DiscontinuitySequenceTag = `#EXT-X-DISCONTINUITY-SEQUENCE` 27 | IndependentSegmentsTag = `#EXT-X-INDEPENDENT-SEGMENTS` 28 | PlaylistTypeTag = `#EXT-X-PLAYLIST-TYPE` 29 | IFramesOnlyTag = `#EXT-X-I-FRAMES-ONLY` 30 | MediaSequenceTag = `#EXT-X-MEDIA-SEQUENCE` 31 | VersionTag = `#EXT-X-VERSION` 32 | 33 | // ByteRange tags 34 | 35 | ByteRangeTag = "BYTERANGE" 36 | 37 | // Encryptable tags 38 | 39 | MethodTag = "METHOD" 40 | URITag = "URI" 41 | IVTag = "IV" 42 | KeyFormatTag = "KEYFORMAT" 43 | KeyFormatVersionsTag = "KEYFORMATVERSIONS" 44 | 45 | // DateRangeItem tags 46 | 47 | IDTag = "ID" 48 | ClassTag = "CLASS" 49 | StartDateTag = "START-DATE" 50 | EndDateTag = "END-DATE" 51 | DurationTag = "DURATION" 52 | PlannedDurationTag = "PLANNED-DURATION" 53 | Scte35CmdTag = "SCTE35-CMD" 54 | Scte35OutTag = "SCTE35-OUT" 55 | Scte35InTag = "SCTE35-IN" 56 | EndOnNextTag = "END-ON-NEXT" 57 | 58 | // PlaybackStart tags 59 | 60 | TimeOffsetTag = "TIME-OFFSET" 61 | PreciseTag = "PRECISE" 62 | 63 | // SessionDataItem tags 64 | 65 | DataIDTag = "DATA-ID" 66 | ValueTag = "VALUE" 67 | LanguageTag = "LANGUAGE" 68 | 69 | // MediaItem tags 70 | 71 | TypeTag = "TYPE" 72 | GroupIDTag = "GROUP-ID" 73 | AssocLanguageTag = "ASSOC-LANGUAGE" 74 | NameTag = "NAME" 75 | AutoSelectTag = "AUTOSELECT" 76 | DefaultTag = "DEFAULT" 77 | ForcedTag = "FORCED" 78 | InStreamIDTag = "INSTREAM-ID" 79 | CharacteristicsTag = "CHARACTERISTICS" 80 | ChannelsTag = "CHANNELS" 81 | StableRenditionIDTag = "STABLE-RENDITION-ID" 82 | 83 | /// PlaylistItem tags 84 | 85 | ResolutionTag = "RESOLUTION" 86 | ProgramIDTag = "PROGRAM-ID" 87 | CodecsTag = "CODECS" 88 | BandwidthTag = "BANDWIDTH" 89 | AverageBandwidthTag = "AVERAGE-BANDWIDTH" 90 | FrameRateTag = "FRAME-RATE" 91 | VideoTag = "VIDEO" 92 | AudioTag = "AUDIO" 93 | SubtitlesTag = "SUBTITLES" 94 | ClosedCaptionsTag = "CLOSED-CAPTIONS" 95 | HDCPLevelTag = "HDCP-LEVEL" 96 | StableVariantIDTag = "STABLE-VARIANT-ID" 97 | 98 | // Values 99 | 100 | NoneValue = "NONE" 101 | YesValue = "YES" 102 | NoValue = "NO" 103 | ) 104 | -------------------------------------------------------------------------------- /m3u8/timeItem.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | const ( 10 | dateTimeFormat = time.RFC3339Nano 11 | ) 12 | 13 | // TimeItem represents EXT-X-PROGRAM-DATE-TIME 14 | type TimeItem struct { 15 | Time time.Time 16 | } 17 | 18 | // NewTimeItem parses a text line and returns a *TimeItem 19 | func NewTimeItem(text string) (*TimeItem, error) { 20 | timeString := strings.Replace(text, TimeItemTag+":", "", -1) 21 | 22 | t, err := ParseTime(timeString) 23 | 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return &TimeItem{ 29 | Time: t, 30 | }, nil 31 | 32 | } 33 | 34 | func (ti *TimeItem) String() string { 35 | return fmt.Sprintf("%s:%s", TimeItemTag, ti.Time.Format(dateTimeFormat)) 36 | } 37 | 38 | // FormatTime returns a string in default m3u8 date time format 39 | func FormatTime(time time.Time) string { 40 | return time.Format(dateTimeFormat) 41 | } 42 | 43 | // ParseTime parses a string in default m3u8 date time format 44 | // and returns time.Time 45 | func ParseTime(value string) (time.Time, error) { 46 | layouts := []string{ 47 | "2006-01-02T15:04:05.999999999Z0700", 48 | "2006-01-02T15:04:05.999999999Z07:00", 49 | "2006-01-02T15:04:05.999999999Z07", 50 | } 51 | var ( 52 | err error 53 | t time.Time 54 | ) 55 | for _, layout := range layouts { 56 | if t, err = time.Parse(layout, value); err == nil { 57 | return t, nil 58 | } 59 | } 60 | return t, err 61 | } 62 | -------------------------------------------------------------------------------- /m3u8/writer.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Write writes a playlist to a string 9 | func Write(pl *Playlist) (string, error) { 10 | var sb strings.Builder 11 | 12 | if !pl.IsValid() { 13 | return "", ErrPlaylistInvalidType 14 | } 15 | writeHeader(&sb, pl) 16 | for _, item := range pl.Items { 17 | sb.WriteString(item.String()) 18 | sb.WriteRune('\n') 19 | } 20 | writeFooter(&sb, pl) 21 | 22 | return sb.String(), nil 23 | } 24 | 25 | func writeHeader(sb *strings.Builder, pl *Playlist) { 26 | sb.WriteString(HeaderTag) 27 | sb.WriteRune('\n') 28 | 29 | if pl.IsMaster() { 30 | writeVersionTag(sb, pl.Version) 31 | writeIndependentSegmentsTag(sb, pl.IndependentSegments) 32 | } else { 33 | if pl.Type != nil { 34 | sb.WriteString(fmt.Sprintf("%s:%s", PlaylistTypeTag, *pl.Type)) 35 | sb.WriteRune('\n') 36 | } 37 | writeVersionTag(sb, pl.Version) 38 | writeIndependentSegmentsTag(sb, pl.IndependentSegments) 39 | if pl.IFramesOnly { 40 | sb.WriteString(IFramesOnlyTag) 41 | sb.WriteRune('\n') 42 | } 43 | sb.WriteString(fmt.Sprintf("%s:%v", MediaSequenceTag, pl.Sequence)) 44 | sb.WriteRune('\n') 45 | writeDiscontinuitySequenceTag(sb, pl.DiscontinuitySequence) 46 | writeCacheTag(sb, pl.Cache) 47 | sb.WriteString(fmt.Sprintf("%s:%v", TargetDurationTag, pl.Target)) 48 | sb.WriteRune('\n') 49 | } 50 | } 51 | 52 | func writeFooter(sb *strings.Builder, pl *Playlist) { 53 | if pl.IsLive() || pl.IsMaster() { 54 | return 55 | } 56 | 57 | sb.WriteString(FooterTag) 58 | sb.WriteRune('\n') 59 | } 60 | 61 | func writeVersionTag(sb *strings.Builder, version *int) { 62 | if version == nil { 63 | return 64 | } 65 | 66 | sb.WriteString(fmt.Sprintf("%s:%v", VersionTag, *version)) 67 | sb.WriteRune('\n') 68 | } 69 | 70 | func writeIndependentSegmentsTag(sb *strings.Builder, toWrite bool) { 71 | if !toWrite { 72 | return 73 | } 74 | 75 | sb.WriteString(IndependentSegmentsTag) 76 | sb.WriteRune('\n') 77 | } 78 | 79 | func writeDiscontinuitySequenceTag(sb *strings.Builder, sequence *int) { 80 | if sequence == nil { 81 | return 82 | } 83 | 84 | sb.WriteString(fmt.Sprintf("%s:%v", DiscontinuitySequenceTag, *sequence)) 85 | sb.WriteRune('\n') 86 | } 87 | 88 | func writeCacheTag(sb *strings.Builder, cache *bool) { 89 | if cache == nil { 90 | return 91 | } 92 | 93 | sb.WriteString(fmt.Sprintf("%s:%s", CacheTag, formatYesNo(*cache))) 94 | sb.WriteRune('\n') 95 | } 96 | -------------------------------------------------------------------------------- /test/byteRange_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/AlekSi/pointer" 7 | "github.com/etherlabsio/go-m3u8/m3u8" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestByteRange_Parse(t *testing.T) { 12 | text := "4500@600" 13 | br, err := m3u8.NewByteRange(text) 14 | 15 | assert.Nil(t, err) 16 | assert.NotNil(t, br.Length) 17 | assert.NotNil(t, br.Start) 18 | 19 | assert.Equal(t, 4500, *br.Length) 20 | assert.Equal(t, 600, *br.Start) 21 | 22 | assertToString(t, text, br) 23 | } 24 | 25 | func TestByteRange_Parse_2(t *testing.T) { 26 | text := "4500" 27 | br, err := m3u8.NewByteRange(text) 28 | 29 | assert.Nil(t, err) 30 | assert.NotNil(t, br.Length) 31 | assert.Nil(t, br.Start) 32 | 33 | assert.Equal(t, 4500, *br.Length) 34 | 35 | assertToString(t, text, br) 36 | } 37 | 38 | func TestByteRange_New(t *testing.T) { 39 | br := &m3u8.ByteRange{ 40 | Length: pointer.ToInt(4500), 41 | Start: pointer.ToInt(200), 42 | } 43 | assert.Equal(t, "4500@200", br.String()) 44 | } 45 | 46 | func TestByteRange_New_2(t *testing.T) { 47 | br := &m3u8.ByteRange{ 48 | Length: pointer.ToInt(4500), 49 | } 50 | assert.Equal(t, "4500", br.String()) 51 | } 52 | -------------------------------------------------------------------------------- /test/common.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/etherlabsio/go-m3u8/m3u8" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func assertNotNilEqual(t *testing.T, expected interface{}, ptr interface{}) { 12 | assert.NotNil(t, ptr) 13 | switch ptr.(type) { 14 | case *string: 15 | s, ok := ptr.(*string) 16 | assert.True(t, ok) 17 | assert.Equal(t, expected, *s) 18 | case *float64: 19 | f, ok := ptr.(*float64) 20 | assert.True(t, ok) 21 | assert.Equal(t, expected, *f) 22 | case *int: 23 | i, ok := ptr.(*int) 24 | assert.True(t, ok) 25 | assert.Equal(t, expected, *i) 26 | case *bool: 27 | b, ok := ptr.(*bool) 28 | assert.True(t, ok) 29 | assert.Equal(t, expected, *b) 30 | default: 31 | t.Fatal("not supported assert type") 32 | } 33 | } 34 | 35 | func assertEqualWithoutNewLine(t *testing.T, expected string, actual string) { 36 | removedNewLine := strings.Replace(expected, "\n", "", -1) 37 | assert.Equal(t, removedNewLine, actual) 38 | } 39 | 40 | func assertToString(t *testing.T, expected string, item m3u8.Item) { 41 | assertEqualWithoutNewLine(t, expected, item.String()) 42 | } 43 | -------------------------------------------------------------------------------- /test/common_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseAttributes(t *testing.T) { 11 | line := "TEST-ID=\"Help\",URI=\"http://test\",ID=33\n" 12 | mapAttr := m3u8.ParseAttributes(line) 13 | 14 | assert.NotNil(t, mapAttr) 15 | assert.Equal(t, "Help", mapAttr["TEST-ID"]) 16 | assert.Equal(t, "http://test", mapAttr["URI"]) 17 | assert.Equal(t, "33", mapAttr["ID"]) 18 | } 19 | -------------------------------------------------------------------------------- /test/dateRangeItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDateRangeItem_Parse(t *testing.T) { 11 | line := `#EXT-X-DATERANGE:ID="splice-6FFFFFF0",CLASS="test_class", 12 | START-DATE="2014-03-05T11:15:00Z", 13 | END-DATE="2014-03-05T11:16:00Z",DURATION=60.1, 14 | PLANNED-DURATION=59.993, 15 | SCTE35-CMD=0xFC002F0000000000FF2, 16 | SCTE35-OUT=0xFC002F0000000000FF0, 17 | SCTE35-IN=0xFC002F0000000000FF1, 18 | END-ON-NEXT=YES 19 | ` 20 | dri, err := m3u8.NewDateRangeItem(line) 21 | 22 | assert.Nil(t, err) 23 | assert.Equal(t, "splice-6FFFFFF0", dri.ID) 24 | assert.Equal(t, "2014-03-05T11:15:00Z", dri.StartDate) 25 | 26 | assertNotNilEqual(t, "test_class", dri.Class) 27 | assertNotNilEqual(t, "2014-03-05T11:16:00Z", dri.EndDate) 28 | assertNotNilEqual(t, 60.1, dri.Duration) 29 | assertNotNilEqual(t, 59.993, dri.PlannedDuration) 30 | assertNotNilEqual(t, "0xFC002F0000000000FF2", dri.Scte35Cmd) 31 | assertNotNilEqual(t, "0xFC002F0000000000FF0", dri.Scte35Out) 32 | assertNotNilEqual(t, "0xFC002F0000000000FF1", dri.Scte35In) 33 | assert.True(t, dri.EndOnNext) 34 | assert.Nil(t, dri.ClientAttributes) 35 | 36 | assertToString(t, line, dri) 37 | } 38 | 39 | func TestDateRangeItem_Parse_2(t *testing.T) { 40 | line := `#EXT-X-DATERANGE:ID="splice-6FFFFFF0", 41 | START-DATE="2014-03-05T11:15:00Z" 42 | ` 43 | dri, err := m3u8.NewDateRangeItem(line) 44 | 45 | assert.Nil(t, err) 46 | assert.Equal(t, "splice-6FFFFFF0", dri.ID) 47 | assert.Equal(t, "2014-03-05T11:15:00Z", dri.StartDate) 48 | 49 | assert.Nil(t, dri.Class) 50 | assert.Nil(t, dri.EndDate) 51 | assert.Nil(t, dri.Duration) 52 | assert.Nil(t, dri.PlannedDuration) 53 | assert.Nil(t, dri.Scte35In) 54 | assert.Nil(t, dri.Scte35Out) 55 | assert.Nil(t, dri.Scte35Cmd) 56 | assert.Nil(t, dri.ClientAttributes) 57 | assert.False(t, dri.EndOnNext) 58 | 59 | assertToString(t, line, dri) 60 | } 61 | 62 | func TestDateRangeItem_Parse_3(t *testing.T) { 63 | line := `#EXT-X-DATERANGE:ID="splice-6FFFFFF0", 64 | START-DATE="2014-03-05T11:15:00Z", 65 | X-CUSTOM-VALUE="test_value" 66 | ` 67 | dri, err := m3u8.NewDateRangeItem(line) 68 | 69 | assert.Nil(t, err) 70 | assert.NotNil(t, dri.ClientAttributes) 71 | assert.Equal(t, "test_value", dri.ClientAttributes["X-CUSTOM-VALUE"]) 72 | 73 | assertToString(t, line, dri) 74 | } 75 | -------------------------------------------------------------------------------- /test/discontinuityItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDiscontinuityItem_Parse(t *testing.T) { 11 | di, err := m3u8.NewDiscontinuityItem() 12 | assert.Nil(t, err) 13 | assert.Equal(t, m3u8.DiscontinuityItemTag+"\n", di.String()) 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/dateRangeScte35.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-TARGETDURATION:60 3 | #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11: 4 | 15:00Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF0 5 | 00014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50 6 | 000008700000000 7 | #EXTINF:20.0, 8 | http://media.example.com/first.ts 9 | #EXTINF:20.0, 10 | http://media.example.com/second.ts 11 | #EXTINF:20.0, 12 | http://media.example.com/third.ts 13 | #EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN= 14 | 0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A00 15 | 08029896F50000008700000000 16 | #EXT-X-ENDLIST 17 | -------------------------------------------------------------------------------- /test/fixtures/encrypted.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-MEDIA-SEQUENCE:7794 4 | #EXT-X-TARGETDURATION:15 5 | 6 | #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" 7 | 8 | #EXTINF:2.833, 9 | http://media.example.com/fileSequence52-A.ts 10 | #EXTINF:15.0, 11 | http://media.example.com/fileSequence52-B.ts 12 | #EXTINF:13.333, 13 | http://media.example.com/fileSequence52-C.ts 14 | 15 | #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53" 16 | 17 | #EXTINF:15.0, 18 | http://media.example.com/fileSequence53-A.ts -------------------------------------------------------------------------------- /test/fixtures/iframes.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:4 3 | #EXT-X-I-FRAMES-ONLY 4 | #EXTINF:4.12, 5 | #EXT-X-BYTERANGE:9400@376 6 | segment1.ts 7 | #EXTINF:3.56, 8 | #EXT-X-BYTERANGE:7144 9 | segment1.ts 10 | #EXTINF:3.82, 11 | #EXT-X-BYTERANGE:10340@1880 12 | segment2.ts -------------------------------------------------------------------------------- /test/fixtures/mapPlaylist.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-PLAYLIST-TYPE:VOD 3 | #EXT-X-VERSION:5 4 | #EXT-X-MEDIA-SEQUENCE:1 5 | #EXT-X-ALLOW-CACHE:NO 6 | #EXT-X-TARGETDURATION:12 7 | #EXT-X-MAP:URI="frelo/prog_index.m3u8",BYTERANGE="4500@600" 8 | #EXT-X-ENDLIST -------------------------------------------------------------------------------- /test/fixtures/master.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-INDEPENDENT-SEGMENTS 3 | #EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" 4 | #EXT-X-START:TIME-OFFSET=20.2 5 | #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=5042000 6 | hls/1080-7mbps/1080-7mbps.m3u8 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=4853000 8 | hls/1080/1080.m3u8 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1280x720,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=2387000 10 | hls/720/720.m3u8 11 | #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=896x504,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=1365000 12 | hls/504/504.m3u8 13 | #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=640x360,CODECS="avc1.66.30,mp4a.40.2",BANDWIDTH=861000 14 | hls/360/360.m3u8 15 | #EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.2",BANDWIDTH=6400 16 | hls/64k/64k.m3u8 17 | -------------------------------------------------------------------------------- /test/fixtures/masterIframes.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" 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" 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" 11 | #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" 12 | audio-only.m3u8 13 | -------------------------------------------------------------------------------- /test/fixtures/playlist-live.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-PLAYLIST-TYPE:EVENT 3 | #EXT-X-VERSION:4 4 | #EXT-X-MEDIA-SEQUENCE:1 5 | #EXT-X-DISCONTINUITY-SEQUENCE:8 6 | #EXT-X-ALLOW-CACHE:NO 7 | #EXT-X-TARGETDURATION:12 8 | #EXTINF:11.344644, 9 | 1080-7mbps00000.ts 10 | #EXT-X-DISCONTINUITY 11 | #EXTINF:11.261233, 12 | 1080-7mbps00001.ts 13 | #EXTINF:7.507489, 14 | 1080-7mbps00002.ts 15 | #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23Z 16 | #EXTINF:11.261233, 17 | 1080-7mbps00003.ts 18 | #EXTINF:11.261233, 19 | 1080-7mbps00004.ts 20 | #EXTINF:7.507489, 21 | 1080-7mbps00005.ts 22 | #EXTINF:11.261233, 23 | 1080-7mbps00006.ts 24 | #EXTINF:11.261233, 25 | 1080-7mbps00007.ts 26 | #EXTINF:7.507478, 27 | 1080-7mbps00008.ts 28 | #EXTINF:11.261233, 29 | 1080-7mbps00009.ts 30 | #EXTINF:11.261233, 31 | 1080-7mbps00010.ts 32 | #EXTINF:7.507478, 33 | 1080-7mbps00011.ts 34 | #EXTINF:11.261233, 35 | 1080-7mbps00012.ts 36 | #EXTINF:11.261233, 37 | 1080-7mbps00013.ts 38 | #EXTINF:7.507489, 39 | 1080-7mbps00014.ts 40 | #EXTINF:11.261233, 41 | 1080-7mbps00015.ts 42 | #EXTINF:11.261233, 43 | 1080-7mbps00016.ts 44 | #EXTINF:7.507489, 45 | 1080-7mbps00017.ts 46 | #EXTINF:11.261233, 47 | 1080-7mbps00018.ts 48 | #EXTINF:11.261233, 49 | 1080-7mbps00019.ts 50 | #EXTINF:7.507478, 51 | 1080-7mbps00020.ts 52 | #EXTINF:11.261233, 53 | 1080-7mbps00021.ts 54 | #EXTINF:11.261233, 55 | 1080-7mbps00022.ts 56 | #EXTINF:7.507478, 57 | 1080-7mbps00023.ts 58 | #EXTINF:11.261233, 59 | 1080-7mbps00024.ts 60 | #EXTINF:11.261233, 61 | 1080-7mbps00025.ts 62 | #EXTINF:7.507489, 63 | 1080-7mbps00026.ts 64 | #EXTINF:11.261233, 65 | 1080-7mbps00027.ts 66 | #EXTINF:11.261233, 67 | 1080-7mbps00028.ts 68 | #EXTINF:7.507489, 69 | 1080-7mbps00029.ts 70 | #EXTINF:11.261233, 71 | 1080-7mbps00030.ts 72 | #EXTINF:11.261233, 73 | 1080-7mbps00031.ts 74 | #EXTINF:7.507478, 75 | 1080-7mbps00032.ts 76 | #EXTINF:11.261233, 77 | 1080-7mbps00033.ts 78 | #EXTINF:11.261233, 79 | 1080-7mbps00034.ts 80 | #EXTINF:7.507489, 81 | 1080-7mbps00035.ts 82 | #EXTINF:11.261233, 83 | 1080-7mbps00036.ts 84 | #EXTINF:11.261222, 85 | 1080-7mbps00037.ts 86 | #EXTINF:7.507489, 87 | 1080-7mbps00038.ts 88 | #EXTINF:11.261233, 89 | 1080-7mbps00039.ts 90 | #EXTINF:11.261233, 91 | 1080-7mbps00040.ts 92 | #EXTINF:7.507489, 93 | 1080-7mbps00041.ts 94 | #EXTINF:11.261233, 95 | 1080-7mbps00042.ts 96 | #EXTINF:11.261233, 97 | 1080-7mbps00043.ts 98 | #EXTINF:7.507478, 99 | 1080-7mbps00044.ts 100 | #EXTINF:11.261233, 101 | 1080-7mbps00045.ts 102 | #EXTINF:11.261233, 103 | 1080-7mbps00046.ts 104 | #EXTINF:7.507489, 105 | 1080-7mbps00047.ts 106 | #EXTINF:11.261233, 107 | 1080-7mbps00048.ts 108 | #EXTINF:11.261222, 109 | 1080-7mbps00049.ts 110 | #EXTINF:7.507489, 111 | 1080-7mbps00050.ts 112 | #EXTINF:11.261233, 113 | 1080-7mbps00051.ts 114 | #EXTINF:11.261233, 115 | 1080-7mbps00052.ts 116 | #EXTINF:7.507489, 117 | 1080-7mbps00053.ts 118 | #EXTINF:11.261233, 119 | 1080-7mbps00054.ts 120 | #EXTINF:11.261233, 121 | 1080-7mbps00055.ts 122 | #EXTINF:7.507478, 123 | 1080-7mbps00056.ts 124 | #EXTINF:11.261233, 125 | 1080-7mbps00057.ts 126 | #EXTINF:11.261233, 127 | 1080-7mbps00058.ts 128 | #EXTINF:7.507489, 129 | 1080-7mbps00059.ts 130 | #EXTINF:11.261233, 131 | 1080-7mbps00060.ts 132 | #EXTINF:11.261222, 133 | 1080-7mbps00061.ts 134 | #EXTINF:7.507489, 135 | 1080-7mbps00062.ts 136 | #EXTINF:11.261233, 137 | 1080-7mbps00063.ts 138 | #EXTINF:11.261233, 139 | 1080-7mbps00064.ts 140 | #EXTINF:7.507489, 141 | 1080-7mbps00065.ts 142 | #EXTINF:11.261233, 143 | 1080-7mbps00066.ts 144 | #EXTINF:11.261233, 145 | 1080-7mbps00067.ts 146 | #EXTINF:7.507478, 147 | 1080-7mbps00068.ts 148 | #EXTINF:11.261233, 149 | 1080-7mbps00069.ts 150 | #EXTINF:11.261233, 151 | 1080-7mbps00070.ts 152 | #EXTINF:7.507489, 153 | 1080-7mbps00071.ts 154 | #EXTINF:11.261233, 155 | 1080-7mbps00072.ts 156 | #EXTINF:11.261233, 157 | 1080-7mbps00073.ts 158 | #EXTINF:7.507489, 159 | 1080-7mbps00074.ts 160 | #EXTINF:11.261222, 161 | 1080-7mbps00075.ts 162 | #EXTINF:11.261233, 163 | 1080-7mbps00076.ts 164 | #EXTINF:7.507489, 165 | 1080-7mbps00077.ts 166 | #EXTINF:11.261233, 167 | 1080-7mbps00078.ts 168 | #EXTINF:11.261233, 169 | 1080-7mbps00079.ts 170 | #EXTINF:7.507478, 171 | 1080-7mbps00080.ts 172 | #EXTINF:11.261233, 173 | 1080-7mbps00081.ts 174 | #EXTINF:11.261233, 175 | 1080-7mbps00082.ts 176 | #EXTINF:7.507489, 177 | 1080-7mbps00083.ts 178 | #EXTINF:11.261233, 179 | 1080-7mbps00084.ts 180 | #EXTINF:11.261233, 181 | 1080-7mbps00085.ts 182 | #EXTINF:7.507489, 183 | 1080-7mbps00086.ts 184 | #EXTINF:11.261222, 185 | 1080-7mbps00087.ts 186 | #EXTINF:11.261233, 187 | 1080-7mbps00088.ts 188 | #EXTINF:7.507489, 189 | 1080-7mbps00089.ts 190 | #EXTINF:11.261233, 191 | 1080-7mbps00090.ts 192 | #EXTINF:11.261233, 193 | 1080-7mbps00091.ts 194 | #EXTINF:7.507478, 195 | 1080-7mbps00092.ts 196 | #EXTINF:11.261233, 197 | 1080-7mbps00093.ts 198 | #EXTINF:11.261233, 199 | 1080-7mbps00094.ts 200 | #EXTINF:7.507489, 201 | 1080-7mbps00095.ts 202 | #EXTINF:11.261233, 203 | 1080-7mbps00096.ts 204 | #EXTINF:11.261233, 205 | 1080-7mbps00097.ts 206 | #EXTINF:7.507489, 207 | 1080-7mbps00098.ts 208 | #EXTINF:11.261222, 209 | 1080-7mbps00099.ts 210 | #EXTINF:11.261233, 211 | 1080-7mbps00100.ts 212 | #EXTINF:7.507489, 213 | 1080-7mbps00101.ts 214 | #EXTINF:11.261233, 215 | 1080-7mbps00102.ts 216 | #EXTINF:11.261233, 217 | 1080-7mbps00103.ts 218 | #EXTINF:7.507478, 219 | 1080-7mbps00104.ts 220 | #EXTINF:11.261233, 221 | 1080-7mbps00105.ts 222 | #EXTINF:11.261233, 223 | 1080-7mbps00106.ts 224 | #EXTINF:7.507489, 225 | 1080-7mbps00107.ts 226 | #EXTINF:11.261233, 227 | 1080-7mbps00108.ts 228 | #EXTINF:11.261233, 229 | 1080-7mbps00109.ts 230 | #EXTINF:7.507489, 231 | 1080-7mbps00110.ts 232 | #EXTINF:11.261233, 233 | 1080-7mbps00111.ts 234 | #EXTINF:11.261233, 235 | 1080-7mbps00112.ts 236 | #EXTINF:7.507478, 237 | 1080-7mbps00113.ts 238 | #EXTINF:11.261233, 239 | 1080-7mbps00114.ts 240 | #EXTINF:11.261233, 241 | 1080-7mbps00115.ts 242 | #EXTINF:7.507478, 243 | 1080-7mbps00116.ts 244 | #EXTINF:11.261233, 245 | 1080-7mbps00117.ts 246 | #EXTINF:7.507478, 247 | 1080-7mbps00118.ts 248 | #EXTINF:11.261233, 249 | 1080-7mbps00119.ts 250 | #EXTINF:11.261233, 251 | 1080-7mbps00120.ts 252 | #EXTINF:7.507489, 253 | 1080-7mbps00121.ts 254 | #EXTINF:11.261233, 255 | 1080-7mbps00122.ts 256 | #EXTINF:11.261233, 257 | 1080-7mbps00123.ts 258 | #EXTINF:7.507489, 259 | 1080-7mbps00124.ts 260 | #EXTINF:11.261222, 261 | 1080-7mbps00125.ts 262 | #EXTINF:11.261233, 263 | 1080-7mbps00126.ts 264 | #EXTINF:7.507489, 265 | 1080-7mbps00127.ts 266 | #EXTINF:11.261233, 267 | 1080-7mbps00128.ts 268 | #EXTINF:11.261233, 269 | 1080-7mbps00129.ts 270 | #EXTINF:7.507478, 271 | 1080-7mbps00130.ts 272 | #EXTINF:11.261233, 273 | 1080-7mbps00131.ts 274 | #EXTINF:11.261233, 275 | 1080-7mbps00132.ts 276 | #EXTINF:7.507489, 277 | 1080-7mbps00133.ts 278 | #EXTINF:11.261233, 279 | 1080-7mbps00134.ts 280 | #EXTINF:11.261233, 281 | 1080-7mbps00135.ts 282 | #EXTINF:7.507489, 283 | 1080-7mbps00136.ts 284 | #EXTINF:1.793444, 285 | 1080-7mbps00137.ts 286 | 287 | -------------------------------------------------------------------------------- /test/fixtures/playlist.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-PLAYLIST-TYPE:VOD 3 | #EXT-X-VERSION:4 4 | #EXT-X-MEDIA-SEQUENCE:1 5 | #EXT-X-DISCONTINUITY-SEQUENCE:8 6 | #EXT-X-ALLOW-CACHE:NO 7 | #EXT-X-TARGETDURATION:12 8 | #EXTINF:11.344644, 9 | 1080-7mbps00000.ts 10 | #EXT-X-DISCONTINUITY 11 | #EXTINF:11.261233, 12 | 1080-7mbps00001.ts 13 | #EXTINF:7.507489, 14 | 1080-7mbps00002.ts 15 | #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23Z 16 | #EXTINF:11.261233, 17 | 1080-7mbps00003.ts 18 | #EXTINF:11.261233, 19 | 1080-7mbps00004.ts 20 | #EXTINF:7.507489, 21 | 1080-7mbps00005.ts 22 | #EXTINF:11.261233, 23 | 1080-7mbps00006.ts 24 | #EXTINF:11.261233, 25 | 1080-7mbps00007.ts 26 | #EXTINF:7.507478, 27 | 1080-7mbps00008.ts 28 | #EXTINF:11.261233, 29 | 1080-7mbps00009.ts 30 | #EXTINF:11.261233, 31 | 1080-7mbps00010.ts 32 | #EXTINF:7.507478, 33 | 1080-7mbps00011.ts 34 | #EXTINF:11.261233, 35 | 1080-7mbps00012.ts 36 | #EXTINF:11.261233, 37 | 1080-7mbps00013.ts 38 | #EXTINF:7.507489, 39 | 1080-7mbps00014.ts 40 | #EXTINF:11.261233, 41 | 1080-7mbps00015.ts 42 | #EXTINF:11.261233, 43 | 1080-7mbps00016.ts 44 | #EXTINF:7.507489, 45 | 1080-7mbps00017.ts 46 | #EXTINF:11.261233, 47 | 1080-7mbps00018.ts 48 | #EXTINF:11.261233, 49 | 1080-7mbps00019.ts 50 | #EXTINF:7.507478, 51 | 1080-7mbps00020.ts 52 | #EXTINF:11.261233, 53 | 1080-7mbps00021.ts 54 | #EXTINF:11.261233, 55 | 1080-7mbps00022.ts 56 | #EXTINF:7.507478, 57 | 1080-7mbps00023.ts 58 | #EXTINF:11.261233, 59 | 1080-7mbps00024.ts 60 | #EXTINF:11.261233, 61 | 1080-7mbps00025.ts 62 | #EXTINF:7.507489, 63 | 1080-7mbps00026.ts 64 | #EXTINF:11.261233, 65 | 1080-7mbps00027.ts 66 | #EXTINF:11.261233, 67 | 1080-7mbps00028.ts 68 | #EXTINF:7.507489, 69 | 1080-7mbps00029.ts 70 | #EXTINF:11.261233, 71 | 1080-7mbps00030.ts 72 | #EXTINF:11.261233, 73 | 1080-7mbps00031.ts 74 | #EXTINF:7.507478, 75 | 1080-7mbps00032.ts 76 | #EXTINF:11.261233, 77 | 1080-7mbps00033.ts 78 | #EXTINF:11.261233, 79 | 1080-7mbps00034.ts 80 | #EXTINF:7.507489, 81 | 1080-7mbps00035.ts 82 | #EXTINF:11.261233, 83 | 1080-7mbps00036.ts 84 | #EXTINF:11.261222, 85 | 1080-7mbps00037.ts 86 | #EXTINF:7.507489, 87 | 1080-7mbps00038.ts 88 | #EXTINF:11.261233, 89 | 1080-7mbps00039.ts 90 | #EXTINF:11.261233, 91 | 1080-7mbps00040.ts 92 | #EXTINF:7.507489, 93 | 1080-7mbps00041.ts 94 | #EXTINF:11.261233, 95 | 1080-7mbps00042.ts 96 | #EXTINF:11.261233, 97 | 1080-7mbps00043.ts 98 | #EXTINF:7.507478, 99 | 1080-7mbps00044.ts 100 | #EXTINF:11.261233, 101 | 1080-7mbps00045.ts 102 | #EXTINF:11.261233, 103 | 1080-7mbps00046.ts 104 | #EXTINF:7.507489, 105 | 1080-7mbps00047.ts 106 | #EXTINF:11.261233, 107 | 1080-7mbps00048.ts 108 | #EXTINF:11.261222, 109 | 1080-7mbps00049.ts 110 | #EXTINF:7.507489, 111 | 1080-7mbps00050.ts 112 | #EXTINF:11.261233, 113 | 1080-7mbps00051.ts 114 | #EXTINF:11.261233, 115 | 1080-7mbps00052.ts 116 | #EXTINF:7.507489, 117 | 1080-7mbps00053.ts 118 | #EXTINF:11.261233, 119 | 1080-7mbps00054.ts 120 | #EXTINF:11.261233, 121 | 1080-7mbps00055.ts 122 | #EXTINF:7.507478, 123 | 1080-7mbps00056.ts 124 | #EXTINF:11.261233, 125 | 1080-7mbps00057.ts 126 | #EXTINF:11.261233, 127 | 1080-7mbps00058.ts 128 | #EXTINF:7.507489, 129 | 1080-7mbps00059.ts 130 | #EXTINF:11.261233, 131 | 1080-7mbps00060.ts 132 | #EXTINF:11.261222, 133 | 1080-7mbps00061.ts 134 | #EXTINF:7.507489, 135 | 1080-7mbps00062.ts 136 | #EXTINF:11.261233, 137 | 1080-7mbps00063.ts 138 | #EXTINF:11.261233, 139 | 1080-7mbps00064.ts 140 | #EXTINF:7.507489, 141 | 1080-7mbps00065.ts 142 | #EXTINF:11.261233, 143 | 1080-7mbps00066.ts 144 | #EXTINF:11.261233, 145 | 1080-7mbps00067.ts 146 | #EXTINF:7.507478, 147 | 1080-7mbps00068.ts 148 | #EXTINF:11.261233, 149 | 1080-7mbps00069.ts 150 | #EXTINF:11.261233, 151 | 1080-7mbps00070.ts 152 | #EXTINF:7.507489, 153 | 1080-7mbps00071.ts 154 | #EXTINF:11.261233, 155 | 1080-7mbps00072.ts 156 | #EXTINF:11.261233, 157 | 1080-7mbps00073.ts 158 | #EXTINF:7.507489, 159 | 1080-7mbps00074.ts 160 | #EXTINF:11.261222, 161 | 1080-7mbps00075.ts 162 | #EXTINF:11.261233, 163 | 1080-7mbps00076.ts 164 | #EXTINF:7.507489, 165 | 1080-7mbps00077.ts 166 | #EXTINF:11.261233, 167 | 1080-7mbps00078.ts 168 | #EXTINF:11.261233, 169 | 1080-7mbps00079.ts 170 | #EXTINF:7.507478, 171 | 1080-7mbps00080.ts 172 | #EXTINF:11.261233, 173 | 1080-7mbps00081.ts 174 | #EXTINF:11.261233, 175 | 1080-7mbps00082.ts 176 | #EXTINF:7.507489, 177 | 1080-7mbps00083.ts 178 | #EXTINF:11.261233, 179 | 1080-7mbps00084.ts 180 | #EXTINF:11.261233, 181 | 1080-7mbps00085.ts 182 | #EXTINF:7.507489, 183 | 1080-7mbps00086.ts 184 | #EXTINF:11.261222, 185 | 1080-7mbps00087.ts 186 | #EXTINF:11.261233, 187 | 1080-7mbps00088.ts 188 | #EXTINF:7.507489, 189 | 1080-7mbps00089.ts 190 | #EXTINF:11.261233, 191 | 1080-7mbps00090.ts 192 | #EXTINF:11.261233, 193 | 1080-7mbps00091.ts 194 | #EXTINF:7.507478, 195 | 1080-7mbps00092.ts 196 | #EXTINF:11.261233, 197 | 1080-7mbps00093.ts 198 | #EXTINF:11.261233, 199 | 1080-7mbps00094.ts 200 | #EXTINF:7.507489, 201 | 1080-7mbps00095.ts 202 | #EXTINF:11.261233, 203 | 1080-7mbps00096.ts 204 | #EXTINF:11.261233, 205 | 1080-7mbps00097.ts 206 | #EXTINF:7.507489, 207 | 1080-7mbps00098.ts 208 | #EXTINF:11.261222, 209 | 1080-7mbps00099.ts 210 | #EXTINF:11.261233, 211 | 1080-7mbps00100.ts 212 | #EXTINF:7.507489, 213 | 1080-7mbps00101.ts 214 | #EXTINF:11.261233, 215 | 1080-7mbps00102.ts 216 | #EXTINF:11.261233, 217 | 1080-7mbps00103.ts 218 | #EXTINF:7.507478, 219 | 1080-7mbps00104.ts 220 | #EXTINF:11.261233, 221 | 1080-7mbps00105.ts 222 | #EXTINF:11.261233, 223 | 1080-7mbps00106.ts 224 | #EXTINF:7.507489, 225 | 1080-7mbps00107.ts 226 | #EXTINF:11.261233, 227 | 1080-7mbps00108.ts 228 | #EXTINF:11.261233, 229 | 1080-7mbps00109.ts 230 | #EXTINF:7.507489, 231 | 1080-7mbps00110.ts 232 | #EXTINF:11.261233, 233 | 1080-7mbps00111.ts 234 | #EXTINF:11.261233, 235 | 1080-7mbps00112.ts 236 | #EXTINF:7.507478, 237 | 1080-7mbps00113.ts 238 | #EXTINF:11.261233, 239 | 1080-7mbps00114.ts 240 | #EXTINF:11.261233, 241 | 1080-7mbps00115.ts 242 | #EXTINF:7.507478, 243 | 1080-7mbps00116.ts 244 | #EXTINF:11.261233, 245 | 1080-7mbps00117.ts 246 | #EXTINF:7.507478, 247 | 1080-7mbps00118.ts 248 | #EXTINF:11.261233, 249 | 1080-7mbps00119.ts 250 | #EXTINF:11.261233, 251 | 1080-7mbps00120.ts 252 | #EXTINF:7.507489, 253 | 1080-7mbps00121.ts 254 | #EXTINF:11.261233, 255 | 1080-7mbps00122.ts 256 | #EXTINF:11.261233, 257 | 1080-7mbps00123.ts 258 | #EXTINF:7.507489, 259 | 1080-7mbps00124.ts 260 | #EXTINF:11.261222, 261 | 1080-7mbps00125.ts 262 | #EXTINF:11.261233, 263 | 1080-7mbps00126.ts 264 | #EXTINF:7.507489, 265 | 1080-7mbps00127.ts 266 | #EXTINF:11.261233, 267 | 1080-7mbps00128.ts 268 | #EXTINF:11.261233, 269 | 1080-7mbps00129.ts 270 | #EXTINF:7.507478, 271 | 1080-7mbps00130.ts 272 | #EXTINF:11.261233, 273 | 1080-7mbps00131.ts 274 | #EXTINF:11.261233, 275 | 1080-7mbps00132.ts 276 | #EXTINF:7.507489, 277 | 1080-7mbps00133.ts 278 | #EXTINF:11.261233, 279 | 1080-7mbps00134.ts 280 | #EXTINF:11.261233, 281 | 1080-7mbps00135.ts 282 | #EXTINF:7.507489, 283 | 1080-7mbps00136.ts 284 | #EXTINF:1.793444, 285 | 1080-7mbps00137.ts 286 | #EXT-X-ENDLIST 287 | -------------------------------------------------------------------------------- /test/fixtures/playlistWithComments.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-PLAYLIST-TYPE:VOD 3 | #EXT-X-VERSION:4 4 | #EXT-X-MEDIA-SEQUENCE:1 5 | #EXT-X-ALLOW-CACHE:NO 6 | #EXT-X-TARGETDURATION:12 7 | #EXTINF:11.344644,anything 8 | 1080-7mbps00000.ts 9 | #EXT-X-DISCONTINUITY 10 | #EXTINF:11.261233,anything 11 | 1080-7mbps00001.ts 12 | #EXTINF:7.507489,anything 13 | 1080-7mbps00002.ts 14 | #EXTINF:11.261233,anything 15 | 1080-7mbps00003.ts 16 | #EXTINF:11.261233,anything 17 | 1080-7mbps00004.ts 18 | #EXTINF:7.507489,anything 19 | 1080-7mbps00005.ts 20 | #EXTINF:11.261233,anything 21 | 1080-7mbps00006.ts 22 | #EXTINF:11.261233,anything 23 | 1080-7mbps00007.ts 24 | #EXTINF:7.507478,anything 25 | 1080-7mbps00008.ts 26 | #EXTINF:11.261233,anything 27 | 1080-7mbps00009.ts 28 | #EXTINF:11.261233,anything 29 | 1080-7mbps00010.ts 30 | #EXTINF:7.507478,anything 31 | 1080-7mbps00011.ts 32 | #EXTINF:11.261233,anything 33 | 1080-7mbps00012.ts 34 | #EXTINF:11.261233,anything 35 | 1080-7mbps00013.ts 36 | #EXTINF:7.507489,anything 37 | 1080-7mbps00014.ts 38 | #EXTINF:11.261233,anything 39 | 1080-7mbps00015.ts 40 | #EXTINF:11.261233,anything 41 | 1080-7mbps00016.ts 42 | #EXTINF:7.507489,anything 43 | 1080-7mbps00017.ts 44 | #EXTINF:11.261233,anything 45 | 1080-7mbps00018.ts 46 | #EXTINF:11.261233,anything 47 | 1080-7mbps00019.ts 48 | #EXTINF:7.507478,anything 49 | 1080-7mbps00020.ts 50 | #EXTINF:11.261233,anything 51 | 1080-7mbps00021.ts 52 | #EXTINF:11.261233,anything 53 | 1080-7mbps00022.ts 54 | #EXTINF:7.507478,anything 55 | 1080-7mbps00023.ts 56 | #EXTINF:11.261233,anything 57 | 1080-7mbps00024.ts 58 | #EXTINF:11.261233,anything 59 | 1080-7mbps00025.ts 60 | #EXTINF:7.507489,anything 61 | 1080-7mbps00026.ts 62 | #EXTINF:11.261233,anything 63 | 1080-7mbps00027.ts 64 | #EXTINF:11.261233,anything 65 | 1080-7mbps00028.ts 66 | #EXTINF:7.507489,anything 67 | 1080-7mbps00029.ts 68 | #EXTINF:11.261233,anything 69 | 1080-7mbps00030.ts 70 | #EXTINF:11.261233,anything 71 | 1080-7mbps00031.ts 72 | #EXTINF:7.507478,anything 73 | 1080-7mbps00032.ts 74 | #EXTINF:11.261233,anything 75 | 1080-7mbps00033.ts 76 | #EXTINF:11.261233,anything 77 | 1080-7mbps00034.ts 78 | #EXTINF:7.507489,anything 79 | 1080-7mbps00035.ts 80 | #EXTINF:11.261233,anything 81 | 1080-7mbps00036.ts 82 | #EXTINF:11.261222,anything 83 | 1080-7mbps00037.ts 84 | #EXTINF:7.507489,anything 85 | 1080-7mbps00038.ts 86 | #EXTINF:11.261233,anything 87 | 1080-7mbps00039.ts 88 | #EXTINF:11.261233,anything 89 | 1080-7mbps00040.ts 90 | #EXTINF:7.507489,anything 91 | 1080-7mbps00041.ts 92 | #EXTINF:11.261233,anything 93 | 1080-7mbps00042.ts 94 | #EXTINF:11.261233,anything 95 | 1080-7mbps00043.ts 96 | #EXTINF:7.507478,anything 97 | 1080-7mbps00044.ts 98 | #EXTINF:11.261233,anything 99 | 1080-7mbps00045.ts 100 | #EXTINF:11.261233,anything 101 | 1080-7mbps00046.ts 102 | #EXTINF:7.507489,anything 103 | 1080-7mbps00047.ts 104 | #EXTINF:11.261233,anything 105 | 1080-7mbps00048.ts 106 | #EXTINF:11.261222,anything 107 | 1080-7mbps00049.ts 108 | #EXTINF:7.507489,anything 109 | 1080-7mbps00050.ts 110 | #EXTINF:11.261233,anything 111 | 1080-7mbps00051.ts 112 | #EXTINF:11.261233,anything 113 | 1080-7mbps00052.ts 114 | #EXTINF:7.507489,anything 115 | 1080-7mbps00053.ts 116 | #EXTINF:11.261233,anything 117 | 1080-7mbps00054.ts 118 | #EXTINF:11.261233,anything 119 | 1080-7mbps00055.ts 120 | #EXTINF:7.507478,anything 121 | 1080-7mbps00056.ts 122 | #EXTINF:11.261233,anything 123 | 1080-7mbps00057.ts 124 | #EXTINF:11.261233,anything 125 | 1080-7mbps00058.ts 126 | #EXTINF:7.507489,anything 127 | 1080-7mbps00059.ts 128 | #EXTINF:11.261233,anything 129 | 1080-7mbps00060.ts 130 | #EXTINF:11.261222,anything 131 | 1080-7mbps00061.ts 132 | #EXTINF:7.507489,anything 133 | 1080-7mbps00062.ts 134 | #EXTINF:11.261233,anything 135 | 1080-7mbps00063.ts 136 | #EXTINF:11.261233,anything 137 | 1080-7mbps00064.ts 138 | #EXTINF:7.507489,anything 139 | 1080-7mbps00065.ts 140 | #EXTINF:11.261233,anything 141 | 1080-7mbps00066.ts 142 | #EXTINF:11.261233,anything 143 | 1080-7mbps00067.ts 144 | #EXTINF:7.507478,anything 145 | 1080-7mbps00068.ts 146 | #EXTINF:11.261233,anything 147 | 1080-7mbps00069.ts 148 | #EXTINF:11.261233,anything 149 | 1080-7mbps00070.ts 150 | #EXTINF:7.507489,anything 151 | 1080-7mbps00071.ts 152 | #EXTINF:11.261233,anything 153 | 1080-7mbps00072.ts 154 | #EXTINF:11.261233,anything 155 | 1080-7mbps00073.ts 156 | #EXTINF:7.507489,anything 157 | 1080-7mbps00074.ts 158 | #EXTINF:11.261222,anything 159 | 1080-7mbps00075.ts 160 | #EXTINF:11.261233,anything 161 | 1080-7mbps00076.ts 162 | #EXTINF:7.507489,anything 163 | 1080-7mbps00077.ts 164 | #EXTINF:11.261233,anything 165 | 1080-7mbps00078.ts 166 | #EXTINF:11.261233,anything 167 | 1080-7mbps00079.ts 168 | #EXTINF:7.507478,anything 169 | 1080-7mbps00080.ts 170 | #EXTINF:11.261233,anything 171 | 1080-7mbps00081.ts 172 | #EXTINF:11.261233,anything 173 | 1080-7mbps00082.ts 174 | #EXTINF:7.507489,anything 175 | 1080-7mbps00083.ts 176 | #EXTINF:11.261233,anything 177 | 1080-7mbps00084.ts 178 | #EXTINF:11.261233,anything 179 | 1080-7mbps00085.ts 180 | #EXTINF:7.507489,anything 181 | 1080-7mbps00086.ts 182 | #EXTINF:11.261222,anything 183 | 1080-7mbps00087.ts 184 | #EXTINF:11.261233,anything 185 | 1080-7mbps00088.ts 186 | #EXTINF:7.507489,anything 187 | 1080-7mbps00089.ts 188 | #EXTINF:11.261233,anything 189 | 1080-7mbps00090.ts 190 | #EXTINF:11.261233,anything 191 | 1080-7mbps00091.ts 192 | #EXTINF:7.507478,anything 193 | 1080-7mbps00092.ts 194 | #EXTINF:11.261233,anything 195 | 1080-7mbps00093.ts 196 | #EXTINF:11.261233,anything 197 | 1080-7mbps00094.ts 198 | #EXTINF:7.507489,anything 199 | 1080-7mbps00095.ts 200 | #EXTINF:11.261233,anything 201 | 1080-7mbps00096.ts 202 | #EXTINF:11.261233,anything 203 | 1080-7mbps00097.ts 204 | #EXTINF:7.507489,anything 205 | 1080-7mbps00098.ts 206 | #EXTINF:11.261222,anything 207 | 1080-7mbps00099.ts 208 | #EXTINF:11.261233,anything 209 | 1080-7mbps00100.ts 210 | #EXTINF:7.507489,anything 211 | 1080-7mbps00101.ts 212 | #EXTINF:11.261233,anything 213 | 1080-7mbps00102.ts 214 | #EXTINF:11.261233,anything 215 | 1080-7mbps00103.ts 216 | #EXTINF:7.507478,anything 217 | 1080-7mbps00104.ts 218 | #EXTINF:11.261233,anything 219 | 1080-7mbps00105.ts 220 | #EXTINF:11.261233,anything 221 | 1080-7mbps00106.ts 222 | #EXTINF:7.507489,anything 223 | 1080-7mbps00107.ts 224 | #EXTINF:11.261233,anything 225 | 1080-7mbps00108.ts 226 | #EXTINF:11.261233,anything 227 | 1080-7mbps00109.ts 228 | #EXTINF:7.507489,anything 229 | 1080-7mbps00110.ts 230 | #EXTINF:11.261233,anything 231 | 1080-7mbps00111.ts 232 | #EXTINF:11.261233,anything 233 | 1080-7mbps00112.ts 234 | #EXTINF:7.507478,anything 235 | 1080-7mbps00113.ts 236 | #EXTINF:11.261233,anything 237 | 1080-7mbps00114.ts 238 | #EXTINF:11.261233,anything 239 | 1080-7mbps00115.ts 240 | #EXTINF:7.507478,anything 241 | 1080-7mbps00116.ts 242 | #EXTINF:11.261233,anything 243 | 1080-7mbps00117.ts 244 | #EXTINF:7.507478,anything 245 | 1080-7mbps00118.ts 246 | #EXTINF:11.261233,anything 247 | 1080-7mbps00119.ts 248 | #EXTINF:11.261233,anything 249 | 1080-7mbps00120.ts 250 | #EXTINF:7.507489,anything 251 | 1080-7mbps00121.ts 252 | #EXTINF:11.261233,anything 253 | 1080-7mbps00122.ts 254 | #EXTINF:11.261233,anything 255 | 1080-7mbps00123.ts 256 | #EXTINF:7.507489,anything 257 | 1080-7mbps00124.ts 258 | #EXTINF:11.261222,anything 259 | 1080-7mbps00125.ts 260 | #EXTINF:11.261233,anything 261 | 1080-7mbps00126.ts 262 | #EXTINF:7.507489,anything 263 | 1080-7mbps00127.ts 264 | #EXTINF:11.261233,anything 265 | 1080-7mbps00128.ts 266 | #EXTINF:11.261233,anything 267 | 1080-7mbps00129.ts 268 | #EXTINF:7.507478,anything 269 | 1080-7mbps00130.ts 270 | #EXTINF:11.261233,anything 271 | 1080-7mbps00131.ts 272 | #EXTINF:11.261233,anything 273 | 1080-7mbps00132.ts 274 | #EXTINF:7.507489,anything 275 | 1080-7mbps00133.ts 276 | #EXTINF:11.261233,anything 277 | 1080-7mbps00134.ts 278 | #EXTINF:11.261233,anything 279 | 1080-7mbps00135.ts 280 | #EXTINF:7.507489,anything 281 | 1080-7mbps00136.ts 282 | #EXTINF:1.793444,anything 283 | 1080-7mbps00137.ts 284 | #EXT-X-ENDLIST 285 | -------------------------------------------------------------------------------- /test/fixtures/sessionData.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-SESSION-DATA:DATA-ID="com.example.lyrics",URI="lyrics.json" 3 | 4 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" 5 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="sp",VALUE="Este es un ejemplo" 6 | -------------------------------------------------------------------------------- /test/fixtures/timestampPlaylist.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:2 3 | #EXT-X-TARGETDURATION:10 4 | #EXT-X-MEDIA-SEQUENCE:1736515 5 | #EXTINF:10, no desc 6 | #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:24:31Z 7 | 20160408T084506-01-1736515.ts 8 | #EXTINF:10, no desc 9 | #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:24:41Z 10 | 20160408T084506-01-1736516.ts 11 | #EXTINF:10, no desc 12 | #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:24:51Z 13 | 20160408T084506-01-1736517.ts 14 | #EXTINF:10, no desc 15 | #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:25:01Z 16 | 20160408T084506-01-1736518.ts 17 | #EXTINF:10, no desc 18 | #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:25:11Z 19 | 20160408T084506-01-1736519.ts 20 | #EXTINF:10, no desc 21 | #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:25:21Z 22 | 20160408T084506-01-1736520.ts 23 | -------------------------------------------------------------------------------- /test/fixtures/variantAngles.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES 3 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/200kbs/prog_index.m3u8" 4 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/200kbs/prog_index.m3u8" 5 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES 6 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/500kbs/prog_index.m3u8" 7 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/500kbs/prog_index.m3u8" 8 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8" 9 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Subtiles",AUTOSELECT=NO,DEFAULT=NO,URI="titles_file" 10 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="captions",NAME="Closed Captions",AUTOSELECT=NO,DEFAULT=NO,URI="captions_file" 11 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="200kbs",AUDIO="aac",AVERAGE-BANDWIDTH=300001,SUBTITLES="subs",CLOSED-CAPTIONS="captions" 12 | Angle1/200kbs/prog_index.m3u8 13 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="500kbs",AUDIO="aac" 14 | Angle1/500kbs/prog_index.m3u8 15 | -------------------------------------------------------------------------------- /test/fixtures/variantAudio.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="eng",ASSOC-LANGUAGE="spoken",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="englo/prog_index.m3u8",FORCED=YES 3 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES,DEFAULT=NO,URI="frelo/prog_index.m3u8" 4 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES,DEFAULT=NO,URI="splo/prog_index.m3u8" 5 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8" 6 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES,DEFAULT=NO,URI="fre/prog_index.m3u8" 7 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES,DEFAULT=NO,URI="sp/prog_index.m3u8" 8 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5",AUDIO="audio-lo" 9 | lo/prog_index.m3u8 10 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2",AUDIO="audio-lo" 11 | hi/prog_index.m3u8 12 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e",AUDIO="audio-hi" 13 | lo/prog_index.m3u8 14 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2",AUDIO="audio-hi" 15 | hi/prog_index.m3u8 16 | -------------------------------------------------------------------------------- /test/keyItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestKeyItem_Parse(t *testing.T) { 11 | line := `#EXT-X-KEY:METHOD=AES-128,URI="http://test.key",IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3"` 12 | 13 | ki, err := m3u8.NewKeyItem(line) 14 | assert.Nil(t, err) 15 | assert.NotNil(t, ki.Encryptable) 16 | assert.Equal(t, "AES-128", ki.Encryptable.Method) 17 | assertNotNilEqual(t, "http://test.key", ki.Encryptable.URI) 18 | assertNotNilEqual(t, "D512BBF", ki.Encryptable.IV) 19 | assertNotNilEqual(t, "identity", ki.Encryptable.KeyFormat) 20 | assertNotNilEqual(t, "1/3", ki.Encryptable.KeyFormatVersions) 21 | 22 | assertToString(t, line, ki) 23 | } 24 | -------------------------------------------------------------------------------- /test/mapItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMapItem_Parse(t *testing.T) { 11 | line := `#EXT-X-MAP:URI="frelo/prog_index.m3u8",BYTERANGE="3500@300"` 12 | 13 | mi, err := m3u8.NewMapItem(line) 14 | assert.Nil(t, err) 15 | assert.Equal(t, "frelo/prog_index.m3u8", mi.URI) 16 | assert.NotNil(t, mi.ByteRange) 17 | assertNotNilEqual(t, 3500, mi.ByteRange.Length) 18 | assertNotNilEqual(t, 300, mi.ByteRange.Start) 19 | 20 | assertToString(t, line, mi) 21 | } 22 | 23 | func TestMapItem_Parse_2(t *testing.T) { 24 | line := `#EXT-X-MAP:URI="frelo/prog_index.m3u8"` 25 | 26 | mi, err := m3u8.NewMapItem(line) 27 | assert.Nil(t, err) 28 | assert.Equal(t, "frelo/prog_index.m3u8", mi.URI) 29 | assert.Nil(t, mi.ByteRange) 30 | 31 | assertToString(t, line, mi) 32 | } 33 | -------------------------------------------------------------------------------- /test/mediaItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMediaItem_Parse(t *testing.T) { 11 | line := `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre", 12 | ASSOC-LANGUAGE="spoken",NAME="Francais",AUTOSELECT=YES, 13 | INSTREAM-ID="SERVICE3",CHARACTERISTICS="public.html", 14 | CHANNELS="6", 15 | "DEFAULT=NO,URI="frelo/prog_index.m3u8",STABLE-RENDITION-ID="1234",FORCED=YES 16 | "` 17 | 18 | mi, err := m3u8.NewMediaItem(line) 19 | assert.Nil(t, err) 20 | assert.Equal(t, "AUDIO", mi.Type) 21 | assert.Equal(t, "audio-lo", mi.GroupID) 22 | assert.Equal(t, "Francais", mi.Name) 23 | 24 | assertNotNilEqual(t, "1234", mi.StableRenditionId) 25 | assertNotNilEqual(t, "fre", mi.Language) 26 | assertNotNilEqual(t, "spoken", mi.AssocLanguage) 27 | assertNotNilEqual(t, true, mi.AutoSelect) 28 | assertNotNilEqual(t, false, mi.Default) 29 | assertNotNilEqual(t, "frelo/prog_index.m3u8", mi.URI) 30 | assertNotNilEqual(t, true, mi.Forced) 31 | assertNotNilEqual(t, "SERVICE3", mi.InStreamID) 32 | assertNotNilEqual(t, "public.html", mi.Characteristics) 33 | assertNotNilEqual(t, "6", mi.Channels) 34 | 35 | expected := "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio-lo\",LANGUAGE=\"fre\",ASSOC-LANGUAGE=\"spoken\",NAME=\"Francais\",AUTOSELECT=YES,DEFAULT=NO,URI=\"frelo/prog_index.m3u8\",FORCED=YES,INSTREAM-ID=\"SERVICE3\",CHARACTERISTICS=\"public.html\",CHANNELS=\"6\",STABLE-RENDITION-ID=\"1234\"" 36 | assertToString(t, expected, mi) 37 | } 38 | -------------------------------------------------------------------------------- /test/playbackStart_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPlaybackStart_Parse(t *testing.T) { 11 | line := `#EXT-X-START:TIME-OFFSET=20.2,PRECISE=YES` 12 | 13 | ps, err := m3u8.NewPlaybackStart(line) 14 | assert.Nil(t, err) 15 | assert.Equal(t, 20.2, ps.TimeOffset) 16 | assertNotNilEqual(t, true, ps.Precise) 17 | 18 | assertToString(t, line, ps) 19 | } 20 | 21 | func TestPlaybackStart_Parse_2(t *testing.T) { 22 | line := `#EXT-X-START:TIME-OFFSET=-12.9` 23 | 24 | ps, err := m3u8.NewPlaybackStart(line) 25 | assert.Nil(t, err) 26 | assert.Equal(t, -12.9, ps.TimeOffset) 27 | assert.Nil(t, ps.Precise) 28 | 29 | assertToString(t, line, ps) 30 | } 31 | -------------------------------------------------------------------------------- /test/playlistItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/AlekSi/pointer" 7 | "github.com/etherlabsio/go-m3u8/m3u8" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestPlaylistItem_Parse(t *testing.T) { 12 | line := `#EXT-X-STREAM-INF:CODECS="avc",BANDWIDTH=540, 13 | PROGRAM-ID=1,RESOLUTION=1920x1080,FRAME-RATE=23.976, 14 | AVERAGE-BANDWIDTH=550,AUDIO="test",VIDEO="test2",STABLE-VARIANT-ID="1234" 15 | SUBTITLES="subs",CLOSED-CAPTIONS="caps",URI="test.url", 16 | NAME="1080p",HDCP-LEVEL=TYPE-0` 17 | 18 | pi, err := m3u8.NewPlaylistItem(line) 19 | assert.Nil(t, err) 20 | assertNotNilEqual(t, "1", pi.ProgramID) 21 | assertNotNilEqual(t, "avc", pi.Codecs) 22 | assert.Equal(t, 540, pi.Bandwidth) 23 | assertNotNilEqual(t, 550, pi.AverageBandwidth) 24 | assertNotNilEqual(t, 1920, pi.Width) 25 | assertNotNilEqual(t, 1080, pi.Height) 26 | assertNotNilEqual(t, 23.976, pi.FrameRate) 27 | assertNotNilEqual(t, "test", pi.Audio) 28 | assertNotNilEqual(t, "test2", pi.Video) 29 | assertNotNilEqual(t, "subs", pi.Subtitles) 30 | assertNotNilEqual(t, "caps", pi.ClosedCaptions) 31 | assert.Equal(t, "test.url", pi.URI) 32 | assertNotNilEqual(t, "1080p", pi.Name) 33 | assert.False(t, pi.IFrame) 34 | assertNotNilEqual(t, "TYPE-0", pi.HDCPLevel) 35 | assertNotNilEqual(t, "1234", pi.StableVariantID) 36 | } 37 | 38 | func TestPlaylistItem_ToString(t *testing.T) { 39 | // No codecs specified 40 | p := &m3u8.PlaylistItem{ 41 | Bandwidth: 540, 42 | URI: "test.url", 43 | } 44 | assert.NotContains(t, p.String(), "CODECS") 45 | 46 | // Level not recognized 47 | p = &m3u8.PlaylistItem{ 48 | Bandwidth: 540, 49 | URI: "test.url", 50 | Level: pointer.ToString("9001"), 51 | } 52 | assert.NotContains(t, p.String(), "CODECS") 53 | 54 | // Audio codec recognized but profile not recognized 55 | p = &m3u8.PlaylistItem{ 56 | Bandwidth: 540, 57 | URI: "test.url", 58 | Profile: pointer.ToString("best"), 59 | Level: pointer.ToString("9001"), 60 | AudioCodec: pointer.ToString("aac-lc"), 61 | } 62 | assert.NotContains(t, p.String(), "CODECS") 63 | 64 | // Profile and level not set, Audio codec recognized 65 | p = &m3u8.PlaylistItem{ 66 | Bandwidth: 540, 67 | URI: "test.url", 68 | AudioCodec: pointer.ToString("aac-lc"), 69 | } 70 | assert.Contains(t, p.String(), "CODECS") 71 | 72 | // Profile and level recognized, audio codec not recognized 73 | p = &m3u8.PlaylistItem{ 74 | Bandwidth: 540, 75 | URI: "test.url", 76 | Profile: pointer.ToString("high"), 77 | Level: pointer.ToString("4.1"), 78 | AudioCodec: pointer.ToString("fuzzy"), 79 | } 80 | assert.NotContains(t, p.String(), "CODECS") 81 | 82 | // Audio codec not set 83 | p = &m3u8.PlaylistItem{ 84 | Bandwidth: 540, 85 | URI: "test.url", 86 | Profile: pointer.ToString("high"), 87 | Level: pointer.ToString("4.1"), 88 | } 89 | assert.Contains(t, p.String(), `CODECS="avc1.640029"`) 90 | 91 | // Audio codec recognized 92 | p = &m3u8.PlaylistItem{ 93 | Bandwidth: 540, 94 | URI: "test.url", 95 | Profile: pointer.ToString("high"), 96 | Level: pointer.ToString("4.1"), 97 | AudioCodec: pointer.ToString("aac-lc"), 98 | } 99 | assert.Contains(t, p.String(), `CODECS="avc1.640029,mp4a.40.2"`) 100 | } 101 | 102 | func TestPlaylistItem_ToString_2(t *testing.T) { 103 | // All fields set 104 | p := &m3u8.PlaylistItem{ 105 | Codecs: pointer.ToString("avc"), 106 | Bandwidth: 540, 107 | URI: "test.url", 108 | Audio: pointer.ToString("test"), 109 | Video: pointer.ToString("test2"), 110 | AverageBandwidth: pointer.ToInt(500), 111 | Subtitles: pointer.ToString("subs"), 112 | FrameRate: pointer.ToFloat64(30), 113 | ClosedCaptions: pointer.ToString("caps"), 114 | Name: pointer.ToString("SD"), 115 | HDCPLevel: pointer.ToString("TYPE-0"), 116 | ProgramID: pointer.ToString("1"), 117 | StableVariantID: pointer.ToString("1234"), 118 | } 119 | 120 | expected := `#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="avc",BANDWIDTH=540,AVERAGE-BANDWIDTH=500,FRAME-RATE=30.000,HDCP-LEVEL=TYPE-0,AUDIO="test",VIDEO="test2",SUBTITLES="subs",CLOSED-CAPTIONS="caps",NAME="SD",STABLE-VARIANT-ID="1234" 121 | test.url` 122 | assert.Equal(t, expected, p.String()) 123 | 124 | // Closed captions is NONE 125 | p = &m3u8.PlaylistItem{ 126 | ProgramID: pointer.ToString("1"), 127 | Width: pointer.ToInt(1920), 128 | Height: pointer.ToInt(1080), 129 | Codecs: pointer.ToString("avc"), 130 | Bandwidth: 540, 131 | URI: "test.url", 132 | ClosedCaptions: pointer.ToString("NONE"), 133 | } 134 | 135 | expected = `#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc",BANDWIDTH=540,CLOSED-CAPTIONS=NONE 136 | test.url` 137 | assert.Equal(t, expected, p.String()) 138 | 139 | // IFrame is true 140 | p = &m3u8.PlaylistItem{ 141 | Codecs: pointer.ToString("avc"), 142 | Bandwidth: 540, 143 | URI: "test.url", 144 | IFrame: true, 145 | Video: pointer.ToString("test2"), 146 | AverageBandwidth: pointer.ToInt(550), 147 | } 148 | 149 | expected = `#EXT-X-I-FRAME-STREAM-INF:CODECS="avc",BANDWIDTH=540,AVERAGE-BANDWIDTH=550,VIDEO="test2",URI="test.url"` 150 | assert.Equal(t, expected, p.String()) 151 | } 152 | 153 | func TestPlaylistItem_GenerateCodecs(t *testing.T) { 154 | assertCodecs(t, "", &m3u8.PlaylistItem{}) 155 | assertCodecs(t, "test", &m3u8.PlaylistItem{Codecs: pointer.ToString("test")}) 156 | assertCodecs(t, "mp4a.40.2", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("aac-lc")}) 157 | assertCodecs(t, "mp4a.40.2", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("AAC-LC")}) 158 | assertCodecs(t, "mp4a.40.5", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("he-aac")}) 159 | assertCodecs(t, "", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("he-aac1")}) 160 | assertCodecs(t, "mp4a.40.34", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("mp3")}) 161 | assertCodecs(t, "avc1.66.30", &m3u8.PlaylistItem{ 162 | Profile: pointer.ToString("baseline"), 163 | Level: pointer.ToString("3.0"), 164 | }) 165 | assertCodecs(t, "avc1.66.30,mp4a.40.2", &m3u8.PlaylistItem{ 166 | Profile: pointer.ToString("baseline"), 167 | Level: pointer.ToString("3.0"), 168 | AudioCodec: pointer.ToString("aac-lc"), 169 | }) 170 | assertCodecs(t, "avc1.66.30,mp4a.40.34", &m3u8.PlaylistItem{ 171 | Profile: pointer.ToString("baseline"), 172 | Level: pointer.ToString("3.0"), 173 | AudioCodec: pointer.ToString("mp3"), 174 | }) 175 | assertCodecs(t, "avc1.42001f", &m3u8.PlaylistItem{ 176 | Profile: pointer.ToString("baseline"), 177 | Level: pointer.ToString("3.1"), 178 | }) 179 | assertCodecs(t, "avc1.42001f,mp4a.40.5", &m3u8.PlaylistItem{ 180 | Profile: pointer.ToString("baseline"), 181 | Level: pointer.ToString("3.1"), 182 | AudioCodec: pointer.ToString("he-aac"), 183 | }) 184 | assertCodecs(t, "avc1.77.30", &m3u8.PlaylistItem{ 185 | Profile: pointer.ToString("main"), 186 | Level: pointer.ToString("3.0"), 187 | }) 188 | assertCodecs(t, "avc1.77.30,mp4a.40.2", &m3u8.PlaylistItem{ 189 | Profile: pointer.ToString("main"), 190 | Level: pointer.ToString("3.0"), 191 | AudioCodec: pointer.ToString("aac-lc"), 192 | }) 193 | assertCodecs(t, "avc1.4d001f", &m3u8.PlaylistItem{ 194 | Profile: pointer.ToString("main"), 195 | Level: pointer.ToString("3.1"), 196 | }) 197 | assertCodecs(t, "avc1.4d0028", &m3u8.PlaylistItem{ 198 | Profile: pointer.ToString("main"), 199 | Level: pointer.ToString("4.0"), 200 | }) 201 | assertCodecs(t, "avc1.4d0029", &m3u8.PlaylistItem{ 202 | Profile: pointer.ToString("main"), 203 | Level: pointer.ToString("4.1"), 204 | }) 205 | assertCodecs(t, "avc1.64001f", &m3u8.PlaylistItem{ 206 | Profile: pointer.ToString("high"), 207 | Level: pointer.ToString("3.1"), 208 | }) 209 | assertCodecs(t, "avc1.640028", &m3u8.PlaylistItem{ 210 | Profile: pointer.ToString("high"), 211 | Level: pointer.ToString("4.0"), 212 | }) 213 | assertCodecs(t, "avc1.640029", &m3u8.PlaylistItem{ 214 | Profile: pointer.ToString("high"), 215 | Level: pointer.ToString("4.1"), 216 | }) 217 | } 218 | 219 | func assertCodecs(t *testing.T, codecs string, p *m3u8.PlaylistItem) { 220 | assert.Equal(t, codecs, p.CodecsString()) 221 | } 222 | -------------------------------------------------------------------------------- /test/playlist_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/AlekSi/pointer" 8 | "github.com/etherlabsio/go-m3u8/m3u8" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestPlaylist_New(t *testing.T) { 13 | p := &m3u8.Playlist{Master: pointer.ToBool(true)} 14 | assert.True(t, p.IsMaster()) 15 | 16 | p, err := m3u8.ReadFile("fixtures/master.m3u8") 17 | assert.Nil(t, err) 18 | assert.True(t, p.IsMaster()) 19 | assert.Equal(t, len(p.Items), 8) 20 | } 21 | 22 | func TestPlaylist_Duration(t *testing.T) { 23 | p := &m3u8.Playlist{ 24 | Items: []m3u8.Item{ 25 | &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, 26 | &m3u8.SegmentItem{Duration: 9.891, Segment: "test_02.ts"}, 27 | &m3u8.SegmentItem{Duration: 10.556, Segment: "test_03.ts"}, 28 | &m3u8.SegmentItem{Duration: 8.790, Segment: "test_04.ts"}, 29 | }, 30 | } 31 | 32 | assert.Equal(t, "40.228", fmt.Sprintf("%.3f", p.Duration())) 33 | } 34 | 35 | func TestPlaylist_Master(t *testing.T) { 36 | // Normal master playlist 37 | p := &m3u8.Playlist{ 38 | Items: []m3u8.Item{ 39 | &m3u8.PlaylistItem{ 40 | ProgramID: pointer.ToString("1"), 41 | URI: "playlist_url", 42 | Bandwidth: 6400, 43 | AudioCodec: pointer.ToString("mp3"), 44 | }, 45 | }, 46 | } 47 | assert.True(t, p.IsMaster()) 48 | 49 | // Media playlist 50 | p = &m3u8.Playlist{ 51 | Items: []m3u8.Item{ 52 | &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, 53 | }, 54 | } 55 | assert.False(t, p.IsMaster()) 56 | 57 | // Forced master tag 58 | p = &m3u8.Playlist{ 59 | Master: pointer.ToBool(true), 60 | } 61 | assert.True(t, p.IsMaster()) 62 | } 63 | 64 | func TestPlaylist_Live(t *testing.T) { 65 | // Normal master playlist 66 | p := &m3u8.Playlist{ 67 | Items: []m3u8.Item{ 68 | &m3u8.PlaylistItem{ 69 | ProgramID: pointer.ToString("1"), 70 | URI: "playlist_url", 71 | Bandwidth: 6400, 72 | AudioCodec: pointer.ToString("mp3"), 73 | }, 74 | }, 75 | } 76 | assert.False(t, p.IsLive()) 77 | 78 | // Media playlist set as live 79 | p = &m3u8.Playlist{ 80 | Items: []m3u8.Item{ 81 | &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, 82 | }, 83 | Live: true, 84 | } 85 | assert.True(t, p.IsLive()) 86 | } 87 | 88 | func TestPlaylist_ToString(t *testing.T) { 89 | p := &m3u8.Playlist{ 90 | Items: []m3u8.Item{ 91 | &m3u8.PlaylistItem{ 92 | ProgramID: pointer.ToString("1"), 93 | URI: "playlist_url", 94 | Bandwidth: 6400, 95 | AudioCodec: pointer.ToString("mp3"), 96 | }, 97 | &m3u8.PlaylistItem{ 98 | ProgramID: pointer.ToString("2"), 99 | URI: "playlist_url", 100 | Bandwidth: 50000, 101 | Width: pointer.ToInt(1920), 102 | Height: pointer.ToInt(1080), 103 | Profile: pointer.ToString("high"), 104 | Level: pointer.ToString("4.1"), 105 | AudioCodec: pointer.ToString("aac-lc"), 106 | }, 107 | }, 108 | } 109 | 110 | expected := `#EXTM3U 111 | #EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",BANDWIDTH=6400 112 | playlist_url 113 | #EXT-X-STREAM-INF:PROGRAM-ID=2,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",BANDWIDTH=50000 114 | playlist_url 115 | ` 116 | 117 | assert.Equal(t, expected, p.String()) 118 | 119 | p = m3u8.NewPlaylistWithItems( 120 | []m3u8.Item{ 121 | &m3u8.SegmentItem{Duration: 11.344644, Segment: "1080-7mbps00000.ts"}, 122 | &m3u8.SegmentItem{Duration: 11.261233, Segment: "1080-7mbps00001.ts"}, 123 | }, 124 | ) 125 | expected = `#EXTM3U 126 | #EXT-X-MEDIA-SEQUENCE:0 127 | #EXT-X-TARGETDURATION:10 128 | #EXTINF:11.344644, 129 | 1080-7mbps00000.ts 130 | #EXTINF:11.261233, 131 | 1080-7mbps00001.ts 132 | #EXT-X-ENDLIST 133 | ` 134 | 135 | assert.Equal(t, expected, p.String()) 136 | } 137 | 138 | func TestPlaylist_Valid(t *testing.T) { 139 | p := m3u8.NewPlaylist() 140 | assert.True(t, p.IsValid()) 141 | 142 | p.AppendItem(&m3u8.PlaylistItem{ 143 | ProgramID: pointer.ToString("1"), 144 | URI: "playlist_url", 145 | Bandwidth: 540, 146 | Width: pointer.ToInt(1920), 147 | Height: pointer.ToInt(1080), 148 | Codecs: pointer.ToString("avc"), 149 | }) 150 | 151 | assert.True(t, p.IsValid()) 152 | assert.Equal(t, 1, len(p.Items)) 153 | 154 | p.AppendItem(&m3u8.PlaylistItem{ 155 | ProgramID: pointer.ToString("1"), 156 | URI: "playlist_url", 157 | Bandwidth: 540, 158 | Width: pointer.ToInt(1920), 159 | Height: pointer.ToInt(1080), 160 | Codecs: pointer.ToString("avc"), 161 | }) 162 | 163 | assert.True(t, p.IsValid()) 164 | assert.Equal(t, 2, len(p.Items)) 165 | 166 | p.AppendItem(&m3u8.SegmentItem{ 167 | Duration: 10.991, 168 | Segment: "test.ts", 169 | }) 170 | 171 | assert.False(t, p.IsValid()) 172 | } 173 | 174 | func TestPlaylist_PlaylistSize(t *testing.T) { 175 | p := m3u8.NewPlaylist() 176 | assert.True(t, p.IsValid()) 177 | 178 | p.AppendItem(&m3u8.PlaylistItem{ 179 | ProgramID: pointer.ToString("1"), 180 | URI: "playlist0_url", 181 | Bandwidth: 540, 182 | Width: pointer.ToInt(1920), 183 | Height: pointer.ToInt(1080), 184 | Codecs: pointer.ToString("avc"), 185 | }) 186 | 187 | p.AppendItem(&m3u8.PlaylistItem{ 188 | ProgramID: pointer.ToString("1"), 189 | URI: "playlist1_url", 190 | Bandwidth: 540, 191 | Width: pointer.ToInt(1920), 192 | Height: pointer.ToInt(1080), 193 | Codecs: pointer.ToString("avc"), 194 | }) 195 | 196 | assert.Equal(t, 2, p.PlaylistSize()) 197 | pi := p.Playlists() 198 | assert.Equal(t, "playlist0_url", pi[0].URI) 199 | assert.Equal(t, "playlist1_url", pi[1].URI) 200 | 201 | } 202 | 203 | func TestPlaylist_Segments(t *testing.T) { 204 | p := &m3u8.Playlist{ 205 | Items: []m3u8.Item{ 206 | &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, 207 | &m3u8.SegmentItem{Duration: 9.891, Segment: "test_02.ts"}, 208 | &m3u8.SegmentItem{Duration: 10.556, Segment: "test_03.ts"}, 209 | &m3u8.SegmentItem{Duration: 8.790, Segment: "test_04.ts"}, 210 | }, 211 | } 212 | 213 | assert.Equal(t, 4, p.SegmentSize()) 214 | si := p.Segments() 215 | assert.Equal(t, "test_01.ts", si[0].Segment) 216 | assert.Equal(t, "test_02.ts", si[1].Segment) 217 | assert.Equal(t, 10.556, si[2].Duration) 218 | assert.Equal(t, 8.790, si[3].Duration) 219 | 220 | } 221 | -------------------------------------------------------------------------------- /test/reader_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestReader(t *testing.T) { 11 | p, err := m3u8.ReadFile("fixtures/master.m3u8") 12 | assert.Nil(t, err) 13 | assert.True(t, p.IsValid()) 14 | assert.True(t, p.IsMaster()) 15 | assert.Nil(t, p.DiscontinuitySequence) 16 | assert.True(t, p.IndependentSegments) 17 | 18 | item := p.Items[0] 19 | assert.IsType(t, &m3u8.SessionKeyItem{}, item) 20 | keyItem := item.(*m3u8.SessionKeyItem) 21 | assert.Equal(t, "AES-128", keyItem.Encryptable.Method) 22 | assertNotNilEqual(t, "https://priv.example.com/key.php?r=52", keyItem.Encryptable.URI) 23 | 24 | item = p.Items[1] 25 | assert.IsType(t, &m3u8.PlaybackStart{}, item) 26 | psi := item.(*m3u8.PlaybackStart) 27 | assert.Equal(t, 20.2, psi.TimeOffset) 28 | 29 | item = p.Items[2] 30 | assert.IsType(t, &m3u8.PlaylistItem{}, item) 31 | pi := item.(*m3u8.PlaylistItem) 32 | assert.Equal(t, "hls/1080-7mbps/1080-7mbps.m3u8", pi.URI) 33 | assertNotNilEqual(t, "1", pi.ProgramID) 34 | assertNotNilEqual(t, 1920, pi.Width) 35 | assertNotNilEqual(t, 1080, pi.Height) 36 | assert.Equal(t, "1920x1080", pi.Resolution.String()) 37 | assert.Equal(t, "avc1.640028,mp4a.40.2", pi.CodecsString()) 38 | assert.Equal(t, 5042000, pi.Bandwidth) 39 | assert.False(t, pi.IFrame) 40 | assert.Nil(t, pi.AverageBandwidth) 41 | 42 | item = p.Items[7] 43 | assert.IsType(t, &m3u8.PlaylistItem{}, item) 44 | pi = item.(*m3u8.PlaylistItem) 45 | assert.Equal(t, "hls/64k/64k.m3u8", pi.URI) 46 | assertNotNilEqual(t, "1", pi.ProgramID) 47 | assert.Nil(t, pi.Height) 48 | assert.Nil(t, pi.Width) 49 | assert.Empty(t, pi.Resolution.String()) 50 | assert.Equal(t, 6400, pi.Bandwidth) 51 | assert.False(t, pi.IFrame) 52 | assert.Nil(t, pi.AverageBandwidth) 53 | 54 | assert.Equal(t, 8, p.ItemSize()) 55 | 56 | item = p.Items[len(p.Items)-1] 57 | assert.IsType(t, &m3u8.PlaylistItem{}, item) 58 | pi = item.(*m3u8.PlaylistItem) 59 | assert.Empty(t, pi.Resolution.String()) 60 | } 61 | 62 | func TestReader_IFrame(t *testing.T) { 63 | p, err := m3u8.ReadFile("fixtures/masterIframes.m3u8") 64 | assert.Nil(t, err) 65 | assert.True(t, p.IsValid()) 66 | assert.True(t, p.IsMaster()) 67 | 68 | assert.Equal(t, 7, p.ItemSize()) 69 | 70 | item := p.Items[1] 71 | assert.IsType(t, &m3u8.PlaylistItem{}, item) 72 | pi := item.(*m3u8.PlaylistItem) 73 | assert.Equal(t, "low/iframe.m3u8", pi.URI) 74 | assert.Equal(t, 86000, pi.Bandwidth) 75 | assert.True(t, pi.IFrame) 76 | } 77 | 78 | func TestReader_MediaPlaylist(t *testing.T) { 79 | p, err := m3u8.ReadFile("fixtures/playlist.m3u8") 80 | assert.Nil(t, err) 81 | assert.True(t, p.IsValid()) 82 | assert.False(t, p.IsMaster()) 83 | 84 | assertNotNilEqual(t, 4, p.Version) 85 | assert.Equal(t, 1, p.Sequence) 86 | assertNotNilEqual(t, 8, p.DiscontinuitySequence) 87 | assertNotNilEqual(t, false, p.Cache) 88 | assert.Equal(t, 12, p.Target) 89 | assertNotNilEqual(t, "VOD", p.Type) 90 | 91 | item := p.Items[0] 92 | assert.IsType(t, &m3u8.SegmentItem{}, item) 93 | si := item.(*m3u8.SegmentItem) 94 | assert.Equal(t, 11.344644, si.Duration) 95 | assert.Nil(t, si.Comment) 96 | 97 | item = p.Items[4] 98 | assert.IsType(t, &m3u8.TimeItem{}, item) 99 | ti := item.(*m3u8.TimeItem) 100 | assert.Equal(t, "2010-02-19T14:54:23Z", m3u8.FormatTime(ti.Time)) 101 | 102 | assert.Equal(t, 140, p.ItemSize()) 103 | } 104 | 105 | func TestReader_PlaylistLiveCheck(t *testing.T) { 106 | p, err := m3u8.ReadFile("fixtures/playlist.m3u8") 107 | assert.Nil(t, err) 108 | assert.True(t, p.IsValid()) 109 | assert.False(t, p.IsLive()) 110 | 111 | p, err = m3u8.ReadFile("fixtures/playlist-live.m3u8") 112 | assert.Nil(t, err) 113 | assert.True(t, p.IsValid()) 114 | assert.True(t, p.IsLive()) 115 | } 116 | func TestReader_IFramePlaylist(t *testing.T) { 117 | p, err := m3u8.ReadFile("fixtures/iframes.m3u8") 118 | assert.Nil(t, err) 119 | assert.True(t, p.IsValid()) 120 | 121 | assert.True(t, p.IFramesOnly) 122 | assert.Equal(t, 3, p.ItemSize()) 123 | 124 | item := p.Items[0] 125 | assert.IsType(t, &m3u8.SegmentItem{}, item) 126 | si := item.(*m3u8.SegmentItem) 127 | assert.Equal(t, 4.12, si.Duration) 128 | assert.NotNil(t, si.ByteRange) 129 | assertNotNilEqual(t, 9400, si.ByteRange.Length) 130 | assertNotNilEqual(t, 376, si.ByteRange.Start) 131 | assert.Equal(t, "segment1.ts", si.Segment) 132 | 133 | item = p.Items[1] 134 | assert.IsType(t, &m3u8.SegmentItem{}, item) 135 | si = item.(*m3u8.SegmentItem) 136 | assert.NotNil(t, si.ByteRange) 137 | assertNotNilEqual(t, 7144, si.ByteRange.Length) 138 | assert.Nil(t, si.ByteRange.Start) 139 | } 140 | 141 | func TestReader_PlaylistWithComments(t *testing.T) { 142 | p, err := m3u8.ReadFile("fixtures/playlistWithComments.m3u8") 143 | assert.Nil(t, err) 144 | assert.True(t, p.IsValid()) 145 | 146 | assert.False(t, p.IsMaster()) 147 | assertNotNilEqual(t, 4, p.Version) 148 | assert.Equal(t, 1, p.Sequence) 149 | assertNotNilEqual(t, false, p.Cache) 150 | assert.Equal(t, 12, p.Target) 151 | assertNotNilEqual(t, "VOD", p.Type) 152 | 153 | item := p.Items[0] 154 | assert.IsType(t, &m3u8.SegmentItem{}, item) 155 | si := item.(*m3u8.SegmentItem) 156 | 157 | assert.Equal(t, 11.344644, si.Duration) 158 | assertNotNilEqual(t, "anything", si.Comment) 159 | 160 | item = p.Items[1] 161 | assert.IsType(t, &m3u8.DiscontinuityItem{}, item) 162 | 163 | assert.Equal(t, 139, p.ItemSize()) 164 | } 165 | 166 | func TestReader_VariantAudio(t *testing.T) { 167 | p, err := m3u8.ReadFile("fixtures/variantAudio.m3u8") 168 | assert.Nil(t, err) 169 | assert.True(t, p.IsValid()) 170 | assert.True(t, p.IsMaster()) 171 | assert.Equal(t, 10, p.ItemSize()) 172 | 173 | item := p.Items[0] 174 | assert.IsType(t, &m3u8.MediaItem{}, item) 175 | mi := item.(*m3u8.MediaItem) 176 | 177 | assert.Equal(t, "AUDIO", mi.Type) 178 | assert.Equal(t, "audio-lo", mi.GroupID) 179 | assert.Equal(t, "English", mi.Name) 180 | assertNotNilEqual(t, "eng", mi.Language) 181 | assertNotNilEqual(t, "spoken", mi.AssocLanguage) 182 | assertNotNilEqual(t, true, mi.AutoSelect) 183 | assertNotNilEqual(t, true, mi.Default) 184 | assertNotNilEqual(t, "englo/prog_index.m3u8", mi.URI) 185 | assertNotNilEqual(t, true, mi.Forced) 186 | } 187 | 188 | func TestReader_VariantAngles(t *testing.T) { 189 | p, err := m3u8.ReadFile("fixtures/variantAngles.m3u8") 190 | assert.Nil(t, err) 191 | assert.True(t, p.IsValid()) 192 | assert.True(t, p.IsMaster()) 193 | assert.Equal(t, 11, p.ItemSize()) 194 | 195 | item := p.Items[1] 196 | assert.IsType(t, &m3u8.MediaItem{}, item) 197 | mi := item.(*m3u8.MediaItem) 198 | 199 | assert.Equal(t, "VIDEO", mi.Type) 200 | assert.Equal(t, "200kbs", mi.GroupID) 201 | assert.Equal(t, "Angle2", mi.Name) 202 | assert.Nil(t, mi.Language) 203 | assertNotNilEqual(t, true, mi.AutoSelect) 204 | assertNotNilEqual(t, false, mi.Default) 205 | assertNotNilEqual(t, "Angle2/200kbs/prog_index.m3u8", mi.URI) 206 | 207 | item = p.Items[9] 208 | assert.IsType(t, &m3u8.PlaylistItem{}, item) 209 | pi := item.(*m3u8.PlaylistItem) 210 | assertNotNilEqual(t, 300001, pi.AverageBandwidth) 211 | assertNotNilEqual(t, "aac", pi.Audio) 212 | assertNotNilEqual(t, "200kbs", pi.Video) 213 | assertNotNilEqual(t, "captions", pi.ClosedCaptions) 214 | assertNotNilEqual(t, "subs", pi.Subtitles) 215 | } 216 | 217 | func TestReader_SessionData(t *testing.T) { 218 | p, err := m3u8.ReadFile("fixtures/sessionData.m3u8") 219 | assert.Nil(t, err) 220 | assert.True(t, p.IsValid()) 221 | assert.Equal(t, 3, p.ItemSize()) 222 | 223 | item := p.Items[0] 224 | assert.IsType(t, &m3u8.SessionDataItem{}, item) 225 | sdi := item.(*m3u8.SessionDataItem) 226 | 227 | assert.Equal(t, "com.example.lyrics", sdi.DataID) 228 | assertNotNilEqual(t, "lyrics.json", sdi.URI) 229 | } 230 | 231 | func TestReader_Encrypted(t *testing.T) { 232 | p, err := m3u8.ReadFile("fixtures/encrypted.m3u8") 233 | assert.Nil(t, err) 234 | assert.True(t, p.IsValid()) 235 | assert.Equal(t, 6, p.ItemSize()) 236 | 237 | item := p.Items[0] 238 | assert.IsType(t, &m3u8.KeyItem{}, item) 239 | ki := item.(*m3u8.KeyItem) 240 | 241 | assert.Equal(t, "AES-128", ki.Encryptable.Method) 242 | assertNotNilEqual(t, "https://priv.example.com/key.php?r=52", ki.Encryptable.URI) 243 | } 244 | 245 | func TestReader_Map(t *testing.T) { 246 | p, err := m3u8.ReadFile("fixtures/mapPlaylist.m3u8") 247 | assert.Nil(t, err) 248 | assert.True(t, p.IsValid()) 249 | assert.Equal(t, 1, p.ItemSize()) 250 | 251 | item := p.Items[0] 252 | assert.IsType(t, &m3u8.MapItem{}, item) 253 | mi := item.(*m3u8.MapItem) 254 | 255 | assert.Equal(t, "frelo/prog_index.m3u8", mi.URI) 256 | assert.NotNil(t, mi.ByteRange) 257 | assertNotNilEqual(t, 4500, mi.ByteRange.Length) 258 | assertNotNilEqual(t, 600, mi.ByteRange.Start) 259 | } 260 | 261 | func TestReader_Timestamp(t *testing.T) { 262 | p, err := m3u8.ReadFile("fixtures/timestampPlaylist.m3u8") 263 | assert.Nil(t, err) 264 | assert.True(t, p.IsValid()) 265 | assert.Equal(t, 6, p.ItemSize()) 266 | 267 | item := p.Items[0] 268 | assert.IsType(t, &m3u8.SegmentItem{}, item) 269 | si := item.(*m3u8.SegmentItem) 270 | 271 | assert.NotNil(t, si.ProgramDateTime) 272 | assert.Equal(t, "2016-04-11T15:24:31Z", m3u8.FormatTime(si.ProgramDateTime.Time)) 273 | } 274 | 275 | func TestReader_DateRange(t *testing.T) { 276 | p, err := m3u8.ReadFile("fixtures/dateRangeScte35.m3u8") 277 | assert.Nil(t, err) 278 | assert.True(t, p.IsValid()) 279 | assert.Equal(t, 5, p.ItemSize()) 280 | 281 | item := &m3u8.DateRangeItem{} 282 | assert.IsType(t, item, p.Items[0]) 283 | assert.IsType(t, item, p.Items[4]) 284 | } 285 | 286 | func TestReader_Invalid(t *testing.T) { 287 | _, err := m3u8.ReadFile("path/to/file") 288 | assert.NotNil(t, err) 289 | } 290 | -------------------------------------------------------------------------------- /test/segmentItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/AlekSi/pointer" 7 | "github.com/etherlabsio/go-m3u8/m3u8" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSegmentItem_Parse(t *testing.T) { 12 | time, err := m3u8.ParseTime("2010-02-19T14:54:23Z") 13 | assert.Nil(t, err) 14 | 15 | item := &m3u8.SegmentItem{ 16 | Duration: 10.991, 17 | Segment: "test.ts", 18 | ProgramDateTime: &m3u8.TimeItem{ 19 | Time: time, 20 | }, 21 | } 22 | 23 | assert.Equal(t, "#EXTINF:10.991,\n#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23Z\ntest.ts", item.String()) 24 | 25 | item = &m3u8.SegmentItem{ 26 | Duration: 10.991, 27 | Segment: "test.ts", 28 | Comment: pointer.ToString("anything"), 29 | } 30 | 31 | assert.Equal(t, "#EXTINF:10.991,anything\ntest.ts", item.String()) 32 | 33 | item = &m3u8.SegmentItem{ 34 | Duration: 10.991, 35 | Segment: "test.ts", 36 | Comment: pointer.ToString("anything"), 37 | ByteRange: &m3u8.ByteRange{ 38 | Length: pointer.ToInt(4500), 39 | Start: pointer.ToInt(600), 40 | }, 41 | } 42 | 43 | assert.Equal(t, "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500@600\ntest.ts", item.String()) 44 | 45 | item = &m3u8.SegmentItem{ 46 | Duration: 10.991, 47 | Segment: "test.ts", 48 | Comment: pointer.ToString("anything"), 49 | ByteRange: &m3u8.ByteRange{ 50 | Length: pointer.ToInt(4500), 51 | }, 52 | } 53 | 54 | assert.Equal(t, "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500\ntest.ts", item.String()) 55 | } 56 | -------------------------------------------------------------------------------- /test/sessionDataItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSessionDataItem_Parse(t *testing.T) { 11 | line := `#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",VALUE="Test",LANGUAGE="en"` 12 | 13 | sdi, err := m3u8.NewSessionDataItem(line) 14 | assert.Nil(t, err) 15 | 16 | assert.Equal(t, "com.test.movie.title", sdi.DataID) 17 | assertNotNilEqual(t, "Test", sdi.Value) 18 | assert.Nil(t, sdi.URI) 19 | assertNotNilEqual(t, "en", sdi.Language) 20 | assertToString(t, line, sdi) 21 | 22 | line = `#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",URI="http://test",LANGUAGE="en"` 23 | sdi, err = m3u8.NewSessionDataItem(line) 24 | assert.Nil(t, err) 25 | 26 | assert.Equal(t, "com.test.movie.title", sdi.DataID) 27 | assert.Nil(t, sdi.Value) 28 | assertNotNilEqual(t, "http://test", sdi.URI) 29 | assertNotNilEqual(t, "en", sdi.Language) 30 | assertToString(t, line, sdi) 31 | } 32 | -------------------------------------------------------------------------------- /test/sessionKeyItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/etherlabsio/go-m3u8/m3u8" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSessionKeyItem_Parse(t *testing.T) { 11 | line := `#EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://test.key",IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3"` 12 | 13 | ski, err := m3u8.NewSessionKeyItem(line) 14 | assert.Nil(t, err) 15 | assert.NotNil(t, ski.Encryptable) 16 | 17 | assert.Equal(t, "AES-128", ski.Encryptable.Method) 18 | assertNotNilEqual(t, "http://test.key", ski.Encryptable.URI) 19 | assertNotNilEqual(t, "D512BBF", ski.Encryptable.IV) 20 | assertNotNilEqual(t, "identity", ski.Encryptable.KeyFormat) 21 | assertNotNilEqual(t, "1/3", ski.Encryptable.KeyFormatVersions) 22 | 23 | assertToString(t, line, ski) 24 | } 25 | -------------------------------------------------------------------------------- /test/timeItem_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/etherlabsio/go-m3u8/m3u8" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestTimeItem_New(t *testing.T) { 12 | timeVar, err := m3u8.ParseTime("2010-02-19T14:54:23.031Z") 13 | assert.Nil(t, err) 14 | ti := &m3u8.TimeItem{ 15 | Time: timeVar, 16 | } 17 | 18 | assert.Equal(t, "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z", ti.String()) 19 | } 20 | 21 | func TestTimeItem_Parse(t *testing.T) { 22 | ti, err := m3u8.NewTimeItem("#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z") 23 | assert.Nil(t, err) 24 | 25 | expected, err := time.Parse(time.RFC3339Nano, "2010-02-19T14:54:23.031Z") 26 | assert.Nil(t, err) 27 | 28 | assert.Equal(t, expected, ti.Time) 29 | } 30 | -------------------------------------------------------------------------------- /test/writer_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/AlekSi/pointer" 7 | "github.com/etherlabsio/go-m3u8/m3u8" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type testCase struct { 12 | p *m3u8.Playlist 13 | expected string 14 | } 15 | 16 | func TestWriter_Master(t *testing.T) { 17 | testCases := []testCase{ 18 | // Master playlist 19 | { 20 | &m3u8.Playlist{ 21 | Target: 10, 22 | Items: []m3u8.Item{ 23 | &m3u8.PlaylistItem{ 24 | ProgramID: pointer.ToString("1"), 25 | URI: "playlist_url", 26 | Bandwidth: 6400, 27 | AudioCodec: pointer.ToString("mp3"), 28 | }, 29 | &m3u8.PlaylistItem{ 30 | ProgramID: pointer.ToString("2"), 31 | URI: "playlist_url", 32 | Bandwidth: 50000, 33 | AudioCodec: pointer.ToString("aac-lc"), 34 | Width: pointer.ToInt(1920), 35 | Height: pointer.ToInt(1080), 36 | Profile: pointer.ToString("high"), 37 | Level: pointer.ToString("4.1"), 38 | }, 39 | &m3u8.SessionDataItem{ 40 | DataID: "com.test.movie.title", 41 | Value: pointer.ToString("Test"), 42 | URI: pointer.ToString("http://test"), 43 | Language: pointer.ToString("en"), 44 | }, 45 | }, 46 | }, 47 | `#EXTM3U 48 | #EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",BANDWIDTH=6400 49 | playlist_url 50 | #EXT-X-STREAM-INF:PROGRAM-ID=2,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",BANDWIDTH=50000 51 | playlist_url 52 | #EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",VALUE="Test",URI="http://test",LANGUAGE="en" 53 | `, 54 | }, 55 | // Master playlist with single stream 56 | { 57 | &m3u8.Playlist{ 58 | Target: 10, 59 | Items: []m3u8.Item{ 60 | &m3u8.PlaylistItem{ 61 | ProgramID: pointer.ToString("1"), 62 | URI: "playlist_url", 63 | Bandwidth: 6400, 64 | AudioCodec: pointer.ToString("mp3"), 65 | }, 66 | }, 67 | }, 68 | `#EXTM3U 69 | #EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",BANDWIDTH=6400 70 | playlist_url 71 | `, 72 | }, 73 | // Master playlist with header options 74 | { 75 | &m3u8.Playlist{ 76 | Target: 10, 77 | Version: pointer.ToInt(6), 78 | IndependentSegments: true, 79 | Items: []m3u8.Item{ 80 | &m3u8.PlaylistItem{ 81 | URI: "playlist_url", 82 | Bandwidth: 6400, 83 | AudioCodec: pointer.ToString("mp3"), 84 | }, 85 | }, 86 | }, 87 | `#EXTM3U 88 | #EXT-X-VERSION:6 89 | #EXT-X-INDEPENDENT-SEGMENTS 90 | #EXT-X-STREAM-INF:CODECS="mp4a.40.34",BANDWIDTH=6400 91 | playlist_url 92 | `, 93 | }, 94 | // New master playlist 95 | { 96 | &m3u8.Playlist{ 97 | Master: pointer.ToBool(true), 98 | }, 99 | `#EXTM3U 100 | `, 101 | }, 102 | // New media playlist 103 | { 104 | &m3u8.Playlist{ 105 | Target: 10, 106 | }, 107 | `#EXTM3U 108 | #EXT-X-MEDIA-SEQUENCE:0 109 | #EXT-X-TARGETDURATION:10 110 | #EXT-X-ENDLIST 111 | `, 112 | }, 113 | // Media playlist 114 | { 115 | &m3u8.Playlist{ 116 | Version: pointer.ToInt(4), 117 | Cache: pointer.ToBool(false), 118 | Target: 6, 119 | Sequence: 1, 120 | DiscontinuitySequence: pointer.ToInt(10), 121 | Type: pointer.ToString("EVENT"), 122 | IFramesOnly: true, 123 | Items: []m3u8.Item{ 124 | &m3u8.SegmentItem{ 125 | Duration: 11.344644, 126 | Segment: "1080-7mbps00000.ts", 127 | }, 128 | }, 129 | }, 130 | `#EXTM3U 131 | #EXT-X-PLAYLIST-TYPE:EVENT 132 | #EXT-X-VERSION:4 133 | #EXT-X-I-FRAMES-ONLY 134 | #EXT-X-MEDIA-SEQUENCE:1 135 | #EXT-X-DISCONTINUITY-SEQUENCE:10 136 | #EXT-X-ALLOW-CACHE:NO 137 | #EXT-X-TARGETDURATION:6 138 | #EXTINF:11.344644, 139 | 1080-7mbps00000.ts 140 | #EXT-X-ENDLIST 141 | `, 142 | }, 143 | // Media playlist with keys 144 | { 145 | &m3u8.Playlist{ 146 | Target: 10, 147 | Version: pointer.ToInt(7), 148 | Items: []m3u8.Item{ 149 | &m3u8.SegmentItem{ 150 | Duration: 11.344644, 151 | Segment: "1080-7mbps00000.ts", 152 | }, 153 | &m3u8.KeyItem{ 154 | Encryptable: &m3u8.Encryptable{ 155 | Method: "AES-128", 156 | URI: pointer.ToString("http://test.key"), 157 | IV: pointer.ToString("D512BBF"), 158 | KeyFormat: pointer.ToString("identity"), 159 | KeyFormatVersions: pointer.ToString("1/3"), 160 | }, 161 | }, 162 | &m3u8.SegmentItem{ 163 | Duration: 11.261233, 164 | Segment: "1080-7mbps00001.ts", 165 | }, 166 | }, 167 | }, 168 | `#EXTM3U 169 | #EXT-X-VERSION:7 170 | #EXT-X-MEDIA-SEQUENCE:0 171 | #EXT-X-TARGETDURATION:10 172 | #EXTINF:11.344644, 173 | 1080-7mbps00000.ts 174 | #EXT-X-KEY:METHOD=AES-128,URI="http://test.key",IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3" 175 | #EXTINF:11.261233, 176 | 1080-7mbps00001.ts 177 | #EXT-X-ENDLIST 178 | `, 179 | }, 180 | } 181 | for _, tc := range testCases { 182 | tc.assert(t) 183 | } 184 | 185 | p := &m3u8.Playlist{ 186 | Target: 10, 187 | Items: []m3u8.Item{ 188 | &m3u8.PlaylistItem{ 189 | ProgramID: pointer.ToString("1"), 190 | Width: pointer.ToInt(1920), 191 | Height: pointer.ToInt(1080), 192 | Codecs: pointer.ToString("avc"), 193 | Bandwidth: 540, 194 | URI: "test.url", 195 | }, 196 | &m3u8.SegmentItem{ 197 | Duration: 10.991, 198 | Segment: "test.ts", 199 | }, 200 | }, 201 | } 202 | _, err := m3u8.Write(p) 203 | assert.Equal(t, m3u8.ErrPlaylistInvalidType, err) 204 | } 205 | 206 | func (tc testCase) assert(t *testing.T) { 207 | assert.Equal(t, tc.expected, tc.p.String()) 208 | } 209 | --------------------------------------------------------------------------------