pax_global_header00006660000000000000000000000064145442567460014533gustar00rootroot0000000000000052 comment=21b9a147306abb54b1c1361778db0af562a2cef4 golang-github-etherlabsio-go-m3u8-1.0.0/000077500000000000000000000000001454425674600177545ustar00rootroot00000000000000golang-github-etherlabsio-go-m3u8-1.0.0/.github/000077500000000000000000000000001454425674600213145ustar00rootroot00000000000000golang-github-etherlabsio-go-m3u8-1.0.0/.github/workflows/000077500000000000000000000000001454425674600233515ustar00rootroot00000000000000golang-github-etherlabsio-go-m3u8-1.0.0/.github/workflows/dependency-review.yml000066400000000000000000000015651454425674600275200ustar00rootroot00000000000000# Dependency Review Action # # 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. # # Source repository: https://github.com/actions/dependency-review-action # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement name: 'Dependency Review' on: [pull_request] permissions: contents: read jobs: dependency-review: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' uses: actions/checkout@v3 - name: 'Dependency Review' uses: actions/dependency-review-action@v2 golang-github-etherlabsio-go-m3u8-1.0.0/.github/workflows/main.yml000066400000000000000000000016441454425674600250250ustar00rootroot00000000000000# This is a basic workflow to help you get started with Actions name: CI # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the "master" branch push: branches: [ "master" ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 - uses: go-semantic-release/action@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }} changelog-file: CHANGELOG.md golang-github-etherlabsio-go-m3u8-1.0.0/.gitignore000066400000000000000000000000061454425674600217400ustar00rootroot00000000000000.idea golang-github-etherlabsio-go-m3u8-1.0.0/.travis.yml000066400000000000000000000003261454425674600220660ustar00rootroot00000000000000language: go go: - "1.13" before_install: - go get -t -v ./... script: - go test -v -race -coverprofile=coverage.txt -covermode=atomic ./test/... after_success: - bash <(curl -s https://codecov.io/bash) golang-github-etherlabsio-go-m3u8-1.0.0/LICENSE000066400000000000000000000020561454425674600207640ustar00rootroot00000000000000MIT License Copyright (c) 2018 Tan Quang Ngo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-etherlabsio-go-m3u8-1.0.0/README.md000066400000000000000000000114141454425674600212340ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/etherlabsio/go-m3u8.svg?branch=master)](https://travis-ci.org/etherlabsio/go-m3u8) [![codecov](https://codecov.io/gh/etherlabsio/go-m3u8/branch/master/graph/badge.svg)](https://codecov.io/gh/etherlabsio/go-m3u8) [![Go Report Card](https://goreportcard.com/badge/github.com/etherlabsio/go-m3u8)](https://goreportcard.com/report/github.com/etherlabsio/go-m3u8) [![GoDoc](https://godoc.org/github.com/etherlabsio/go-m3u8/m3u8?status.svg)](https://godoc.org/github.com/etherlabsio/go-m3u8/m3u8) # go-m3u8 Golang package for m3u8 (ported m3u8 gem https://github.com/sethdeckard/m3u8) `go-m3u8` provides easy generation and parsing of m3u8 playlists defined in the HTTP Live Streaming (HLS) Internet Draft published by Apple. * The library completely implements version 20 of the HLS Internet Draft. * Provides parsing of an m3u8 playlist into an object model from any File, io.Reader or string. * Provides ability to write playlist to a string via String() * Distinction between a master and media playlist is handled automatically (single Playlist class). * 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). ## Installation `go get github.com/etherlabsio/go-m3u8` ## Usage (creating playlists) Create a master playlist and child playlists for adaptive bitrate streaming: ```go import ( "github.com/etherlabsio/go-m3u8/m3u8" "github.com/AlekSi/pointer" ) playlist := m3u8.NewPlaylist() ``` Create a new playlist item: ```go item := &m3u8.PlaylistItem{ Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Profile: pointer.ToString("high"), Level: pointer.ToString("4.1"), AudioCodec: pointer.ToString("aac-lc"), Bandwidth: 540, URI: "test.url", } playlist.AppendItem(item) ``` Add alternate audio, camera angles, closed captions and subtitles by creating MediaItem instances and adding them to the Playlist: ```go item := &m3u8.MediaItem{ Type: "AUDIO", GroupID: "audio-lo", Name: "Francais", Language: pointer.ToString("fre"), AssocLanguage: pointer.ToString("spoken"), AutoSelect: pointer.ToBool(true), Default: pointer.ToBool(false), Forced: pointer.ToBool(true), URI: pointer.ToString("frelo/prog_index.m3u8"), } playlist.AppendItem(item) ``` 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): ```go playlist := &m3u8.Playlist{ Target: 12, Sequence: 1, Version: pointer.ToInt(1), Cache: pointer.ToBool(false), Items: []m3u8.Item{ &m3u8.SegmentItem{ Duration: 11, Segment: "test.ts", }, }, } ``` You can also access the playlist as a string: ```go var str string str = playlist.String() ... fmt.Print(playlist) ``` Alternatively you can set codecs rather than having it generated automatically: ```go item := &m3u8.PlaylistItem{ Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Codecs: pointer.ToString("avc1.66.30,mp4a.40.2"), Bandwidth: 540, URI: "test.url", } ``` ## Usage (parsing playlists) Parse from file ```go playlist, err := m3u8.ReadFile("path/to/file") ``` Read from string ```go playlist, err := m3u8.ReadString(string) ``` Read from generic `io.Reader` ```go playlist, err := m3u8.Read(reader) ``` Access items in playlist: ```go gore> playlist.Items[0] (*m3u8.SessionKeyItem)#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" gore> playlist.Items[1] (*m3u8.PlaybackStart)#EXT-X-START:TIME-OFFSET=20.2 ``` ## Misc Codecs: * Values for audio_codec (codec name): aac-lc, he-aac, mp3 * Values for profile (H.264 Profile): baseline, main, high. * Values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1. Not all Levels and Profiles can be combined and validation is not currently implemented, consult H.264 documentation for further details. ## Contributing 1. Fork it https://github.com/etherlabsio/go-m3u8/fork 2. Create your feature branch `git checkout -b my-new-feature` 3. Run tests `go test ./test/...`, make sure they all pass and new features are covered 4. Commit your changes `git commit -am "Add new features"` 5. Push to the branch `git push origin my-new-feature` 6. Create a new Pull Request ## License MIT License - See [LICENSE](https://github.com/etherlabsio/go-m3u8/blob/master/LICENSE) for details [![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) golang-github-etherlabsio-go-m3u8-1.0.0/go.mod000066400000000000000000000002021454425674600210540ustar00rootroot00000000000000module github.com/etherlabsio/go-m3u8 go 1.13 require ( github.com/AlekSi/pointer v1.0.0 github.com/stretchr/testify v1.3.0 ) golang-github-etherlabsio-go-m3u8-1.0.0/go.sum000066400000000000000000000014111454425674600211040ustar00rootroot00000000000000github.com/AlekSi/pointer v1.0.0 h1:KWCWzsvFxNLcmM5XmiqHsGTTsuwZMsLFwWF9Y+//bNE= github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/000077500000000000000000000000001454425674600205505ustar00rootroot00000000000000golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/byteRange.go000066400000000000000000000014421454425674600230200ustar00rootroot00000000000000package m3u8 import ( "fmt" "strconv" "strings" ) // ByteRange represents sub range of a resource type ByteRange struct { Length *int Start *int } // NewByteRange parses a text line in playlist file and returns a *ByteRange func NewByteRange(text string) (*ByteRange, error) { if text == "" { return nil, nil } values := strings.Split(text, "@") lengthValue, err := strconv.Atoi(values[0]) if err != nil { return nil, err } br := ByteRange{Length: &lengthValue} if len(values) >= 2 { startValue, err := strconv.Atoi(values[1]) if err != nil { return &br, err } br.Start = &startValue } return &br, nil } func (br *ByteRange) String() string { if br.Start == nil { return fmt.Sprintf("%d", *br.Length) } return fmt.Sprintf("%d@%d", *br.Length, *br.Start) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/codecs.go000066400000000000000000000026451454425674600223460ustar00rootroot00000000000000package m3u8 import "strings" var ( // AudioCodecMap maps audio codec to representation AudioCodecMap = map[string]string{ "aac-lc": "mp4a.40.2", "he-aac": "mp4a.40.5", "mp3": "mp4a.40.34", } // BaselineCodecMap maps baseline profile with levels to representation BaselineCodecMap = map[string]string{ "3.0": "avc1.66.30", "3.1": "avc1.42001f", } // MainCodecMap maps main profile with levels to representation MainCodecMap = map[string]string{ "3.0": "avc1.77.30", "3.1": "avc1.4d001f", "4.0": "avc1.4d0028", "4.1": "avc1.4d0029", } // HighCodecMap maps high profile with levels to representation HighCodecMap = map[string]string{ "3.0": "avc1.64001e", "3.1": "avc1.64001f", "3.2": "avc1.640020", "4.0": "avc1.640028", "4.1": "avc1.640029", "4.2": "avc1.64002a", "5.0": "avc1.640032", "5.1": "avc1.640033", "5.2": "avc1.640034", } ) func audioCodec(codec *string) *string { if codec == nil { return nil } key := strings.ToLower(*codec) value, ok := AudioCodecMap[key] if !ok { return nil } return &value } func videoCodec(profile *string, level *string) *string { if profile == nil || level == nil { return nil } var value string var ok bool switch *profile { case "baseline": value, ok = BaselineCodecMap[*level] case "main": value, ok = MainCodecMap[*level] case "high": value, ok = HighCodecMap[*level] } if !ok { return nil } return &value } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/common.go000066400000000000000000000033701454425674600223720ustar00rootroot00000000000000package m3u8 import ( "regexp" "strconv" "strings" ) const ( quotedFormatString = `%s="%v"` formatString = `%s=%v` frameRateFormatString = `%s=%.3f` ) var ( parseRegex = regexp.MustCompile(`([A-z0-9-]+)\s*=\s*("[^"]*"|[^,]*)`) ) // ParseAttributes parses a text line in playlist and returns an attributes map func ParseAttributes(text string) map[string]string { res := make(map[string]string) value := strings.Replace(text, "\n", "", -1) matches := parseRegex.FindAllStringSubmatch(value, -1) for _, match := range matches { if len(match) >= 3 { key := match[1] value := strings.Replace(match[2], `"`, "", -1) res[key] = value } } return res } func parseFloat(attributes map[string]string, key string) (*float64, error) { stringValue, ok := attributes[key] if !ok { return nil, nil } value, err := strconv.ParseFloat(stringValue, 64) if err != nil { return nil, err } return &value, nil } func parseInt(attributes map[string]string, key string) (*int, error) { stringValue, ok := attributes[key] if !ok { return nil, nil } int64Value, err := strconv.ParseInt(stringValue, 0, 0) if err != nil { return nil, err } value := int(int64Value) return &value, nil } func parseYesNo(attributes map[string]string, key string) *bool { stringValue, ok := attributes[key] if !ok { return nil } val := false if stringValue == YesValue { val = true } return &val } func formatYesNo(value bool) string { if value { return YesValue } return NoValue } func attributeExists(key string, attributes map[string]string) bool { _, ok := attributes[key] return ok } func pointerTo(attributes map[string]string, key string) *string { value, ok := attributes[key] if !ok { return nil } return &value } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/dateRangeItem.go000066400000000000000000000063551454425674600236210ustar00rootroot00000000000000package m3u8 import ( "fmt" "strconv" "strings" ) // DateRangeItem represents a #EXT-X-DATERANGE tag type DateRangeItem struct { ID string Class *string StartDate string EndDate *string Duration *float64 PlannedDuration *float64 Scte35Cmd *string Scte35Out *string Scte35In *string EndOnNext bool ClientAttributes map[string]string } // NewDateRangeItem parses a text line in playlist and returns a *DateRangeItem func NewDateRangeItem(text string) (*DateRangeItem, error) { attributes := ParseAttributes(text) duration, err := parseFloat(attributes, DurationTag) if err != nil { return nil, err } plannedDuartion, err := parseFloat(attributes, PlannedDurationTag) if err != nil { return nil, err } return &DateRangeItem{ ID: attributes[IDTag], Class: pointerTo(attributes, ClassTag), StartDate: attributes[StartDateTag], EndDate: pointerTo(attributes, EndDateTag), Duration: duration, PlannedDuration: plannedDuartion, Scte35Cmd: pointerTo(attributes, Scte35CmdTag), Scte35Out: pointerTo(attributes, Scte35OutTag), Scte35In: pointerTo(attributes, Scte35InTag), EndOnNext: attributeExists(EndOnNextTag, attributes), ClientAttributes: parseClientAttributes(attributes), }, nil } func (dri *DateRangeItem) String() string { var slice []string slice = append(slice, fmt.Sprintf(quotedFormatString, IDTag, dri.ID)) if dri.Class != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, ClassTag, *dri.Class)) } slice = append(slice, fmt.Sprintf(quotedFormatString, StartDateTag, dri.StartDate)) if dri.EndDate != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, EndDateTag, *dri.EndDate)) } if dri.Duration != nil { slice = append(slice, fmt.Sprintf(formatString, DurationTag, *dri.Duration)) } if dri.PlannedDuration != nil { slice = append(slice, fmt.Sprintf(formatString, PlannedDurationTag, *dri.PlannedDuration)) } clientAttributes := formatClientAttributes(dri.ClientAttributes) slice = append(slice, clientAttributes...) if dri.Scte35Cmd != nil { slice = append(slice, fmt.Sprintf(formatString, Scte35CmdTag, *dri.Scte35Cmd)) } if dri.Scte35Out != nil { slice = append(slice, fmt.Sprintf(formatString, Scte35OutTag, *dri.Scte35Out)) } if dri.Scte35In != nil { slice = append(slice, fmt.Sprintf(formatString, Scte35InTag, *dri.Scte35In)) } if dri.EndOnNext { slice = append(slice, fmt.Sprintf(`%s=YES`, EndOnNextTag)) } return fmt.Sprintf("%s:%s", DateRangeItemTag, strings.Join(slice, ",")) } func parseClientAttributes(attributes map[string]string) map[string]string { result := make(map[string]string) hasCA := false for key, value := range attributes { if strings.HasPrefix(key, "X-") { result[key] = value hasCA = true } } if hasCA { return result } return nil } func formatClientAttributes(ca map[string]string) []string { if ca == nil { return nil } var slice []string for key, value := range ca { formatString := `%s=%s` _, err := strconv.ParseFloat(value, 64) if err != nil { formatString = `%s="%s"` } slice = append(slice, fmt.Sprintf(formatString, key, value)) } return slice } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/discontinuityItem.go000066400000000000000000000007021454425674600246220ustar00rootroot00000000000000package m3u8 import "fmt" // DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a // discontinuity between the SegmentItems that proceed and follow it. type DiscontinuityItem struct{} // NewDiscontinuityItem returns a *DiscontinuityItem func NewDiscontinuityItem() (*DiscontinuityItem, error) { return &DiscontinuityItem{}, nil } func (di *DiscontinuityItem) String() string { return fmt.Sprintf("%s\n", DiscontinuityItemTag) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/encryptable.go000066400000000000000000000024371454425674600234150ustar00rootroot00000000000000package m3u8 import ( "fmt" "strings" ) // Encryptable is common representation for KeyItem and SessionKeyItem type Encryptable struct { Method string URI *string IV *string KeyFormat *string KeyFormatVersions *string } // NewEncryptable takes an attributes map and returns an *Encryptable func NewEncryptable(attributes map[string]string) *Encryptable { return &Encryptable{ Method: attributes[MethodTag], URI: pointerTo(attributes, URITag), IV: pointerTo(attributes, IVTag), KeyFormat: pointerTo(attributes, KeyFormatTag), KeyFormatVersions: pointerTo(attributes, KeyFormatVersionsTag), } } func (e *Encryptable) String() string { var slice []string slice = append(slice, fmt.Sprintf(formatString, MethodTag, e.Method)) if e.URI != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, URITag, *e.URI)) } if e.IV != nil { slice = append(slice, fmt.Sprintf(formatString, IVTag, *e.IV)) } if e.KeyFormat != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, KeyFormatTag, *e.KeyFormat)) } if e.KeyFormatVersions != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, KeyFormatVersionsTag, *e.KeyFormatVersions)) } return strings.Join(slice, ",") } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/errors.go000066400000000000000000000020411454425674600224100ustar00rootroot00000000000000package m3u8 import "errors" var ( // ErrPlaylistInvalid represents playlist error when playlist does not start with #EXTM3U ErrPlaylistInvalid = errors.New("invalid playlist, must start with #EXTM3U") // ErrPlaylistInvalidType represents playlist error when it's mixed between master and media playlist ErrPlaylistInvalidType = errors.New("invalid playlist, mixed master and media") // ErrResolutionInvalid represents error when a resolution is invalid ErrResolutionInvalid = errors.New("invalid resolution") // ErrBandwidthMissing represents error when a segment does not have bandwidth ErrBandwidthMissing = errors.New("missing bandwidth") // ErrBandwidthInvalid represents error when a bandwidth is invalid ErrBandwidthInvalid = errors.New("invalid bandwidth") // ErrSegmentItemInvalid represents error when a segment item is invalid ErrSegmentItemInvalid = errors.New("invalid segment item") // ErrPlaylistItemInvalid represents error when a playlist item is invalid ErrPlaylistItemInvalid = errors.New("invalid playlist item") ) golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/keyItem.go000066400000000000000000000007021454425674600225050ustar00rootroot00000000000000package m3u8 import "fmt" // KeyItem represents a set of EXT-X-KEY attributes type KeyItem struct { Encryptable *Encryptable } // NewKeyItem parses a text line and returns a *KeyItem func NewKeyItem(text string) (*KeyItem, error) { attributes := ParseAttributes(text) return &KeyItem{ Encryptable: NewEncryptable(attributes), }, nil } func (ki *KeyItem) String() string { return fmt.Sprintf("%s:%v", KeyItemTag, ki.Encryptable.String()) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/mapItem.go000066400000000000000000000013431454425674600224740ustar00rootroot00000000000000package m3u8 import "fmt" // MapItem represents a EXT-X-MAP tag which specifies how to obtain the Media // Initialization Section type MapItem struct { URI string ByteRange *ByteRange } // NewMapItem parses a text line and returns a *MapItem func NewMapItem(text string) (*MapItem, error) { attributes := ParseAttributes(text) br, err := NewByteRange(attributes[ByteRangeTag]) if err != nil { return nil, err } return &MapItem{ URI: attributes[URITag], ByteRange: br, }, nil } func (mi *MapItem) String() string { if mi.ByteRange == nil { return fmt.Sprintf(`%s:%s="%s"`, MapItemTag, URITag, mi.URI) } return fmt.Sprintf(`%s:%s="%s",%s="%v"`, MapItemTag, URITag, mi.URI, ByteRangeTag, mi.ByteRange) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/mediaItem.go000066400000000000000000000054751454425674600230100ustar00rootroot00000000000000package m3u8 import ( "fmt" "strings" ) // MediaItem represents a set of EXT-X-MEDIA attributes type MediaItem struct { Type string GroupID string Name string Language *string AssocLanguage *string AutoSelect *bool Default *bool Forced *bool URI *string InStreamID *string Characteristics *string Channels *string StableRenditionId *string } // NewMediaItem parses a text line and returns a *MediaItem func NewMediaItem(text string) (*MediaItem, error) { attributes := ParseAttributes(text) return &MediaItem{ Type: attributes[TypeTag], GroupID: attributes[GroupIDTag], Name: attributes[NameTag], Language: pointerTo(attributes, LanguageTag), AssocLanguage: pointerTo(attributes, AssocLanguageTag), AutoSelect: parseYesNo(attributes, AutoSelectTag), Default: parseYesNo(attributes, DefaultTag), Forced: parseYesNo(attributes, ForcedTag), URI: pointerTo(attributes, URITag), InStreamID: pointerTo(attributes, InStreamIDTag), Characteristics: pointerTo(attributes, CharacteristicsTag), Channels: pointerTo(attributes, ChannelsTag), StableRenditionId: pointerTo(attributes, StableRenditionIDTag), }, nil } func (mi *MediaItem) String() string { slice := []string{ fmt.Sprintf(formatString, TypeTag, mi.Type), fmt.Sprintf(quotedFormatString, GroupIDTag, mi.GroupID), } if mi.Language != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, LanguageTag, *mi.Language)) } if mi.AssocLanguage != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, AssocLanguageTag, *mi.AssocLanguage)) } slice = append(slice, fmt.Sprintf(quotedFormatString, NameTag, mi.Name)) if mi.AutoSelect != nil { slice = append(slice, fmt.Sprintf(formatString, AutoSelectTag, formatYesNo(*mi.AutoSelect))) } if mi.Default != nil { slice = append(slice, fmt.Sprintf(formatString, DefaultTag, formatYesNo(*mi.Default))) } if mi.URI != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, URITag, *mi.URI)) } if mi.Forced != nil { slice = append(slice, fmt.Sprintf(formatString, ForcedTag, formatYesNo(*mi.Forced))) } if mi.InStreamID != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, InStreamIDTag, *mi.InStreamID)) } if mi.Characteristics != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, CharacteristicsTag, *mi.Characteristics)) } if mi.Channels != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, ChannelsTag, *mi.Channels)) } if mi.StableRenditionId != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, StableRenditionIDTag, *mi.StableRenditionId)) } return fmt.Sprintf("%s:%s", MediaItemTag, strings.Join(slice, ",")) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/playbackStart.go000066400000000000000000000015641454425674600237110ustar00rootroot00000000000000package m3u8 import ( "fmt" "strconv" "strings" ) // PlaybackStart represents a #EXT-X-START tag and attributes type PlaybackStart struct { TimeOffset float64 Precise *bool } // NewPlaybackStart parses a text line and returns a *PlaybackStart func NewPlaybackStart(text string) (*PlaybackStart, error) { attributes := ParseAttributes(text) timeOffset, err := strconv.ParseFloat(attributes[TimeOffsetTag], 64) if err != nil { return nil, err } return &PlaybackStart{ TimeOffset: timeOffset, Precise: parseYesNo(attributes, PreciseTag), }, nil } func (ps *PlaybackStart) String() string { slice := []string{fmt.Sprintf(formatString, TimeOffsetTag, ps.TimeOffset)} if ps.Precise != nil { slice = append(slice, fmt.Sprintf(formatString, PreciseTag, formatYesNo(*ps.Precise))) } return fmt.Sprintf(`%s:%s`, PlaybackStartTag, strings.Join(slice, ",")) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/playlist.go000066400000000000000000000057071454425674600227510ustar00rootroot00000000000000// Package m3u8 provides utilities for parsing and generating m3u8 playlists package m3u8 // Item represents an item in a playlist type Item interface { String() string } // Playlist represents an m3u8 playlist, it can be a master playlist or a set // of media segments type Playlist struct { Items []Item Version *int Cache *bool Target int Sequence int DiscontinuitySequence *int Type *string IFramesOnly bool IndependentSegments bool Live bool Master *bool } func (pl *Playlist) String() string { s, err := Write(pl) if err != nil { return "" } return s } // NewPlaylist returns a playlist with default target 10 func NewPlaylist() *Playlist { return &Playlist{ Target: 10, Live: true, } } // NewPlaylistWithItems returns a playlist with a list of items func NewPlaylistWithItems(items []Item) *Playlist { return &Playlist{ Target: 10, Items: items, } } // AppendItem appends an item to the playlist func (pl *Playlist) AppendItem(item Item) { pl.Items = append(pl.Items, item) } // IsLive checks if playlist is live (not vod) func (pl *Playlist) IsLive() bool { if pl.IsMaster() { return false } return pl.Live } // IsMaster checks if a playlist is a master playlist func (pl *Playlist) IsMaster() bool { if pl.Master != nil { return *pl.Master } plSize := pl.PlaylistSize() smSize := pl.SegmentSize() if plSize <= 0 && smSize <= 0 { return false } return plSize > 0 } // PlaylistSize returns number of playlist items in a playlist func (pl *Playlist) PlaylistSize() int { result := 0 for _, item := range pl.Items { if _, ok := item.(*PlaylistItem); ok { result++ } } return result } // Playlists returns list of segment items in a playlist func (pl *Playlist) Playlists() []*PlaylistItem { var p []*PlaylistItem for _, i := range pl.Items { if pi, ok := i.(*PlaylistItem); ok { p = append(p, pi) } } return p } // SegmentSize returns number of segment items in a playlist func (pl *Playlist) SegmentSize() int { result := 0 for _, item := range pl.Items { if _, ok := (item).(*SegmentItem); ok { result++ } } return result } // Segments returns list of segment items in a playlist func (pl *Playlist) Segments() []*SegmentItem { var s []*SegmentItem for _, i := range pl.Items { if si, ok := i.(*SegmentItem); ok { s = append(s, si) } } return s } // ItemSize returns number of items in a playlist func (pl *Playlist) ItemSize() int { return len(pl.Items) } // IsValid checks if a playlist is valid or not func (pl *Playlist) IsValid() bool { return !(pl.PlaylistSize() > 0 && pl.SegmentSize() > 0) } // Duration returns duration of a media playlist func (pl *Playlist) Duration() float64 { duration := 0.0 for _, item := range pl.Items { if segmentItem, ok := item.(*SegmentItem); ok { duration += segmentItem.Duration } } return duration } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/playlistItem.go000066400000000000000000000127421454425674600235650ustar00rootroot00000000000000package m3u8 import ( "fmt" "strconv" "strings" ) // PlaylistItem represents a set of EXT-X-STREAM-INF or // EXT-X-I-FRAME-STREAM-INF attributes type PlaylistItem struct { Bandwidth int URI string IFrame bool Name *string Width *int Height *int AverageBandwidth *int ProgramID *string Codecs *string AudioCodec *string Profile *string Level *string Video *string Audio *string Subtitles *string ClosedCaptions *string FrameRate *float64 HDCPLevel *string Resolution *Resolution StableVariantID *string } // NewPlaylistItem parses a text line and returns a *PlaylistItem func NewPlaylistItem(text string) (*PlaylistItem, error) { attributes := ParseAttributes(text) resolution, err := parseResolution(attributes, ResolutionTag) if err != nil { return nil, err } var width, height *int if resolution != nil { width = &resolution.Width height = &resolution.Height } averageBandwidth, err := parseInt(attributes, AverageBandwidthTag) if err != nil { return nil, err } frameRate, err := parseFloat(attributes, FrameRateTag) if err != nil { return nil, err } if frameRate != nil && *frameRate <= 0 { frameRate = nil } bandwidth, err := parseBandwidth(attributes, BandwidthTag) if err != nil { return nil, err } return &PlaylistItem{ ProgramID: pointerTo(attributes, ProgramIDTag), Codecs: pointerTo(attributes, CodecsTag), Width: width, Height: height, Bandwidth: bandwidth, AverageBandwidth: averageBandwidth, FrameRate: frameRate, Video: pointerTo(attributes, VideoTag), Audio: pointerTo(attributes, AudioTag), URI: attributes[URITag], Subtitles: pointerTo(attributes, SubtitlesTag), ClosedCaptions: pointerTo(attributes, ClosedCaptionsTag), Name: pointerTo(attributes, NameTag), HDCPLevel: pointerTo(attributes, HDCPLevelTag), Resolution: resolution, StableVariantID: pointerTo(attributes, StableVariantIDTag), }, nil } func (pi *PlaylistItem) String() string { var slice []string // Check resolution if pi.Resolution == nil && pi.Width != nil && pi.Height != nil { r := &Resolution{ Width: *pi.Width, Height: *pi.Height, } pi.Resolution = r } if pi.ProgramID != nil { slice = append(slice, fmt.Sprintf(formatString, ProgramIDTag, *pi.ProgramID)) } if pi.Resolution != nil { slice = append(slice, fmt.Sprintf(formatString, ResolutionTag, pi.Resolution.String())) } codecs := formatCodecs(pi) if codecs != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, CodecsTag, *codecs)) } slice = append(slice, fmt.Sprintf(formatString, BandwidthTag, pi.Bandwidth)) if pi.AverageBandwidth != nil { slice = append(slice, fmt.Sprintf(formatString, AverageBandwidthTag, *pi.AverageBandwidth)) } if pi.FrameRate != nil { slice = append(slice, fmt.Sprintf(frameRateFormatString, FrameRateTag, *pi.FrameRate)) } if pi.HDCPLevel != nil { slice = append(slice, fmt.Sprintf(formatString, HDCPLevelTag, *pi.HDCPLevel)) } if pi.Audio != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, AudioTag, *pi.Audio)) } if pi.Video != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, VideoTag, *pi.Video)) } if pi.Subtitles != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, SubtitlesTag, *pi.Subtitles)) } if pi.ClosedCaptions != nil { cc := *pi.ClosedCaptions fs := quotedFormatString if cc == NoneValue { fs = formatString } slice = append(slice, fmt.Sprintf(fs, ClosedCaptionsTag, cc)) } if pi.Name != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, NameTag, *pi.Name)) } if pi.StableVariantID != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, StableVariantIDTag, *pi.StableVariantID)) } attributesString := strings.Join(slice, ",") if pi.IFrame { return fmt.Sprintf(`%s:%s,%s="%s"`, PlaylistIframeTag, attributesString, URITag, pi.URI) } return fmt.Sprintf("%s:%s\n%s", PlaylistItemTag, attributesString, pi.URI) } // CodecsString returns the string representation of codecs for a playlist item func (pi *PlaylistItem) CodecsString() string { codecsPtr := formatCodecs(pi) if codecsPtr == nil { return "" } return *codecsPtr } func formatCodecs(pi *PlaylistItem) *string { if pi.Codecs != nil { return pi.Codecs } videoCodecPtr := videoCodec(pi.Profile, pi.Level) // profile or level were specified but not recognized any codecs if !(pi.Profile == nil && pi.Level == nil) && videoCodecPtr == nil { return nil } audioCodecPtr := audioCodec(pi.AudioCodec) // audio codec was specified but not recognized if !(pi.AudioCodec == nil) && audioCodecPtr == nil { return nil } var slice []string if videoCodecPtr != nil { slice = append(slice, *videoCodecPtr) } if audioCodecPtr != nil { slice = append(slice, *audioCodecPtr) } if len(slice) <= 0 { return nil } value := strings.Join(slice, ",") return &value } func parseBandwidth(attributes map[string]string, key string) (int, error) { bw, ok := attributes[key] if !ok { return 0, ErrBandwidthMissing } bandwidth, err := strconv.ParseInt(bw, 0, 0) if err != nil { return 0, ErrBandwidthInvalid } return int(bandwidth), nil } func parseResolution(attributes map[string]string, key string) (*Resolution, error) { resolution, ok := attributes[key] if !ok { return nil, nil } return NewResolution(resolution) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/reader.go000066400000000000000000000144101454425674600223410ustar00rootroot00000000000000package m3u8 import ( "bytes" "fmt" "io" "io/ioutil" "strings" ) type state struct { open bool currentItem Item master bool } // ReadString parses a text string and returns a playlist func ReadString(text string) (*Playlist, error) { return Read(strings.NewReader(text)) } // ReadFile reads text from a file and returns a playlist func ReadFile(path string) (*Playlist, error) { f, err := ioutil.ReadFile(path) if err != nil { return nil, err } return Read(bytes.NewReader(f)) } // Read reads text from an io.Reader and returns a playlist func Read(reader io.Reader) (*Playlist, error) { var buf bytes.Buffer _, err := buf.ReadFrom(reader) if err != nil { return nil, err } pl := NewPlaylist() st := &state{} eof := false header := true for !eof { line, err := buf.ReadString('\n') if err == io.EOF { eof = true } else if err != nil { return nil, err } value := strings.TrimSpace(line) if header && value != HeaderTag { return nil, ErrPlaylistInvalid } if err := parseLine(value, pl, st); err != nil { return nil, err } header = false } return pl, nil } func parseLine(line string, pl *Playlist, st *state) error { var err error switch { // basic tags case matchTag(line, VersionTag): pl.Version, err = parseIntPtr(line, VersionTag) // media segment tags case matchTag(line, SegmentItemTag): st.currentItem, err = NewSegmentItem(line) st.master = false st.open = true case matchTag(line, DiscontinuityItemTag): st.master = false st.open = false item, err := NewDiscontinuityItem() if err != nil { return parseError(line, err) } pl.Items = append(pl.Items, item) case matchTag(line, ByteRangeItemTag): value := strings.Replace(line, ByteRangeItemTag+":", "", -1) value = strings.Replace(value, "\n", "", -1) br, err := NewByteRange(value) if err != nil { return parseError(line, err) } mit, ok := st.currentItem.(*MapItem) if ok { mit.ByteRange = br st.currentItem = mit } else { sit, ok := st.currentItem.(*SegmentItem) if ok { sit.ByteRange = br st.currentItem = sit } } case matchTag(line, KeyItemTag): item, err := NewKeyItem(line) if err != nil { return parseError(line, err) } pl.Items = append(pl.Items, item) case matchTag(line, MapItemTag): item, err := NewMapItem(line) if err != nil { return parseError(line, err) } pl.Items = append(pl.Items, item) case matchTag(line, TimeItemTag): pdt, err := NewTimeItem(line) if err != nil { return parseError(line, err) } if st.open { item, ok := st.currentItem.(*SegmentItem) if !ok { return parseError(line, ErrSegmentItemInvalid) } item.ProgramDateTime = pdt } else { pl.Items = append(pl.Items, pdt) } case matchTag(line, DateRangeItemTag): dri, err := NewDateRangeItem(line) if err != nil { return parseError(line, err) } pl.Items = append(pl.Items, dri) // media playlist tags case matchTag(line, MediaSequenceTag): pl.Sequence, err = parseIntValue(line, MediaSequenceTag) case matchTag(line, DiscontinuitySequenceTag): pl.DiscontinuitySequence, err = parseIntPtr(line, DiscontinuitySequenceTag) case matchTag(line, CacheTag): ptr := parseYesNoPtr(line, CacheTag) pl.Cache = ptr case matchTag(line, TargetDurationTag): pl.Target, err = parseIntValue(line, TargetDurationTag) case matchTag(line, IFramesOnlyTag): pl.IFramesOnly = true case matchTag(line, PlaylistTypeTag): pl.Type = parseStringPtr(line, PlaylistTypeTag) // master playlist tags case matchTag(line, MediaItemTag): st.open = false mi, err := NewMediaItem(line) if err != nil { return parseError(line, err) } pl.Items = append(pl.Items, mi) case matchTag(line, SessionDataItemTag): sdi, err := NewSessionDataItem(line) if err != nil { return parseError(line, err) } pl.Items = append(pl.Items, sdi) case matchTag(line, SessionKeyItemTag): ski, err := NewSessionKeyItem(line) if err != nil { return parseError(line, err) } pl.Items = append(pl.Items, ski) case matchTag(line, PlaylistItemTag): st.master = true st.open = true pi, err := NewPlaylistItem(line) if err != nil { return parseError(line, err) } st.currentItem = pi case matchTag(line, PlaylistIframeTag): st.master = true st.open = false pi, err := NewPlaylistItem(line) if err != nil { return parseError(line, err) } pi.IFrame = true pl.Items = append(pl.Items, pi) st.currentItem = pi // universal tags case matchTag(line, PlaybackStartTag): ps, err := NewPlaybackStart(line) if err != nil { return parseError(line, err) } pl.Items = append(pl.Items, ps) case matchTag(line, IndependentSegmentsTag): pl.IndependentSegments = true case matchTag(line, FooterTag): pl.Live = false default: if st.currentItem != nil && st.open { return parseNextLine(line, pl, st) } } return parseError(line, err) } func parseNextLine(line string, pl *Playlist, st *state) error { value := strings.Replace(line, "\n", "", -1) value = strings.Replace(value, "\r", "", -1) if st.master { // PlaylistItem it, ok := st.currentItem.(*PlaylistItem) if !ok { return parseError(line, ErrPlaylistItemInvalid) } it.URI = value pl.Items = append(pl.Items, it) } else { // SegmentItem it, ok := st.currentItem.(*SegmentItem) if !ok { return parseError(line, ErrSegmentItemInvalid) } it.Segment = value pl.Items = append(pl.Items, it) } st.open = false return nil } func matchTag(line, tag string) bool { return strings.HasPrefix(line, tag) && !strings.HasPrefix(line, tag+"-") } func parseIntValue(line string, tag string) (int, error) { var v int _, err := fmt.Sscanf(line, tag+":%d", &v) return v, err } func parseIntPtr(line string, tag string) (*int, error) { var ptr int _, err := fmt.Sscanf(line, tag+":%d", &ptr) return &ptr, err } func parseStringPtr(line string, tag string) *string { value := strings.Replace(line, tag+":", "", -1) if value == "" { return nil } return &value } func parseYesNoPtr(line string, tag string) *bool { value := strings.Replace(line, tag+":", "", -1) var b bool if value == YesValue { b = true } else { b = false } return &b } func parseError(line string, err error) error { if err == nil { return nil } return fmt.Errorf("error: %v when parsing playlist error for line: %s", err, line) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/resolution.go000066400000000000000000000013771454425674600233120ustar00rootroot00000000000000package m3u8 import ( "fmt" "strconv" "strings" ) // Resolution represents a resolution for a playlist item, e.g: 1920x1080 type Resolution struct { Width int Height int } func (r *Resolution) String() string { if r == nil { return "" } return fmt.Sprintf("%dx%d", r.Width, r.Height) } // NewResolution parses a string and returns a *Resolution func NewResolution(text string) (*Resolution, error) { values := strings.Split(text, "x") if len(values) <= 1 { return nil, ErrResolutionInvalid } width, err := strconv.ParseInt(values[0], 0, 0) if err != nil { return nil, err } height, err := strconv.ParseInt(values[1], 0, 0) if err != nil { return nil, err } return &Resolution{ Width: int(width), Height: int(height), }, nil } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/segmentItem.go000066400000000000000000000023431454425674600233620ustar00rootroot00000000000000package m3u8 import ( "fmt" "strconv" "strings" ) // SegmentItem represents EXTINF attributes with the URI that follows, // optionally allowing an EXT-X-BYTERANGE tag to be set. type SegmentItem struct { Duration float64 Segment string Comment *string ProgramDateTime *TimeItem ByteRange *ByteRange } // NewSegmentItem parses a text line and returns a *SegmentItem func NewSegmentItem(text string) (*SegmentItem, error) { var si SegmentItem line := strings.Replace(text, SegmentItemTag+":", "", -1) line = strings.Replace(line, "\n", "", -1) values := strings.Split(line, ",") d, err := strconv.ParseFloat(values[0], 64) if err != nil { return nil, err } si.Duration = d if len(values) > 1 && values[1] != "" { si.Comment = &values[1] } return &si, nil } func (si *SegmentItem) String() string { date := "" if si.ProgramDateTime != nil { date = fmt.Sprintf("%v\n", si.ProgramDateTime) } byteRange := "" if si.ByteRange != nil { byteRange = fmt.Sprintf("\n%s:%v", ByteRangeItemTag, si.ByteRange.String()) } comment := "" if si.Comment != nil { comment = *si.Comment } return fmt.Sprintf("%s:%v,%s%s\n%s%s", SegmentItemTag, si.Duration, comment, byteRange, date, si.Segment) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/sessionDataItem.go000066400000000000000000000021361454425674600241750ustar00rootroot00000000000000package m3u8 import ( "fmt" "strings" ) // SessionDataItem represents a set of EXT-X-SESSION-DATA attributes type SessionDataItem struct { DataID string Value *string URI *string Language *string } // NewSessionDataItem parses a text line and returns a *SessionDataItem func NewSessionDataItem(text string) (*SessionDataItem, error) { attributes := ParseAttributes(text) return &SessionDataItem{ DataID: attributes[DataIDTag], Value: pointerTo(attributes, ValueTag), URI: pointerTo(attributes, URITag), Language: pointerTo(attributes, LanguageTag), }, nil } func (sdi *SessionDataItem) String() string { slice := []string{fmt.Sprintf(quotedFormatString, DataIDTag, sdi.DataID)} if sdi.Value != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, ValueTag, *sdi.Value)) } if sdi.URI != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, URITag, *sdi.URI)) } if sdi.Language != nil { slice = append(slice, fmt.Sprintf(quotedFormatString, LanguageTag, *sdi.Language)) } return fmt.Sprintf(`%s:%s`, SessionDataItemTag, strings.Join(slice, ",")) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/sessionKeyItem.go000066400000000000000000000010131454425674600240450ustar00rootroot00000000000000package m3u8 import "fmt" // SessionKeyItem represents a set of EXT-X-SESSION-KEY attributes type SessionKeyItem struct { Encryptable *Encryptable } // NewSessionKeyItem parses a text line and returns a *SessionKeyItem func NewSessionKeyItem(text string) (*SessionKeyItem, error) { attributes := ParseAttributes(text) return &SessionKeyItem{ Encryptable: NewEncryptable(attributes), }, nil } func (ski *SessionKeyItem) String() string { return fmt.Sprintf("%s:%v", SessionKeyItemTag, ski.Encryptable.String()) } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/tags.go000066400000000000000000000054571454425674600220500ustar00rootroot00000000000000package m3u8 const ( // Item tags SessionKeyItemTag = `#EXT-X-SESSION-KEY` KeyItemTag = `#EXT-X-KEY` DiscontinuityItemTag = `#EXT-X-DISCONTINUITY` TimeItemTag = `#EXT-X-PROGRAM-DATE-TIME` DateRangeItemTag = `#EXT-X-DATERANGE` MapItemTag = `#EXT-X-MAP` SessionDataItemTag = `#EXT-X-SESSION-DATA` SegmentItemTag = `#EXTINF` ByteRangeItemTag = `#EXT-X-BYTERANGE` PlaybackStartTag = `#EXT-X-START` MediaItemTag = `#EXT-X-MEDIA` PlaylistItemTag = `#EXT-X-STREAM-INF` PlaylistIframeTag = `#EXT-X-I-FRAME-STREAM-INF` // Playlist tags HeaderTag = `#EXTM3U` FooterTag = `#EXT-X-ENDLIST` TargetDurationTag = `#EXT-X-TARGETDURATION` CacheTag = `#EXT-X-ALLOW-CACHE` DiscontinuitySequenceTag = `#EXT-X-DISCONTINUITY-SEQUENCE` IndependentSegmentsTag = `#EXT-X-INDEPENDENT-SEGMENTS` PlaylistTypeTag = `#EXT-X-PLAYLIST-TYPE` IFramesOnlyTag = `#EXT-X-I-FRAMES-ONLY` MediaSequenceTag = `#EXT-X-MEDIA-SEQUENCE` VersionTag = `#EXT-X-VERSION` // ByteRange tags ByteRangeTag = "BYTERANGE" // Encryptable tags MethodTag = "METHOD" URITag = "URI" IVTag = "IV" KeyFormatTag = "KEYFORMAT" KeyFormatVersionsTag = "KEYFORMATVERSIONS" // DateRangeItem tags IDTag = "ID" ClassTag = "CLASS" StartDateTag = "START-DATE" EndDateTag = "END-DATE" DurationTag = "DURATION" PlannedDurationTag = "PLANNED-DURATION" Scte35CmdTag = "SCTE35-CMD" Scte35OutTag = "SCTE35-OUT" Scte35InTag = "SCTE35-IN" EndOnNextTag = "END-ON-NEXT" // PlaybackStart tags TimeOffsetTag = "TIME-OFFSET" PreciseTag = "PRECISE" // SessionDataItem tags DataIDTag = "DATA-ID" ValueTag = "VALUE" LanguageTag = "LANGUAGE" // MediaItem tags TypeTag = "TYPE" GroupIDTag = "GROUP-ID" AssocLanguageTag = "ASSOC-LANGUAGE" NameTag = "NAME" AutoSelectTag = "AUTOSELECT" DefaultTag = "DEFAULT" ForcedTag = "FORCED" InStreamIDTag = "INSTREAM-ID" CharacteristicsTag = "CHARACTERISTICS" ChannelsTag = "CHANNELS" StableRenditionIDTag = "STABLE-RENDITION-ID" /// PlaylistItem tags ResolutionTag = "RESOLUTION" ProgramIDTag = "PROGRAM-ID" CodecsTag = "CODECS" BandwidthTag = "BANDWIDTH" AverageBandwidthTag = "AVERAGE-BANDWIDTH" FrameRateTag = "FRAME-RATE" VideoTag = "VIDEO" AudioTag = "AUDIO" SubtitlesTag = "SUBTITLES" ClosedCaptionsTag = "CLOSED-CAPTIONS" HDCPLevelTag = "HDCP-LEVEL" StableVariantIDTag = "STABLE-VARIANT-ID" // Values NoneValue = "NONE" YesValue = "YES" NoValue = "NO" ) golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/timeItem.go000066400000000000000000000022351454425674600226560ustar00rootroot00000000000000package m3u8 import ( "fmt" "strings" "time" ) const ( dateTimeFormat = time.RFC3339Nano ) // TimeItem represents EXT-X-PROGRAM-DATE-TIME type TimeItem struct { Time time.Time } // NewTimeItem parses a text line and returns a *TimeItem func NewTimeItem(text string) (*TimeItem, error) { timeString := strings.Replace(text, TimeItemTag+":", "", -1) t, err := ParseTime(timeString) if err != nil { return nil, err } return &TimeItem{ Time: t, }, nil } func (ti *TimeItem) String() string { return fmt.Sprintf("%s:%s", TimeItemTag, ti.Time.Format(dateTimeFormat)) } // FormatTime returns a string in default m3u8 date time format func FormatTime(time time.Time) string { return time.Format(dateTimeFormat) } // ParseTime parses a string in default m3u8 date time format // and returns time.Time func ParseTime(value string) (time.Time, error) { layouts := []string{ "2006-01-02T15:04:05.999999999Z0700", "2006-01-02T15:04:05.999999999Z07:00", "2006-01-02T15:04:05.999999999Z07", } var ( err error t time.Time ) for _, layout := range layouts { if t, err = time.Parse(layout, value); err == nil { return t, nil } } return t, err } golang-github-etherlabsio-go-m3u8-1.0.0/m3u8/writer.go000066400000000000000000000037641454425674600224250ustar00rootroot00000000000000package m3u8 import ( "fmt" "strings" ) // Write writes a playlist to a string func Write(pl *Playlist) (string, error) { var sb strings.Builder if !pl.IsValid() { return "", ErrPlaylistInvalidType } writeHeader(&sb, pl) for _, item := range pl.Items { sb.WriteString(item.String()) sb.WriteRune('\n') } writeFooter(&sb, pl) return sb.String(), nil } func writeHeader(sb *strings.Builder, pl *Playlist) { sb.WriteString(HeaderTag) sb.WriteRune('\n') if pl.IsMaster() { writeVersionTag(sb, pl.Version) writeIndependentSegmentsTag(sb, pl.IndependentSegments) } else { if pl.Type != nil { sb.WriteString(fmt.Sprintf("%s:%s", PlaylistTypeTag, *pl.Type)) sb.WriteRune('\n') } writeVersionTag(sb, pl.Version) writeIndependentSegmentsTag(sb, pl.IndependentSegments) if pl.IFramesOnly { sb.WriteString(IFramesOnlyTag) sb.WriteRune('\n') } sb.WriteString(fmt.Sprintf("%s:%v", MediaSequenceTag, pl.Sequence)) sb.WriteRune('\n') writeDiscontinuitySequenceTag(sb, pl.DiscontinuitySequence) writeCacheTag(sb, pl.Cache) sb.WriteString(fmt.Sprintf("%s:%v", TargetDurationTag, pl.Target)) sb.WriteRune('\n') } } func writeFooter(sb *strings.Builder, pl *Playlist) { if pl.IsLive() || pl.IsMaster() { return } sb.WriteString(FooterTag) sb.WriteRune('\n') } func writeVersionTag(sb *strings.Builder, version *int) { if version == nil { return } sb.WriteString(fmt.Sprintf("%s:%v", VersionTag, *version)) sb.WriteRune('\n') } func writeIndependentSegmentsTag(sb *strings.Builder, toWrite bool) { if !toWrite { return } sb.WriteString(IndependentSegmentsTag) sb.WriteRune('\n') } func writeDiscontinuitySequenceTag(sb *strings.Builder, sequence *int) { if sequence == nil { return } sb.WriteString(fmt.Sprintf("%s:%v", DiscontinuitySequenceTag, *sequence)) sb.WriteRune('\n') } func writeCacheTag(sb *strings.Builder, cache *bool) { if cache == nil { return } sb.WriteString(fmt.Sprintf("%s:%s", CacheTag, formatYesNo(*cache))) sb.WriteRune('\n') } golang-github-etherlabsio-go-m3u8-1.0.0/test/000077500000000000000000000000001454425674600207335ustar00rootroot00000000000000golang-github-etherlabsio-go-m3u8-1.0.0/test/byteRange_test.go000066400000000000000000000017131454425674600242430ustar00rootroot00000000000000package test import ( "testing" "github.com/AlekSi/pointer" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestByteRange_Parse(t *testing.T) { text := "4500@600" br, err := m3u8.NewByteRange(text) assert.Nil(t, err) assert.NotNil(t, br.Length) assert.NotNil(t, br.Start) assert.Equal(t, 4500, *br.Length) assert.Equal(t, 600, *br.Start) assertToString(t, text, br) } func TestByteRange_Parse_2(t *testing.T) { text := "4500" br, err := m3u8.NewByteRange(text) assert.Nil(t, err) assert.NotNil(t, br.Length) assert.Nil(t, br.Start) assert.Equal(t, 4500, *br.Length) assertToString(t, text, br) } func TestByteRange_New(t *testing.T) { br := &m3u8.ByteRange{ Length: pointer.ToInt(4500), Start: pointer.ToInt(200), } assert.Equal(t, "4500@200", br.String()) } func TestByteRange_New_2(t *testing.T) { br := &m3u8.ByteRange{ Length: pointer.ToInt(4500), } assert.Equal(t, "4500", br.String()) } golang-github-etherlabsio-go-m3u8-1.0.0/test/common.go000066400000000000000000000017171454425674600225600ustar00rootroot00000000000000package test import ( "strings" "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func assertNotNilEqual(t *testing.T, expected interface{}, ptr interface{}) { assert.NotNil(t, ptr) switch ptr.(type) { case *string: s, ok := ptr.(*string) assert.True(t, ok) assert.Equal(t, expected, *s) case *float64: f, ok := ptr.(*float64) assert.True(t, ok) assert.Equal(t, expected, *f) case *int: i, ok := ptr.(*int) assert.True(t, ok) assert.Equal(t, expected, *i) case *bool: b, ok := ptr.(*bool) assert.True(t, ok) assert.Equal(t, expected, *b) default: t.Fatal("not supported assert type") } } func assertEqualWithoutNewLine(t *testing.T, expected string, actual string) { removedNewLine := strings.Replace(expected, "\n", "", -1) assert.Equal(t, removedNewLine, actual) } func assertToString(t *testing.T, expected string, item m3u8.Item) { assertEqualWithoutNewLine(t, expected, item.String()) } golang-github-etherlabsio-go-m3u8-1.0.0/test/common_test.go000066400000000000000000000006341454425674600236140ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestParseAttributes(t *testing.T) { line := "TEST-ID=\"Help\",URI=\"http://test\",ID=33\n" mapAttr := m3u8.ParseAttributes(line) assert.NotNil(t, mapAttr) assert.Equal(t, "Help", mapAttr["TEST-ID"]) assert.Equal(t, "http://test", mapAttr["URI"]) assert.Equal(t, "33", mapAttr["ID"]) } golang-github-etherlabsio-go-m3u8-1.0.0/test/dateRangeItem_test.go000066400000000000000000000040341454425674600250330ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestDateRangeItem_Parse(t *testing.T) { line := `#EXT-X-DATERANGE:ID="splice-6FFFFFF0",CLASS="test_class", START-DATE="2014-03-05T11:15:00Z", END-DATE="2014-03-05T11:16:00Z",DURATION=60.1, PLANNED-DURATION=59.993, SCTE35-CMD=0xFC002F0000000000FF2, SCTE35-OUT=0xFC002F0000000000FF0, SCTE35-IN=0xFC002F0000000000FF1, END-ON-NEXT=YES ` dri, err := m3u8.NewDateRangeItem(line) assert.Nil(t, err) assert.Equal(t, "splice-6FFFFFF0", dri.ID) assert.Equal(t, "2014-03-05T11:15:00Z", dri.StartDate) assertNotNilEqual(t, "test_class", dri.Class) assertNotNilEqual(t, "2014-03-05T11:16:00Z", dri.EndDate) assertNotNilEqual(t, 60.1, dri.Duration) assertNotNilEqual(t, 59.993, dri.PlannedDuration) assertNotNilEqual(t, "0xFC002F0000000000FF2", dri.Scte35Cmd) assertNotNilEqual(t, "0xFC002F0000000000FF0", dri.Scte35Out) assertNotNilEqual(t, "0xFC002F0000000000FF1", dri.Scte35In) assert.True(t, dri.EndOnNext) assert.Nil(t, dri.ClientAttributes) assertToString(t, line, dri) } func TestDateRangeItem_Parse_2(t *testing.T) { line := `#EXT-X-DATERANGE:ID="splice-6FFFFFF0", START-DATE="2014-03-05T11:15:00Z" ` dri, err := m3u8.NewDateRangeItem(line) assert.Nil(t, err) assert.Equal(t, "splice-6FFFFFF0", dri.ID) assert.Equal(t, "2014-03-05T11:15:00Z", dri.StartDate) assert.Nil(t, dri.Class) assert.Nil(t, dri.EndDate) assert.Nil(t, dri.Duration) assert.Nil(t, dri.PlannedDuration) assert.Nil(t, dri.Scte35In) assert.Nil(t, dri.Scte35Out) assert.Nil(t, dri.Scte35Cmd) assert.Nil(t, dri.ClientAttributes) assert.False(t, dri.EndOnNext) assertToString(t, line, dri) } func TestDateRangeItem_Parse_3(t *testing.T) { line := `#EXT-X-DATERANGE:ID="splice-6FFFFFF0", START-DATE="2014-03-05T11:15:00Z", X-CUSTOM-VALUE="test_value" ` dri, err := m3u8.NewDateRangeItem(line) assert.Nil(t, err) assert.NotNil(t, dri.ClientAttributes) assert.Equal(t, "test_value", dri.ClientAttributes["X-CUSTOM-VALUE"]) assertToString(t, line, dri) } golang-github-etherlabsio-go-m3u8-1.0.0/test/discontinuityItem_test.go000066400000000000000000000004401454425674600260430ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestDiscontinuityItem_Parse(t *testing.T) { di, err := m3u8.NewDiscontinuityItem() assert.Nil(t, err) assert.Equal(t, m3u8.DiscontinuityItemTag+"\n", di.String()) } golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/000077500000000000000000000000001454425674600226045ustar00rootroot00000000000000golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/dateRangeScte35.m3u8000066400000000000000000000010611454425674600262010ustar00rootroot00000000000000#EXTM3U #EXT-X-TARGETDURATION:60 #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11: 15:00Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF0 00014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50 000008700000000 #EXTINF:20.0, http://media.example.com/first.ts #EXTINF:20.0, http://media.example.com/second.ts #EXTINF:20.0, http://media.example.com/third.ts #EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN= 0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A00 08029896F50000008700000000 #EXT-X-ENDLIST golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/encrypted.m3u8000066400000000000000000000007131454425674600253200ustar00rootroot00000000000000#EXTM3U #EXT-X-VERSION:3 #EXT-X-MEDIA-SEQUENCE:7794 #EXT-X-TARGETDURATION:15 #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" #EXTINF:2.833, http://media.example.com/fileSequence52-A.ts #EXTINF:15.0, http://media.example.com/fileSequence52-B.ts #EXTINF:13.333, http://media.example.com/fileSequence52-C.ts #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53" #EXTINF:15.0, http://media.example.com/fileSequence53-A.tsgolang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/iframes.m3u8000066400000000000000000000003071454425674600247500ustar00rootroot00000000000000#EXTM3U #EXT-X-VERSION:4 #EXT-X-I-FRAMES-ONLY #EXTINF:4.12, #EXT-X-BYTERANGE:9400@376 segment1.ts #EXTINF:3.56, #EXT-X-BYTERANGE:7144 segment1.ts #EXTINF:3.82, #EXT-X-BYTERANGE:10340@1880 segment2.tsgolang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/mapPlaylist.m3u8000066400000000000000000000003031454425674600256150ustar00rootroot00000000000000#EXTM3U #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-VERSION:5 #EXT-X-MEDIA-SEQUENCE:1 #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:12 #EXT-X-MAP:URI="frelo/prog_index.m3u8",BYTERANGE="4500@600" #EXT-X-ENDLISTgolang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/master.m3u8000066400000000000000000000014711454425674600246200ustar00rootroot00000000000000#EXTM3U #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" #EXT-X-START:TIME-OFFSET=20.2 #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=5042000 hls/1080-7mbps/1080-7mbps.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=4853000 hls/1080/1080.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1280x720,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=2387000 hls/720/720.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=896x504,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=1365000 hls/504/504.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=640x360,CODECS="avc1.66.30,mp4a.40.2",BANDWIDTH=861000 hls/360/360.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.2",BANDWIDTH=6400 hls/64k/64k.m3u8 golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/masterIframes.m3u8000066400000000000000000000006701454425674600261270ustar00rootroot00000000000000#EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 low/audio-video.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=2560000 mid/audio-video.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=7680000 hi/audio-video.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" audio-only.m3u8 golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/playlist-live.m3u8000066400000000000000000000124541454425674600261260ustar00rootroot00000000000000#EXTM3U #EXT-X-PLAYLIST-TYPE:EVENT #EXT-X-VERSION:4 #EXT-X-MEDIA-SEQUENCE:1 #EXT-X-DISCONTINUITY-SEQUENCE:8 #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:12 #EXTINF:11.344644, 1080-7mbps00000.ts #EXT-X-DISCONTINUITY #EXTINF:11.261233, 1080-7mbps00001.ts #EXTINF:7.507489, 1080-7mbps00002.ts #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23Z #EXTINF:11.261233, 1080-7mbps00003.ts #EXTINF:11.261233, 1080-7mbps00004.ts #EXTINF:7.507489, 1080-7mbps00005.ts #EXTINF:11.261233, 1080-7mbps00006.ts #EXTINF:11.261233, 1080-7mbps00007.ts #EXTINF:7.507478, 1080-7mbps00008.ts #EXTINF:11.261233, 1080-7mbps00009.ts #EXTINF:11.261233, 1080-7mbps00010.ts #EXTINF:7.507478, 1080-7mbps00011.ts #EXTINF:11.261233, 1080-7mbps00012.ts #EXTINF:11.261233, 1080-7mbps00013.ts #EXTINF:7.507489, 1080-7mbps00014.ts #EXTINF:11.261233, 1080-7mbps00015.ts #EXTINF:11.261233, 1080-7mbps00016.ts #EXTINF:7.507489, 1080-7mbps00017.ts #EXTINF:11.261233, 1080-7mbps00018.ts #EXTINF:11.261233, 1080-7mbps00019.ts #EXTINF:7.507478, 1080-7mbps00020.ts #EXTINF:11.261233, 1080-7mbps00021.ts #EXTINF:11.261233, 1080-7mbps00022.ts #EXTINF:7.507478, 1080-7mbps00023.ts #EXTINF:11.261233, 1080-7mbps00024.ts #EXTINF:11.261233, 1080-7mbps00025.ts #EXTINF:7.507489, 1080-7mbps00026.ts #EXTINF:11.261233, 1080-7mbps00027.ts #EXTINF:11.261233, 1080-7mbps00028.ts #EXTINF:7.507489, 1080-7mbps00029.ts #EXTINF:11.261233, 1080-7mbps00030.ts #EXTINF:11.261233, 1080-7mbps00031.ts #EXTINF:7.507478, 1080-7mbps00032.ts #EXTINF:11.261233, 1080-7mbps00033.ts #EXTINF:11.261233, 1080-7mbps00034.ts #EXTINF:7.507489, 1080-7mbps00035.ts #EXTINF:11.261233, 1080-7mbps00036.ts #EXTINF:11.261222, 1080-7mbps00037.ts #EXTINF:7.507489, 1080-7mbps00038.ts #EXTINF:11.261233, 1080-7mbps00039.ts #EXTINF:11.261233, 1080-7mbps00040.ts #EXTINF:7.507489, 1080-7mbps00041.ts #EXTINF:11.261233, 1080-7mbps00042.ts #EXTINF:11.261233, 1080-7mbps00043.ts #EXTINF:7.507478, 1080-7mbps00044.ts #EXTINF:11.261233, 1080-7mbps00045.ts #EXTINF:11.261233, 1080-7mbps00046.ts #EXTINF:7.507489, 1080-7mbps00047.ts #EXTINF:11.261233, 1080-7mbps00048.ts #EXTINF:11.261222, 1080-7mbps00049.ts #EXTINF:7.507489, 1080-7mbps00050.ts #EXTINF:11.261233, 1080-7mbps00051.ts #EXTINF:11.261233, 1080-7mbps00052.ts #EXTINF:7.507489, 1080-7mbps00053.ts #EXTINF:11.261233, 1080-7mbps00054.ts #EXTINF:11.261233, 1080-7mbps00055.ts #EXTINF:7.507478, 1080-7mbps00056.ts #EXTINF:11.261233, 1080-7mbps00057.ts #EXTINF:11.261233, 1080-7mbps00058.ts #EXTINF:7.507489, 1080-7mbps00059.ts #EXTINF:11.261233, 1080-7mbps00060.ts #EXTINF:11.261222, 1080-7mbps00061.ts #EXTINF:7.507489, 1080-7mbps00062.ts #EXTINF:11.261233, 1080-7mbps00063.ts #EXTINF:11.261233, 1080-7mbps00064.ts #EXTINF:7.507489, 1080-7mbps00065.ts #EXTINF:11.261233, 1080-7mbps00066.ts #EXTINF:11.261233, 1080-7mbps00067.ts #EXTINF:7.507478, 1080-7mbps00068.ts #EXTINF:11.261233, 1080-7mbps00069.ts #EXTINF:11.261233, 1080-7mbps00070.ts #EXTINF:7.507489, 1080-7mbps00071.ts #EXTINF:11.261233, 1080-7mbps00072.ts #EXTINF:11.261233, 1080-7mbps00073.ts #EXTINF:7.507489, 1080-7mbps00074.ts #EXTINF:11.261222, 1080-7mbps00075.ts #EXTINF:11.261233, 1080-7mbps00076.ts #EXTINF:7.507489, 1080-7mbps00077.ts #EXTINF:11.261233, 1080-7mbps00078.ts #EXTINF:11.261233, 1080-7mbps00079.ts #EXTINF:7.507478, 1080-7mbps00080.ts #EXTINF:11.261233, 1080-7mbps00081.ts #EXTINF:11.261233, 1080-7mbps00082.ts #EXTINF:7.507489, 1080-7mbps00083.ts #EXTINF:11.261233, 1080-7mbps00084.ts #EXTINF:11.261233, 1080-7mbps00085.ts #EXTINF:7.507489, 1080-7mbps00086.ts #EXTINF:11.261222, 1080-7mbps00087.ts #EXTINF:11.261233, 1080-7mbps00088.ts #EXTINF:7.507489, 1080-7mbps00089.ts #EXTINF:11.261233, 1080-7mbps00090.ts #EXTINF:11.261233, 1080-7mbps00091.ts #EXTINF:7.507478, 1080-7mbps00092.ts #EXTINF:11.261233, 1080-7mbps00093.ts #EXTINF:11.261233, 1080-7mbps00094.ts #EXTINF:7.507489, 1080-7mbps00095.ts #EXTINF:11.261233, 1080-7mbps00096.ts #EXTINF:11.261233, 1080-7mbps00097.ts #EXTINF:7.507489, 1080-7mbps00098.ts #EXTINF:11.261222, 1080-7mbps00099.ts #EXTINF:11.261233, 1080-7mbps00100.ts #EXTINF:7.507489, 1080-7mbps00101.ts #EXTINF:11.261233, 1080-7mbps00102.ts #EXTINF:11.261233, 1080-7mbps00103.ts #EXTINF:7.507478, 1080-7mbps00104.ts #EXTINF:11.261233, 1080-7mbps00105.ts #EXTINF:11.261233, 1080-7mbps00106.ts #EXTINF:7.507489, 1080-7mbps00107.ts #EXTINF:11.261233, 1080-7mbps00108.ts #EXTINF:11.261233, 1080-7mbps00109.ts #EXTINF:7.507489, 1080-7mbps00110.ts #EXTINF:11.261233, 1080-7mbps00111.ts #EXTINF:11.261233, 1080-7mbps00112.ts #EXTINF:7.507478, 1080-7mbps00113.ts #EXTINF:11.261233, 1080-7mbps00114.ts #EXTINF:11.261233, 1080-7mbps00115.ts #EXTINF:7.507478, 1080-7mbps00116.ts #EXTINF:11.261233, 1080-7mbps00117.ts #EXTINF:7.507478, 1080-7mbps00118.ts #EXTINF:11.261233, 1080-7mbps00119.ts #EXTINF:11.261233, 1080-7mbps00120.ts #EXTINF:7.507489, 1080-7mbps00121.ts #EXTINF:11.261233, 1080-7mbps00122.ts #EXTINF:11.261233, 1080-7mbps00123.ts #EXTINF:7.507489, 1080-7mbps00124.ts #EXTINF:11.261222, 1080-7mbps00125.ts #EXTINF:11.261233, 1080-7mbps00126.ts #EXTINF:7.507489, 1080-7mbps00127.ts #EXTINF:11.261233, 1080-7mbps00128.ts #EXTINF:11.261233, 1080-7mbps00129.ts #EXTINF:7.507478, 1080-7mbps00130.ts #EXTINF:11.261233, 1080-7mbps00131.ts #EXTINF:11.261233, 1080-7mbps00132.ts #EXTINF:7.507489, 1080-7mbps00133.ts #EXTINF:11.261233, 1080-7mbps00134.ts #EXTINF:11.261233, 1080-7mbps00135.ts #EXTINF:7.507489, 1080-7mbps00136.ts #EXTINF:1.793444, 1080-7mbps00137.ts golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/playlist.m3u8000066400000000000000000000124701454425674600251670ustar00rootroot00000000000000#EXTM3U #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-VERSION:4 #EXT-X-MEDIA-SEQUENCE:1 #EXT-X-DISCONTINUITY-SEQUENCE:8 #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:12 #EXTINF:11.344644, 1080-7mbps00000.ts #EXT-X-DISCONTINUITY #EXTINF:11.261233, 1080-7mbps00001.ts #EXTINF:7.507489, 1080-7mbps00002.ts #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23Z #EXTINF:11.261233, 1080-7mbps00003.ts #EXTINF:11.261233, 1080-7mbps00004.ts #EXTINF:7.507489, 1080-7mbps00005.ts #EXTINF:11.261233, 1080-7mbps00006.ts #EXTINF:11.261233, 1080-7mbps00007.ts #EXTINF:7.507478, 1080-7mbps00008.ts #EXTINF:11.261233, 1080-7mbps00009.ts #EXTINF:11.261233, 1080-7mbps00010.ts #EXTINF:7.507478, 1080-7mbps00011.ts #EXTINF:11.261233, 1080-7mbps00012.ts #EXTINF:11.261233, 1080-7mbps00013.ts #EXTINF:7.507489, 1080-7mbps00014.ts #EXTINF:11.261233, 1080-7mbps00015.ts #EXTINF:11.261233, 1080-7mbps00016.ts #EXTINF:7.507489, 1080-7mbps00017.ts #EXTINF:11.261233, 1080-7mbps00018.ts #EXTINF:11.261233, 1080-7mbps00019.ts #EXTINF:7.507478, 1080-7mbps00020.ts #EXTINF:11.261233, 1080-7mbps00021.ts #EXTINF:11.261233, 1080-7mbps00022.ts #EXTINF:7.507478, 1080-7mbps00023.ts #EXTINF:11.261233, 1080-7mbps00024.ts #EXTINF:11.261233, 1080-7mbps00025.ts #EXTINF:7.507489, 1080-7mbps00026.ts #EXTINF:11.261233, 1080-7mbps00027.ts #EXTINF:11.261233, 1080-7mbps00028.ts #EXTINF:7.507489, 1080-7mbps00029.ts #EXTINF:11.261233, 1080-7mbps00030.ts #EXTINF:11.261233, 1080-7mbps00031.ts #EXTINF:7.507478, 1080-7mbps00032.ts #EXTINF:11.261233, 1080-7mbps00033.ts #EXTINF:11.261233, 1080-7mbps00034.ts #EXTINF:7.507489, 1080-7mbps00035.ts #EXTINF:11.261233, 1080-7mbps00036.ts #EXTINF:11.261222, 1080-7mbps00037.ts #EXTINF:7.507489, 1080-7mbps00038.ts #EXTINF:11.261233, 1080-7mbps00039.ts #EXTINF:11.261233, 1080-7mbps00040.ts #EXTINF:7.507489, 1080-7mbps00041.ts #EXTINF:11.261233, 1080-7mbps00042.ts #EXTINF:11.261233, 1080-7mbps00043.ts #EXTINF:7.507478, 1080-7mbps00044.ts #EXTINF:11.261233, 1080-7mbps00045.ts #EXTINF:11.261233, 1080-7mbps00046.ts #EXTINF:7.507489, 1080-7mbps00047.ts #EXTINF:11.261233, 1080-7mbps00048.ts #EXTINF:11.261222, 1080-7mbps00049.ts #EXTINF:7.507489, 1080-7mbps00050.ts #EXTINF:11.261233, 1080-7mbps00051.ts #EXTINF:11.261233, 1080-7mbps00052.ts #EXTINF:7.507489, 1080-7mbps00053.ts #EXTINF:11.261233, 1080-7mbps00054.ts #EXTINF:11.261233, 1080-7mbps00055.ts #EXTINF:7.507478, 1080-7mbps00056.ts #EXTINF:11.261233, 1080-7mbps00057.ts #EXTINF:11.261233, 1080-7mbps00058.ts #EXTINF:7.507489, 1080-7mbps00059.ts #EXTINF:11.261233, 1080-7mbps00060.ts #EXTINF:11.261222, 1080-7mbps00061.ts #EXTINF:7.507489, 1080-7mbps00062.ts #EXTINF:11.261233, 1080-7mbps00063.ts #EXTINF:11.261233, 1080-7mbps00064.ts #EXTINF:7.507489, 1080-7mbps00065.ts #EXTINF:11.261233, 1080-7mbps00066.ts #EXTINF:11.261233, 1080-7mbps00067.ts #EXTINF:7.507478, 1080-7mbps00068.ts #EXTINF:11.261233, 1080-7mbps00069.ts #EXTINF:11.261233, 1080-7mbps00070.ts #EXTINF:7.507489, 1080-7mbps00071.ts #EXTINF:11.261233, 1080-7mbps00072.ts #EXTINF:11.261233, 1080-7mbps00073.ts #EXTINF:7.507489, 1080-7mbps00074.ts #EXTINF:11.261222, 1080-7mbps00075.ts #EXTINF:11.261233, 1080-7mbps00076.ts #EXTINF:7.507489, 1080-7mbps00077.ts #EXTINF:11.261233, 1080-7mbps00078.ts #EXTINF:11.261233, 1080-7mbps00079.ts #EXTINF:7.507478, 1080-7mbps00080.ts #EXTINF:11.261233, 1080-7mbps00081.ts #EXTINF:11.261233, 1080-7mbps00082.ts #EXTINF:7.507489, 1080-7mbps00083.ts #EXTINF:11.261233, 1080-7mbps00084.ts #EXTINF:11.261233, 1080-7mbps00085.ts #EXTINF:7.507489, 1080-7mbps00086.ts #EXTINF:11.261222, 1080-7mbps00087.ts #EXTINF:11.261233, 1080-7mbps00088.ts #EXTINF:7.507489, 1080-7mbps00089.ts #EXTINF:11.261233, 1080-7mbps00090.ts #EXTINF:11.261233, 1080-7mbps00091.ts #EXTINF:7.507478, 1080-7mbps00092.ts #EXTINF:11.261233, 1080-7mbps00093.ts #EXTINF:11.261233, 1080-7mbps00094.ts #EXTINF:7.507489, 1080-7mbps00095.ts #EXTINF:11.261233, 1080-7mbps00096.ts #EXTINF:11.261233, 1080-7mbps00097.ts #EXTINF:7.507489, 1080-7mbps00098.ts #EXTINF:11.261222, 1080-7mbps00099.ts #EXTINF:11.261233, 1080-7mbps00100.ts #EXTINF:7.507489, 1080-7mbps00101.ts #EXTINF:11.261233, 1080-7mbps00102.ts #EXTINF:11.261233, 1080-7mbps00103.ts #EXTINF:7.507478, 1080-7mbps00104.ts #EXTINF:11.261233, 1080-7mbps00105.ts #EXTINF:11.261233, 1080-7mbps00106.ts #EXTINF:7.507489, 1080-7mbps00107.ts #EXTINF:11.261233, 1080-7mbps00108.ts #EXTINF:11.261233, 1080-7mbps00109.ts #EXTINF:7.507489, 1080-7mbps00110.ts #EXTINF:11.261233, 1080-7mbps00111.ts #EXTINF:11.261233, 1080-7mbps00112.ts #EXTINF:7.507478, 1080-7mbps00113.ts #EXTINF:11.261233, 1080-7mbps00114.ts #EXTINF:11.261233, 1080-7mbps00115.ts #EXTINF:7.507478, 1080-7mbps00116.ts #EXTINF:11.261233, 1080-7mbps00117.ts #EXTINF:7.507478, 1080-7mbps00118.ts #EXTINF:11.261233, 1080-7mbps00119.ts #EXTINF:11.261233, 1080-7mbps00120.ts #EXTINF:7.507489, 1080-7mbps00121.ts #EXTINF:11.261233, 1080-7mbps00122.ts #EXTINF:11.261233, 1080-7mbps00123.ts #EXTINF:7.507489, 1080-7mbps00124.ts #EXTINF:11.261222, 1080-7mbps00125.ts #EXTINF:11.261233, 1080-7mbps00126.ts #EXTINF:7.507489, 1080-7mbps00127.ts #EXTINF:11.261233, 1080-7mbps00128.ts #EXTINF:11.261233, 1080-7mbps00129.ts #EXTINF:7.507478, 1080-7mbps00130.ts #EXTINF:11.261233, 1080-7mbps00131.ts #EXTINF:11.261233, 1080-7mbps00132.ts #EXTINF:7.507489, 1080-7mbps00133.ts #EXTINF:11.261233, 1080-7mbps00134.ts #EXTINF:11.261233, 1080-7mbps00135.ts #EXTINF:7.507489, 1080-7mbps00136.ts #EXTINF:1.793444, 1080-7mbps00137.ts #EXT-X-ENDLIST golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/playlistWithComments.m3u8000066400000000000000000000144721454425674600275350ustar00rootroot00000000000000#EXTM3U #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-VERSION:4 #EXT-X-MEDIA-SEQUENCE:1 #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:12 #EXTINF:11.344644,anything 1080-7mbps00000.ts #EXT-X-DISCONTINUITY #EXTINF:11.261233,anything 1080-7mbps00001.ts #EXTINF:7.507489,anything 1080-7mbps00002.ts #EXTINF:11.261233,anything 1080-7mbps00003.ts #EXTINF:11.261233,anything 1080-7mbps00004.ts #EXTINF:7.507489,anything 1080-7mbps00005.ts #EXTINF:11.261233,anything 1080-7mbps00006.ts #EXTINF:11.261233,anything 1080-7mbps00007.ts #EXTINF:7.507478,anything 1080-7mbps00008.ts #EXTINF:11.261233,anything 1080-7mbps00009.ts #EXTINF:11.261233,anything 1080-7mbps00010.ts #EXTINF:7.507478,anything 1080-7mbps00011.ts #EXTINF:11.261233,anything 1080-7mbps00012.ts #EXTINF:11.261233,anything 1080-7mbps00013.ts #EXTINF:7.507489,anything 1080-7mbps00014.ts #EXTINF:11.261233,anything 1080-7mbps00015.ts #EXTINF:11.261233,anything 1080-7mbps00016.ts #EXTINF:7.507489,anything 1080-7mbps00017.ts #EXTINF:11.261233,anything 1080-7mbps00018.ts #EXTINF:11.261233,anything 1080-7mbps00019.ts #EXTINF:7.507478,anything 1080-7mbps00020.ts #EXTINF:11.261233,anything 1080-7mbps00021.ts #EXTINF:11.261233,anything 1080-7mbps00022.ts #EXTINF:7.507478,anything 1080-7mbps00023.ts #EXTINF:11.261233,anything 1080-7mbps00024.ts #EXTINF:11.261233,anything 1080-7mbps00025.ts #EXTINF:7.507489,anything 1080-7mbps00026.ts #EXTINF:11.261233,anything 1080-7mbps00027.ts #EXTINF:11.261233,anything 1080-7mbps00028.ts #EXTINF:7.507489,anything 1080-7mbps00029.ts #EXTINF:11.261233,anything 1080-7mbps00030.ts #EXTINF:11.261233,anything 1080-7mbps00031.ts #EXTINF:7.507478,anything 1080-7mbps00032.ts #EXTINF:11.261233,anything 1080-7mbps00033.ts #EXTINF:11.261233,anything 1080-7mbps00034.ts #EXTINF:7.507489,anything 1080-7mbps00035.ts #EXTINF:11.261233,anything 1080-7mbps00036.ts #EXTINF:11.261222,anything 1080-7mbps00037.ts #EXTINF:7.507489,anything 1080-7mbps00038.ts #EXTINF:11.261233,anything 1080-7mbps00039.ts #EXTINF:11.261233,anything 1080-7mbps00040.ts #EXTINF:7.507489,anything 1080-7mbps00041.ts #EXTINF:11.261233,anything 1080-7mbps00042.ts #EXTINF:11.261233,anything 1080-7mbps00043.ts #EXTINF:7.507478,anything 1080-7mbps00044.ts #EXTINF:11.261233,anything 1080-7mbps00045.ts #EXTINF:11.261233,anything 1080-7mbps00046.ts #EXTINF:7.507489,anything 1080-7mbps00047.ts #EXTINF:11.261233,anything 1080-7mbps00048.ts #EXTINF:11.261222,anything 1080-7mbps00049.ts #EXTINF:7.507489,anything 1080-7mbps00050.ts #EXTINF:11.261233,anything 1080-7mbps00051.ts #EXTINF:11.261233,anything 1080-7mbps00052.ts #EXTINF:7.507489,anything 1080-7mbps00053.ts #EXTINF:11.261233,anything 1080-7mbps00054.ts #EXTINF:11.261233,anything 1080-7mbps00055.ts #EXTINF:7.507478,anything 1080-7mbps00056.ts #EXTINF:11.261233,anything 1080-7mbps00057.ts #EXTINF:11.261233,anything 1080-7mbps00058.ts #EXTINF:7.507489,anything 1080-7mbps00059.ts #EXTINF:11.261233,anything 1080-7mbps00060.ts #EXTINF:11.261222,anything 1080-7mbps00061.ts #EXTINF:7.507489,anything 1080-7mbps00062.ts #EXTINF:11.261233,anything 1080-7mbps00063.ts #EXTINF:11.261233,anything 1080-7mbps00064.ts #EXTINF:7.507489,anything 1080-7mbps00065.ts #EXTINF:11.261233,anything 1080-7mbps00066.ts #EXTINF:11.261233,anything 1080-7mbps00067.ts #EXTINF:7.507478,anything 1080-7mbps00068.ts #EXTINF:11.261233,anything 1080-7mbps00069.ts #EXTINF:11.261233,anything 1080-7mbps00070.ts #EXTINF:7.507489,anything 1080-7mbps00071.ts #EXTINF:11.261233,anything 1080-7mbps00072.ts #EXTINF:11.261233,anything 1080-7mbps00073.ts #EXTINF:7.507489,anything 1080-7mbps00074.ts #EXTINF:11.261222,anything 1080-7mbps00075.ts #EXTINF:11.261233,anything 1080-7mbps00076.ts #EXTINF:7.507489,anything 1080-7mbps00077.ts #EXTINF:11.261233,anything 1080-7mbps00078.ts #EXTINF:11.261233,anything 1080-7mbps00079.ts #EXTINF:7.507478,anything 1080-7mbps00080.ts #EXTINF:11.261233,anything 1080-7mbps00081.ts #EXTINF:11.261233,anything 1080-7mbps00082.ts #EXTINF:7.507489,anything 1080-7mbps00083.ts #EXTINF:11.261233,anything 1080-7mbps00084.ts #EXTINF:11.261233,anything 1080-7mbps00085.ts #EXTINF:7.507489,anything 1080-7mbps00086.ts #EXTINF:11.261222,anything 1080-7mbps00087.ts #EXTINF:11.261233,anything 1080-7mbps00088.ts #EXTINF:7.507489,anything 1080-7mbps00089.ts #EXTINF:11.261233,anything 1080-7mbps00090.ts #EXTINF:11.261233,anything 1080-7mbps00091.ts #EXTINF:7.507478,anything 1080-7mbps00092.ts #EXTINF:11.261233,anything 1080-7mbps00093.ts #EXTINF:11.261233,anything 1080-7mbps00094.ts #EXTINF:7.507489,anything 1080-7mbps00095.ts #EXTINF:11.261233,anything 1080-7mbps00096.ts #EXTINF:11.261233,anything 1080-7mbps00097.ts #EXTINF:7.507489,anything 1080-7mbps00098.ts #EXTINF:11.261222,anything 1080-7mbps00099.ts #EXTINF:11.261233,anything 1080-7mbps00100.ts #EXTINF:7.507489,anything 1080-7mbps00101.ts #EXTINF:11.261233,anything 1080-7mbps00102.ts #EXTINF:11.261233,anything 1080-7mbps00103.ts #EXTINF:7.507478,anything 1080-7mbps00104.ts #EXTINF:11.261233,anything 1080-7mbps00105.ts #EXTINF:11.261233,anything 1080-7mbps00106.ts #EXTINF:7.507489,anything 1080-7mbps00107.ts #EXTINF:11.261233,anything 1080-7mbps00108.ts #EXTINF:11.261233,anything 1080-7mbps00109.ts #EXTINF:7.507489,anything 1080-7mbps00110.ts #EXTINF:11.261233,anything 1080-7mbps00111.ts #EXTINF:11.261233,anything 1080-7mbps00112.ts #EXTINF:7.507478,anything 1080-7mbps00113.ts #EXTINF:11.261233,anything 1080-7mbps00114.ts #EXTINF:11.261233,anything 1080-7mbps00115.ts #EXTINF:7.507478,anything 1080-7mbps00116.ts #EXTINF:11.261233,anything 1080-7mbps00117.ts #EXTINF:7.507478,anything 1080-7mbps00118.ts #EXTINF:11.261233,anything 1080-7mbps00119.ts #EXTINF:11.261233,anything 1080-7mbps00120.ts #EXTINF:7.507489,anything 1080-7mbps00121.ts #EXTINF:11.261233,anything 1080-7mbps00122.ts #EXTINF:11.261233,anything 1080-7mbps00123.ts #EXTINF:7.507489,anything 1080-7mbps00124.ts #EXTINF:11.261222,anything 1080-7mbps00125.ts #EXTINF:11.261233,anything 1080-7mbps00126.ts #EXTINF:7.507489,anything 1080-7mbps00127.ts #EXTINF:11.261233,anything 1080-7mbps00128.ts #EXTINF:11.261233,anything 1080-7mbps00129.ts #EXTINF:7.507478,anything 1080-7mbps00130.ts #EXTINF:11.261233,anything 1080-7mbps00131.ts #EXTINF:11.261233,anything 1080-7mbps00132.ts #EXTINF:7.507489,anything 1080-7mbps00133.ts #EXTINF:11.261233,anything 1080-7mbps00134.ts #EXTINF:11.261233,anything 1080-7mbps00135.ts #EXTINF:7.507489,anything 1080-7mbps00136.ts #EXTINF:1.793444,anything 1080-7mbps00137.ts #EXT-X-ENDLIST golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/sessionData.m3u8000066400000000000000000000003761454425674600256050ustar00rootroot00000000000000#EXTM3U #EXT-X-SESSION-DATA:DATA-ID="com.example.lyrics",URI="lyrics.json" #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="sp",VALUE="Este es un ejemplo" golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/timestampPlaylist.m3u8000066400000000000000000000012201454425674600270420ustar00rootroot00000000000000#EXTM3U #EXT-X-VERSION:2 #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:1736515 #EXTINF:10, no desc #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:24:31Z 20160408T084506-01-1736515.ts #EXTINF:10, no desc #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:24:41Z 20160408T084506-01-1736516.ts #EXTINF:10, no desc #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:24:51Z 20160408T084506-01-1736517.ts #EXTINF:10, no desc #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:25:01Z 20160408T084506-01-1736518.ts #EXTINF:10, no desc #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:25:11Z 20160408T084506-01-1736519.ts #EXTINF:10, no desc #EXT-X-PROGRAM-DATE-TIME:2016-04-11T15:25:21Z 20160408T084506-01-1736520.ts golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/variantAngles.m3u8000066400000000000000000000024661454425674600261300ustar00rootroot00000000000000#EXTM3U #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/200kbs/prog_index.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/200kbs/prog_index.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/500kbs/prog_index.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/500kbs/prog_index.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Subtiles",AUTOSELECT=NO,DEFAULT=NO,URI="titles_file" #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="captions",NAME="Closed Captions",AUTOSELECT=NO,DEFAULT=NO,URI="captions_file" #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" Angle1/200kbs/prog_index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="500kbs",AUDIO="aac" Angle1/500kbs/prog_index.m3u8 golang-github-etherlabsio-go-m3u8-1.0.0/test/fixtures/variantAudio.m3u8000066400000000000000000000023501454425674600257500ustar00rootroot00000000000000#EXTM3U #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 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES,DEFAULT=NO,URI="frelo/prog_index.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES,DEFAULT=NO,URI="splo/prog_index.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES,DEFAULT=NO,URI="fre/prog_index.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES,DEFAULT=NO,URI="sp/prog_index.m3u8" #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5",AUDIO="audio-lo" lo/prog_index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2",AUDIO="audio-lo" hi/prog_index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e",AUDIO="audio-hi" lo/prog_index.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2",AUDIO="audio-hi" hi/prog_index.m3u8 golang-github-etherlabsio-go-m3u8-1.0.0/test/keyItem_test.go000066400000000000000000000012451454425674600237320ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestKeyItem_Parse(t *testing.T) { line := `#EXT-X-KEY:METHOD=AES-128,URI="http://test.key",IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3"` ki, err := m3u8.NewKeyItem(line) assert.Nil(t, err) assert.NotNil(t, ki.Encryptable) assert.Equal(t, "AES-128", ki.Encryptable.Method) assertNotNilEqual(t, "http://test.key", ki.Encryptable.URI) assertNotNilEqual(t, "D512BBF", ki.Encryptable.IV) assertNotNilEqual(t, "identity", ki.Encryptable.KeyFormat) assertNotNilEqual(t, "1/3", ki.Encryptable.KeyFormatVersions) assertToString(t, line, ki) } golang-github-etherlabsio-go-m3u8-1.0.0/test/mapItem_test.go000066400000000000000000000013541454425674600237200ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestMapItem_Parse(t *testing.T) { line := `#EXT-X-MAP:URI="frelo/prog_index.m3u8",BYTERANGE="3500@300"` mi, err := m3u8.NewMapItem(line) assert.Nil(t, err) assert.Equal(t, "frelo/prog_index.m3u8", mi.URI) assert.NotNil(t, mi.ByteRange) assertNotNilEqual(t, 3500, mi.ByteRange.Length) assertNotNilEqual(t, 300, mi.ByteRange.Start) assertToString(t, line, mi) } func TestMapItem_Parse_2(t *testing.T) { line := `#EXT-X-MAP:URI="frelo/prog_index.m3u8"` mi, err := m3u8.NewMapItem(line) assert.Nil(t, err) assert.Equal(t, "frelo/prog_index.m3u8", mi.URI) assert.Nil(t, mi.ByteRange) assertToString(t, line, mi) } golang-github-etherlabsio-go-m3u8-1.0.0/test/mediaItem_test.go000066400000000000000000000025651454425674600242270ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestMediaItem_Parse(t *testing.T) { line := `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre", ASSOC-LANGUAGE="spoken",NAME="Francais",AUTOSELECT=YES, INSTREAM-ID="SERVICE3",CHARACTERISTICS="public.html", CHANNELS="6", "DEFAULT=NO,URI="frelo/prog_index.m3u8",STABLE-RENDITION-ID="1234",FORCED=YES "` mi, err := m3u8.NewMediaItem(line) assert.Nil(t, err) assert.Equal(t, "AUDIO", mi.Type) assert.Equal(t, "audio-lo", mi.GroupID) assert.Equal(t, "Francais", mi.Name) assertNotNilEqual(t, "1234", mi.StableRenditionId) assertNotNilEqual(t, "fre", mi.Language) assertNotNilEqual(t, "spoken", mi.AssocLanguage) assertNotNilEqual(t, true, mi.AutoSelect) assertNotNilEqual(t, false, mi.Default) assertNotNilEqual(t, "frelo/prog_index.m3u8", mi.URI) assertNotNilEqual(t, true, mi.Forced) assertNotNilEqual(t, "SERVICE3", mi.InStreamID) assertNotNilEqual(t, "public.html", mi.Characteristics) assertNotNilEqual(t, "6", mi.Channels) 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\"" assertToString(t, expected, mi) } golang-github-etherlabsio-go-m3u8-1.0.0/test/playbackStart_test.go000066400000000000000000000011711454425674600251250ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestPlaybackStart_Parse(t *testing.T) { line := `#EXT-X-START:TIME-OFFSET=20.2,PRECISE=YES` ps, err := m3u8.NewPlaybackStart(line) assert.Nil(t, err) assert.Equal(t, 20.2, ps.TimeOffset) assertNotNilEqual(t, true, ps.Precise) assertToString(t, line, ps) } func TestPlaybackStart_Parse_2(t *testing.T) { line := `#EXT-X-START:TIME-OFFSET=-12.9` ps, err := m3u8.NewPlaybackStart(line) assert.Nil(t, err) assert.Equal(t, -12.9, ps.TimeOffset) assert.Nil(t, ps.Precise) assertToString(t, line, ps) } golang-github-etherlabsio-go-m3u8-1.0.0/test/playlistItem_test.go000066400000000000000000000163101454425674600250020ustar00rootroot00000000000000package test import ( "testing" "github.com/AlekSi/pointer" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestPlaylistItem_Parse(t *testing.T) { line := `#EXT-X-STREAM-INF:CODECS="avc",BANDWIDTH=540, PROGRAM-ID=1,RESOLUTION=1920x1080,FRAME-RATE=23.976, AVERAGE-BANDWIDTH=550,AUDIO="test",VIDEO="test2",STABLE-VARIANT-ID="1234" SUBTITLES="subs",CLOSED-CAPTIONS="caps",URI="test.url", NAME="1080p",HDCP-LEVEL=TYPE-0` pi, err := m3u8.NewPlaylistItem(line) assert.Nil(t, err) assertNotNilEqual(t, "1", pi.ProgramID) assertNotNilEqual(t, "avc", pi.Codecs) assert.Equal(t, 540, pi.Bandwidth) assertNotNilEqual(t, 550, pi.AverageBandwidth) assertNotNilEqual(t, 1920, pi.Width) assertNotNilEqual(t, 1080, pi.Height) assertNotNilEqual(t, 23.976, pi.FrameRate) assertNotNilEqual(t, "test", pi.Audio) assertNotNilEqual(t, "test2", pi.Video) assertNotNilEqual(t, "subs", pi.Subtitles) assertNotNilEqual(t, "caps", pi.ClosedCaptions) assert.Equal(t, "test.url", pi.URI) assertNotNilEqual(t, "1080p", pi.Name) assert.False(t, pi.IFrame) assertNotNilEqual(t, "TYPE-0", pi.HDCPLevel) assertNotNilEqual(t, "1234", pi.StableVariantID) } func TestPlaylistItem_ToString(t *testing.T) { // No codecs specified p := &m3u8.PlaylistItem{ Bandwidth: 540, URI: "test.url", } assert.NotContains(t, p.String(), "CODECS") // Level not recognized p = &m3u8.PlaylistItem{ Bandwidth: 540, URI: "test.url", Level: pointer.ToString("9001"), } assert.NotContains(t, p.String(), "CODECS") // Audio codec recognized but profile not recognized p = &m3u8.PlaylistItem{ Bandwidth: 540, URI: "test.url", Profile: pointer.ToString("best"), Level: pointer.ToString("9001"), AudioCodec: pointer.ToString("aac-lc"), } assert.NotContains(t, p.String(), "CODECS") // Profile and level not set, Audio codec recognized p = &m3u8.PlaylistItem{ Bandwidth: 540, URI: "test.url", AudioCodec: pointer.ToString("aac-lc"), } assert.Contains(t, p.String(), "CODECS") // Profile and level recognized, audio codec not recognized p = &m3u8.PlaylistItem{ Bandwidth: 540, URI: "test.url", Profile: pointer.ToString("high"), Level: pointer.ToString("4.1"), AudioCodec: pointer.ToString("fuzzy"), } assert.NotContains(t, p.String(), "CODECS") // Audio codec not set p = &m3u8.PlaylistItem{ Bandwidth: 540, URI: "test.url", Profile: pointer.ToString("high"), Level: pointer.ToString("4.1"), } assert.Contains(t, p.String(), `CODECS="avc1.640029"`) // Audio codec recognized p = &m3u8.PlaylistItem{ Bandwidth: 540, URI: "test.url", Profile: pointer.ToString("high"), Level: pointer.ToString("4.1"), AudioCodec: pointer.ToString("aac-lc"), } assert.Contains(t, p.String(), `CODECS="avc1.640029,mp4a.40.2"`) } func TestPlaylistItem_ToString_2(t *testing.T) { // All fields set p := &m3u8.PlaylistItem{ Codecs: pointer.ToString("avc"), Bandwidth: 540, URI: "test.url", Audio: pointer.ToString("test"), Video: pointer.ToString("test2"), AverageBandwidth: pointer.ToInt(500), Subtitles: pointer.ToString("subs"), FrameRate: pointer.ToFloat64(30), ClosedCaptions: pointer.ToString("caps"), Name: pointer.ToString("SD"), HDCPLevel: pointer.ToString("TYPE-0"), ProgramID: pointer.ToString("1"), StableVariantID: pointer.ToString("1234"), } 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" test.url` assert.Equal(t, expected, p.String()) // Closed captions is NONE p = &m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Codecs: pointer.ToString("avc"), Bandwidth: 540, URI: "test.url", ClosedCaptions: pointer.ToString("NONE"), } expected = `#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc",BANDWIDTH=540,CLOSED-CAPTIONS=NONE test.url` assert.Equal(t, expected, p.String()) // IFrame is true p = &m3u8.PlaylistItem{ Codecs: pointer.ToString("avc"), Bandwidth: 540, URI: "test.url", IFrame: true, Video: pointer.ToString("test2"), AverageBandwidth: pointer.ToInt(550), } expected = `#EXT-X-I-FRAME-STREAM-INF:CODECS="avc",BANDWIDTH=540,AVERAGE-BANDWIDTH=550,VIDEO="test2",URI="test.url"` assert.Equal(t, expected, p.String()) } func TestPlaylistItem_GenerateCodecs(t *testing.T) { assertCodecs(t, "", &m3u8.PlaylistItem{}) assertCodecs(t, "test", &m3u8.PlaylistItem{Codecs: pointer.ToString("test")}) assertCodecs(t, "mp4a.40.2", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("aac-lc")}) assertCodecs(t, "mp4a.40.2", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("AAC-LC")}) assertCodecs(t, "mp4a.40.5", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("he-aac")}) assertCodecs(t, "", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("he-aac1")}) assertCodecs(t, "mp4a.40.34", &m3u8.PlaylistItem{AudioCodec: pointer.ToString("mp3")}) assertCodecs(t, "avc1.66.30", &m3u8.PlaylistItem{ Profile: pointer.ToString("baseline"), Level: pointer.ToString("3.0"), }) assertCodecs(t, "avc1.66.30,mp4a.40.2", &m3u8.PlaylistItem{ Profile: pointer.ToString("baseline"), Level: pointer.ToString("3.0"), AudioCodec: pointer.ToString("aac-lc"), }) assertCodecs(t, "avc1.66.30,mp4a.40.34", &m3u8.PlaylistItem{ Profile: pointer.ToString("baseline"), Level: pointer.ToString("3.0"), AudioCodec: pointer.ToString("mp3"), }) assertCodecs(t, "avc1.42001f", &m3u8.PlaylistItem{ Profile: pointer.ToString("baseline"), Level: pointer.ToString("3.1"), }) assertCodecs(t, "avc1.42001f,mp4a.40.5", &m3u8.PlaylistItem{ Profile: pointer.ToString("baseline"), Level: pointer.ToString("3.1"), AudioCodec: pointer.ToString("he-aac"), }) assertCodecs(t, "avc1.77.30", &m3u8.PlaylistItem{ Profile: pointer.ToString("main"), Level: pointer.ToString("3.0"), }) assertCodecs(t, "avc1.77.30,mp4a.40.2", &m3u8.PlaylistItem{ Profile: pointer.ToString("main"), Level: pointer.ToString("3.0"), AudioCodec: pointer.ToString("aac-lc"), }) assertCodecs(t, "avc1.4d001f", &m3u8.PlaylistItem{ Profile: pointer.ToString("main"), Level: pointer.ToString("3.1"), }) assertCodecs(t, "avc1.4d0028", &m3u8.PlaylistItem{ Profile: pointer.ToString("main"), Level: pointer.ToString("4.0"), }) assertCodecs(t, "avc1.4d0029", &m3u8.PlaylistItem{ Profile: pointer.ToString("main"), Level: pointer.ToString("4.1"), }) assertCodecs(t, "avc1.64001f", &m3u8.PlaylistItem{ Profile: pointer.ToString("high"), Level: pointer.ToString("3.1"), }) assertCodecs(t, "avc1.640028", &m3u8.PlaylistItem{ Profile: pointer.ToString("high"), Level: pointer.ToString("4.0"), }) assertCodecs(t, "avc1.640029", &m3u8.PlaylistItem{ Profile: pointer.ToString("high"), Level: pointer.ToString("4.1"), }) } func assertCodecs(t *testing.T, codecs string, p *m3u8.PlaylistItem) { assert.Equal(t, codecs, p.CodecsString()) } golang-github-etherlabsio-go-m3u8-1.0.0/test/playlist_test.go000066400000000000000000000121721454425674600241650ustar00rootroot00000000000000package test import ( "fmt" "testing" "github.com/AlekSi/pointer" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestPlaylist_New(t *testing.T) { p := &m3u8.Playlist{Master: pointer.ToBool(true)} assert.True(t, p.IsMaster()) p, err := m3u8.ReadFile("fixtures/master.m3u8") assert.Nil(t, err) assert.True(t, p.IsMaster()) assert.Equal(t, len(p.Items), 8) } func TestPlaylist_Duration(t *testing.T) { p := &m3u8.Playlist{ Items: []m3u8.Item{ &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, &m3u8.SegmentItem{Duration: 9.891, Segment: "test_02.ts"}, &m3u8.SegmentItem{Duration: 10.556, Segment: "test_03.ts"}, &m3u8.SegmentItem{Duration: 8.790, Segment: "test_04.ts"}, }, } assert.Equal(t, "40.228", fmt.Sprintf("%.3f", p.Duration())) } func TestPlaylist_Master(t *testing.T) { // Normal master playlist p := &m3u8.Playlist{ Items: []m3u8.Item{ &m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), URI: "playlist_url", Bandwidth: 6400, AudioCodec: pointer.ToString("mp3"), }, }, } assert.True(t, p.IsMaster()) // Media playlist p = &m3u8.Playlist{ Items: []m3u8.Item{ &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, }, } assert.False(t, p.IsMaster()) // Forced master tag p = &m3u8.Playlist{ Master: pointer.ToBool(true), } assert.True(t, p.IsMaster()) } func TestPlaylist_Live(t *testing.T) { // Normal master playlist p := &m3u8.Playlist{ Items: []m3u8.Item{ &m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), URI: "playlist_url", Bandwidth: 6400, AudioCodec: pointer.ToString("mp3"), }, }, } assert.False(t, p.IsLive()) // Media playlist set as live p = &m3u8.Playlist{ Items: []m3u8.Item{ &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, }, Live: true, } assert.True(t, p.IsLive()) } func TestPlaylist_ToString(t *testing.T) { p := &m3u8.Playlist{ Items: []m3u8.Item{ &m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), URI: "playlist_url", Bandwidth: 6400, AudioCodec: pointer.ToString("mp3"), }, &m3u8.PlaylistItem{ ProgramID: pointer.ToString("2"), URI: "playlist_url", Bandwidth: 50000, Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Profile: pointer.ToString("high"), Level: pointer.ToString("4.1"), AudioCodec: pointer.ToString("aac-lc"), }, }, } expected := `#EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",BANDWIDTH=6400 playlist_url #EXT-X-STREAM-INF:PROGRAM-ID=2,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",BANDWIDTH=50000 playlist_url ` assert.Equal(t, expected, p.String()) p = m3u8.NewPlaylistWithItems( []m3u8.Item{ &m3u8.SegmentItem{Duration: 11.344644, Segment: "1080-7mbps00000.ts"}, &m3u8.SegmentItem{Duration: 11.261233, Segment: "1080-7mbps00001.ts"}, }, ) expected = `#EXTM3U #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:10 #EXTINF:11.344644, 1080-7mbps00000.ts #EXTINF:11.261233, 1080-7mbps00001.ts #EXT-X-ENDLIST ` assert.Equal(t, expected, p.String()) } func TestPlaylist_Valid(t *testing.T) { p := m3u8.NewPlaylist() assert.True(t, p.IsValid()) p.AppendItem(&m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), URI: "playlist_url", Bandwidth: 540, Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Codecs: pointer.ToString("avc"), }) assert.True(t, p.IsValid()) assert.Equal(t, 1, len(p.Items)) p.AppendItem(&m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), URI: "playlist_url", Bandwidth: 540, Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Codecs: pointer.ToString("avc"), }) assert.True(t, p.IsValid()) assert.Equal(t, 2, len(p.Items)) p.AppendItem(&m3u8.SegmentItem{ Duration: 10.991, Segment: "test.ts", }) assert.False(t, p.IsValid()) } func TestPlaylist_PlaylistSize(t *testing.T) { p := m3u8.NewPlaylist() assert.True(t, p.IsValid()) p.AppendItem(&m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), URI: "playlist0_url", Bandwidth: 540, Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Codecs: pointer.ToString("avc"), }) p.AppendItem(&m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), URI: "playlist1_url", Bandwidth: 540, Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Codecs: pointer.ToString("avc"), }) assert.Equal(t, 2, p.PlaylistSize()) pi := p.Playlists() assert.Equal(t, "playlist0_url", pi[0].URI) assert.Equal(t, "playlist1_url", pi[1].URI) } func TestPlaylist_Segments(t *testing.T) { p := &m3u8.Playlist{ Items: []m3u8.Item{ &m3u8.SegmentItem{Duration: 10.991, Segment: "test_01.ts"}, &m3u8.SegmentItem{Duration: 9.891, Segment: "test_02.ts"}, &m3u8.SegmentItem{Duration: 10.556, Segment: "test_03.ts"}, &m3u8.SegmentItem{Duration: 8.790, Segment: "test_04.ts"}, }, } assert.Equal(t, 4, p.SegmentSize()) si := p.Segments() assert.Equal(t, "test_01.ts", si[0].Segment) assert.Equal(t, "test_02.ts", si[1].Segment) assert.Equal(t, 10.556, si[2].Duration) assert.Equal(t, 8.790, si[3].Duration) } golang-github-etherlabsio-go-m3u8-1.0.0/test/reader_test.go000066400000000000000000000202701454425674600235640ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestReader(t *testing.T) { p, err := m3u8.ReadFile("fixtures/master.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.True(t, p.IsMaster()) assert.Nil(t, p.DiscontinuitySequence) assert.True(t, p.IndependentSegments) item := p.Items[0] assert.IsType(t, &m3u8.SessionKeyItem{}, item) keyItem := item.(*m3u8.SessionKeyItem) assert.Equal(t, "AES-128", keyItem.Encryptable.Method) assertNotNilEqual(t, "https://priv.example.com/key.php?r=52", keyItem.Encryptable.URI) item = p.Items[1] assert.IsType(t, &m3u8.PlaybackStart{}, item) psi := item.(*m3u8.PlaybackStart) assert.Equal(t, 20.2, psi.TimeOffset) item = p.Items[2] assert.IsType(t, &m3u8.PlaylistItem{}, item) pi := item.(*m3u8.PlaylistItem) assert.Equal(t, "hls/1080-7mbps/1080-7mbps.m3u8", pi.URI) assertNotNilEqual(t, "1", pi.ProgramID) assertNotNilEqual(t, 1920, pi.Width) assertNotNilEqual(t, 1080, pi.Height) assert.Equal(t, "1920x1080", pi.Resolution.String()) assert.Equal(t, "avc1.640028,mp4a.40.2", pi.CodecsString()) assert.Equal(t, 5042000, pi.Bandwidth) assert.False(t, pi.IFrame) assert.Nil(t, pi.AverageBandwidth) item = p.Items[7] assert.IsType(t, &m3u8.PlaylistItem{}, item) pi = item.(*m3u8.PlaylistItem) assert.Equal(t, "hls/64k/64k.m3u8", pi.URI) assertNotNilEqual(t, "1", pi.ProgramID) assert.Nil(t, pi.Height) assert.Nil(t, pi.Width) assert.Empty(t, pi.Resolution.String()) assert.Equal(t, 6400, pi.Bandwidth) assert.False(t, pi.IFrame) assert.Nil(t, pi.AverageBandwidth) assert.Equal(t, 8, p.ItemSize()) item = p.Items[len(p.Items)-1] assert.IsType(t, &m3u8.PlaylistItem{}, item) pi = item.(*m3u8.PlaylistItem) assert.Empty(t, pi.Resolution.String()) } func TestReader_IFrame(t *testing.T) { p, err := m3u8.ReadFile("fixtures/masterIframes.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.True(t, p.IsMaster()) assert.Equal(t, 7, p.ItemSize()) item := p.Items[1] assert.IsType(t, &m3u8.PlaylistItem{}, item) pi := item.(*m3u8.PlaylistItem) assert.Equal(t, "low/iframe.m3u8", pi.URI) assert.Equal(t, 86000, pi.Bandwidth) assert.True(t, pi.IFrame) } func TestReader_MediaPlaylist(t *testing.T) { p, err := m3u8.ReadFile("fixtures/playlist.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.False(t, p.IsMaster()) assertNotNilEqual(t, 4, p.Version) assert.Equal(t, 1, p.Sequence) assertNotNilEqual(t, 8, p.DiscontinuitySequence) assertNotNilEqual(t, false, p.Cache) assert.Equal(t, 12, p.Target) assertNotNilEqual(t, "VOD", p.Type) item := p.Items[0] assert.IsType(t, &m3u8.SegmentItem{}, item) si := item.(*m3u8.SegmentItem) assert.Equal(t, 11.344644, si.Duration) assert.Nil(t, si.Comment) item = p.Items[4] assert.IsType(t, &m3u8.TimeItem{}, item) ti := item.(*m3u8.TimeItem) assert.Equal(t, "2010-02-19T14:54:23Z", m3u8.FormatTime(ti.Time)) assert.Equal(t, 140, p.ItemSize()) } func TestReader_PlaylistLiveCheck(t *testing.T) { p, err := m3u8.ReadFile("fixtures/playlist.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.False(t, p.IsLive()) p, err = m3u8.ReadFile("fixtures/playlist-live.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.True(t, p.IsLive()) } func TestReader_IFramePlaylist(t *testing.T) { p, err := m3u8.ReadFile("fixtures/iframes.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.True(t, p.IFramesOnly) assert.Equal(t, 3, p.ItemSize()) item := p.Items[0] assert.IsType(t, &m3u8.SegmentItem{}, item) si := item.(*m3u8.SegmentItem) assert.Equal(t, 4.12, si.Duration) assert.NotNil(t, si.ByteRange) assertNotNilEqual(t, 9400, si.ByteRange.Length) assertNotNilEqual(t, 376, si.ByteRange.Start) assert.Equal(t, "segment1.ts", si.Segment) item = p.Items[1] assert.IsType(t, &m3u8.SegmentItem{}, item) si = item.(*m3u8.SegmentItem) assert.NotNil(t, si.ByteRange) assertNotNilEqual(t, 7144, si.ByteRange.Length) assert.Nil(t, si.ByteRange.Start) } func TestReader_PlaylistWithComments(t *testing.T) { p, err := m3u8.ReadFile("fixtures/playlistWithComments.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.False(t, p.IsMaster()) assertNotNilEqual(t, 4, p.Version) assert.Equal(t, 1, p.Sequence) assertNotNilEqual(t, false, p.Cache) assert.Equal(t, 12, p.Target) assertNotNilEqual(t, "VOD", p.Type) item := p.Items[0] assert.IsType(t, &m3u8.SegmentItem{}, item) si := item.(*m3u8.SegmentItem) assert.Equal(t, 11.344644, si.Duration) assertNotNilEqual(t, "anything", si.Comment) item = p.Items[1] assert.IsType(t, &m3u8.DiscontinuityItem{}, item) assert.Equal(t, 139, p.ItemSize()) } func TestReader_VariantAudio(t *testing.T) { p, err := m3u8.ReadFile("fixtures/variantAudio.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.True(t, p.IsMaster()) assert.Equal(t, 10, p.ItemSize()) item := p.Items[0] assert.IsType(t, &m3u8.MediaItem{}, item) mi := item.(*m3u8.MediaItem) assert.Equal(t, "AUDIO", mi.Type) assert.Equal(t, "audio-lo", mi.GroupID) assert.Equal(t, "English", mi.Name) assertNotNilEqual(t, "eng", mi.Language) assertNotNilEqual(t, "spoken", mi.AssocLanguage) assertNotNilEqual(t, true, mi.AutoSelect) assertNotNilEqual(t, true, mi.Default) assertNotNilEqual(t, "englo/prog_index.m3u8", mi.URI) assertNotNilEqual(t, true, mi.Forced) } func TestReader_VariantAngles(t *testing.T) { p, err := m3u8.ReadFile("fixtures/variantAngles.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.True(t, p.IsMaster()) assert.Equal(t, 11, p.ItemSize()) item := p.Items[1] assert.IsType(t, &m3u8.MediaItem{}, item) mi := item.(*m3u8.MediaItem) assert.Equal(t, "VIDEO", mi.Type) assert.Equal(t, "200kbs", mi.GroupID) assert.Equal(t, "Angle2", mi.Name) assert.Nil(t, mi.Language) assertNotNilEqual(t, true, mi.AutoSelect) assertNotNilEqual(t, false, mi.Default) assertNotNilEqual(t, "Angle2/200kbs/prog_index.m3u8", mi.URI) item = p.Items[9] assert.IsType(t, &m3u8.PlaylistItem{}, item) pi := item.(*m3u8.PlaylistItem) assertNotNilEqual(t, 300001, pi.AverageBandwidth) assertNotNilEqual(t, "aac", pi.Audio) assertNotNilEqual(t, "200kbs", pi.Video) assertNotNilEqual(t, "captions", pi.ClosedCaptions) assertNotNilEqual(t, "subs", pi.Subtitles) } func TestReader_SessionData(t *testing.T) { p, err := m3u8.ReadFile("fixtures/sessionData.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.Equal(t, 3, p.ItemSize()) item := p.Items[0] assert.IsType(t, &m3u8.SessionDataItem{}, item) sdi := item.(*m3u8.SessionDataItem) assert.Equal(t, "com.example.lyrics", sdi.DataID) assertNotNilEqual(t, "lyrics.json", sdi.URI) } func TestReader_Encrypted(t *testing.T) { p, err := m3u8.ReadFile("fixtures/encrypted.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.Equal(t, 6, p.ItemSize()) item := p.Items[0] assert.IsType(t, &m3u8.KeyItem{}, item) ki := item.(*m3u8.KeyItem) assert.Equal(t, "AES-128", ki.Encryptable.Method) assertNotNilEqual(t, "https://priv.example.com/key.php?r=52", ki.Encryptable.URI) } func TestReader_Map(t *testing.T) { p, err := m3u8.ReadFile("fixtures/mapPlaylist.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.Equal(t, 1, p.ItemSize()) item := p.Items[0] assert.IsType(t, &m3u8.MapItem{}, item) mi := item.(*m3u8.MapItem) assert.Equal(t, "frelo/prog_index.m3u8", mi.URI) assert.NotNil(t, mi.ByteRange) assertNotNilEqual(t, 4500, mi.ByteRange.Length) assertNotNilEqual(t, 600, mi.ByteRange.Start) } func TestReader_Timestamp(t *testing.T) { p, err := m3u8.ReadFile("fixtures/timestampPlaylist.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.Equal(t, 6, p.ItemSize()) item := p.Items[0] assert.IsType(t, &m3u8.SegmentItem{}, item) si := item.(*m3u8.SegmentItem) assert.NotNil(t, si.ProgramDateTime) assert.Equal(t, "2016-04-11T15:24:31Z", m3u8.FormatTime(si.ProgramDateTime.Time)) } func TestReader_DateRange(t *testing.T) { p, err := m3u8.ReadFile("fixtures/dateRangeScte35.m3u8") assert.Nil(t, err) assert.True(t, p.IsValid()) assert.Equal(t, 5, p.ItemSize()) item := &m3u8.DateRangeItem{} assert.IsType(t, item, p.Items[0]) assert.IsType(t, item, p.Items[4]) } func TestReader_Invalid(t *testing.T) { _, err := m3u8.ReadFile("path/to/file") assert.NotNil(t, err) } golang-github-etherlabsio-go-m3u8-1.0.0/test/segmentItem_test.go000066400000000000000000000023661454425674600246110ustar00rootroot00000000000000package test import ( "testing" "github.com/AlekSi/pointer" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestSegmentItem_Parse(t *testing.T) { time, err := m3u8.ParseTime("2010-02-19T14:54:23Z") assert.Nil(t, err) item := &m3u8.SegmentItem{ Duration: 10.991, Segment: "test.ts", ProgramDateTime: &m3u8.TimeItem{ Time: time, }, } assert.Equal(t, "#EXTINF:10.991,\n#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23Z\ntest.ts", item.String()) item = &m3u8.SegmentItem{ Duration: 10.991, Segment: "test.ts", Comment: pointer.ToString("anything"), } assert.Equal(t, "#EXTINF:10.991,anything\ntest.ts", item.String()) item = &m3u8.SegmentItem{ Duration: 10.991, Segment: "test.ts", Comment: pointer.ToString("anything"), ByteRange: &m3u8.ByteRange{ Length: pointer.ToInt(4500), Start: pointer.ToInt(600), }, } assert.Equal(t, "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500@600\ntest.ts", item.String()) item = &m3u8.SegmentItem{ Duration: 10.991, Segment: "test.ts", Comment: pointer.ToString("anything"), ByteRange: &m3u8.ByteRange{ Length: pointer.ToInt(4500), }, } assert.Equal(t, "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500\ntest.ts", item.String()) } golang-github-etherlabsio-go-m3u8-1.0.0/test/sessionDataItem_test.go000066400000000000000000000015361454425674600254220ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestSessionDataItem_Parse(t *testing.T) { line := `#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",VALUE="Test",LANGUAGE="en"` sdi, err := m3u8.NewSessionDataItem(line) assert.Nil(t, err) assert.Equal(t, "com.test.movie.title", sdi.DataID) assertNotNilEqual(t, "Test", sdi.Value) assert.Nil(t, sdi.URI) assertNotNilEqual(t, "en", sdi.Language) assertToString(t, line, sdi) line = `#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",URI="http://test",LANGUAGE="en"` sdi, err = m3u8.NewSessionDataItem(line) assert.Nil(t, err) assert.Equal(t, "com.test.movie.title", sdi.DataID) assert.Nil(t, sdi.Value) assertNotNilEqual(t, "http://test", sdi.URI) assertNotNilEqual(t, "en", sdi.Language) assertToString(t, line, sdi) } golang-github-etherlabsio-go-m3u8-1.0.0/test/sessionKeyItem_test.go000066400000000000000000000013041454425674600252720ustar00rootroot00000000000000package test import ( "testing" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestSessionKeyItem_Parse(t *testing.T) { line := `#EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://test.key",IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3"` ski, err := m3u8.NewSessionKeyItem(line) assert.Nil(t, err) assert.NotNil(t, ski.Encryptable) assert.Equal(t, "AES-128", ski.Encryptable.Method) assertNotNilEqual(t, "http://test.key", ski.Encryptable.URI) assertNotNilEqual(t, "D512BBF", ski.Encryptable.IV) assertNotNilEqual(t, "identity", ski.Encryptable.KeyFormat) assertNotNilEqual(t, "1/3", ski.Encryptable.KeyFormatVersions) assertToString(t, line, ski) } golang-github-etherlabsio-go-m3u8-1.0.0/test/timeItem_test.go000066400000000000000000000012101454425674600240700ustar00rootroot00000000000000package test import ( "testing" "time" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) func TestTimeItem_New(t *testing.T) { timeVar, err := m3u8.ParseTime("2010-02-19T14:54:23.031Z") assert.Nil(t, err) ti := &m3u8.TimeItem{ Time: timeVar, } assert.Equal(t, "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z", ti.String()) } func TestTimeItem_Parse(t *testing.T) { ti, err := m3u8.NewTimeItem("#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z") assert.Nil(t, err) expected, err := time.Parse(time.RFC3339Nano, "2010-02-19T14:54:23.031Z") assert.Nil(t, err) assert.Equal(t, expected, ti.Time) } golang-github-etherlabsio-go-m3u8-1.0.0/test/writer_test.go000066400000000000000000000112201454425674600236310ustar00rootroot00000000000000package test import ( "testing" "github.com/AlekSi/pointer" "github.com/etherlabsio/go-m3u8/m3u8" "github.com/stretchr/testify/assert" ) type testCase struct { p *m3u8.Playlist expected string } func TestWriter_Master(t *testing.T) { testCases := []testCase{ // Master playlist { &m3u8.Playlist{ Target: 10, Items: []m3u8.Item{ &m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), URI: "playlist_url", Bandwidth: 6400, AudioCodec: pointer.ToString("mp3"), }, &m3u8.PlaylistItem{ ProgramID: pointer.ToString("2"), URI: "playlist_url", Bandwidth: 50000, AudioCodec: pointer.ToString("aac-lc"), Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Profile: pointer.ToString("high"), Level: pointer.ToString("4.1"), }, &m3u8.SessionDataItem{ DataID: "com.test.movie.title", Value: pointer.ToString("Test"), URI: pointer.ToString("http://test"), Language: pointer.ToString("en"), }, }, }, `#EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",BANDWIDTH=6400 playlist_url #EXT-X-STREAM-INF:PROGRAM-ID=2,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",BANDWIDTH=50000 playlist_url #EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",VALUE="Test",URI="http://test",LANGUAGE="en" `, }, // Master playlist with single stream { &m3u8.Playlist{ Target: 10, Items: []m3u8.Item{ &m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), URI: "playlist_url", Bandwidth: 6400, AudioCodec: pointer.ToString("mp3"), }, }, }, `#EXTM3U #EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",BANDWIDTH=6400 playlist_url `, }, // Master playlist with header options { &m3u8.Playlist{ Target: 10, Version: pointer.ToInt(6), IndependentSegments: true, Items: []m3u8.Item{ &m3u8.PlaylistItem{ URI: "playlist_url", Bandwidth: 6400, AudioCodec: pointer.ToString("mp3"), }, }, }, `#EXTM3U #EXT-X-VERSION:6 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-STREAM-INF:CODECS="mp4a.40.34",BANDWIDTH=6400 playlist_url `, }, // New master playlist { &m3u8.Playlist{ Master: pointer.ToBool(true), }, `#EXTM3U `, }, // New media playlist { &m3u8.Playlist{ Target: 10, }, `#EXTM3U #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:10 #EXT-X-ENDLIST `, }, // Media playlist { &m3u8.Playlist{ Version: pointer.ToInt(4), Cache: pointer.ToBool(false), Target: 6, Sequence: 1, DiscontinuitySequence: pointer.ToInt(10), Type: pointer.ToString("EVENT"), IFramesOnly: true, Items: []m3u8.Item{ &m3u8.SegmentItem{ Duration: 11.344644, Segment: "1080-7mbps00000.ts", }, }, }, `#EXTM3U #EXT-X-PLAYLIST-TYPE:EVENT #EXT-X-VERSION:4 #EXT-X-I-FRAMES-ONLY #EXT-X-MEDIA-SEQUENCE:1 #EXT-X-DISCONTINUITY-SEQUENCE:10 #EXT-X-ALLOW-CACHE:NO #EXT-X-TARGETDURATION:6 #EXTINF:11.344644, 1080-7mbps00000.ts #EXT-X-ENDLIST `, }, // Media playlist with keys { &m3u8.Playlist{ Target: 10, Version: pointer.ToInt(7), Items: []m3u8.Item{ &m3u8.SegmentItem{ Duration: 11.344644, Segment: "1080-7mbps00000.ts", }, &m3u8.KeyItem{ Encryptable: &m3u8.Encryptable{ Method: "AES-128", URI: pointer.ToString("http://test.key"), IV: pointer.ToString("D512BBF"), KeyFormat: pointer.ToString("identity"), KeyFormatVersions: pointer.ToString("1/3"), }, }, &m3u8.SegmentItem{ Duration: 11.261233, Segment: "1080-7mbps00001.ts", }, }, }, `#EXTM3U #EXT-X-VERSION:7 #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:10 #EXTINF:11.344644, 1080-7mbps00000.ts #EXT-X-KEY:METHOD=AES-128,URI="http://test.key",IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3" #EXTINF:11.261233, 1080-7mbps00001.ts #EXT-X-ENDLIST `, }, } for _, tc := range testCases { tc.assert(t) } p := &m3u8.Playlist{ Target: 10, Items: []m3u8.Item{ &m3u8.PlaylistItem{ ProgramID: pointer.ToString("1"), Width: pointer.ToInt(1920), Height: pointer.ToInt(1080), Codecs: pointer.ToString("avc"), Bandwidth: 540, URI: "test.url", }, &m3u8.SegmentItem{ Duration: 10.991, Segment: "test.ts", }, }, } _, err := m3u8.Write(p) assert.Equal(t, m3u8.ErrPlaylistInvalidType, err) } func (tc testCase) assert(t *testing.T) { assert.Equal(t, tc.expected, tc.p.String()) }