Full Code of kuu/hls-parser for AI

master 837d023c721a cached
113 files
331.3 KB
114.6k tokens
209 symbols
1 requests
Download .txt
Showing preview only (366K chars total). Download the full file or copy to clipboard to get everything.
Repository: kuu/hls-parser
Branch: master
Commit: 837d023c721a
Files: 113
Total size: 331.3 KB

Directory structure:
gitextract_hu9vpdm4/

├── .github/
│   └── workflows/
│       ├── release.yml
│       └── tests.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── index.ts
├── package.json
├── parse.ts
├── stringify.ts
├── test/
│   ├── fixtures/
│   │   ├── m3u8/
│   │   │   ├── 8.1-Simple-Media-Playlist.m3u8
│   │   │   ├── 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.m3u8
│   │   │   ├── 8.11-EXT-X-CUE-OUT-Media-Playlist.m3u8
│   │   │   ├── 8.2-Live-Media-Playlist_using-HTTPS.m3u8
│   │   │   ├── 8.3-Playlist-with-encrypted-Media-Segments.m3u8
│   │   │   ├── 8.4-Master-Playlist.m3u8
│   │   │   ├── 8.5-Master-Playlist-with-I-Frames.m3u8
│   │   │   ├── 8.6-Master-Playlist-with-Alternative-audio.m3u8
│   │   │   ├── 8.7-Master-Playlist-with-Alternative-video.m3u8
│   │   │   ├── 8.8-Session-Data-in-a-Master-Playlist.m3u8
│   │   │   ├── 8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.m3u8
│   │   │   ├── Low-Latency_Example-01_Low-Latency_HLS_Playlist.m3u8
│   │   │   ├── Low-Latency_Example-02_Playlist_Delta_Update.m3u8
│   │   │   ├── Low-Latency_Example-03_Byterange-addressed_Parts-01.m3u8
│   │   │   ├── Low-Latency_Example-03_Byterange-addressed_Parts-02.m3u8
│   │   │   ├── Low-Latency_Example-03_Byterange-addressed_Parts-03.m3u8
│   │   │   ├── Multiple-rendition-groups.m3u8
│   │   │   ├── RedundantSegments.m3u8
│   │   │   ├── SCTE-35_01.m3u8
│   │   │   ├── SCTE-35_02.m3u8
│   │   │   ├── SCTE-35_03.m3u8
│   │   │   ├── SCTE-35_04.m3u8
│   │   │   ├── SCTE-35_05.m3u8
│   │   │   ├── SCTE-35_06.m3u8
│   │   │   ├── SCTE-35_07.m3u8
│   │   │   ├── Streaming-Examples_bipbop_16x9_variant.m3u8
│   │   │   └── Streaming-Examples_img_bipbop_adv_example_ts_master.m3u8
│   │   └── objects/
│   │       ├── 8.1-Simple-Media-Playlist.js
│   │       ├── 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js
│   │       ├── 8.11-EXT-X-CUE-OUT-Media-Playlist.js
│   │       ├── 8.2-Live-Media-Playlist_using-HTTPS.js
│   │       ├── 8.3-Playlist-with-encrypted-Media-Segments.js
│   │       ├── 8.4-Master-Playlist.js
│   │       ├── 8.5-Master-Playlist-with-I-Frames.js
│   │       ├── 8.6-Master-Playlist-with-Alternative-audio.js
│   │       ├── 8.7-Master-Playlist-with-Alternative-video.js
│   │       ├── 8.8-Session-Data-in-a-Master-Playlist.js
│   │       ├── 8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js
│   │       ├── Low-Latency_Example-01_Low-Latency_HLS_Playlist.js
│   │       ├── Low-Latency_Example-02_Playlist_Delta_Update.js
│   │       ├── Low-Latency_Example-03_Byterange-addressed_Parts-01.js
│   │       ├── Low-Latency_Example-03_Byterange-addressed_Parts-02.js
│   │       ├── Low-Latency_Example-03_Byterange-addressed_Parts-03.js
│   │       ├── Multiple-rendition-groups.js
│   │       ├── RedundantSegments.js
│   │       ├── SCTE-35_01.js
│   │       ├── SCTE-35_02.js
│   │       ├── SCTE-35_03.js
│   │       ├── SCTE-35_04.js
│   │       ├── SCTE-35_05.js
│   │       ├── SCTE-35_06.js
│   │       ├── SCTE-35_07.js
│   │       ├── Streaming-Examples_bipbop_16x9_variant.js
│   │       └── Streaming-Examples_img_bipbop_adv_example_ts_master.js
│   ├── helpers/
│   │   ├── fixtures.js
│   │   ├── matchers.js
│   │   └── utils.js
│   └── spec/
│       ├── 4_Playlists/
│       │   └── 4.3_Playlist-Tags/
│       │       ├── 4.3.1_Basic-Tags/
│       │       │   ├── 4.3.1.1_EXTM3U.spec.js
│       │       │   └── 4.3.1.2_EXT-X-VERSION.spec.js
│       │       ├── 4.3.2_Media-Segment-Tags/
│       │       │   ├── 4.3.2.1_EXTINF.spec.js
│       │       │   ├── 4.3.2.2_EXT-X-BYTERANGE.spec.js
│       │       │   ├── 4.3.2.3_EXT-X-DISCONTINUITY.spec.js
│       │       │   ├── 4.3.2.4_EXT-X-KEY.spec.js
│       │       │   ├── 4.3.2.5_EXT-X-MAP.spec.js
│       │       │   ├── 4.3.2.6_EXT-X-PROGRAM-DATE-TIME.spec.js
│       │       │   ├── 4.3.2.7_EXT-X-DATERANGE.spec.js
│       │       │   ├── 4.3.2_Media-Segment-Tags.spec.js
│       │       │   └── 4.4.4.7_EXT-X-GAP.spec.js
│       │       ├── 4.3.3_Media-Playlist-Tags/
│       │       │   ├── 4.3.3.1_EXT-X-TARGETDURATION.spec.js
│       │       │   ├── 4.3.3.2_EXT-X-MEDIA-SEQUENCE.spec.js
│       │       │   ├── 4.3.3.3_EXT-X-DISCONTINUITY-SEQUENCE.spec.js
│       │       │   ├── 4.3.3.4_EXT-X-ENDLIST.spec.js
│       │       │   ├── 4.3.3.5_EXT-X-PLAYLIST-TYPE.spec.js
│       │       │   ├── 4.3.3.6_EXT-X-I-FRAMES-ONLY.spec.js
│       │       │   ├── 4.3.3.7_EXT-X-CUE-OUT.spec.js
│       │       │   └── 4.3.3_Media-Playlist-Tags.spec.js
│       │       ├── 4.3.4_Master-Playlist-Tags/
│       │       │   ├── 4.3.4.1_EXT-X-MEDIA.spec.js
│       │       │   ├── 4.3.4.2_EXT-X-STREAM-INF.spec.js
│       │       │   ├── 4.3.4.2_EXT-X-STREAM-INF_2.spec.js
│       │       │   ├── 4.3.4.3_EXT-X-I-FRAME-STREAM-INF.spec.js
│       │       │   ├── 4.3.4.4_EXT-X-SESSION-DATA.spec.js
│       │       │   ├── 4.3.4.5_EXT-X-SESSION-KEY.spec.js
│       │       │   └── 4.3.4_Master-Playlist-Tags.spec.js
│       │       └── 4.3.5_Media-or-Master-Playlist-Tags/
│       │           ├── 4.3.5.1_EXT-X-INDEPENDENT-SEGMENTS.spec.js
│       │           └── 4.3.5.2_EXT-X-START.spec.js
│       ├── 7_Protocol-version-compatibility/
│       │   └── 7_EXT-X-VERSION.spec.js
│       ├── Apple-Low-Latency/
│       │   └── New_Media_Playlist_Tags_for_Low-Latency_HLS/
│       │       ├── 01_EXT-X-SERVER-CONTROL.spec.js
│       │       ├── 02_EXT-X-PART-INF.spec.js
│       │       ├── 03_EXT-X-PART.spec.js
│       │       ├── 04_EXT-X-PRELOAD-HINT.spec.js
│       │       ├── 05_EXT-X-RENDITION-REPORT.spec.js
│       │       └── 06_EXT-X-SKIP.spec.js
│       ├── Apple_HLS_Overview/
│       │   └── 02_Using_HLS.spec.js
│       ├── HLSJS-LHLS/
│       │   ├── 01_EXT-X-PREFETCH.spec.js
│       │   └── 02_EXT-X-PREFETCH-DISCONTINUITY.spec.js
│       ├── misc/
│       │   ├── multiple-rendition-groups.js
│       │   └── scte-35.spec.js
│       ├── parser.spec.js
│       ├── stringify.spec.js
│       └── utils.spec.js
├── tsconfig.json
├── types.ts
└── utils.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/release.yml
================================================
name: release
on:
  push:
    branches:
      - main
  workflow_dispatch: {}
concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false
permissions:
  id-token: write  # Required for OIDC
  contents: read
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm run build --if-present
      - run: npm test
      - run: npm publish


================================================
FILE: .github/workflows/tests.yml
================================================
name: HLS parser tests
on: [ push ]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [ 'current', 'lts/*', 'lts/-1' ]
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test
      - run: npm run build


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Editors
.idea/
.vscode/

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Typescript intermediate files
tsconfig.tsbuildinfo
*.d.ts
*.js
!test/**/*.js

# Dependency directories
node_modules
jspm_packages

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

dist


================================================
FILE: .npmignore
================================================
# Logs
logs
*.log
npm-debug.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules
jspm_packages

# Editors
.idea/
.vscode/

# Optional npm cache directory
.npm

# Optional REPL history
.node_repl_history

test
.github/
.node-version
.travis.yml
.npmignore
data-structure.png
tsconfig.tsbuildinfo
webpack.config.js
LICENSE
*.ts
!*.d.ts


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Kuu Miyazaki

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.

================================================
FILE: README.md
================================================
[![HLS parser tests](https://github.com/kuu/hls-parser/actions/workflows/tests.yml/badge.svg)](https://github.com/kuu/hls-parser/actions/workflows/tests.yml)
[![Coverage Status](https://coveralls.io/repos/github/kuu/hls-parser/badge.svg?branch=master)](https://coveralls.io/github/kuu/hls-parser?branch=master)
[![Known Vulnerabilities](https://snyk.io/test/github/kuu/hls-parser/badge.svg)](https://snyk.io/test/github/kuu/hls-parser)
[![npm Downloads](https://img.shields.io/npm/dw/hls-parser.svg?style=flat-square)](https://npmjs.com/hls-parser)
[![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)


# hls-parser

Provides synchronous functions to read/write HLS playlists (conforms to [the HLS spec rev.23](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23), [the Apple Low-Latency Spec rev. 2020/02/05](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification), and [HLS.js's Low-Latency spec](https://github.com/video-dev/hlsjs-rfcs/blob/lhls-spec/proposals/0001-lhls.md))

## Install
[![NPM](https://nodei.co/npm/hls-parser.png?mini=true)](https://nodei.co/npm/hls-parser/)
[![](https://data.jsdelivr.com/v1/package/npm/hls-parser/badge)](https://www.jsdelivr.com/package/npm/hls-parser?path=dist)

## Usage
```js
import { parse, types, stringify } from 'hls-parser';

// Parse the playlist
const playlist = parse(textData);
// You can access the playlist as a JS object
if (playlist.isMasterPlaylist) {
  // Master playlist
} else {
  // Media playlist
}
// Create a new playlist
const {MediaPlaylist, Segment} = types;
const obj = new MediaPlaylist({
  targetDuration: 9,
  playlistType: 'VOD',
  segments: [
    new Segment({
      uri: 'low/1.m3u8',
      duration: 9
    })
  ]
});
// Convert the object into a text
stringify(obj);
/*
#EXTM3U
#EXT-X-TARGETDURATION:9
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:9,
low/1.m3u8
*/
```

## API

### `HLS.parse(str)`
Converts a text playlist into a structured JS object

#### params
| Name    | Type   | Required | Default | Description   |
| ------- | ------ | -------- | ------- | ------------- |
| str     | string | Yes      | N/A     | A text data that conforms to [the HLS playlist spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.1) |

#### return value
An instance of either `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.)

### `HLS.stringify(obj, postProcess)`
Converts a JS object into a plain text playlist

#### params
| Name    | Type   | Required | Default | Description   |
| ------- | ------ | -------- | ------- | ------------- |
| obj     | `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.)  | Yes      | N/A     | An object returned by `HLS.parse()` or a manually created object |
| postProcess     | PostProcess  | No      | undefined | A function to be called for each segment or variant to manipulate the output. |

##### `PostProcess`
| Property         | Type          | Required | Default | Description   |
| ---------------- | ------------- | -------- | ------- | ------------- |
| `segmentProcessor` | (lines: string[], start: number, end: number, segment: Segment, i: number) => void | No      | undefined     | A function to manipulate the segment output.  |
| `variantProcessor` | (lines: string[], start: number, end: number, variant: Variant, i: number) => void | No      | undefined     | A function to manipulate the variant output.  |


#### return value
A text data that conforms to [the HLS playlist spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.1)

### `HLS.setOptions(obj)`
Updates the option values

#### params
| Name    | Type   | Required | Default | Description   |
| ------- | ------ | -------- | ------- | ------------- |
| obj     | Object | Yes      | {}     | An object holding option values which will be used to overwrite the internal option values.  |

##### supported options
| Name       | Type    | Default | Description   |
| ---------- | ------- | ------- | ------------- |
| `strictMode` | boolean | false   | If true, the function throws an error when `parse`/`stringify` failed. If false, the function just logs the error and continues to run.|
| `allowClosedCaptionsNone` | boolean | false | If true, `CLOSED-CAPTIONS` attribute on the `EXT-X-STREAM-INF` tag will be set to the enumerated-string value NONE when there are no closed-captions. See [CLOSED-CAPTIONS](https://tools.ietf.org/html/rfc8216#section-4.3.4.2) |
| `silent` | boolean | false   | If true, `console.error` will be suppressed.|

### `HLS.getOptions()`
Retrieves the current option values

#### return value
A cloned object containing the current option values

### `HLS.types`
An object that holds all the classes described below.


## Data format
This section describes the structure of the object returned by `parse()` method.

![data structure](./data-structure.png)

### `Data`
| Property         | Type          | Required | Default | Description   |
| ---------------- | ------------- | -------- | ------- | ------------- |
| `type` | string     | Yes      | N/A     | Either `playlist` or `segment` or `part`}  |

### `Playlist` (extends `Data`)
| Property         | Type          | Required | Default | Description   |
| ---------------- | ------------- | -------- | ------- | ------------- |
| `isMasterPlaylist` | boolean     | Yes      | N/A     | `true` if this playlist is a master playlist  |
| `uri`              | string | No      | undefined     | Playlist URL  |
| `version`          | number | No       | undefined      | See [EXT-X-VERSION](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.1.2) |
| `independentSegments` | boolean | No       | false      | See [EXT-X-INDEPENDENT-SEGMENTS](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.5.1) |
| `start` | object({offset: number, precise: boolean}) | No       | undefined      | See [EXT-X-START](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.5.2) |
| `source` | string     | No      | undefined     | The unprocessed text of the playlist  |

### `MasterPlaylist` (extends `Playlist`)
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `variants`        | [`Variant`]  | No       | []        | See [ EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) and [EXT-X-I-FRAME-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.3)  |
| `currentVariant`  | number   | No       | undefined | Array index that points to the chosen item in `variants` |
| `sessionDataList` | [`SessionData`]  | No       | []        | See [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) |
| `sessionKeyList`      | [`Key`]    | No       | [] | See [EXT-X-SESSION-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.5) |

### `Variant`
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `uri`        | string  | Yes       | N/A        | URI of the variant playlist  |
| `isIFrameOnly`  | boolean   | No       | undefined | `true` if the variant is an I-frame media playlist. See [EXT-X-I-FRAME-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.3) |
| `bandwidth` | number  | Yes       | N/A        | See BANDWIDTH attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) |
| `averageBandwidth`      | number    | No       | undefined | See AVERAGE-BANDWIDTH attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) |
| `score`      | number    | No       | undefined | See SCORE attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.6.2) |
| `codecs`      | string    | No       | undefined | See CODECS attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) |
| `resolution`      | object ({width: number, height: number})   | No       | undefined | See RESOLUTION attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) |
| `frameRate`      | number    | No       | undefined | See FRAME-RATE attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) |
| `hdcpLevel`      | string    | No       | undefined | See HDCP-LEVEL attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) |
| `allowedCpc`   | [object ({format: string, cpcList: [string]})]  | No       | undefined | See ALLOWED-CPC attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.6.2) |
| `videoRange`      | string {"SDR","HLG","PQ"}    | No       | undefined | See VIDEO-RANGE attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.6.2) |
| `stableVariantId`      | string   | No       | undefined | See STABLE-VARIANT-ID attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.6.2) |
| `audio`      | [`Rendition`(type='AUDIO')]    | No       | [] | See AUDIO attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) |
| `video`      | [`Rendition`(type='VIDEO')]    | No       | [] | See VIDEO attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2)  |
| `subtitles`      | [`Rendition`(type='SUBTITLES')]    | No       | [] | See SUBTITLES attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2)  |
| `closedCaptions`      | [`Rendition`(type='CLOSED-CAPTIONS')]    | No       | [] | See CLOSED-CAPTIONS attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) |
| `currentRenditions`      | object ({audio: number, video: number, subtitles: number, closedCaptions: number})   | No       | {} | A hash object that contains array indices that points to the chosen `Rendition` for each type |

### `Rendition`
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `type`  | string   | Yes       | N/A | See TYPE attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `uri`        | string  | No       | undefined        | See URI attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1)  |
| `groupId`  | string   | Yes       | N/A | See GROUP-ID attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `language` | string  | No       | undefined       | See LANGUAGE attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `assocLanguage` | string  | No       | undefined       | See ASSOC-LANGUAGE attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `name`  | string   | Yes       | N/A | See NAME attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `isDefault`  | boolean   | No       | false | See DEFAULT attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `autoselect`  | boolean   | No       | false | See AUTOSELECT attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `forced`  | boolean   | No       | false | See FORCED attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `instreamId`  | string   | No       | undefined | See INSTREAM-ID attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `characteristics`  | string   | No       | undefined | See CHARACTERISTICS attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |
| `channels`  | string   | No       | undefined | See CHANNELS attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) |

### `SessionData`
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `id`  | string   | Yes       | N/A | See DATA-ID attribute in [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) |
| `value`  | string   | No       | undefined | See VALUE attribute in [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) |
| `uri`        | string  | No       | undefined        | See URI attribute in [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4)  |
| `language`  | string   | No       | undefined | See LANGUAGE attribute in [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) |

### `MediaPlaylist` (extends `Playlist`)
| Property                    | Type     | Required | Default   | Description   |
| --------------------------- | -------- | -------- | --------- | ------------- |
| `targetDuration`            | number | Yes       | N/A        | See [EXT-X-TARGETDURATION](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1) |
| `mediaSequenceBase`         | number | No       | 0        | See [EXT-X-MEDIA-SEQUENCE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.2) |
| `discontinuitySequenceBase` | number | No       | 0        | See [EXT-X-DISCONTINUITY-SEQUENCE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.3) |
| `endlist`                   | boolean | No       | false        | See [EXT-X-ENDLIST](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.4) |
| `playlistType`              | string | No       | undefined        | See [EXT-X-PLAYLIST-TYPE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.5) |
| `isIFrame`                  | boolean | No       | undefined        | See [EXT-X-I-FRAMES-ONLY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.6) |
| `segments`                  | [`Segment`] | No       | []        | A list of available segments |
| `prefetchSegments`          | [`PrefetchSegment`] | No       | []        | A list of available prefetch segments |
| `lowLatencyCompatibility`  | object ({canBlockReload: boolean, canSkipUntil: number, holdBack: number, partHoldBack: number})   | No       | undefined | See `CAN-BLOCK-RELOAD`, `CAN-SKIP-UNTIL`, `HOLD-BACK`, and `PART-HOLD-BACK` attributes in [EXT-X-SERVER-CONTROL](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3281374) |
| `partTargetDuration`            | number | No*      | undefined        | *Required if the playlist contains one or more `EXT-X-PART` tags. See [EXT-X-PART-INF](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282434) |
| `renditionReports`  | [`RenditionReport`]   | No       | [] | Update status of the associated renditions |
| `skip`  | number   | No       | 0 | See [EXT-X-SKIP](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282433) |

### `Segment` (extends `Data`)
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `uri`        | string  | Yes*       | N/A        | URI of the media segment. *Not required if the segment contains `EXT-X-PRELOAD-HINT` tag |
| `duration`  | number   | Yes*       | N/A | See [EXTINF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.1) *Not required if the segment contains `EXT-X-PRELOAD-HINT` tag |
| `title`  | string   | No       | undefined | See [EXTINF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.1) |
| `byterange`  | object ({length: number, offset: number})   | No       | undefined | See [EXT-X-BYTERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2) |
| `discontinuity`  | boolean   | No       | undefined | See [EXT-X-DISCONTINUITY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.3) |
| `mediaSequenceNumber`  | number   | No       | 0 | See the description about 'Media Sequence Number' in [3. Media Segments](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#page-5) |
| `discontinuitySequence`  | number   | No       | 0 | See the description about 'Discontinuity Sequence Number' in [6.2.1. General Server Responsibilities](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.1) |
| `key`  | `Key`   | No       | undefined | See [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) |
| `map`  | `MediaInitializationSection`   | No       | undefined | See [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) |
| `programDateTime`  | `Date`   | No       | undefined | See [EXT-X-PROGRAM-DATE-TIME](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.6) |
| `dateRange`  | `DateRange`   | No       | undefined | See [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |
| `markers`  | [`SpliceInfo`]   | No       | [] | SCTE-35 messages associated with this segment|
| `parts`  | [`PartialSegment`]   | No       | [] | Partial Segments that constitute this segment |
| `gap` | boolean | No | undefined | See [EXT-X-GAP](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.7) |

### `PartialSegment` (extends `Data`)
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `hint`        | boolean  | No       | false        | `true` indicates a hinted resource (`TYPE=PART`) See [EXT-X-PRELOAD-HINT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3526694) |
| `uri`        | string  | Yes       | N/A        | See `URI` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) |
| `duration`  | number   | Yes*       | N/A | See `DURATION` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) *Not required if `hint` is `true`|
| `independent`  | boolean   | No       | undefined | See `INDEPENDENT` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) |
| `byterange`  | object ({length: number, offset: number})   | No       | undefined | See `BYTERANGE` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) |
| `gap`  | boolean   | No       | undefined | See `GAP` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) |

### `PrefetchSegment` (extends `Data`)
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `uri`        | string  | Yes       | N/A        | See value of [EXT-X-PREFETCH](https://github.com/video-dev/hlsjs-rfcs/blob/lhls-spec/proposals/0001-lhls.md) |
| `discontinuity`  | boolean   | No       | undefined | See [EXT-X-PREFETCH-DISCONTINUITY](https://github.com/video-dev/hlsjs-rfcs/blob/lhls-spec/proposals/0001-lhls.md) |
| `mediaSequenceNumber`  | number   | No       | 0 | See the description about 'Media Sequence Number' in [3. Media Segments](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#page-5) |
| `discontinuitySequence`  | number   | No       | 0 | See the description about 'Discontinuity Sequence Number' in [6.2.1. General Server Responsibilities](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.1) |

### `Key`
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `method`  | string   | Yes       | N/A | See METHOD attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) |
| `uri`        | string  | No       | undefined        | See URI attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) |
| `iv`        | `ArrayBuffer`(length=16)   | No       | undefined        | See IV attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) |
| `format`  | string   | No       | undefined | See KEYFORMAT attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) |
| `formatVersion`  | string   | No       | undefined | See KEYFORMATVERSIONS attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) |

### `MediaInitializationSection`
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `hint`        | boolean  | No       | false        | `true` indicates a hinted resource (`TYPE=MAP`) See [EXT-X-PRELOAD-HINT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3526694) |
| `uri`        | string  | Yes       | N/A        | See URI attribute in [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) |
| `byterange`        | object ({length: number, offset: number})   | No       | undefined        | See BYTERANGE attribute in [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) |

### `DateRange`
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `id`        | string  | Yes       | N/A        | See ID attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |
| `classId`        | string   | No       | undefined        | See CLASS attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |
| `start`        | `Date`  | No       | undefined        | See START-DATE attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |
| `end`        | `Date`  | No       | undefined        | See END-DATE attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |
| `duration`        | number  | No       | undefined        | See DURATION attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |
| `plannedDuration`        | number  | No       | undefined        | See PLANNED-DURATION attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |
| `endOnNext`        | boolean  | No       | undefined        | See END-ON-NEXT attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |
| `attributes`        | object  | No       | {}        | A hash object that holds SCTE35 attributes and user defined attributes. See SCTE35-* and X-<client-attribute> attributes in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |

### `SpliceInfo`
Only `EXT-X-CUE-OUT` and `EXT-X-CUE-IN` tags are supported. Other SCTE-35-related tags are stored as raw (string) values.

| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `type`  | string   | Yes       | N/A | {'OUT', 'IN', 'RAW'} |
| `duration`        | number   | No       | undefined        | Required if the `type` is 'OUT' |
| `tagName`        | string   | No       | undefined        | Holds the tag name if any unsupported tag are found. Required if the `type` is 'RAW' |
| `value`        | string   | No       | undefined        | Holds a raw (string) value for the unsupported tag. |

### `RenditionReport`
| Property          | Type     | Required | Default   | Description   |
| ----------------- | -------- | -------- | --------- | ------------- |
| `uri`        | string  | Yes       | N/A        | See `URI` attribute in [EXT-X-RENDITION-REPORT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282435) |
| `lastMSN`        | number  | No       | undefined  | See `LAST-MSN` attribute in [EXT-X-RENDITION-REPORT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282435) |
| `lastPart`        | number  | No       | undefined | See `LAST-PART` attribute in [EXT-X-RENDITION-REPORT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282435) |


================================================
FILE: index.ts
================================================
/*! Copyright Kuu Miyazaki. SPDX-License-Identifier: MIT */
import { getOptions, setOptions } from './utils';
import parse from './parse';
import stringify from './stringify';
import * as types from './types';

export {
  parse,
  stringify,
  types,
  getOptions,
  setOptions
};


================================================
FILE: package.json
================================================
{
  "name": "hls-parser",
  "version": "0.16.1",
  "description": "A simple library to read/write HLS playlists",
  "main": "index.js",
  "types": "index.d.ts",
  "browser": "dist/hls-parser.min.js",
  "scripts": {
    "lint": "xo",
    "type-check": "tsc --noEmit",
    "audit": "npm audit --audit-level high",
    "build": "rm -fR ./dist; tsc ; webpack --mode development ; webpack --mode production",
    "test": "npm run lint && npm run build && npm run audit && ava --verbose",
    "test-offline": "npm run lint && npm run build && ava --verbose"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/kuu/hls-parser.git"
  },
  "keywords": [
    "HLS",
    "media",
    "video",
    "audio",
    "streaming"
  ],
  "author": "Kuu Miyazaki",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/kuu/hls-parser/issues"
  },
  "homepage": "https://github.com/kuu/hls-parser#readme",
  "devDependencies": {
    "@ava/typescript": "^6.0.0",
    "@babel/core": "^7.28.4",
    "@babel/eslint-parser": "^7.28.4",
    "@babel/preset-env": "^7.28.3",
    "@tsconfig/node18": "^18.2.4",
    "ava": "^6.4.1",
    "babel-loader": "^10.0.0",
    "eslint-plugin-unicorn": "^61.0.2",
    "rewire": "^9.0.1",
    "terser-webpack-plugin": "^5.3.14",
    "ts-loader": "^9.5.4",
    "typescript": "^5.9.3",
    "webpack": "^5.102.1",
    "webpack-cli": "^6.0.1",
    "xo": "^0.60.0"
  },
  "ava": {
    "typescript": {
      "compile": "tsc",
      "extensions": [
        "ts",
        "js"
      ],
      "rewritePaths": {}
    }
  },
  "xo": {
    "esnext": true,
    "space": true,
    "rules": {
      "arrow-body-style": 0,
      "ava/no-ignored-test-files": 0,
      "camelcase": 0,
      "comma-dangle": 0,
      "capitalized-comments": 0,
      "dot-notation": 0,
      "guard-for-in": 0,
      "import/extensions": 0,
      "import/no-dynamic-require": 0,
      "new-cap": 0,
      "no-bitwise": 0,
      "no-cond-assign": 0,
      "no-mixed-operators": 0,
      "no-multi-assign": 0,
      "no-use-extend-native/no-use-extend-native": 0,
      "object-curly-newline": 0,
      "operator-linebreak": 0,
      "padding-line-between-statements": 0,
      "quotes": 0,
      "unicorn/catch-error-name": 0,
      "unicorn/filename-case": 0,
      "unicorn/no-lonely-if": 0,
      "unicorn/no-useless-spread": 0,
      "unicorn/no-zero-fractions": 0,
      "unicorn/numeric-separators-style": 0,
      "unicorn/prefer-code-point": 0,
      "unicorn/prefer-module": 0,
      "unicorn/prefer-switch": 0,
      "unicorn/prevent-abbreviations": 0,
      "unicorn/switch-case-braces": 0
    },
    "overrides": [
      {
        "files": "test/**/*.js",
        "rules": {
          "unicorn/no-array-push-push": 0
        }
      },
      {
        "files": "*.ts",
        "rules": {
          "n/file-extension-in-import": 0,
          "@typescript-eslint/array-type": 1,
          "@typescript-eslint/ban-types": 1,
          "@typescript-eslint/comma-dangle": 0,
          "@typescript-eslint/consistent-type-imports": 0,
          "@typescript-eslint/dot-notation": 0,
          "@typescript-eslint/member-delimiter-style": 0,
          "@typescript-eslint/naming-convention": 0,
          "@typescript-eslint/no-unsafe-call": 0,
          "@typescript-eslint/no-unsafe-argument": 0,
          "@typescript-eslint/no-unsafe-assignment": 0,
          "@typescript-eslint/no-unsafe-return": 0,
          "@typescript-eslint/object-curly-spacing": 0,
          "@typescript-eslint/padding-line-between-statements": 0,
          "@typescript-eslint/prefer-optional-chain": 1,
          "@typescript-eslint/prefer-nullish-coalescing": 0,
          "@typescript-eslint/quotes": 0,
          "@typescript-eslint/restrict-template-expressions": 0,
          "@typescript-eslint/restrict-plus-operands": 0,
          "unicorn/prefer-export-from": 0
        }
      }
    ],
    "settings": {
      "import/resolver": {
        "node": {}
      }
    }
  }
}


================================================
FILE: parse.ts
================================================
import * as utils from './utils';
import {
  AllowedCpc,
  ExtInfo,
  Rendition,
  Resolution,
  TagParam,
  UserAttribute,
  Variant,
  SessionData,
  Key,
  MediaInitializationSection,
  Byterange,
  DateRange,
  SpliceInfo,
  MasterPlaylist,
  MediaPlaylist,
  Segment,
  PartialSegment,
  PrefetchSegment,
  RenditionReport,
  ContentSteering
} from './types';

function unquote(str: string | undefined) {
  return utils.trim(str, '"');
}

type TagCategory = 'Basic' | 'Segment' | 'MasterPlaylist' | 'MediaPlaylist' | 'MediaorMasterPlaylist' | 'Unknown';

function getTagCategory(tagName: string): TagCategory {
  switch (tagName) {
    case 'EXTM3U':
    case 'EXT-X-VERSION':
    case 'EXT-X-CONTENT-STEERING':
      return 'Basic';
    case 'EXTINF':
    case 'EXT-X-BYTERANGE':
    case 'EXT-X-DISCONTINUITY':
    case 'EXT-X-PREFETCH-DISCONTINUITY':
    case 'EXT-X-KEY':
    case 'EXT-X-MAP':
    case 'EXT-X-PROGRAM-DATE-TIME':
    case 'EXT-X-DATERANGE':
    case 'EXT-X-CUE-OUT':
    case 'EXT-X-CUE-IN':
    case 'EXT-X-CUE-OUT-CONT':
    case 'EXT-X-CUE':
    case 'EXT-OATCLS-SCTE35':
    case 'EXT-X-ASSET':
    case 'EXT-X-SCTE35':
    case 'EXT-X-PART':
    case 'EXT-X-PRELOAD-HINT':
    case 'EXT-X-GAP':
      return 'Segment';
    case 'EXT-X-TARGETDURATION':
    case 'EXT-X-MEDIA-SEQUENCE':
    case 'EXT-X-DISCONTINUITY-SEQUENCE':
    case 'EXT-X-ENDLIST':
    case 'EXT-X-PLAYLIST-TYPE':
    case 'EXT-X-I-FRAMES-ONLY':
    case 'EXT-X-SERVER-CONTROL':
    case 'EXT-X-PART-INF':
    case 'EXT-X-PREFETCH':
    case 'EXT-X-RENDITION-REPORT':
    case 'EXT-X-SKIP':
      return 'MediaPlaylist';
    case 'EXT-X-MEDIA':
    case 'EXT-X-STREAM-INF':
    case 'EXT-X-I-FRAME-STREAM-INF':
    case 'EXT-X-SESSION-DATA':
    case 'EXT-X-SESSION-KEY':
      return 'MasterPlaylist';
    case 'EXT-X-INDEPENDENT-SEGMENTS':
    case 'EXT-X-START':
    case 'EXT-X-DEFINE':
      return 'MediaorMasterPlaylist';
    default:
      return 'Unknown';
  }
}

function parseEXTINF(param: string): ExtInfo {
  const pair = utils.splitAt(param, ',') as [string, string];
  return {duration: utils.toNumber(pair[0]), title: decodeURIComponent(escape(pair[1]))};
}

function parseBYTERANGE(param: string): Byterange {
  const pair = utils.splitAt(param, '@');
  return {length: utils.toNumber(pair[0]), offset: pair[1] ? utils.toNumber(pair[1]) : -1};
}

function parseResolution(str: string): Resolution {
  const pair = utils.splitAt(str, 'x') as [string, string];
  return {width: utils.toNumber(pair[0]), height: utils.toNumber(pair[1])};
}

function parseAllowedCpc(str: string): AllowedCpc[] {
  const message = 'ALLOWED-CPC: Each entry must consit of KEYFORMAT and Content Protection Configuration';
  const list = str.split(',');
  if (list.length === 0) {
    utils.INVALIDPLAYLIST(message);
  }
  const allowedCpcList: AllowedCpc[] = [];
  for (const item of list) {
    const [format, cpcText] = utils.splitAt(item, ':');
    if (!format || !cpcText) {
      utils.INVALIDPLAYLIST(message);
      continue;
    }
    allowedCpcList.push({format, cpcList: cpcText.split('/')});
  }
  return allowedCpcList;
}

function parseIV(str: string): Uint8Array {
  const iv = utils.hexToByteSequence(str);
  if (iv.length !== 16) {
    utils.INVALIDPLAYLIST('IV must be a 128-bit unsigned integer');
  }
  return iv;
}

function parseUserAttribute(str: string): UserAttribute {
  if (str.startsWith('"')) {
    return unquote(str)!;
  }
  if (str.startsWith('0x') || str.startsWith('0X')) {
    return utils.hexToByteSequence(str);
  }
  return utils.toNumber(str);
}

function setCompatibleVersionOfKey(params: Record<string, any>, attributes: Record<string, any>) {
  if (attributes['IV'] && params.compatibleVersion < 2) {
    params.compatibleVersion = 2;
  }
  if ((attributes['KEYFORMAT'] || attributes['KEYFORMATVERSIONS']) && params.compatibleVersion < 5) {
    params.compatibleVersion = 5;
  }
}

function parseAttributeList(param): Record<string, any> {
  const attributes = {};
  for (const item of utils.splitByCommaWithPreservingQuotes(param)) {
    const [key, value] = utils.splitAt(item, '=');
    const val = unquote(value)!;
    switch (key) {
      case 'URI':
        attributes[key] = val;
        break;
      case 'START-DATE':
      case 'END-DATE':
        attributes[key] = new Date(val);
        break;
      case 'IV':
        attributes[key] = parseIV(val);
        break;
      case 'BYTERANGE':
        attributes[key] = parseBYTERANGE(val);
        break;
      case 'RESOLUTION':
        attributes[key] = parseResolution(val);
        break;
      case 'ALLOWED-CPC':
        attributes[key] = parseAllowedCpc(val);
        break;
      case 'END-ON-NEXT':
      case 'DEFAULT':
      case 'AUTOSELECT':
      case 'FORCED':
      case 'PRECISE':
      case 'CAN-BLOCK-RELOAD':
      case 'INDEPENDENT':
      case 'GAP':
        attributes[key] = val === 'YES';
        break;
      case 'DURATION':
      case 'PLANNED-DURATION':
      case 'BANDWIDTH':
      case 'AVERAGE-BANDWIDTH':
      case 'FRAME-RATE':
      case 'TIME-OFFSET':
      case 'CAN-SKIP-UNTIL':
      case 'HOLD-BACK':
      case 'PART-HOLD-BACK':
      case 'PART-TARGET':
      case 'BYTERANGE-START':
      case 'BYTERANGE-LENGTH':
      case 'LAST-MSN':
      case 'LAST-PART':
      case 'SKIPPED-SEGMENTS':
      case 'SCORE':
      case 'PROGRAM-ID':
        attributes[key] = utils.toNumber(val);
        break;
      default:
        if (key.startsWith('SCTE35-')) {
          attributes[key] = utils.hexToByteSequence(val);
        } else if (key.startsWith('X-')) {
          attributes[key] = parseUserAttribute(value!);
        } else {
          if (key === 'VIDEO-RANGE' && val !== 'SDR' && val !== 'HLG' && val !== 'PQ') {
            utils.INVALIDPLAYLIST(`VIDEO-RANGE: unknown value "${val}"`);
          }
          attributes[key] = val;
        }
    }
  }
  return attributes;
}

function parseTagParam(name: string, param): TagParam {
  switch (name) {
    case 'EXTM3U':
    case 'EXT-X-DISCONTINUITY':
    case 'EXT-X-ENDLIST':
    case 'EXT-X-I-FRAMES-ONLY':
    case 'EXT-X-INDEPENDENT-SEGMENTS':
    case 'EXT-X-CUE-IN':
    case 'EXT-X-GAP':
      return [null, null];
    case 'EXT-X-VERSION':
    case 'EXT-X-TARGETDURATION':
    case 'EXT-X-MEDIA-SEQUENCE':
    case 'EXT-X-DISCONTINUITY-SEQUENCE':
      return [utils.toNumber(param), null];
    case 'EXT-X-CUE-OUT':
      // For backwards compatibility: attributes list is optional,
      // if only a number is found, use it as the duration
      if (!Number.isNaN(Number(param))) {
        return [utils.toNumber(param), null];
      }
      // If attributes are found, parse them out (i.e. DURATION)
      return [null, parseAttributeList(param)];
    case 'EXT-X-KEY':
    case 'EXT-X-MAP':
    case 'EXT-X-DATERANGE':
    case 'EXT-X-MEDIA':
    case 'EXT-X-STREAM-INF':
    case 'EXT-X-I-FRAME-STREAM-INF':
    case 'EXT-X-SESSION-DATA':
    case 'EXT-X-SESSION-KEY':
    case 'EXT-X-START':
    case 'EXT-X-SERVER-CONTROL':
    case 'EXT-X-PART-INF':
    case 'EXT-X-PART':
    case 'EXT-X-PRELOAD-HINT':
    case 'EXT-X-RENDITION-REPORT':
    case 'EXT-X-SKIP':
    case 'EXT-X-DEFINE':
      return [null, parseAttributeList(param)];
    case 'EXTINF':
      return [parseEXTINF(param), null];
    case 'EXT-X-BYTERANGE':
      return [parseBYTERANGE(param), null];
    case 'EXT-X-PROGRAM-DATE-TIME':
      return [new Date(param), null];
    case 'EXT-X-PLAYLIST-TYPE':
      return [param, null]; // <EVENT|VOD>
    default:
      return [param, null]; // Unknown tag
  }
}

function MIXEDTAGS() {
  utils.INVALIDPLAYLIST(`The file contains both media and master playlist tags.`);
}

function splitTag(line: string): [string, string | null] {
  const index = line.indexOf(':');
  if (index === -1) {
    return [line.slice(1).trim(), null];
  }
  return [line.slice(1, index).trim(), line.slice(index + 1).trim()];
}

function parseRendition({attributes}: Tag): Rendition {
  const rendition = new Rendition({
    type: attributes['TYPE'],
    uri: attributes['URI'],
    groupId: attributes['GROUP-ID'],
    language: attributes['LANGUAGE'],
    assocLanguage: attributes['ASSOC-LANGUAGE'],
    name: attributes['NAME'],
    isDefault: attributes['DEFAULT'],
    autoselect: attributes['AUTOSELECT'],
    forced: attributes['FORCED'],
    instreamId: attributes['INSTREAM-ID'],
    characteristics: attributes['CHARACTERISTICS'],
    channels: attributes['CHANNELS'],
    pathwayId: attributes['PATHWAY-ID']
  });
  return rendition;
}

function checkRedundantRendition(renditions, rendition): string {
  let defaultFound = false;
  for (const item of renditions) {
    if (item.name === rendition.name) {
      return 'All EXT-X-MEDIA tags in the same Group MUST have different NAME attributes.';
    }
    if (item.isDefault) {
      defaultFound = true;
    }
  }
  if (defaultFound && rendition.isDefault) {
    return 'EXT-X-MEDIA A Group MUST NOT have more than one member with a DEFAULT attribute of YES.';
  }
  return '';
}

function addRendition(variant, line, type) {
  const rendition = parseRendition(line);
  const renditions = variant[utils.camelify(type)];
  const errorMessage = checkRedundantRendition(renditions, rendition);
  if (errorMessage) {
    utils.INVALIDPLAYLIST(errorMessage);
  }
  renditions.push(rendition);
  if (rendition.isDefault) {
    variant.currentRenditions[utils.camelify(type)] = renditions.length - 1;
  }
}

function matchTypes(attrs, variant, params) {
  for (const type of ['AUDIO', 'VIDEO', 'SUBTITLES', 'CLOSED-CAPTIONS']) {
    if (type === 'CLOSED-CAPTIONS' && attrs[type] === 'NONE') {
      params.isClosedCaptionsNone = true;
      variant.closedCaptions = [];
    } else if (attrs[type] && !variant[utils.camelify(type)].some(item => item.groupId === attrs[type])) {
      utils.INVALIDPLAYLIST(`${type} attribute MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag whose TYPE attribute is ${type}.`);
    }
  }
}

function parseVariant(lines, variantAttrs, uri: string, iFrameOnly: boolean, params: Record<string, any>): Variant {
  const variant = new Variant({
    uri,
    bandwidth: variantAttrs['BANDWIDTH'],
    averageBandwidth: variantAttrs['AVERAGE-BANDWIDTH'],
    score: variantAttrs['SCORE'],
    codecs: variantAttrs['CODECS'],
    resolution: variantAttrs['RESOLUTION'],
    frameRate: variantAttrs['FRAME-RATE'],
    hdcpLevel: variantAttrs['HDCP-LEVEL'],
    allowedCpc: variantAttrs['ALLOWED-CPC'],
    videoRange: variantAttrs['VIDEO-RANGE'],
    stableVariantId: variantAttrs['STABLE-VARIANT-ID'],
    pathwayId: variantAttrs['STABLE-PATHWAY-ID'],
    programId: variantAttrs['PROGRAM-ID']
  });
  for (const line of lines) {
    if (line.name === 'EXT-X-MEDIA') {
      const renditionAttrs = line.attributes;
      const renditionType = renditionAttrs['TYPE'];
      if (!renditionType || !renditionAttrs['GROUP-ID']) {
        utils.INVALIDPLAYLIST('EXT-X-MEDIA TYPE attribute is REQUIRED.');
      }
      if (variantAttrs[renditionType] === renditionAttrs['GROUP-ID']) {
        addRendition(variant, line, renditionType);
        if (renditionType === 'CLOSED-CAPTIONS') {
          for (const {instreamId} of variant.closedCaptions) {
            if (instreamId && instreamId.startsWith('SERVICE') && params.compatibleVersion < 7) {
              params.compatibleVersion = 7;
              break;
            }
          }
        }
      }
    }
  }
  matchTypes(variantAttrs, variant, params);
  variant.isIFrameOnly = iFrameOnly;
  return variant;
}

function sameKey(key1: Key, key2: Key): boolean {
  if (key1.method !== key2.method) {
    return false;
  }
  if (key1.uri !== key2.uri) {
    return false;
  }
  if (key1.iv) {
    if (!key2.iv) {
      return false;
    }
    if (key1.iv.byteLength !== key2.iv.byteLength) {
      return false;
    }
    for (let i = 0; i < key1.iv.byteLength; i++) {
      if (key1.iv[i] !== key2.iv[i]) {
        return false;
      }
    }
  } else if (key2.iv) {
    return false;
  }
  if (key1.format !== key2.format) {
    return false;
  }
  if (key1.formatVersion !== key2.formatVersion) {
    return false;
  }
  return true;
}

function parseMasterPlaylist(lines: Line[], params: Record<string, any>): MasterPlaylist {
  const playlist = new MasterPlaylist();
  let variantIsScored = false;
  for (const [index, line] of lines.entries()) {
    const {name, value, attributes} = mapTo<Tag>(line);
    if (name === 'EXT-X-VERSION') {
      playlist.version = value;
    } else if (name === 'EXT-X-CONTENT-STEERING-SERVER') {
      const contentSteering = new ContentSteering({
        serverUri: attributes['SERVER-URI'],
        pathwayId: attributes['PATHWAY-ID']
      });
      playlist.contentSteering = contentSteering;
    } else if (name === 'EXT-X-STREAM-INF') {
      const uri = lines[index + 1];
      if (typeof uri !== 'string' || uri.startsWith('#EXT')) {
        utils.INVALIDPLAYLIST('EXT-X-STREAM-INF must be followed by a URI line');
      }
      const variant = parseVariant(lines, attributes, uri as string, false, params);
      if (variant) {
        if (typeof variant.score === 'number') {
          variantIsScored = true;
          if (variant.score < 0) {
            utils.INVALIDPLAYLIST('SCORE attribute on EXT-X-STREAM-INF must be positive decimal-floating-point number.');
          }
        }
        playlist.variants.push(variant);
      }
    } else if (name === 'EXT-X-I-FRAME-STREAM-INF') {
      const variant = parseVariant(lines, attributes, attributes.URI, true, params);
      if (variant) {
        playlist.variants.push(variant);
      }
    } else if (name === 'EXT-X-SESSION-DATA') {
      const sessionData = new SessionData({
        id: attributes['DATA-ID'],
        value: attributes['VALUE'],
        uri: attributes['URI'],
        language: attributes['LANGUAGE']
      });
      if (playlist.sessionDataList.some(item => item.id === sessionData.id && item.language === sessionData.language)) {
        utils.INVALIDPLAYLIST('A Playlist MUST NOT contain more than one EXT-X-SESSION-DATA tag with the same DATA-ID attribute and the same LANGUAGE attribute.');
      }
      playlist.sessionDataList.push(sessionData);
    } else if (name === 'EXT-X-SESSION-KEY') {
      if (attributes['METHOD'] === 'NONE') {
        utils.INVALIDPLAYLIST('EXT-X-SESSION-KEY: The value of the METHOD attribute MUST NOT be NONE');
      }
      const sessionKey = new Key({
        method: attributes['METHOD'],
        uri: attributes['URI'],
        iv: attributes['IV'],
        format: attributes['KEYFORMAT'],
        formatVersion: attributes['KEYFORMATVERSIONS']
      });
      if (playlist.sessionKeyList.some(item => sameKey(item, sessionKey))) {
        utils.INVALIDPLAYLIST('A Master Playlist MUST NOT contain more than one EXT-X-SESSION-KEY tag with the same METHOD, URI, IV, KEYFORMAT, and KEYFORMATVERSIONS attribute values.');
      }
      setCompatibleVersionOfKey(params, attributes);
      playlist.sessionKeyList.push(sessionKey);
    } else if (name === 'EXT-X-INDEPENDENT-SEGMENTS') {
      if (playlist.independentSegments) {
        utils.INVALIDPLAYLIST('EXT-X-INDEPENDENT-SEGMENTS tag MUST NOT appear more than once in a Playlist');
      }
      playlist.independentSegments = true;
    } else if (name === 'EXT-X-START') {
      if (playlist.start) {
        utils.INVALIDPLAYLIST('EXT-X-START tag MUST NOT appear more than once in a Playlist');
      }
      if (typeof attributes['TIME-OFFSET'] !== 'number') {
        utils.INVALIDPLAYLIST('EXT-X-START: TIME-OFFSET attribute is REQUIRED');
      }
      playlist.start = {offset: attributes['TIME-OFFSET'], precise: attributes['PRECISE'] || false};
    } else if (name === 'EXT-X-DEFINE') {
      playlist.defines ||= [];
      playlist.defines.push(attributes);
    }
  }
  if (variantIsScored) {
    for (const variant of playlist.variants) {
      if (typeof variant.score !== 'number') {
        utils.INVALIDPLAYLIST('If any Variant Stream contains the SCORE attribute, then all Variant Streams in the Master Playlist SHOULD have a SCORE attribute');
      }
    }
  }
  if (params.isClosedCaptionsNone) {
    for (const variant of playlist.variants) {
      if (variant.closedCaptions.length > 0) {
        utils.INVALIDPLAYLIST('If there is a variant with CLOSED-CAPTIONS attribute of NONE, all EXT-X-STREAM-INF tags MUST have this attribute with a value of NONE');
      }
    }
  }
  return playlist;
}

function parseDateRange(attributes) {
  const attrs: Record<string, any> = {};
  for (const key of Object.keys(attributes)) {
    if (key.startsWith('SCTE35-') || key.startsWith('X-')) {
      attrs[key] = attributes[key];
    }
  }
  const dateRange = new DateRange({
    id: attributes['ID'],
    classId: attributes['CLASS'],
    start: attributes['START-DATE'],
    cue: attributes['CUE'],
    end: attributes['END-DATE'],
    duration: attributes['DURATION'],
    plannedDuration: attributes['PLANNED-DURATION'],
    endOnNext: attributes['END-ON-NEXT'],
    attributes: attrs
  });
  return dateRange;
}

function parseSegment(lines: Line[], uri: string, start: number, end: number, mediaSequenceNumber: number, discontinuitySequence: number, params: Record<string, any>): Segment {
  const segment = new Segment({uri, mediaSequenceNumber, discontinuitySequence});
  let mapHint = false;
  let partHint = false;
  for (let i = start; i <= end; i++) {
    const {name, value, attributes} = mapTo<Tag>(lines[i]);
    if (name === 'EXTINF') {
      if (!Number.isInteger(value.duration) && params.compatibleVersion < 3) {
        params.compatibleVersion = 3;
      }
      if (Math.round(value.duration) > params.targetDuration) {
        utils.INVALIDPLAYLIST('EXTINF duration, when rounded to the nearest integer, MUST be less than or equal to the target duration');
      }
      segment.duration = value.duration;
      segment.title = value.title;
    } else if (name === 'EXT-X-BYTERANGE') {
      if (params.compatibleVersion < 4) {
        params.compatibleVersion = 4;
      }
      segment.byterange = value;
    } else if (name === 'EXT-X-DISCONTINUITY') {
      if (segment.parts.length > 0) {
        utils.INVALIDPLAYLIST('EXT-X-DISCONTINUITY must appear before the first EXT-X-PART tag of the Parent Segment.');
      }
      segment.discontinuity = true;
    } else if (name === 'EXT-X-GAP') {
      if (params.compatibleVersion < 8) {
        params.compatibleVersion = 8;
      }
      segment.gap = true;
    } else if (name === 'EXT-X-KEY') {
      if (segment.parts.length > 0) {
        utils.INVALIDPLAYLIST('EXT-X-KEY must appear before the first EXT-X-PART tag of the Parent Segment.');
      }
      setCompatibleVersionOfKey(params, attributes);
      segment.key = new Key({
        method: attributes['METHOD'],
        uri: attributes['URI'],
        iv: attributes['IV'],
        format: attributes['KEYFORMAT'],
        formatVersion: attributes['KEYFORMATVERSIONS']
      });
    } else if (name === 'EXT-X-MAP') {
      if (segment.parts.length > 0) {
        utils.INVALIDPLAYLIST('EXT-X-MAP must appear before the first EXT-X-PART tag of the Parent Segment.');
      }
      if (params.compatibleVersion < 5) {
        params.compatibleVersion = 5;
      }
      params.hasMap = true;
      segment.map = new MediaInitializationSection({
        uri: attributes['URI'],
        byterange: attributes['BYTERANGE']
      });
    } else if (name === 'EXT-X-PROGRAM-DATE-TIME') {
      segment.programDateTime = value;
    } else if (name === 'EXT-X-DATERANGE') {
      segment.dateRange = parseDateRange(attributes);
    } else if (name === 'EXT-X-CUE-OUT') {
      segment.markers.push(new SpliceInfo({
        type: 'OUT',
        duration: (attributes && attributes.DURATION) || value
      }));
    } else if (name === 'EXT-X-CUE-IN') {
      segment.markers.push(new SpliceInfo({
        type: 'IN'
      }));
    } else if (
      name === 'EXT-X-CUE-OUT-CONT' ||
      name === 'EXT-X-CUE' ||
      name === 'EXT-OATCLS-SCTE35' ||
      name === 'EXT-X-ASSET' ||
      name === 'EXT-X-SCTE35'
    ) {
      segment.markers.push(new SpliceInfo({
        type: 'RAW',
        tagName: name,
        value
      }));
    } else if (name === 'EXT-X-PRELOAD-HINT' && !attributes['TYPE']) {
      utils.INVALIDPLAYLIST('EXT-X-PRELOAD-HINT: TYPE attribute is mandatory');
    } else if (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'PART' && partHint) {
      utils.INVALIDPLAYLIST('Servers should not add more than one EXT-X-PRELOAD-HINT tag with the same TYPE attribute to a Playlist.');
    } else if ((name === 'EXT-X-PART' || name === 'EXT-X-PRELOAD-HINT') && !attributes['URI']) {
      utils.INVALIDPLAYLIST('EXT-X-PART / EXT-X-PRELOAD-HINT: URI attribute is mandatory');
    } else if (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'MAP') {
      if (mapHint) {
        utils.INVALIDPLAYLIST('Servers should not add more than one EXT-X-PRELOAD-HINT tag with the same TYPE attribute to a Playlist.');
      }
      mapHint = true;
      params.hasMap = true;
      segment.map = new MediaInitializationSection({
        hint: true,
        uri: attributes['URI'],
        byterange: {length: attributes['BYTERANGE-LENGTH'], offset: attributes['BYTERANGE-START'] || 0}
      });
    } else if (name === 'EXT-X-PART' || (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'PART')) {
      if (name === 'EXT-X-PART' && !attributes['DURATION']) {
        utils.INVALIDPLAYLIST('EXT-X-PART: DURATION attribute is mandatory');
      }
      if (name === 'EXT-X-PRELOAD-HINT') {
        partHint = true;
      }
      const partialSegment = new PartialSegment({
        hint: (name === 'EXT-X-PRELOAD-HINT'),
        uri: attributes['URI'],
        byterange: (name === 'EXT-X-PART' ? attributes['BYTERANGE'] : {length: attributes['BYTERANGE-LENGTH'], offset: attributes['BYTERANGE-START'] || 0}),
        duration: attributes['DURATION'],
        independent: attributes['INDEPENDENT'],
        gap: attributes['GAP']
      });
      if (segment.gap && !partialSegment.gap) {
        // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.2.1
        utils.INVALIDPLAYLIST('Partial segments must have GAP=YES if they are in a gap (EXT-X-GAP)');
      }
      segment.parts.push(partialSegment);
    }
  }
  return segment;
}

function parsePrefetchSegment(lines: Line[], uri: any, start: number, end: number, mediaSequenceNumber: number, discontinuitySequence: number, params: Record<string, any>): PrefetchSegment {
  const segment = new PrefetchSegment({uri, mediaSequenceNumber, discontinuitySequence});
  for (let i = start; i <= end; i++) {
    const {name, attributes} = lines[i] as Tag;
    if (name === 'EXTINF') {
      utils.INVALIDPLAYLIST('A prefetch segment must not be advertised with an EXTINF tag.');
    } else if (name === 'EXT-X-DISCONTINUITY') {
      utils.INVALIDPLAYLIST('A prefetch segment must not be advertised with an EXT-X-DISCONTINUITY tag.');
    } else if (name === 'EXT-X-PREFETCH-DISCONTINUITY') {
      segment.discontinuity = true;
    } else if (name === 'EXT-X-KEY') {
      setCompatibleVersionOfKey(params, attributes);
      segment.key = new Key({
        method: attributes['METHOD'],
        uri: attributes['URI'],
        iv: attributes['IV'],
        format: attributes['KEYFORMAT'],
        formatVersion: attributes['KEYFORMATVERSIONS']
      });
    } else if (name === 'EXT-X-MAP') {
      utils.INVALIDPLAYLIST('Prefetch segments must not be advertised with an EXT-X-MAP tag.');
    }
  }
  return segment;
}

function parseMediaPlaylist(lines: Line[], params: Record<string, any>): MediaPlaylist {
  const playlist = new MediaPlaylist();
  let segmentStart = -1;
  let mediaSequence = 0;
  let discontinuityFound = false;
  let prefetchFound = false;
  let discontinuitySequence = 0;
  let currentKey: Key | null = null;
  let currentMap: MediaInitializationSection | null = null;
  let containsParts = false;
  for (const [index, line] of lines.entries()) {
    const {name, value, attributes, category} = mapTo<Tag>(line);
    if (category === 'Segment') {
      if (segmentStart === -1) {
        segmentStart = index;
      }
      if (name === 'EXT-X-DISCONTINUITY') {
        discontinuityFound = true;
      }
      continue;
    }
    if (name === 'EXT-X-VERSION') {
      if (playlist.version === undefined) {
        playlist.version = value;
      } else {
        utils.INVALIDPLAYLIST('A Playlist file MUST NOT contain more than one EXT-X-VERSION tag.');
      }
    } else if (name === 'EXT-X-TARGETDURATION') {
      playlist.targetDuration = params.targetDuration = value;
    } else if (name === 'EXT-X-MEDIA-SEQUENCE') {
      if (playlist.segments.length > 0) {
        utils.INVALIDPLAYLIST('The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.');
      }
      playlist.mediaSequenceBase = mediaSequence = value;
    } else if (name === 'EXT-X-DISCONTINUITY-SEQUENCE') {
      if (playlist.segments.length > 0) {
        utils.INVALIDPLAYLIST('The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.');
      }
      if (discontinuityFound) {
        utils.INVALIDPLAYLIST('The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before any EXT-X-DISCONTINUITY tag.');
      }
      playlist.discontinuitySequenceBase = discontinuitySequence = value;
    } else if (name === 'EXT-X-ENDLIST') {
      playlist.endlist = true;
    } else if (name === 'EXT-X-PLAYLIST-TYPE') {
      playlist.playlistType = value;
    } else if (name === 'EXT-X-I-FRAMES-ONLY') {
      if (params.compatibleVersion < 4) {
        params.compatibleVersion = 4;
      }
      playlist.isIFrame = true;
    } else if (name === 'EXT-X-INDEPENDENT-SEGMENTS') {
      if (playlist.independentSegments) {
        utils.INVALIDPLAYLIST('EXT-X-INDEPENDENT-SEGMENTS tag MUST NOT appear more than once in a Playlist');
      }
      playlist.independentSegments = true;
    } else if (name === 'EXT-X-START') {
      if (playlist.start) {
        utils.INVALIDPLAYLIST('EXT-X-START tag MUST NOT appear more than once in a Playlist');
      }
      if (typeof attributes['TIME-OFFSET'] !== 'number') {
        utils.INVALIDPLAYLIST('EXT-X-START: TIME-OFFSET attribute is REQUIRED');
      }
      playlist.start = {offset: attributes['TIME-OFFSET'], precise: attributes['PRECISE'] || false};
    } else if (name === 'EXT-X-SERVER-CONTROL') {
      if (!attributes['CAN-BLOCK-RELOAD']) {
        utils.INVALIDPLAYLIST('EXT-X-SERVER-CONTROL: CAN-BLOCK-RELOAD=YES is mandatory for Low-Latency HLS');
      }
      playlist.lowLatencyCompatibility = {
        canBlockReload: attributes['CAN-BLOCK-RELOAD'],
        canSkipUntil: attributes['CAN-SKIP-UNTIL'],
        holdBack: attributes['HOLD-BACK'],
        partHoldBack: attributes['PART-HOLD-BACK']
      };
    } else if (name === 'EXT-X-PART-INF') {
      if (!attributes['PART-TARGET']) {
        utils.INVALIDPLAYLIST('EXT-X-PART-INF: PART-TARGET attribute is mandatory');
      }
      playlist.partTargetDuration = attributes['PART-TARGET'];
    } else if (name === 'EXT-X-RENDITION-REPORT') {
      if (!attributes['URI']) {
        utils.INVALIDPLAYLIST('EXT-X-RENDITION-REPORT: URI attribute is mandatory');
      }
      if (attributes['URI'].search(/^[a-z]+:/) === 0) {
        utils.INVALIDPLAYLIST('EXT-X-RENDITION-REPORT: URI must be relative to the playlist uri');
      }
      playlist.renditionReports.push(new RenditionReport({
        uri: attributes['URI'],
        lastMSN: attributes['LAST-MSN'],
        lastPart: attributes['LAST-PART']
      }));
    } else if (name === 'EXT-X-SKIP') {
      if (!attributes['SKIPPED-SEGMENTS']) {
        utils.INVALIDPLAYLIST('EXT-X-SKIP: SKIPPED-SEGMENTS attribute is mandatory');
      }
      if (params.compatibleVersion < 9) {
        params.compatibleVersion = 9;
      }
      playlist.skip = attributes['SKIPPED-SEGMENTS'];
      mediaSequence += playlist.skip;
    } else if (name === 'EXT-X-PREFETCH') {
      const segment = parsePrefetchSegment(lines, value, segmentStart === -1 ? index : segmentStart, index - 1, mediaSequence++, discontinuitySequence, params);
      if (segment) {
        if (segment.discontinuity) {
          segment.discontinuitySequence++;
          discontinuitySequence = segment.discontinuitySequence;
        }
        if (segment.key) {
          currentKey = segment.key;
        } else {
          segment.key = currentKey;
        }
        playlist.prefetchSegments.push(segment);
      }
      prefetchFound = true;
      segmentStart = -1;
    } else if (name === 'EXT-X-DEFINE') {
      playlist.defines ||= [];
      playlist.defines.push(attributes);
    } else if (name === 'EXT-X-DATERANGE') {
      const dateRange = parseDateRange(attributes);
      playlist.dateRanges.push(dateRange);
    } else if (typeof line === 'string') {
      // uri
      if (segmentStart === -1) {
        utils.INVALIDPLAYLIST('A URI line is not preceded by any segment tags');
      }
      if (!playlist.targetDuration) {
        utils.INVALIDPLAYLIST('The EXT-X-TARGETDURATION tag is REQUIRED');
      }
      if (prefetchFound) {
        utils.INVALIDPLAYLIST('These segments must appear after all complete segments.');
      }
      const segment = parseSegment(lines, line, segmentStart, index - 1, mediaSequence++, discontinuitySequence, params);
      if (segment) {
        [discontinuitySequence, currentKey, currentMap] = addSegment(playlist, segment, discontinuitySequence, currentKey!, currentMap!);
        if (!containsParts && segment.parts.length > 0) {
          containsParts = true;
        }
      }
      segmentStart = -1;
    }
  }
  if (segmentStart !== -1) {
    const segment = parseSegment(lines, '', segmentStart, lines.length - 1, mediaSequence++, discontinuitySequence, params);
    if (segment) {
      const {parts} = segment;
      if (parts.length > 0 && !playlist.endlist && !parts.at(-1)?.hint) {
        utils.INVALIDPLAYLIST('If the Playlist contains EXT-X-PART tags and does not contain an EXT-X-ENDLIST tag, the Playlist must contain an EXT-X-PRELOAD-HINT tag with a TYPE=PART attribute');
      }
      // @ts-expect-error TODO check if this is not a bug the third argument should be a discontinuitySequence
      addSegment(playlist, segment, currentKey, currentMap);
      if (!containsParts && segment.parts.length > 0) {
        containsParts = true;
      }
    }
  }
  checkDateRange(playlist.segments);
  if (playlist.lowLatencyCompatibility) {
    checkLowLatencyCompatibility(playlist, containsParts);
  }
  return playlist;
}

function addSegment(playlist: MediaPlaylist, segment: Segment, discontinuitySequence: number, currentKey?: Key, currentMap?: MediaInitializationSection): [number, Key, MediaInitializationSection] {
  const {discontinuity, key, map, byterange, uri} = segment;
  if (discontinuity) {
    segment.discontinuitySequence = discontinuitySequence + 1;
  }
  if (!key) {
    segment.key = currentKey;
  }
  if (!map) {
    segment.map = currentMap!;
  }
  if (byterange && byterange.offset === -1) {
    const {segments} = playlist;
    if (segments.length > 0) {
      const prevSegment = segments.at(-1)!;
      if (prevSegment.byterange && prevSegment.uri === uri) {
        byterange.offset = prevSegment.byterange.offset + prevSegment.byterange.length;
      } else {
        utils.INVALIDPLAYLIST('If offset of EXT-X-BYTERANGE is not present, a previous Media Segment MUST be a sub-range of the same media resource');
      }
    } else {
      utils.INVALIDPLAYLIST('If offset of EXT-X-BYTERANGE is not present, a previous Media Segment MUST appear in the Playlist file');
    }
  }
  playlist.segments.push(segment);
  return [segment.discontinuitySequence, segment.key!, segment.map];
}

function checkDateRange(segments: Segment[]) {
  const earliestDates = new Map();
  const rangeList = new Map();
  let hasDateRange = false;
  let hasProgramDateTime = false;
  for (let i = segments.length - 1; i >= 0; i--) {
    const {programDateTime, dateRange} = segments[i];
    if (programDateTime) {
      hasProgramDateTime = true;
    }
    if (dateRange && dateRange.start) {
      hasDateRange = true;
      if (dateRange.endOnNext && (dateRange.end || dateRange.duration)) {
        utils.INVALIDPLAYLIST('An EXT-X-DATERANGE tag with an END-ON-NEXT=YES attribute MUST NOT contain DURATION or END-DATE attributes.');
      }
      const start = dateRange.start.getTime();
      const duration = dateRange.duration || 0;
      if (dateRange.end && dateRange.duration) {
        if ((start + duration * 1000) !== dateRange.end.getTime()) {
          utils.INVALIDPLAYLIST('END-DATE MUST be equal to the value of the START-DATE attribute plus the value of the DURATION');
        }
      }
      if (dateRange.endOnNext) {
        dateRange.end = earliestDates.get(dateRange.classId);
      }
      earliestDates.set(dateRange.classId, dateRange.start);
      const end = dateRange.end ? dateRange.end.getTime() : dateRange.start.getTime() + (dateRange.duration || 0) * 1000;
      const range = rangeList.get(dateRange.classId);
      if (range) {
        for (const entry of range) {
          if ((entry.start <= start && entry.end > start) || (entry.start >= start && entry.start < end)) {
            utils.INVALIDPLAYLIST('DATERANGE tags with the same CLASS should not overlap');
          }
        }
        range.push({start, end});
      } else if (dateRange.classId) {
        rangeList.set(dateRange.classId, [{start, end}]);
      }
    }
  }
  if (hasDateRange && !hasProgramDateTime) {
    utils.INVALIDPLAYLIST('If a Playlist contains an EXT-X-DATERANGE tag, it MUST also contain at least one EXT-X-PROGRAM-DATE-TIME tag.');
  }
}

function checkLowLatencyCompatibility({lowLatencyCompatibility, targetDuration, partTargetDuration, segments, renditionReports}: any, containsParts) {
  const {canSkipUntil, holdBack, partHoldBack} = lowLatencyCompatibility;
  if (canSkipUntil < targetDuration * 6) {
    utils.INVALIDPLAYLIST('The Skip Boundary must be at least six times the EXT-X-TARGETDURATION.');
  }
  // Its value is a floating-point number of seconds and .
  if (holdBack < targetDuration * 3) {
    utils.INVALIDPLAYLIST('HOLD-BACK must be at least three times the EXT-X-TARGETDURATION.');
  }
  if (containsParts) {
    if (partTargetDuration === undefined) {
      utils.INVALIDPLAYLIST('EXT-X-PART-INF is required if a Playlist contains one or more EXT-X-PART tags');
    }
    if (partHoldBack === undefined) {
      utils.INVALIDPLAYLIST('EXT-X-PART: PART-HOLD-BACK attribute is mandatory');
    }
    if (partHoldBack < partTargetDuration) {
      utils.INVALIDPLAYLIST('PART-HOLD-BACK must be at least PART-TARGET');
    }
    for (const [segmentIndex, {parts}] of segments.entries()) {
      if (parts.length > 0 && segmentIndex < segments.length - 3) {
        utils.INVALIDPLAYLIST('Remove EXT-X-PART tags from the Playlist after they are greater than three target durations from the end of the Playlist.');
      }
      for (const [partIndex, {duration}] of parts.entries()) {
        if (duration === undefined) {
          continue;
        }
        if (duration > partTargetDuration) {
          utils.INVALIDPLAYLIST('PART-TARGET is the maximum duration of any Partial Segment');
        }
        if (partIndex < parts.length - 1 && duration < partTargetDuration * 0.85) {
          utils.INVALIDPLAYLIST('All Partial Segments except the last part of a segment must have a duration of at least 85% of PART-TARGET');
        }
      }
    }
  }
  for (const report of renditionReports) {
    const lastSegment = segments.at(-1);
    report.lastMSN ??= lastSegment.mediaSequenceNumber;
    if ((report.lastPart === null || report.lastPart === undefined) && lastSegment.parts.length > 0) {
      report.lastPart = lastSegment.parts.length - 1;
    }
  }
}

function CHECKTAGCATEGORY(category: TagCategory, params: Record<string, any>) {
  if (category === 'Segment' || category === 'MediaPlaylist') {
    if (params.isMasterPlaylist === undefined) {
      params.isMasterPlaylist = false;
      return;
    }
    if (params.isMasterPlaylist) {
      MIXEDTAGS();
    }
    return;
  }
  if (category === 'MasterPlaylist') {
    if (params.isMasterPlaylist === undefined) {
      params.isMasterPlaylist = true;
      return;
    }
    if (params.isMasterPlaylist === false) {
      MIXEDTAGS();
    }
  }
  // category === 'Basic' or 'MediaorMasterPlaylist' or 'Unknown'
}

type Tag = {
  name: string;
  category: TagCategory;
  value: any;
  attributes: any;
};

function parseTag(line: string, params: Record<string, any>): Tag | null {
  const [name, param] = splitTag(line);
  const category = getTagCategory(name);
  CHECKTAGCATEGORY(category, params);
  if (category === 'Unknown') {
    return null;
  }
  if (category === 'MediaPlaylist' && name !== 'EXT-X-RENDITION-REPORT' && name !== 'EXT-X-PREFETCH') {
    if (params.hash[name]) {
      utils.INVALIDPLAYLIST('There MUST NOT be more than one Media Playlist tag of each type in any Media Playlist');
    }
    params.hash[name] = true;
  }
  const [value, attributes] = parseTagParam(name, param);
  return {name, category, value, attributes};
}

type Line = string | Tag;

function lexicalParse(text: string, params: Record<string, any>): Line[] {
  const lines: Line[] = [];
  for (const l of text.split('\n')) {
    // V8 has garbage collection issues when cleaning up substrings split from strings greater
    // than 13 characters so before we continue we need to safely copy over each line so that it
    // doesn't hold any reference to the containing string.
    const line = l.trim();
    if (!line) {
      // empty line
      continue;
    }
    if (line.startsWith('#')) {
      if (line.startsWith('#EXT')) {
        // tag
        const tag = parseTag(line, params);
        if (tag) {
          lines.push(tag);
        }
      }
      // comment
      continue;
    }
    // uri
    lines.push(line);
  }
  if (lines.length === 0 || (lines[0] as Tag).name !== 'EXTM3U') {
    utils.INVALIDPLAYLIST('The EXTM3U tag MUST be the first line.');
  }
  return lines;
}

function semanticParse(lines: Line[], params: Record<string, any>): MasterPlaylist | MediaPlaylist {
  let playlist;
  if (params.isMasterPlaylist) {
    playlist = parseMasterPlaylist(lines, params);
  } else {
    playlist = parseMediaPlaylist(lines, params);
    if (!playlist.isIFrame && params.hasMap && params.compatibleVersion < 6) {
      params.compatibleVersion = 6;
    }
  }
  if (params.compatibleVersion > 1) {
    if (!playlist.version || playlist.version < params.compatibleVersion) {
      utils.INVALIDPLAYLIST(`EXT-X-VERSION needs to be ${params.compatibleVersion} or higher.`);
    }
  }
  return playlist;
}

function parse(text: string): MasterPlaylist | MediaPlaylist {
  const params: Record<string, any> = {
    version: undefined,
    isMasterPlaylist: undefined,
    hasMap: false,
    targetDuration: 0,
    compatibleVersion: 1,
    isClosedCaptionsNone: false,
    hash: {}
  };

  const lines = lexicalParse(text, params);
  const playlist = semanticParse(lines, params);
  playlist.source = text;
  return playlist;
}

function mapTo<T extends object>(value: T | string): Partial<T> {
  return typeof value === 'string' ? {} : value;
}

export default parse;


================================================
FILE: stringify.ts
================================================
import * as utils from './utils';
import {
  Byterange,
  DateRange, Key,
  MasterPlaylist,
  MediaInitializationSection,
  MediaPlaylist,
  PartialSegment,
  Rendition,
  Segment,
  SessionData,
  SpliceInfo,
  Variant,
  PostProcess,
  ContentSteering,
} from './types';

const ALLOW_REDUNDANCY = [
  '#EXTINF',
  '#EXT-X-BYTERANGE',
  '#EXT-X-DISCONTINUITY',
  '#EXT-X-STREAM-INF',
  '#EXT-X-CUE-OUT',
  '#EXT-X-CUE-IN',
  '#EXT-X-KEY',
  '#EXT-X-MAP'
];

const SKIP_IF_REDUNDANT = [
  '#EXT-X-MEDIA'
];

class LineArray extends Array<string> {
  baseUri?: string;

  constructor(baseUri?: string) {
    super();
    this.baseUri = baseUri;
  }

  override push(...elems: string[]) {
    // redundancy check
    for (const elem of elems) {
      if (!elem.startsWith('#')) {
        super.push(elem);
        continue;
      }
      if (ALLOW_REDUNDANCY.some(item => elem.startsWith(item))) {
        super.push(elem);
        continue;
      }
      if (this.includes(elem)) {
        if (SKIP_IF_REDUNDANT.some(item => elem.startsWith(item))) {
          continue;
        }
        utils.INVALIDPLAYLIST(`Redundant item (${elem})`);
      }
      super.push(elem);
    }
    return this.length;
  }

  override join(separator: string | undefined = ','): string {
    for (let i = this.length - 1; i >= 0; i--) {
      if (!this[i]) {
        this.splice(i, 1);
      }
    }
    return super.join(separator);
  }
}

function buildDecimalFloatingNumber(num: number, fixed?: number) {
  let roundFactor = 1000;
  if (fixed) {
    roundFactor = 10 ** fixed;
  }
  const rounded = Math.round(num * roundFactor) / roundFactor;
  return fixed ? rounded.toFixed(fixed) : rounded;
}

function getNumberOfDecimalPlaces(num: number) {
  const str = num.toString(10);
  const index = str.indexOf('.');
  if (index === -1) {
    return 0;
  }
  return str.length - index - 1;
}

function buildMasterPlaylist(lines: LineArray, playlist: MasterPlaylist, postProcess: PostProcess | undefined) {
  if (playlist.contentSteering) {
    lines.push(buildContentSteeringServer(playlist.contentSteering));
  }
  for (const sessionData of playlist.sessionDataList) {
    lines.push(buildSessionData(sessionData));
  }
  for (const sessionKey of playlist.sessionKeyList) {
    lines.push(buildKey(sessionKey, true));
  }
  for (const [i, variant] of playlist.variants.entries()) {
    const base = lines.length;
    buildVariant(lines, variant);
    if (postProcess?.variantProcessor) {
      postProcess.variantProcessor(lines, base, lines.length - 1, variant, i);
    }
  }
}

function buildContentSteeringServer(contentSteering: ContentSteering) {
  const attrs = [
    `SERVER-URI="${contentSteering.serverUri}"`,
    `PATHWAY-ID="${contentSteering.pathwayId}"`
  ];
  return `#EXT-X-CONTENT-STEERING:${attrs.join(',')}`;
}

function buildSessionData(sessionData: SessionData) {
  const attrs = [`DATA-ID="${sessionData.id}"`];
  if (sessionData.language) {
    attrs.push(`LANGUAGE="${sessionData.language}"`);
  }
  if (sessionData.value) {
    attrs.push(`VALUE="${sessionData.value}"`);
  } else if (sessionData.uri) {
    attrs.push(`URI="${sessionData.uri}"`);
  }
  return `#EXT-X-SESSION-DATA:${attrs.join(',')}`;
}

function buildKey(key: Key, isSessionKey?: boolean) {
  const name = isSessionKey ? '#EXT-X-SESSION-KEY' : '#EXT-X-KEY';
  const attrs = [`METHOD=${key.method}`];
  if (key.uri) {
    attrs.push(`URI="${key.uri}"`);
  }
  if (key.iv) {
    if (key.iv.byteLength !== 16) {
      utils.INVALIDPLAYLIST('IV must be a 128-bit unsigned integer');
    }
    attrs.push(`IV=${utils.byteSequenceToHex(key.iv)}`);
  }
  if (key.format) {
    attrs.push(`KEYFORMAT="${key.format}"`);
  }
  if (key.formatVersion) {
    attrs.push(`KEYFORMATVERSIONS="${key.formatVersion}"`);
  }
  return `${name}:${attrs.join(',')}`;
}

function buildVariant(lines: LineArray, variant: Variant) {
  const name = variant.isIFrameOnly ? '#EXT-X-I-FRAME-STREAM-INF' : '#EXT-X-STREAM-INF';
  const attrs = [`BANDWIDTH=${variant.bandwidth}`];
  if (variant.averageBandwidth) {
    attrs.push(`AVERAGE-BANDWIDTH=${variant.averageBandwidth}`);
  }
  if (variant.isIFrameOnly) {
    attrs.push(`URI="${variant.uri}"`);
  }
  if (variant.codecs) {
    attrs.push(`CODECS="${variant.codecs}"`);
  }
  if (variant.resolution) {
    attrs.push(`RESOLUTION=${variant.resolution.width}x${variant.resolution.height}`);
  }
  if (variant.frameRate) {
    attrs.push(`FRAME-RATE=${buildDecimalFloatingNumber(variant.frameRate, 3)}`);
  }
  if (variant.hdcpLevel) {
    attrs.push(`HDCP-LEVEL=${variant.hdcpLevel}`);
  }
  if (variant.audio.length > 0) {
    attrs.push(`AUDIO="${variant.audio[0].groupId}"`);
    for (const rendition of variant.audio) {
      lines.push(buildRendition(rendition));
    }
  }
  if (variant.video.length > 0) {
    attrs.push(`VIDEO="${variant.video[0].groupId}"`);
    for (const rendition of variant.video) {
      lines.push(buildRendition(rendition));
    }
  }
  if (variant.subtitles.length > 0) {
    attrs.push(`SUBTITLES="${variant.subtitles[0].groupId}"`);
    for (const rendition of variant.subtitles) {
      lines.push(buildRendition(rendition));
    }
  }
  if (utils.getOptions().allowClosedCaptionsNone && variant.closedCaptions.length === 0) {
    attrs.push(`CLOSED-CAPTIONS=NONE`);
  } else if (variant.closedCaptions.length > 0) {
    attrs.push(`CLOSED-CAPTIONS="${variant.closedCaptions[0].groupId}"`);
    for (const rendition of variant.closedCaptions) {
      lines.push((buildRendition(rendition)));
    }
  }
  if (variant.score) {
    attrs.push(`SCORE=${variant.score}`);
  }
  if (variant.allowedCpc) {
    const list: string[] = [];
    for (const {format, cpcList} of variant.allowedCpc) {
      list.push(`${format}:${cpcList.join('/')}`);
    }
    attrs.push(`ALLOWED-CPC="${list.join(',')}"`);
  }
  if (variant.videoRange) {
    attrs.push(`VIDEO-RANGE=${variant.videoRange}`);
  }
  if (variant.stableVariantId) {
    attrs.push(`STABLE-VARIANT-ID="${variant.stableVariantId}"`);
  }
  if (variant.pathwayId) {
    attrs.push(`PATHWAY-ID="${variant.pathwayId}"`);
  }
  if (variant.programId) {
    attrs.push(`PROGRAM-ID=${variant.programId}`);
  }
  lines.push(`${name}:${attrs.join(',')}`);
  if (!variant.isIFrameOnly) {
    lines.push(`${variant.uri}`);
  }
}

function buildRendition(rendition: Rendition) {
  const attrs = [
    `TYPE=${rendition.type}`,
    `GROUP-ID="${rendition.groupId}"`,
    `NAME="${rendition.name}"`
  ];
  if (rendition.isDefault !== undefined) {
    attrs.push(`DEFAULT=${rendition.isDefault ? 'YES' : 'NO'}`);
  }
  if (rendition.autoselect !== undefined) {
    attrs.push(`AUTOSELECT=${rendition.autoselect ? 'YES' : 'NO'}`);
  }
  if (rendition.forced !== undefined) {
    attrs.push(`FORCED=${rendition.forced ? 'YES' : 'NO'}`);
  }
  if (rendition.language) {
    attrs.push(`LANGUAGE="${rendition.language}"`);
  }
  if (rendition.assocLanguage) {
    attrs.push(`ASSOC-LANGUAGE="${rendition.assocLanguage}"`);
  }
  if (rendition.instreamId) {
    attrs.push(`INSTREAM-ID="${rendition.instreamId}"`);
  }
  if (rendition.characteristics) {
    attrs.push(`CHARACTERISTICS="${rendition.characteristics}"`);
  }
  if (rendition.channels) {
    attrs.push(`CHANNELS="${rendition.channels}"`);
  }
  if (rendition.uri) {
    attrs.push(`URI="${rendition.uri}"`);
  }
  return `#EXT-X-MEDIA:${attrs.join(',')}`;
}

function buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist, postProcess: PostProcess | undefined) {
  let lastKey = '';
  let lastMap = '';
  let unclosedCueIn = false;

  if (playlist.targetDuration) {
    lines.push(`#EXT-X-TARGETDURATION:${playlist.targetDuration}`);
  }
  if (playlist.lowLatencyCompatibility) {
    const {canBlockReload, canSkipUntil, holdBack, partHoldBack} = playlist.lowLatencyCompatibility;
    const params: string[] = [];
    params.push(`CAN-BLOCK-RELOAD=${canBlockReload ? 'YES' : 'NO'}`);
    if (canSkipUntil !== undefined) {
      params.push(`CAN-SKIP-UNTIL=${canSkipUntil}`);
    }
    if (holdBack !== undefined) {
      params.push(`HOLD-BACK=${holdBack}`);
    }
    if (partHoldBack !== undefined) {
      params.push(`PART-HOLD-BACK=${partHoldBack}`);
    }
    lines.push(`#EXT-X-SERVER-CONTROL:${params.join(',')}`);
  }
  if (playlist.partTargetDuration) {
    lines.push(`#EXT-X-PART-INF:PART-TARGET=${playlist.partTargetDuration}`);
  }
  if (playlist.mediaSequenceBase) {
    lines.push(`#EXT-X-MEDIA-SEQUENCE:${playlist.mediaSequenceBase}`);
  }
  if (playlist.discontinuitySequenceBase) {
    lines.push(`#EXT-X-DISCONTINUITY-SEQUENCE:${playlist.discontinuitySequenceBase}`);
  }
  if (playlist.playlistType) {
    lines.push(`#EXT-X-PLAYLIST-TYPE:${playlist.playlistType}`);
  }
  if (playlist.isIFrame) {
    lines.push(`#EXT-X-I-FRAMES-ONLY`);
  }
  if (playlist.skip > 0) {
    lines.push(`#EXT-X-SKIP:SKIPPED-SEGMENTS=${playlist.skip}`);
  }
  for (const dateRange of playlist.dateRanges) {
    lines.push(buildDateRange(dateRange));
  }
  for (const [i, segment] of playlist.segments.entries()) {
    const base = lines.length;
    let markerType = '';
    [lastKey, lastMap, markerType] = buildSegment(lines, segment, lastKey, lastMap, playlist.version);
    if (markerType === 'OUT') {
      unclosedCueIn = true;
    } else if (markerType === 'IN' && unclosedCueIn) {
      unclosedCueIn = false;
    }
    if (postProcess?.segmentProcessor) {
      postProcess.segmentProcessor(lines, base, lines.length - 1, segment, i);
    }
  }
  if (playlist.playlistType === 'VOD' && unclosedCueIn) {
    lines.push('#EXT-X-CUE-IN');
  }
  if (playlist.prefetchSegments.length > 2) {
    utils.INVALIDPLAYLIST('The server must deliver no more than two prefetch segments');
  }
  for (const segment of playlist.prefetchSegments) {
    if (segment.discontinuity) {
      lines.push(`#EXT-X-PREFETCH-DISCONTINUITY`);
    }
    lines.push(`#EXT-X-PREFETCH:${segment.uri}`);
  }
  if (playlist.endlist) {
    lines.push(`#EXT-X-ENDLIST`);
  }
  for (const report of playlist.renditionReports) {
    const params: string[] = [];
    params.push(`URI="${report.uri}"`, `LAST-MSN=${report.lastMSN}`);
    if (report.lastPart !== undefined) {
      params.push(`LAST-PART=${report.lastPart}`);
    }
    lines.push(`#EXT-X-RENDITION-REPORT:${params.join(',')}`);
  }
}

function buildSegment(lines: LineArray, segment: Segment, lastKey: string, lastMap: string, version = 1) {
  let hint = false;
  let markerType = '';

  if (segment.discontinuity) {
    lines.push(`#EXT-X-DISCONTINUITY`);
  }
  if (segment.gap) {
    lines.push(`#EXT-X-GAP`);
  }
  if (segment.key) {
    const line = buildKey(segment.key);
    if (line !== lastKey) {
      lines.push(line);
      lastKey = line;
    }
  }
  if (segment.map) {
    const line = buildMap(segment.map);
    if (line !== lastMap) {
      lines.push(line);
      lastMap = line;
    }
  }
  if (segment.programDateTime) {
    lines.push(`#EXT-X-PROGRAM-DATE-TIME:${utils.formatDate(segment.programDateTime)}`);
  }
  if (segment.dateRange) {
    lines.push(buildDateRange(segment.dateRange));
  }
  if (segment.markers.length > 0) {
    markerType = buildMarkers(lines, segment.markers);
  }
  if (segment.parts.length > 0) {
    hint = buildParts(lines, segment.parts);
  }
  if (hint) {
    return [lastKey, lastMap];
  }
  if (typeof segment.duration === 'number' && !Number.isNaN(segment.duration)) {
    const duration = version < 3 ? Math.round(segment.duration) : buildDecimalFloatingNumber(segment.duration, getNumberOfDecimalPlaces(segment.duration));
    lines.push(`#EXTINF:${duration},${unescape(encodeURIComponent(segment.title || ''))}`);
  }
  if (segment.byterange) {
    lines.push(`#EXT-X-BYTERANGE:${buildByteRange(segment.byterange)}`);
  }
  Array.prototype.push.call(lines, `${segment.uri}`); // URIs could be redundant when EXT-X-BYTERANGE is used
  return [lastKey, lastMap, markerType];
}

function buildMap(map: MediaInitializationSection) {
  const attrs = [`URI="${map.uri}"`];
  if (map.byterange) {
    attrs.push(`BYTERANGE="${buildByteRange(map.byterange)}"`);
  }
  return `#EXT-X-MAP:${attrs.join(',')}`;
}

function buildByteRange({offset, length}: Byterange) {
  return `${length}@${offset}`;
}

function buildDateRange(dateRange: DateRange) {
  const attrs = [
    `ID="${dateRange.id}"`
  ];
  if (dateRange.start) {
    attrs.push(`START-DATE="${utils.formatDate(dateRange.start)}"`);
  }
  if (dateRange.cue) {
    attrs.push(`CUE="${dateRange.cue}"`);
  }
  if (dateRange.end) {
    attrs.push(`END-DATE="${utils.formatDate(dateRange.end)}"`);
  }
  if (dateRange.duration) {
    attrs.push(`DURATION=${dateRange.duration}`);
  }
  if (dateRange.plannedDuration) {
    attrs.push(`PLANNED-DURATION=${dateRange.plannedDuration}`);
  }
  if (dateRange.classId) {
    attrs.push(`CLASS="${dateRange.classId}"`);
  }
  if (dateRange.endOnNext) {
    attrs.push(`END-ON-NEXT=YES`);
  }
  for (const key of Object.keys(dateRange.attributes)) {
    if (key.startsWith('X-')) {
      if (typeof dateRange.attributes[key] === 'number') {
        attrs.push(`${key}=${dateRange.attributes[key]}`);
      } else {
        attrs.push(`${key}="${dateRange.attributes[key]}"`);
      }
    } else if (key.startsWith('SCTE35-')) {
      attrs.push(`${key}=${utils.byteSequenceToHex(dateRange.attributes[key])}`);
    }
  }
  return `#EXT-X-DATERANGE:${attrs.join(',')}`;
}

function buildMarkers(lines: LineArray, markers: SpliceInfo[]) {
  let type = '';
  for (const marker of markers) {
    if (marker.type === 'OUT') {
      type = 'OUT';
      lines.push(`#EXT-X-CUE-OUT:DURATION=${marker.duration}`);
    } else if (marker.type === 'IN') {
      type = 'IN';
      lines.push('#EXT-X-CUE-IN');
    } else if (marker.type === 'RAW') {
      const value = marker.value ? `:${marker.value}` : '';
      lines.push(`#${marker.tagName}${value}`);
    }
  }
  return type;
}

function buildParts(lines: LineArray, parts: PartialSegment[]) {
  let hint = false;
  for (const part of parts) {
    if (part.hint) {
      const params: string[] = [];
      params.push('TYPE=PART', `URI="${part.uri}"`);
      if (part.byterange) {
        const {offset, length} = part.byterange;
        params.push(`BYTERANGE-START=${offset}`);
        if (length) {
          params.push(`BYTERANGE-LENGTH=${length}`);
        }
      }
      lines.push(`#EXT-X-PRELOAD-HINT:${params.join(',')}`);
      hint = true;
    } else {
      const params: string[] = [];
      params.push(`DURATION=${part.duration}`, `URI="${part.uri}"`);
      if (part.byterange) {
        params.push(`BYTERANGE=${buildByteRange(part.byterange)}`);
      }
      if (part.independent) {
        params.push('INDEPENDENT=YES');
      }
      if (part.gap) {
        params.push('GAP=YES');
      }
      lines.push(`#EXT-X-PART:${params.join(',')}`);
    }
  }
  return hint;
}

function buildDefines(define: Record<string, string>) {
  const attrs: string[] = [];
  for (const attr in define) {
    attrs.push(`${attr}="${define[attr]}"`);
  }
  return `#EXT-X-DEFINE:${attrs.join(',')}`;
}

function stringify(playlist: MasterPlaylist | MediaPlaylist, postProcess?: PostProcess): string {
  utils.PARAMCHECK(playlist);
  utils.ASSERT('Not a playlist', playlist.type === 'playlist');
  const lines = new LineArray(playlist.uri);
  lines.push('#EXTM3U');
  if (playlist.version) {
    lines.push(`#EXT-X-VERSION:${playlist.version}`);
  }
  if (playlist.independentSegments) {
    lines.push('#EXT-X-INDEPENDENT-SEGMENTS');
  }
  if (playlist.start) {
    lines.push(`#EXT-X-START:TIME-OFFSET=${buildDecimalFloatingNumber(playlist.start.offset)}${playlist.start.precise ? ',PRECISE=YES' : ''}`);
  }
  if (playlist.defines) {
    for (const session of playlist.defines) {
      lines.push(buildDefines(session));
    }
  }
  if (playlist.isMasterPlaylist) {
    buildMasterPlaylist(lines, playlist, postProcess);
  } else {
    buildMediaPlaylist(lines, playlist, postProcess);
  }
  // console.log('<<<');
  // console.log(lines.join('\n'));
  // console.log('>>>');
  return lines.join('\n');
}

export default stringify;


================================================
FILE: test/fixtures/m3u8/8.1-Simple-Media-Playlist.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST


================================================
FILE: test/fixtures/m3u8/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.m3u8
================================================
# This example shows two EXT-X-DATERANGE tags that describe a single
# Date Range, with a SCTE-35 "out" splice_insert() command that is
# subsequently updated with an SCTE-35 "in" splice_insert() command.

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:30

#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
#EXTINF:30,
http://media.example.com/01.ts
#EXTINF:30,
http://media.example.com/02.ts
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000
#EXTINF:30,
http://ads.example.com/ad-01.ts
#EXTINF:30,
http://ads.example.com/ad-02.ts
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000
#EXTINF:30,
http://media.example.com/03.ts
#EXTINF:3.003,
http://media.example.com/04.ts


================================================
FILE: test/fixtures/m3u8/8.11-EXT-X-CUE-OUT-Media-Playlist.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXTINF:9.009,
http://media.example.com/first.ts
#EXT-X-CUE-OUT:DURATION=15
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST


================================================
FILE: test/fixtures/m3u8/8.2-Live-Media-Playlist_using-HTTPS.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:2680

#EXTINF:7.975,
https://priv.example.com/fileSequence2680.ts
#EXTINF:7.941,
https://priv.example.com/fileSequence2681.ts
#EXTINF:7.975,
https://priv.example.com/fileSequence2682.ts


================================================
FILE: test/fixtures/m3u8/8.3-Playlist-with-encrypted-Media-Segments.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:15
#EXT-X-MEDIA-SEQUENCE:7794

#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,
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,
http://media.example.com/fileSequence53-A.ts


================================================
FILE: test/fixtures/m3u8/8.4-Master-Playlist.m3u8
================================================
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000,CODECS="avc1.640029,mp4a.40.2"
http://example.com/low.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000,CODECS="avc1.640029,mp4a.40.2"
http://example.com/mid.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000,CODECS="avc1.640029,mp4a.40.2"
http://example.com/hi.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
http://example.com/audio-only.m3u8


================================================
FILE: test/fixtures/m3u8/8.5-Master-Playlist-with-I-Frames.m3u8
================================================
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="avc1.640029,mp4a.40.2"
low/audio-video.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8",CODECS="avc1.640029"
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="avc1.640029,mp4a.40.2"
mid/audio-video.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8",CODECS="avc1.640029"
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="avc1.640029,mp4a.40.2"
hi/audio-video.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8",CODECS="avc1.640029"
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
audio-only.m3u8


================================================
FILE: test/fixtures/m3u8/8.6-Master-Playlist-with-Alternative-audio.m3u8
================================================
# In this example, the CODECS attributes have been condensed for space.
# A '\' is used to indicate that the tag continues on the following
# line with whitespace removed:

#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="main/english-audio.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de",URI="main/german-audio.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en",URI="commentary/audio-only.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="mp4a.40.2",AUDIO="aac"
low/video-only.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="mp4a.40.2",AUDIO="aac"
mid/video-only.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="mp4a.40.2",AUDIO="aac"
hi/video-only.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac"
main/english-audio.m3u8


================================================
FILE: test/fixtures/m3u8/8.7-Master-Playlist-with-Alternative-video.m3u8
================================================
# This example shows 3 different video Renditions (Main, Centerfield
# and Dugout), and 3 different Variant Streams (low, mid and high).  In
# this example, clients that did not support the EXT-X-MEDIA tag and
# the VIDEO attribute of the EXT-X-STREAM-INF tag would only be able to
# play the video Rendition "Main".
# Since the EXT-X-STREAM-INF tag has no AUDIO attribute, all video
# Renditions would be required to contain the audio.
#
# In this example, the CODECS attributes have been condensed for space.
# A '\' is used to indicate that the tag continues on the following
# line with whitespace removed:

#EXTM3U
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="avc1.640029,mp4a.40.2",VIDEO="low"
low/main/audio-video.m3u8

#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=NO,URI="mid/main/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=YES,URI="mid/centerfield/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="avc1.640029,mp4a.40.2",VIDEO="mid"
mid/main/audio-video.m3u8

#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="avc1.640029,mp4a.40.2",VIDEO="hi"
hi/main/audio-video.m3u8


================================================
FILE: test/fixtures/m3u8/8.8-Session-Data-in-a-Master-Playlist.m3u8
================================================
# In this example, only the EXT-X-SESSION-DATA is shown:
#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="es",VALUE="Este es un ejemplo"


================================================
FILE: test/fixtures/m3u8/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.m3u8
================================================
# In this example, the CODECS attributes have been condensed for space.
# A '\' is used to indicate that the tag continues on the following
# line with whitespace removed:

#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.easy-to-read",URI="main/english-audio.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.easy-to-read",URI="main/german-audio.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.easy-to-read",URI="commentary/audio-only.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="mp4a.40.2",AUDIO="aac"
low/video-only.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="mp4a.40.2",AUDIO="aac"
mid/video-only.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="mp4a.40.2",AUDIO="aac"
hi/video-only.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac"
main/english-audio.m3u8


================================================
FILE: test/fixtures/m3u8/Low-Latency_Example-01_Low-Latency_HLS_Playlist.m3u8
================================================
#EXTM3U
# This Playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_msn=273&_HLS_part=2
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:4
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1
#EXT-X-PART-INF:PART-TARGET=0.33334
#EXT-X-MEDIA-SEQUENCE:266
#EXT-X-MAP:URI="init.mp4"
#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z
#EXTINF:4.00008,
fileSequence266.mp4
#EXTINF:4.00008,
fileSequence267.mp4
#EXTINF:4.00008,
fileSequence268.mp4
#EXTINF:4.00008,
fileSequence269.mp4
#EXTINF:4.00008,
fileSequence270.mp4
#EXT-X-PART:DURATION=0.33334,URI="filePart271.0.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.1.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.2.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.3.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.4.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart271.5.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.6.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.7.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4"
#EXTINF:4.00008,
fileSequence271.mp4
#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z
#EXT-X-PART:DURATION=0.33334,URI="filePart272.a.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.b.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.c.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.d.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.e.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.f.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart272.g.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.h.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.i.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.j.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.k.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.l.mp4"
#EXTINF:4.00008,
fileSequence272.mp4
#EXT-X-PART:DURATION=0.33334,URI="filePart273.0.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.3.mp4"

#EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=2
#EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=1


================================================
FILE: test/fixtures/m3u8/Low-Latency_Example-02_Playlist_Delta_Update.m3u8
================================================
#EXTM3U
# Following the example above, this Playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_msn=273&_HLS_part=3 &_HLS_skip=YES
#EXT-X-VERSION:9
#EXT-X-TARGETDURATION:4
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1
#EXT-X-PART-INF:PART-TARGET=0.33334
#EXT-X-MEDIA-SEQUENCE:266
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
#EXTINF:4.00008,
fileSequence269.mp4
#EXTINF:4.00008,
fileSequence270.mp4
#EXT-X-PART:DURATION=0.33334,URI="filePart271.0.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.1.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.2.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.3.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.4.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart271.5.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.6.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.7.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4"
#EXTINF:4.00008,
fileSequence271.mp4
#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z
#EXT-X-PART:DURATION=0.33334,URI="filePart272.a.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.b.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.c.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.d.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.e.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.f.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart272.g.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.h.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.i.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.j.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.k.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart272.l.mp4"
#EXTINF:4.00008,
fileSequence272.mp4
#EXT-X-PART:DURATION=0.33334,URI="filePart273.0.mp4",INDEPENDENT=YES
#EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4"
#EXT-X-PART:DURATION=0.33334,URI="filePart273.3.mp4"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.4.mp4"

#EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=3
#EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=3


================================================
FILE: test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-01.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:9
#EXT-X-TARGETDURATION:4
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1.02
#EXT-X-PART-INF:PART-TARGET=1.02
#EXT-X-MEDIA-SEQUENCE:266
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
#EXTINF:4.00008,
fileSequence269.mp4
#EXTINF:4.00008,
fileSequence270.mp4
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=20000@0
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=23000@20000
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=18000@43000
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence271.mp4",BYTERANGE-START=61000


================================================
FILE: test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-02.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:9
#EXT-X-TARGETDURATION:4
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1.02
#EXT-X-PART-INF:PART-TARGET=1.02
#EXT-X-MEDIA-SEQUENCE:266
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
#EXTINF:4.00008,
fileSequence269.mp4
#EXTINF:4.00008,
fileSequence270.mp4
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=20000@0
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=23000@20000
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=18000@43000
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=19000@61000
#EXTINF:4.00008,
fileSequence271.mp4
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence272.mp4",BYTERANGE-START=0


================================================
FILE: test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-03.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:9
#EXT-X-TARGETDURATION:4
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1.02
#EXT-X-PART-INF:PART-TARGET=1.02
#EXT-X-MEDIA-SEQUENCE:266
#EXT-X-SKIP:SKIPPED-SEGMENTS=3
#EXTINF:4.00008,
fileSequence269.mp4
#EXTINF:4.00008,
fileSequence270.mp4
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=20000@0
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=23000@20000
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=18000@43000
#EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=19000@61000
#EXTINF:4.00008,
fileSequence271.mp4
#EXT-X-PART:DURATION=1.02,URI="fileSequence272.mp4",BYTERANGE=21000@0
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence272.mp4",BYTERANGE-START=21000


================================================
FILE: test/fixtures/m3u8/Multiple-rendition-groups.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="English",DEFAULT=YES,URI="aac_high_eng.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="Japanese",DEFAULT=NO,URI="aac_high_jp.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO="aac_high"
1080p.m3u8
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="English",DEFAULT=YES,URI="aac_mid_eng.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="Japanese",DEFAULT=NO,URI="aac_mid_jp.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO="aac_mid"
720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO="aac_mid"
540p.m3u8
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="English",DEFAULT=YES,URI="aac_low_eng.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="Japanese",DEFAULT=NO,URI="aac_low_jp.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO="aac_low"
360p.m3u8

================================================
FILE: test/fixtures/m3u8/RedundantSegments.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-TARGETDURATION:10
#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:9.009,
#EXT-X-BYTERANGE:128@256
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST


================================================
FILE: test/fixtures/m3u8/SCTE-35_01.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:8.008,
1.ts
#EXT-X-CUE-OUT:DURATION=15
#EXTINF:8,
2.ts
#EXTINF:7,
3.ts
#EXT-X-CUE-IN
#EXTINF:8.008,
4.ts
#EXTINF:8.008,
5.ts
#EXT-X-ENDLIST


================================================
FILE: test/fixtures/m3u8/SCTE-35_02.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:8.008,
1.ts
#EXT-X-CUE-OUT:DURATION=23
#EXTINF:8,
2.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23
#EXTINF:8,
3.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23
#EXTINF:7,
4.ts
#EXT-X-CUE-IN
#EXTINF:8.008,
5.ts
#EXT-X-ENDLIST


================================================
FILE: test/fixtures/m3u8/SCTE-35_03.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:8.008,
1.ts
#EXT-X-CUE:DURATION="15.0",ID="0",TYPE="SpliceOut",TIME="414.171"
#EXTINF:8,
2.ts
#EXTINF:7,
3.ts
#EXTINF:8.008,
4.ts
#EXTINF:8.008,
5.ts
#EXT-X-ENDLIST


================================================
FILE: test/fixtures/m3u8/SCTE-35_04.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:8.008,
1.ts
#EXT-OATCLS-SCTE35:/DA0AAAAAAAAAAAABQb+ADAQ6QAeAhxDVUVJQAAAO3/PAAEUrEoICAAAAAAg+2UBNAAANvrtoQ==
#EXT-X-ASSET:CAID=0x0000000020FB6501
#EXT-X-CUE-OUT:DURATION=15
#EXTINF:8,
2.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=5.939,Duration=25.0,SCTE35=/DA0AAAA+…AAg+2UBNAAANvrtoQ==
#EXTINF:7,
3.ts
#EXT-X-CUE-IN
#EXTINF:8.008,
4.ts
#EXTINF:8.008,
5.ts
#EXT-X-ENDLIST


================================================
FILE: test/fixtures/m3u8/SCTE-35_05.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:8.008,
1.ts
#EXT-X-SCTE35:TYPE=0x34,DURATION=15.0,CUE-OUT=YES,UPID="0x08:0x9425BC",CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”pIViS5”
#EXTINF:8,
2.ts
#EXTINF:7,
3.ts
#EXT-X-SCTE35:TYPE=0x35,CUE-IN=YES,CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”f6UrRd”
#EXTINF:8.008,
4.ts
#EXTINF:8.008,
5.ts
#EXT-X-ENDLIST


================================================
FILE: test/fixtures/m3u8/SCTE-35_06.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:8.008,
1.ts
#EXT-X-CUE-OUT:DURATION=23
#EXTINF:8,
2.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23
#EXTINF:8,
3.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23
#EXTINF:7,
4.ts
#EXT-X-CUE-IN
#EXTINF:8.008,
5.ts
#EXT-X-CUE-OUT:DURATION=23
#EXTINF:8,
6.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23
#EXTINF:8,
7.ts
#EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23
#EXTINF:7,
8.ts
#EXT-X-CUE-IN
#EXTINF:8.008,
9.ts
#EXT-X-ENDLIST


================================================
FILE: test/fixtures/m3u8/SCTE-35_07.m3u8
================================================
# This example shows two EXT-X-DATERANGE tags that describe a single
# Date Range, with a SCTE-35 "out" splice_insert() command that is
# subsequently updated with an SCTE-35 "in" splice_insert() command.

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:30

#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z
#EXTINF:30,
http://media.example.com/01.ts
#EXTINF:30,
http://media.example.com/02.ts
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2023-10-09T06:16:00.820Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC30250001D1F7E25300FFF0140565239AA07FEFFE015C3F90FE00526362000101010000A7C1792D
#EXTINF:30,
http://ads.example.com/ad-01.ts
#EXTINF:30,
http://ads.example.com/ad-02.ts
#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2023-10-09T06:16:00.820Z",END-DATE="2023-10-09T06:17:01.514Z",DURATION=60.694
#EXTINF:30,
http://media.example.com/03.ts
#EXTINF:3.003,
http://media.example.com/04.ts


================================================
FILE: test/fixtures/m3u8/Streaming-Examples_bipbop_16x9_variant.m3u8
================================================
#EXTM3U

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",NAME="BipBop Audio 1",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="eng"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",NAME="BipBop Audio 2",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="eng",URI="alternate_audio_aac/prog_index.m3u8"


#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/eng/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="en",URI="subtitles/eng_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/fra/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="fr",URI="subtitles/fra_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/spa/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="es",URI="subtitles/spa_forced/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/jpn/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語 (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="ja",URI="subtitles/jpn_forced/prog_index.m3u8"


#EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS="mp4a.40.2, avc1.4d400d",RESOLUTION=416x234,AUDIO="bipbop_audio",SUBTITLES="subs"
gear1/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=28451,URI="gear1/iframe_index.m3u8",CODECS="avc1.4d400d"

#EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs"
gear2/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=181534,URI="gear2/iframe_index.m3u8",CODECS="avc1.4d401e"

#EXT-X-STREAM-INF:BANDWIDTH=915905,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=960x540,AUDIO="bipbop_audio",SUBTITLES="subs"
gear3/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=297056,URI="gear3/iframe_index.m3u8",CODECS="avc1.4d401f"

#EXT-X-STREAM-INF:BANDWIDTH=1030138,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1280x720,AUDIO="bipbop_audio",SUBTITLES="subs"
gear4/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=339492,URI="gear4/iframe_index.m3u8",CODECS="avc1.4d401f"

#EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs"
gear5/prog_index.m3u8
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=669554,URI="gear5/iframe_index.m3u8",CODECS="avc1.4d401f"

#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS="mp4a.40.2",AUDIO="bipbop_audio",SUBTITLES="subs"
gear0/prog_index.m3u8


================================================
FILE: test/fixtures/m3u8/Streaming-Examples_img_bipbop_adv_example_ts_master.m3u8
================================================
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="2",URI="a1/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",URI="s1/en/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc1",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",INSTREAM-ID="CC1"

#EXT-X-STREAM-INF:BANDWIDTH=2227464,AVERAGE-BANDWIDTH=2218327,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v5/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=8178040,AVERAGE-BANDWIDTH=8144656,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v9/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=6453202,AVERAGE-BANDWIDTH=6307144,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v8/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5054232,AVERAGE-BANDWIDTH=4775338,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v7/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3289288,AVERAGE-BANDWIDTH=3240596,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v6/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1296989,AVERAGE-BANDWIDTH=1292926,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=768x432,FRAME-RATE=30.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v4/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=922242,AVERAGE-BANDWIDTH=914722,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=30.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v3/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=553010,AVERAGE-BANDWIDTH=541239,CODECS="avc1.640015,mp4a.40.2",RESOLUTION=480x270,FRAME-RATE=30.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v2/prog_index.m3u8

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="6",URI="a2/prog_index.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=2448841,AVERAGE-BANDWIDTH=2439704,CODECS="avc1.640020,ac-3",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v5/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=8399417,AVERAGE-BANDWIDTH=8366033,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v9/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=6674579,AVERAGE-BANDWIDTH=6528521,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v8/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5275609,AVERAGE-BANDWIDTH=4996715,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v7/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3510665,AVERAGE-BANDWIDTH=3461973,CODECS="avc1.640020,ac-3",RESOLUTION=1280x720,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v6/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1518366,AVERAGE-BANDWIDTH=1514303,CODECS="avc1.64001e,ac-3",RESOLUTION=768x432,FRAME-RATE=30.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v4/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1143619,AVERAGE-BANDWIDTH=1136099,CODECS="avc1.64001e,ac-3",RESOLUTION=640x360,FRAME-RATE=30.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v3/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=774387,AVERAGE-BANDWIDTH=762616,CODECS="avc1.640015,ac-3",RESOLUTION=480x270,FRAME-RATE=30.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v2/prog_index.m3u8

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud3",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="6",URI="a3/prog_index.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=2256841,AVERAGE-BANDWIDTH=2247704,CODECS="avc1.640020,ec-3",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v5/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=8207417,AVERAGE-BANDWIDTH=8174033,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v9/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=6482579,AVERAGE-BANDWIDTH=6336521,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v8/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5083609,AVERAGE-BANDWIDTH=4804715,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v7/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3318665,AVERAGE-BANDWIDTH=3269973,CODECS="avc1.640020,ec-3",RESOLUTION=1280x720,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v6/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1326366,AVERAGE-BANDWIDTH=1322303,CODECS="avc1.64001e,ec-3",RESOLUTION=768x432,FRAME-RATE=30.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v4/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=951619,AVERAGE-BANDWIDTH=944099,CODECS="avc1.64001e,ec-3",RESOLUTION=640x360,FRAME-RATE=30.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v3/prog_index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=582387,AVERAGE-BANDWIDTH=570616,CODECS="avc1.640015,ec-3",RESOLUTION=480x270,FRAME-RATE=30.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1"
v2/prog_index.m3u8


#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=186522,AVERAGE-BANDWIDTH=182077,URI="v7/iframe_index.m3u8",CODECS="avc1.64002a",RESOLUTION=1920x1080
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=133856,AVERAGE-BANDWIDTH=129936,URI="v6/iframe_index.m3u8",CODECS="avc1.640020",RESOLUTION=1280x720
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=98136,AVERAGE-BANDWIDTH=94286,URI="v5/iframe_index.m3u8",CODECS="avc1.640020",RESOLUTION=960x540
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=76704,AVERAGE-BANDWIDTH=74767,URI="v4/iframe_index.m3u8",CODECS="avc1.64001e",RESOLUTION=768x432
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=64078,AVERAGE-BANDWIDTH=62251,URI="v3/iframe_index.m3u8",CODECS="avc1.64001e",RESOLUTION=640x360
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=38728,AVERAGE-BANDWIDTH=37866,URI="v2/iframe_index.m3u8",CODECS="avc1.640015",RESOLUTION=480x270


================================================
FILE: test/fixtures/objects/8.1-Simple-Media-Playlist.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const playlist = new MediaPlaylist({
  version: 3,
  targetDuration: 10,
  segments: createSegments(),
  endlist: true
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'http://media.example.com/first.ts',
    duration: 9.009,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/second.ts',
    duration: 9.009,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/third.ts',
    duration: 3.003,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js
================================================
const {MediaPlaylist, Segment, DateRange} = require('../../../types');
const utils = require('../../../utils');

const playlist = new MediaPlaylist({
  version: 3,
  targetDuration: 30,
  segments: createSegments()
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'http://media.example.com/01.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0,
    programDateTime: new Date('2014-03-05T11:14:00Z')
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/02.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://ads.example.com/ad-01.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0,
    dateRange: new DateRange({
      id: 'splice-6FFFFFF0',
      start: new Date('2014-03-05T11:15:00Z'),
      plannedDuration: 59.993,
      attributes: {
        'SCTE35-OUT': utils.hexToByteSequence('0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000')
      }
    })
  }));
  segments.push(new Segment({
    uri: 'http://ads.example.com/ad-02.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 3,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/03.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 4,
    discontinuitySequence: 0,
    dateRange: new DateRange({
      id: 'splice-6FFFFFF0',
      duration: 59.993,
      attributes: {
        'SCTE35-IN': utils.hexToByteSequence('0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000')
      }
    })
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/04.ts',
    duration: 3.003,
    title: '',
    mediaSequenceNumber: 5,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.11-EXT-X-CUE-OUT-Media-Playlist.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const playlist = new MediaPlaylist({
  version: 3,
  targetDuration: 10,
  segments: createSegments(),
  endlist: true
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'http://media.example.com/first.ts',
    duration: 9.009,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/second.ts',
    duration: 9.009,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0,
    markers: [{
      type: 'OUT',
      duration: 15
    }]
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/third.ts',
    duration: 3.003,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.2-Live-Media-Playlist_using-HTTPS.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const mediaSequenceBase = 2680;

const playlist = new MediaPlaylist({
  version: 3,
  targetDuration: 8,
  mediaSequenceBase,
  segments: createSegments()
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'https://priv.example.com/fileSequence2680.ts',
    duration: 7.975,
    title: '',
    mediaSequenceNumber: mediaSequenceBase + 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'https://priv.example.com/fileSequence2681.ts',
    duration: 7.941,
    title: '',
    mediaSequenceNumber: mediaSequenceBase + 1,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'https://priv.example.com/fileSequence2682.ts',
    duration: 7.975,
    title: '',
    mediaSequenceNumber: mediaSequenceBase + 2,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.3-Playlist-with-encrypted-Media-Segments.js
================================================
const {MediaPlaylist, Segment, Key} = require('../../../types');

const mediaSequenceBase = 7794;

const key1 = new Key({method: 'AES-128', uri: 'https://priv.example.com/key.php?r=52'});
const key2 = new Key({method: 'AES-128', uri: 'https://priv.example.com/key.php?r=53'});

const playlist = new MediaPlaylist({
  version: 3,
  targetDuration: 15,
  mediaSequenceBase,
  segments: createSegments()
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'http://media.example.com/fileSequence52-A.ts',
    duration: 2.833,
    title: '',
    mediaSequenceNumber: mediaSequenceBase + 0,
    discontinuitySequence: 0,
    key: key1
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/fileSequence52-B.ts',
    duration: 15.0,
    title: '',
    mediaSequenceNumber: mediaSequenceBase + 1,
    discontinuitySequence: 0,
    key: key1
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/fileSequence52-C.ts',
    duration: 13.333,
    title: '',
    mediaSequenceNumber: mediaSequenceBase + 2,
    discontinuitySequence: 0,
    key: key1
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/fileSequence53-A.ts',
    duration: 15.0,
    title: '',
    mediaSequenceNumber: mediaSequenceBase + 3,
    discontinuitySequence: 0,
    key: key2
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.4-Master-Playlist.js
================================================
const {MasterPlaylist, Variant} = require('../../../types');

const playlist = new MasterPlaylist({
  variants: createVariants()
});

function createVariants() {
  const variants = [];
  variants.push(new Variant({
    uri: 'http://example.com/low.m3u8',
    bandwidth: 1280000,
    averageBandwidth: 1000000,
    codecs: 'avc1.640029,mp4a.40.2'
  }));
  variants.push(new Variant({
    uri: 'http://example.com/mid.m3u8',
    bandwidth: 2560000,
    averageBandwidth: 2000000,
    codecs: 'avc1.640029,mp4a.40.2'
  }));
  variants.push(new Variant({
    uri: 'http://example.com/hi.m3u8',
    bandwidth: 7680000,
    averageBandwidth: 6000000,
    codecs: 'avc1.640029,mp4a.40.2'
  }));
  variants.push(new Variant({
    uri: 'http://example.com/audio-only.m3u8',
    bandwidth: 65000,
    codecs: 'mp4a.40.5'
  }));
  return variants;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.5-Master-Playlist-with-I-Frames.js
================================================
const {MasterPlaylist, Variant} = require('../../../types');

const playlist = new MasterPlaylist({
  variants: createVariants()
});

function createVariants() {
  const variants = [];
  variants.push(new Variant({
    uri: 'low/audio-video.m3u8',
    bandwidth: 1280000,
    codecs: 'avc1.640029,mp4a.40.2'
  }));
  variants.push(new Variant({
    uri: 'low/iframe.m3u8',
    isIFrameOnly: true,
    bandwidth: 86000,
    codecs: 'avc1.640029'
  }));
  variants.push(new Variant({
    uri: 'mid/audio-video.m3u8',
    bandwidth: 2560000,
    codecs: 'avc1.640029,mp4a.40.2'
  }));
  variants.push(new Variant({
    uri: 'mid/iframe.m3u8',
    isIFrameOnly: true,
    bandwidth: 150000,
    codecs: 'avc1.640029'
  }));
  variants.push(new Variant({
    uri: 'hi/audio-video.m3u8',
    bandwidth: 7680000,
    codecs: 'avc1.640029,mp4a.40.2'
  }));
  variants.push(new Variant({
    uri: 'hi/iframe.m3u8',
    isIFrameOnly: true,
    bandwidth: 550000,
    codecs: 'avc1.640029'
  }));
  variants.push(new Variant({
    uri: 'audio-only.m3u8',
    bandwidth: 65000,
    codecs: 'mp4a.40.5'
  }));
  return variants;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.6-Master-Playlist-with-Alternative-audio.js
================================================
const {MasterPlaylist, Variant, Rendition} = require('../../../types');

const renditions = createRendition();

function createRendition() {
  const renditions = [];
  renditions.push(new Rendition({
    type: 'AUDIO',
    uri: 'main/english-audio.m3u8',
    groupId: 'aac',
    language: 'en',
    name: 'English',
    isDefault: true,
    autoselect: true
  }));
  renditions.push(new Rendition({
    type: 'AUDIO',
    uri: 'main/german-audio.m3u8',
    groupId: 'aac',
    language: 'de',
    name: 'Deutsch',
    isDefault: false,
    autoselect: true
  }));
  renditions.push(new Rendition({
    type: 'AUDIO',
    uri: 'commentary/audio-only.m3u8',
    groupId: 'aac',
    language: 'en',
    name: 'Commentary',
    isDefault: false,
    autoselect: false
  }));
  return renditions;
}

const playlist = new MasterPlaylist({
  variants: createVariants()
});

function createVariants() {
  const variants = [];
  variants.push(new Variant({
    uri: 'low/video-only.m3u8',
    bandwidth: 1280000,
    codecs: 'mp4a.40.2',
    audio: renditions,
    currentRenditions: {audio: 0}
  }));
  variants.push(new Variant({
    uri: 'mid/video-only.m3u8',
    bandwidth: 2560000,
    codecs: 'mp4a.40.2',
    audio: renditions,
    currentRenditions: {audio: 0}
  }));
  variants.push(new Variant({
    uri: 'hi/video-only.m3u8',
    bandwidth: 7680000,
    codecs: 'mp4a.40.2',
    audio: renditions,
    currentRenditions: {audio: 0}
  }));
  variants.push(new Variant({
    uri: 'main/english-audio.m3u8',
    bandwidth: 65000,
    codecs: 'mp4a.40.5',
    audio: renditions,
    currentRenditions: {audio: 0}
  }));
  return variants;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.7-Master-Playlist-with-Alternative-video.js
================================================
const {MasterPlaylist, Variant, Rendition} = require('../../../types');

function createRendition(groupId) {
  const renditions = [];
  renditions.push(new Rendition({
    type: 'VIDEO',
    uri: `${groupId}/main/audio-video.m3u8`,
    groupId,
    name: 'Main',
    isDefault: !(groupId === 'mid')
  }));
  renditions.push(new Rendition({
    type: 'VIDEO',
    uri: `${groupId}/centerfield/audio-video.m3u8`,
    groupId,
    name: 'Centerfield',
    isDefault: groupId === 'mid'
  }));
  renditions.push(new Rendition({
    type: 'VIDEO',
    uri: `${groupId}/dugout/audio-video.m3u8`,
    groupId,
    name: 'Dugout',
    isDefault: false
  }));
  return renditions;
}

const playlist = new MasterPlaylist({
  variants: createVariants()
});

function createVariants() {
  const variants = [];
  variants.push(new Variant({
    uri: 'low/main/audio-video.m3u8',
    bandwidth: 1280000,
    codecs: 'avc1.640029,mp4a.40.2',
    video: createRendition('low'),
    currentRenditions: {video: 0}
  }));
  variants.push(new Variant({
    uri: 'mid/main/audio-video.m3u8',
    bandwidth: 2560000,
    codecs: 'avc1.640029,mp4a.40.2',
    video: createRendition('mid'),
    currentRenditions: {video: 1}
  }));
  variants.push(new Variant({
    uri: 'hi/main/audio-video.m3u8',
    bandwidth: 7680000,
    codecs: 'avc1.640029,mp4a.40.2',
    video: createRendition('hi')
  }));
  return variants;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.8-Session-Data-in-a-Master-Playlist.js
================================================
const {MasterPlaylist, SessionData} = require('../../../types');

const playlist = new MasterPlaylist({
  sessionDataList: createSetssionDataList()
});

function createSetssionDataList() {
  const setssionDataList = [];
  setssionDataList.push(new SessionData({
    id: 'com.example.lyrics',
    uri: 'lyrics.json'
  }));
  setssionDataList.push(new SessionData({
    id: 'com.example.title',
    language: 'en',
    value: 'This is an example'
  }));
  setssionDataList.push(new SessionData({
    id: 'com.example.title',
    language: 'es',
    value: 'Este es un ejemplo'
  }));
  return setssionDataList;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js
================================================
const {MasterPlaylist, Variant, Rendition} = require('../../../types');

const renditions = createRendition();

function createRendition() {
  const renditions = [];
  renditions.push(new Rendition({
    type: 'AUDIO',
    uri: 'main/english-audio.m3u8',
    groupId: 'aac',
    language: 'en',
    name: 'English',
    isDefault: true,
    autoselect: true,
    characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read'
  }));
  renditions.push(new Rendition({
    type: 'AUDIO',
    uri: 'main/german-audio.m3u8',
    groupId: 'aac',
    language: 'de',
    name: 'Deutsch',
    isDefault: false,
    autoselect: true,
    characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read'
  }));
  renditions.push(new Rendition({
    type: 'AUDIO',
    uri: 'commentary/audio-only.m3u8',
    groupId: 'aac',
    language: 'en',
    name: 'Commentary',
    isDefault: false,
    autoselect: false,
    characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read'
  }));
  return renditions;
}

const playlist = new MasterPlaylist({
  variants: createVariants()
});

function createVariants() {
  const variants = [];
  variants.push(new Variant({
    uri: 'low/video-only.m3u8',
    bandwidth: 1280000,
    codecs: 'mp4a.40.2',
    audio: renditions,
    currentRenditions: {audio: 0}
  }));
  variants.push(new Variant({
    uri: 'mid/video-only.m3u8',
    bandwidth: 2560000,
    codecs: 'mp4a.40.2',
    audio: renditions,
    currentRenditions: {audio: 0}
  }));
  variants.push(new Variant({
    uri: 'hi/video-only.m3u8',
    bandwidth: 7680000,
    codecs: 'mp4a.40.2',
    audio: renditions,
    currentRenditions: {audio: 0}
  }));
  variants.push(new Variant({
    uri: 'main/english-audio.m3u8',
    bandwidth: 65000,
    codecs: 'mp4a.40.5',
    audio: renditions,
    currentRenditions: {audio: 0}
  }));
  return variants;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/Low-Latency_Example-01_Low-Latency_HLS_Playlist.js
================================================
const {MediaPlaylist, Segment, PartialSegment, MediaInitializationSection, RenditionReport} = require('../../../types');

const playlist = new MediaPlaylist({
  version: 6,
  targetDuration: 4,
  mediaSequenceBase: 266,
  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24, partHoldBack: 1},
  partTargetDuration: 0.33334,
  segments: createSegments(),
  renditionReports: createRenditionReports()
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'fileSequence266.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 266,
    discontinuitySequence: 0,
    programDateTime: new Date('2019-02-14T02:13:36.106Z'),
    map: new MediaInitializationSection({uri: 'init.mp4'})
  }));
  segments.push(new Segment({
    uri: 'fileSequence267.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 267,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence268.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 268,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence269.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 269,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence270.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 270,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence271.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 271,
    discontinuitySequence: 0,
    parts: createParts1()
  }));
  segments.push(new Segment({
    uri: 'fileSequence272.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 272,
    discontinuitySequence: 0,
    programDateTime: new Date('2019-02-14T02:14:00.106Z'),
    parts: createParts2()
  }));
  segments.push(new Segment({
    mediaSequenceNumber: 273,
    parts: createParts3()
  }));
  return segments;
}

function createRenditionReports() {
  const reports = [];
  reports.push(new RenditionReport({
    uri: '../1M/waitForMSN.php',
    lastMSN: 273,
    lastPart: 2
  }));
  reports.push(new RenditionReport({
    uri: '../4M/waitForMSN.php',
    lastMSN: 273,
    lastPart: 1
  }));
  return reports;
}

function createParts1() {
  const parts = [];
  for (let i = 0; i < 12; i++) {
    parts.push(new PartialSegment({
      uri: `filePart271.${i}.mp4`,
      duration: 0.33334,
      independent: (i === 4 || i === 8)
    }));
  }
  return parts;
}

function createParts2() {
  const parts = [];
  const aCode = 'a'.charCodeAt(0);
  for (let i = 0; i < 12; i++) {
    parts.push(new PartialSegment({
      uri: `filePart272.${String.fromCharCode(aCode + i)}.mp4`,
      duration: 0.33334,
      independent: (i === 5)
    }));
  }
  return parts;
}

function createParts3() {
  const parts = [];
  for (let i = 0; i < 4; i++) {
    parts.push(new PartialSegment({
      uri: `filePart273.${i}.mp4`,
      duration: 0.33334,
      independent: (i === 0),
      hint: (i === 3)
    }));
  }
  return parts;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/Low-Latency_Example-02_Playlist_Delta_Update.js
================================================
const {MediaPlaylist, Segment, PartialSegment, RenditionReport} = require('../../../types');

const playlist = new MediaPlaylist({
  version: 9,
  targetDuration: 4,
  mediaSequenceBase: 266,
  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.0},
  partTargetDuration: 0.33334,
  skip: 3,
  segments: createSegments(),
  renditionReports: createRenditionReports()
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'fileSequence269.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 269,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence270.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 270,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence271.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 271,
    discontinuitySequence: 0,
    parts: createParts1()
  }));
  segments.push(new Segment({
    uri: 'fileSequence272.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 272,
    discontinuitySequence: 0,
    programDateTime: new Date('2019-02-14T02:14:00.106Z'),
    parts: createParts2()
  }));
  segments.push(new Segment({
    mediaSequenceNumber: 273,
    parts: createParts3()
  }));
  return segments;
}

function createRenditionReports() {
  const reports = [];
  reports.push(new RenditionReport({
    uri: '../1M/waitForMSN.php',
    lastMSN: 273,
    lastPart: 3
  }));
  reports.push(new RenditionReport({
    uri: '../4M/waitForMSN.php',
    lastMSN: 273,
    lastPart: 3
  }));
  return reports;
}

function createParts1() {
  const parts = [];
  for (let i = 0; i < 12; i++) {
    parts.push(new PartialSegment({
      uri: `filePart271.${i}.mp4`,
      duration: 0.33334,
      independent: (i === 4 || i === 8)
    }));
  }
  return parts;
}

function createParts2() {
  const parts = [];
  const aCode = 'a'.charCodeAt(0);
  for (let i = 0; i < 12; i++) {
    parts.push(new PartialSegment({
      uri: `filePart272.${String.fromCharCode(aCode + i)}.mp4`,
      duration: 0.33334,
      independent: (i === 5)
    }));
  }
  return parts;
}

function createParts3() {
  const parts = [];
  for (let i = 0; i < 5; i++) {
    parts.push(new PartialSegment({
      uri: `filePart273.${i}.mp4`,
      duration: 0.33334,
      independent: (i === 0),
      hint: (i === 4)
    }));
  }
  return parts;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-01.js
================================================
const {MediaPlaylist, Segment, PartialSegment} = require('../../../types');

const playlist = new MediaPlaylist({
  version: 9,
  targetDuration: 4,
  mediaSequenceBase: 266,
  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02},
  partTargetDuration: 1.02,
  skip: 3,
  segments: createSegments()
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'fileSequence269.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 269,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence270.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 270,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    mediaSequenceNumber: 271,
    parts: createParts()
  }));
  return segments;
}

function createParts() {
  const parts = [];
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 0, length: 20000}
  }));
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 20000, length: 23000}
  }));
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 43000, length: 18000}
  }));
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    byterange: {offset: 61000},
    hint: true
  }));
  return parts;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-02.js
================================================
const {MediaPlaylist, Segment, PartialSegment} = require('../../../types');

const playlist = new MediaPlaylist({
  version: 9,
  targetDuration: 4,
  mediaSequenceBase: 266,
  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02},
  partTargetDuration: 1.02,
  skip: 3,
  segments: createSegments()
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'fileSequence269.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 269,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence270.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 270,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence271.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 271,
    discontinuitySequence: 0,
    parts: createParts()
  }));
  segments.push(new Segment({
    mediaSequenceNumber: 272,
    parts: [new PartialSegment({
      uri: 'fileSequence272.mp4',
      byterange: {offset: 0},
      hint: true
    })]
  }));
  return segments;
}

function createParts() {
  const parts = [];
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 0, length: 20000}
  }));
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 20000, length: 23000}
  }));
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 43000, length: 18000}
  }));
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 61000, length: 19000}
  }));
  return parts;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-03.js
================================================
const {MediaPlaylist, Segment, PartialSegment} = require('../../../types');

const playlist = new MediaPlaylist({
  version: 9,
  targetDuration: 4,
  mediaSequenceBase: 266,
  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02},
  partTargetDuration: 1.02,
  skip: 3,
  segments: createSegments()
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'fileSequence269.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 269,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence270.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 270,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'fileSequence271.mp4',
    duration: 4.00008,
    title: '',
    mediaSequenceNumber: 271,
    discontinuitySequence: 0,
    parts: createParts()
  }));
  segments.push(new Segment({
    mediaSequenceNumber: 272,
    parts: [
      new PartialSegment({
        uri: 'fileSequence272.mp4',
        duration: 1.02,
        byterange: {offset: 0, length: 21000}
      }),
      new PartialSegment({
        uri: 'fileSequence272.mp4',
        byterange: {offset: 21000},
        hint: true
      })
    ]
  }));
  return segments;
}

function createParts() {
  const parts = [];
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 0, length: 20000}
  }));
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 20000, length: 23000}
  }));
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 43000, length: 18000}
  }));
  parts.push(new PartialSegment({
    uri: 'fileSequence271.mp4',
    duration: 1.02,
    byterange: {offset: 61000, length: 19000}
  }));
  return parts;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/Multiple-rendition-groups.js
================================================
const {MasterPlaylist, Variant, Rendition} = require('../../../types');

const renditions = [
  new Rendition({type: 'AUDIO', groupId: 'aac_high', name: 'English', isDefault: true, uri: 'aac_high_eng.m3u8'}),
  new Rendition({type: 'AUDIO', groupId: 'aac_high', name: 'Japanese', isDefault: false, uri: 'aac_high_jp.m3u8'}),
  new Rendition({type: 'AUDIO', groupId: 'aac_mid', name: 'English', isDefault: true, uri: 'aac_mid_eng.m3u8'}),
  new Rendition({type: 'AUDIO', groupId: 'aac_mid', name: 'Japanese', isDefault: false, uri: 'aac_mid_jp.m3u8'}),
  new Rendition({type: 'AUDIO', groupId: 'aac_low', name: 'English', isDefault: true, uri: 'aac_low_eng.m3u8'}),
  new Rendition({type: 'AUDIO', groupId: 'aac_low', name: 'Japanese', isDefault: false, uri: 'aac_low_jp.m3u8'}),
];
const variants = [
  {uri: '1080p.m3u8', bandwidth: 6000000, audioId: 'aac_high'},
  {uri: '720p.m3u8', bandwidth: 3000000, audioId: 'aac_mid'},
  {uri: '540p.m3u8', bandwidth: 1500000, audioId: 'aac_mid'},
  {uri: '360p.m3u8', bandwidth: 1000000, audioId: 'aac_low'},
].map(
  ({uri, bandwidth, audioId}) => new Variant({
    uri, bandwidth, audio: renditions.filter(({groupId}) => groupId === audioId)
  })
);

const playlist = new MasterPlaylist({
  version: 4,
  independentSegments: true,
  variants,
});

module.exports = playlist;


================================================
FILE: test/fixtures/objects/RedundantSegments.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const playlist = new MediaPlaylist({
  version: 4,
  targetDuration: 10,
  segments: createSegments(),
  endlist: true
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'http://media.example.com/first.ts',
    duration: 9.009,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/second.ts',
    duration: 9.009,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/second.ts',
    byterange: {offset: 256, length: 128},
    duration: 9.009,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/third.ts',
    duration: 3.003,
    title: '',
    mediaSequenceNumber: 3,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/SCTE-35_01.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const playlist = new MediaPlaylist({
  playlistType: 'VOD',
  version: 3,
  targetDuration: 8,
  segments: createSegments(),
  endlist: true
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: '1.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '2.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0,
    markers: [{
      type: 'OUT',
      duration: 15.0
    }]
  }));
  segments.push(new Segment({
    uri: '3.ts',
    duration: 7,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '4.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 3,
    discontinuitySequence: 0,
    markers: [{
      type: 'IN'
    }]
  }));
  segments.push(new Segment({
    uri: '5.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 4,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/SCTE-35_02.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const playlist = new MediaPlaylist({
  playlistType: 'VOD',
  version: 3,
  targetDuration: 8,
  segments: createSegments(),
  endlist: true
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: '1.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '2.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0,
    markers: [{
      type: 'OUT',
      duration: 23.0
    }]
  }));
  segments.push(new Segment({
    uri: '3.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-CUE-OUT-CONT',
      value: 'ElapsedTime=8,Duration=23'
    }]
  }));
  segments.push(new Segment({
    uri: '4.ts',
    duration: 7,
    title: '',
    mediaSequenceNumber: 3,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-CUE-OUT-CONT',
      value: 'ElapsedTime=16,Duration=23'
    }]
  }));
  segments.push(new Segment({
    uri: '5.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 4,
    discontinuitySequence: 0,
    markers: [{
      type: 'IN'
    }]
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/SCTE-35_03.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const playlist = new MediaPlaylist({
  playlistType: 'VOD',
  version: 3,
  targetDuration: 8,
  segments: createSegments(),
  endlist: true
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: '1.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '2.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-CUE',
      value: 'DURATION="15.0",ID="0",TYPE="SpliceOut",TIME="414.171"'
    }]
  }));
  segments.push(new Segment({
    uri: '3.ts',
    duration: 7,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '4.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 3,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '5.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 4,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/SCTE-35_04.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const playlist = new MediaPlaylist({
  playlistType: 'VOD',
  version: 3,
  targetDuration: 8,
  segments: createSegments(),
  endlist: true
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: '1.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '2.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0,
    markers: [
      {
        type: 'RAW',
        tagName: 'EXT-OATCLS-SCTE35',
        value: '/DA0AAAAAAAAAAAABQb+ADAQ6QAeAhxDVUVJQAAAO3/PAAEUrEoICAAAAAAg+2UBNAAANvrtoQ=='
      },
      {
        type: 'RAW',
        tagName: 'EXT-X-ASSET',
        value: 'CAID=0x0000000020FB6501'
      },
      {
        type: 'OUT',
        duration: 15.0
      }
    ]
  }));
  segments.push(new Segment({
    uri: '3.ts',
    duration: 7,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-CUE-OUT-CONT',
      value: 'ElapsedTime=5.939,Duration=25.0,SCTE35=/DA0AAAA+…AAg+2UBNAAANvrtoQ=='
    }]
  }));
  segments.push(new Segment({
    uri: '4.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 3,
    discontinuitySequence: 0,
    markers: [{
      type: 'IN'
    }]
  }));
  segments.push(new Segment({
    uri: '5.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 4,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/SCTE-35_05.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const playlist = new MediaPlaylist({
  playlistType: 'VOD',
  version: 3,
  targetDuration: 8,
  segments: createSegments(),
  endlist: true
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: '1.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '2.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-SCTE35',
      value: 'TYPE=0x34,DURATION=15.0,CUE-OUT=YES,UPID="0x08:0x9425BC",CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”pIViS5”'
    }]
  }));
  segments.push(new Segment({
    uri: '3.ts',
    duration: 7,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '4.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 3,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-SCTE35',
      value: 'TYPE=0x35,CUE-IN=YES,CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”f6UrRd”'
    }]
  }));
  segments.push(new Segment({
    uri: '5.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 4,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/SCTE-35_06.js
================================================
const {MediaPlaylist, Segment} = require('../../../types');

const playlist = new MediaPlaylist({
  playlistType: 'VOD',
  version: 3,
  targetDuration: 8,
  segments: createSegments(),
  endlist: true
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: '1.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: '2.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0,
    markers: [{
      type: 'OUT',
      duration: 23.0
    }]
  }));
  segments.push(new Segment({
    uri: '3.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-CUE-OUT-CONT',
      value: 'ElapsedTime=8,Duration=23'
    }]
  }));
  segments.push(new Segment({
    uri: '4.ts',
    duration: 7,
    title: '',
    mediaSequenceNumber: 3,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-CUE-OUT-CONT',
      value: 'ElapsedTime=16,Duration=23'
    }]
  }));
  segments.push(new Segment({
    uri: '5.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 4,
    discontinuitySequence: 0,
    markers: [{
      type: 'IN'
    }]
  }));
  segments.push(new Segment({
    uri: '6.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 5,
    discontinuitySequence: 0,
    markers: [{
      type: 'OUT',
      duration: 23.0
    }]
  }));
  segments.push(new Segment({
    uri: '7.ts',
    duration: 8,
    title: '',
    mediaSequenceNumber: 6,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-CUE-OUT-CONT',
      value: 'ElapsedTime=8,Duration=23'
    }]
  }));
  segments.push(new Segment({
    uri: '8.ts',
    duration: 7,
    title: '',
    mediaSequenceNumber: 7,
    discontinuitySequence: 0,
    markers: [{
      type: 'RAW',
      tagName: 'EXT-X-CUE-OUT-CONT',
      value: 'ElapsedTime=16,Duration=23'
    }]
  }));
  segments.push(new Segment({
    uri: '9.ts',
    duration: 8.008,
    title: '',
    mediaSequenceNumber: 8,
    discontinuitySequence: 0,
    markers: [{
      type: 'IN'
    }]
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/SCTE-35_07.js
================================================
const {MediaPlaylist, Segment, DateRange} = require('../../../types');
const utils = require('../../../utils');

const playlist = new MediaPlaylist({
  version: 3,
  targetDuration: 30,
  segments: createSegments()
});

function createSegments() {
  const segments = [];
  segments.push(new Segment({
    uri: 'http://media.example.com/01.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 0,
    discontinuitySequence: 0,
    programDateTime: new Date('2014-03-05T11:14:00Z')
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/02.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 1,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://ads.example.com/ad-01.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 2,
    discontinuitySequence: 0,
    dateRange: new DateRange({
      id: 'splice-6FFFFFF0',
      start: new Date('2023-10-09T06:16:00.820Z'),
      plannedDuration: 59.993,
      attributes: {
        'SCTE35-OUT': utils.hexToByteSequence('0xFC30250001D1F7E25300FFF0140565239AA07FEFFE015C3F90FE00526362000101010000A7C1792D')
      }
    })
  }));
  segments.push(new Segment({
    uri: 'http://ads.example.com/ad-02.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 3,
    discontinuitySequence: 0
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/03.ts',
    duration: 30,
    title: '',
    mediaSequenceNumber: 4,
    discontinuitySequence: 0,
    dateRange: new DateRange({
      id: 'splice-6FFFFFF0',
      start: new Date('2023-10-09T06:16:00.820Z'),
      end: new Date('2023-10-09T06:17:01.514Z'),
      duration: 60.694
    })
  }));
  segments.push(new Segment({
    uri: 'http://media.example.com/04.ts',
    duration: 3.003,
    title: '',
    mediaSequenceNumber: 5,
    discontinuitySequence: 0
  }));
  return segments;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/Streaming-Examples_bipbop_16x9_variant.js
================================================
const {MasterPlaylist, Variant, Rendition} = require('../../../types');

const renditions = {
  bipbop_audio: [
    new Rendition({
      type: 'AUDIO',
      groupId: 'bipbop_audio',
      language: 'eng',
      name: 'BipBop Audio 1',
      autoselect: true,
      isDefault: true
    }),
    new Rendition({
      type: 'AUDIO',
      uri: 'alternate_audio_aac/prog_index.m3u8',
      groupId: 'bipbop_audio',
      language: 'eng',
      name: 'BipBop Audio 2',
      autoselect: false,
      isDefault: false
    })
  ],
  subs: [
    new Rendition({
      type: 'SUBTITLES',
      uri: 'subtitles/eng/prog_index.m3u8',
      groupId: 'subs',
      language: 'en',
      name: 'English',
      autoselect: true,
      isDefault: true,
      forced: false,
      characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound'
    }),
    new Rendition({
      type: 'SUBTITLES',
      uri: 'subtitles/eng_forced/prog_index.m3u8',
      groupId: 'subs',
      language: 'en',
      name: 'English (Forced)',
      autoselect: false,
      isDefault: false,
      forced: true
    }),
    new Rendition({
      type: 'SUBTITLES',
      uri: 'subtitles/fra/prog_index.m3u8',
      groupId: 'subs',
      language: 'fr',
      name: 'Français',
      autoselect: true,
      isDefault: false,
      forced: false,
      characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound'
    }),
    new Rendition({
      type: 'SUBTITLES',
      uri: 'subtitles/fra_forced/prog_index.m3u8',
      groupId: 'subs',
      language: 'fr',
      name: 'Français (Forced)',
      autoselect: false,
      isDefault: false,
      forced: true
    }),
    new Rendition({
      type: 'SUBTITLES',
      uri: 'subtitles/spa/prog_index.m3u8',
      groupId: 'subs',
      language: 'es',
      name: 'Español',
      autoselect: true,
      isDefault: false,
      forced: false,
      characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound'
    }),
    new Rendition({
      type: 'SUBTITLES',
      uri: 'subtitles/spa_forced/prog_index.m3u8',
      groupId: 'subs',
      language: 'es',
      name: 'Español (Forced)',
      autoselect: false,
      isDefault: false,
      forced: true
    }),
    new Rendition({
      type: 'SUBTITLES',
      uri: 'subtitles/jpn/prog_index.m3u8',
      groupId: 'subs',
      language: 'ja',
      name: '日本語',
      autoselect: true,
      isDefault: false,
      forced: false,
      characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound'
    }),
    new Rendition({
      type: 'SUBTITLES',
      uri: 'subtitles/jpn_forced/prog_index.m3u8',
      groupId: 'subs',
      language: 'ja',
      name: '日本語 (Forced)',
      autoselect: false,
      isDefault: false,
      forced: true
    })
  ]
};

const playlist = new MasterPlaylist({
  variants: createVariants()
});

function createVariants() {
  const variants = [];
  variants.push(new Variant({
    uri: 'gear1/prog_index.m3u8',
    bandwidth: 263851,
    codecs: 'mp4a.40.2, avc1.4d400d',
    resolution: {width: 416, height: 234},
    audio: renditions.bipbop_audio,
    subtitles: renditions.subs
  }));
  variants.push(new Variant({
    uri: 'gear1/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 28451,
    codecs: 'avc1.4d400d'
  }));
  variants.push(new Variant({
    uri: 'gear2/prog_index.m3u8',
    bandwidth: 577610,
    codecs: 'mp4a.40.2, avc1.4d401e',
    resolution: {width: 640, height: 360},
    audio: renditions.bipbop_audio,
    subtitles: renditions.subs
  }));
  variants.push(new Variant({
    uri: 'gear2/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 181534,
    codecs: 'avc1.4d401e'
  }));
  variants.push(new Variant({
    uri: 'gear3/prog_index.m3u8',
    bandwidth: 915905,
    codecs: 'mp4a.40.2, avc1.4d401f',
    resolution: {width: 960, height: 540},
    audio: renditions.bipbop_audio,
    subtitles: renditions.subs
  }));
  variants.push(new Variant({
    uri: 'gear3/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 297056,
    codecs: 'avc1.4d401f'
  }));
  variants.push(new Variant({
    uri: 'gear4/prog_index.m3u8',
    bandwidth: 1030138,
    codecs: 'mp4a.40.2, avc1.4d401f',
    resolution: {width: 1280, height: 720},
    audio: renditions.bipbop_audio,
    subtitles: renditions.subs
  }));
  variants.push(new Variant({
    uri: 'gear4/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 339492,
    codecs: 'avc1.4d401f'
  }));
  variants.push(new Variant({
    uri: 'gear5/prog_index.m3u8',
    bandwidth: 1924009,
    codecs: 'mp4a.40.2, avc1.4d401f',
    resolution: {width: 1920, height: 1080},
    audio: renditions.bipbop_audio,
    subtitles: renditions.subs
  }));
  variants.push(new Variant({
    uri: 'gear5/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 669554,
    codecs: 'avc1.4d401f'
  }));
  variants.push(new Variant({
    uri: 'gear0/prog_index.m3u8',
    bandwidth: 41457,
    codecs: 'mp4a.40.2',
    audio: renditions.bipbop_audio,
    subtitles: renditions.subs
  }));
  return variants;
}

module.exports = playlist;


================================================
FILE: test/fixtures/objects/Streaming-Examples_img_bipbop_adv_example_ts_master.js
================================================
const {MasterPlaylist, Variant, Rendition} = require('../../../types');

const renditions = {
  aud1: [
    new Rendition({
      type: 'AUDIO',
      uri: 'a1/prog_index.m3u8',
      groupId: 'aud1',
      language: 'en',
      name: 'English',
      autoselect: true,
      isDefault: true,
      channels: '2'
    })
  ],
  aud2: [
    new Rendition({
      type: 'AUDIO',
      uri: 'a2/prog_index.m3u8',
      groupId: 'aud2',
      language: 'en',
      name: 'English',
      autoselect: true,
      isDefault: true,
      channels: '6'
    })
  ],
  aud3: [
    new Rendition({
      type: 'AUDIO',
      uri: 'a3/prog_index.m3u8',
      groupId: 'aud3',
      language: 'en',
      name: 'English',
      autoselect: true,
      isDefault: true,
      channels: '6'
    })
  ],
  cc1: [
    new Rendition({
      type: 'CLOSED-CAPTIONS',
      groupId: 'cc1',
      language: 'en',
      name: 'English',
      autoselect: true,
      isDefault: true,
      instreamId: 'CC1'
    })
  ],
  sub1: [
    new Rendition({
      type: 'SUBTITLES',
      uri: 's1/en/prog_index.m3u8',
      groupId: 'sub1',
      language: 'en',
      name: 'English',
      autoselect: true,
      isDefault: true,
      forced: false
    })
  ]
};

const playlist = new MasterPlaylist({
  version: 6,
  independentSegments: true,
  variants: createVariants()
});

function createVariants() {
  const variants = [];
  variants.push(new Variant({
    uri: 'v5/prog_index.m3u8',
    bandwidth: 2227464,
    averageBandwidth: 2218327,
    codecs: 'avc1.640020,mp4a.40.2',
    resolution: {width: 960, height: 540},
    frameRate: 60.0,
    audio: renditions.aud1,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v9/prog_index.m3u8',
    bandwidth: 8178040,
    averageBandwidth: 8144656,
    codecs: 'avc1.64002a,mp4a.40.2',
    resolution: {width: 1920, height: 1080},
    frameRate: 60.0,
    audio: renditions.aud1,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v8/prog_index.m3u8',
    bandwidth: 6453202,
    averageBandwidth: 6307144,
    codecs: 'avc1.64002a,mp4a.40.2',
    resolution: {width: 1920, height: 1080},
    frameRate: 60.0,
    audio: renditions.aud1,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v7/prog_index.m3u8',
    bandwidth: 5054232,
    averageBandwidth: 4775338,
    codecs: 'avc1.64002a,mp4a.40.2',
    resolution: {width: 1920, height: 1080},
    frameRate: 60.0,
    audio: renditions.aud1,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v6/prog_index.m3u8',
    bandwidth: 3289288,
    averageBandwidth: 3240596,
    codecs: 'avc1.640020,mp4a.40.2',
    resolution: {width: 1280, height: 720},
    frameRate: 60.0,
    audio: renditions.aud1,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v4/prog_index.m3u8',
    bandwidth: 1296989,
    averageBandwidth: 1292926,
    codecs: 'avc1.64001e,mp4a.40.2',
    resolution: {width: 768, height: 432},
    frameRate: 30.0,
    audio: renditions.aud1,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v3/prog_index.m3u8',
    bandwidth: 922242,
    averageBandwidth: 914722,
    codecs: 'avc1.64001e,mp4a.40.2',
    resolution: {width: 640, height: 360},
    frameRate: 30.0,
    audio: renditions.aud1,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v2/prog_index.m3u8',
    bandwidth: 553010,
    averageBandwidth: 541239,
    codecs: 'avc1.640015,mp4a.40.2',
    resolution: {width: 480, height: 270},
    frameRate: 30.0,
    audio: renditions.aud1,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v5/prog_index.m3u8',
    bandwidth: 2448841,
    averageBandwidth: 2439704,
    codecs: 'avc1.640020,ac-3',
    resolution: {width: 960, height: 540},
    frameRate: 60.0,
    audio: renditions.aud2,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v9/prog_index.m3u8',
    bandwidth: 8399417,
    averageBandwidth: 8366033,
    codecs: 'avc1.64002a,ac-3',
    resolution: {width: 1920, height: 1080},
    frameRate: 60.0,
    audio: renditions.aud2,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v8/prog_index.m3u8',
    bandwidth: 6674579,
    averageBandwidth: 6528521,
    codecs: 'avc1.64002a,ac-3',
    resolution: {width: 1920, height: 1080},
    frameRate: 60.0,
    audio: renditions.aud2,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v7/prog_index.m3u8',
    bandwidth: 5275609,
    averageBandwidth: 4996715,
    codecs: 'avc1.64002a,ac-3',
    resolution: {width: 1920, height: 1080},
    frameRate: 60.0,
    audio: renditions.aud2,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v6/prog_index.m3u8',
    bandwidth: 3510665,
    averageBandwidth: 3461973,
    codecs: 'avc1.640020,ac-3',
    resolution: {width: 1280, height: 720},
    frameRate: 60.0,
    audio: renditions.aud2,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v4/prog_index.m3u8',
    bandwidth: 1518366,
    averageBandwidth: 1514303,
    codecs: 'avc1.64001e,ac-3',
    resolution: {width: 768, height: 432},
    frameRate: 30.0,
    audio: renditions.aud2,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v3/prog_index.m3u8',
    bandwidth: 1143619,
    averageBandwidth: 1136099,
    codecs: 'avc1.64001e,ac-3',
    resolution: {width: 640, height: 360},
    frameRate: 30.0,
    audio: renditions.aud2,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v2/prog_index.m3u8',
    bandwidth: 774387,
    averageBandwidth: 762616,
    codecs: 'avc1.640015,ac-3',
    resolution: {width: 480, height: 270},
    frameRate: 30.0,
    audio: renditions.aud2,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v5/prog_index.m3u8',
    bandwidth: 2256841,
    averageBandwidth: 2247704,
    codecs: 'avc1.640020,ec-3',
    resolution: {width: 960, height: 540},
    frameRate: 60.0,
    audio: renditions.aud3,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v9/prog_index.m3u8',
    bandwidth: 8207417,
    averageBandwidth: 8174033,
    codecs: 'avc1.64002a,ec-3',
    resolution: {width: 1920, height: 1080},
    frameRate: 60.0,
    audio: renditions.aud3,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v8/prog_index.m3u8',
    bandwidth: 6482579,
    averageBandwidth: 6336521,
    codecs: 'avc1.64002a,ec-3',
    resolution: {width: 1920, height: 1080},
    frameRate: 60.0,
    audio: renditions.aud3,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v7/prog_index.m3u8',
    bandwidth: 5083609,
    averageBandwidth: 4804715,
    codecs: 'avc1.64002a,ec-3',
    resolution: {width: 1920, height: 1080},
    frameRate: 60.0,
    audio: renditions.aud3,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v6/prog_index.m3u8',
    bandwidth: 3318665,
    averageBandwidth: 3269973,
    codecs: 'avc1.640020,ec-3',
    resolution: {width: 1280, height: 720},
    frameRate: 60.0,
    audio: renditions.aud3,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v4/prog_index.m3u8',
    bandwidth: 1326366,
    averageBandwidth: 1322303,
    codecs: 'avc1.64001e,ec-3',
    resolution: {width: 768, height: 432},
    frameRate: 30.0,
    audio: renditions.aud3,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v3/prog_index.m3u8',
    bandwidth: 951619,
    averageBandwidth: 944099,
    codecs: 'avc1.64001e,ec-3',
    resolution: {width: 640, height: 360},
    frameRate: 30.0,
    audio: renditions.aud3,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v2/prog_index.m3u8',
    bandwidth: 582387,
    averageBandwidth: 570616,
    codecs: 'avc1.640015,ec-3',
    resolution: {width: 480, height: 270},
    frameRate: 30.0,
    audio: renditions.aud3,
    subtitles: renditions.sub1,
    closedCaptions: renditions.cc1
  }));
  variants.push(new Variant({
    uri: 'v7/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 186522,
    averageBandwidth: 182077,
    codecs: 'avc1.64002a',
    resolution: {width: 1920, height: 1080}
  }));
  variants.push(new Variant({
    uri: 'v6/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 133856,
    averageBandwidth: 129936,
    codecs: 'avc1.640020',
    resolution: {width: 1280, height: 720}
  }));
  variants.push(new Variant({
    uri: 'v5/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 98136,
    averageBandwidth: 94286,
    codecs: 'avc1.640020',
    resolution: {width: 960, height: 540}
  }));
  variants.push(new Variant({
    uri: 'v4/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 76704,
    averageBandwidth: 74767,
    codecs: 'avc1.64001e',
    resolution: {width: 768, height: 432}
  }));
  variants.push(new Variant({
    uri: 'v3/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 64078,
    averageBandwidth: 62251,
    codecs: 'avc1.64001e',
    resolution: {width: 640, height: 360}
  }));
  variants.push(new Variant({
    uri: 'v2/iframe_index.m3u8',
    isIFrameOnly: true,
    bandwidth: 38728,
    averageBandwidth: 37866,
    codecs: 'avc1.640015',
    resolution: {width: 480, height: 270}
  }));
  return variants;
}

module.exports = playlist;


================================================
FILE: test/helpers/fixtures.js
================================================
const path = require('node:path');
const fs = require('node:fs');

const fixtures = [];
const baseDir = path.join(__dirname, '../fixtures/m3u8');
const filenames = fs.readdirSync(baseDir);

for (const filename of filenames) {
  if (filename.endsWith('.m3u8')) {
    const name = path.basename(filename, '.m3u8');
    const filepath = path.join(baseDir, filename);
    const m3u8 = fs.readFileSync(filepath, 'utf8');
    const object = require(`../fixtures/objects/${name}.js`);
    fixtures.push({name, m3u8, object});
  }
}

module.exports = fixtures;


================================================
FILE: test/helpers/matchers.js
================================================
function removeSpaceFromLine(line) {
  let inside = false;
  let str = '';
  for (const ch of line) {
    if (ch === '"') {
      inside = !inside;
    } else if (!inside && ch === ' ') {
      continue;
    }
    str += ch;
  }
  return str;
}

function strip(playlist) {
  playlist = playlist.trim();
  const filtered = [];
  for (let line of playlist.split('\n')) {
    line = removeSpaceFromLine(line);
    if (line.startsWith('#')) {
      if (line.startsWith('#EXT')) {
        filtered.push(line);
      }
    } else {
      filtered.push(line);
    }
  }
  return filtered.join('\n');
}

function equalPlaylist(t, expected, actual) {
  expected &&= strip(expected);
  actual &&= strip(actual);
  if (expected === actual) {
    return t.pass();
  }
  t.fail(`expected="${expected}", actual="${actual}"`);
}

function notEqualPlaylist(t, expected, actual) {
  expected &&= strip(expected);
  actual &&= strip(actual);
  if (expected === actual) {
    t.fail(`expected="${expected}", actual="${actual}"`);
    return t.fail();
  }
  t.pass();
}

module.exports = {
  equalPlaylist,
  notEqualPlaylist
};


================================================
FILE: test/helpers/utils.js
================================================
const HLS = require('../..');

HLS.setOptions({strictMode: true});

function parsePass(t, text) {
  let obj;
  try {
    obj = HLS.parse(text);
  } catch (err) {
    t.fail(err.stack);
  }
  t.truthy(obj);
  return obj;
}

function stringifyPass(t, obj) {
  let text;
  try {
    text = HLS.stringify(obj);
  } catch (err) {
    t.fail(err.stack);
  }
  t.truthy(text);
  return text;
}

function bothPass(t, text) {
  const obj = parsePass(t, text);
  return stringifyPass(t, obj);
}

function parseFail(t, text) {
  try {
    HLS.parse(text);
  } catch (err) {
    return t.truthy(err);
  }
  t.fail('HLS.parse() did not fail');
}

function stringifyFail(t, obj) {
  try {
    HLS.stringify(obj);
  } catch (err) {
    return t.truthy(err);
  }
  t.fail('HLS.stringify() did not fail');
}

function stripSpaces(text) {
  const chars = [];
  let insideDoubleQuotes = false;
  for (const ch of text) {
    if (ch === '"') {
      insideDoubleQuotes = !insideDoubleQuotes;
    } else if (ch === ' ') {
      if (!insideDoubleQuotes) {
        continue;
      }
    }
    chars.push(ch);
  }
  return chars.join('');
}

function stripCommentsAndEmptyLines(text) {
  const lines = [];
  for (const l of text.split('\n')) {
    const line = l.trim();
    if (!line) {
      // empty line
      continue;
    }
    if (line.startsWith('#')) {
      if (line.startsWith('#EXT')) {
        // tag
        lines.push(stripSpaces(line));
      }
      // comment
      continue;
    }
    // uri
    lines.push(line);
  }
  return lines.join('\n');
}

module.exports = {
  parsePass,
  stringifyPass,
  bothPass,
  parseFail,
  stringifyFail,
  stripCommentsAndEmptyLines
};


================================================
FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.1_EXTM3U.spec.js
================================================
const test = require('ava');
const utils = require('../../../../helpers/utils');

// It MUST be the first line of every Media Playlist and
// every Master Playlist.
test('#EXTM3U-01', t => {
  // Media Playlist
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXTINF:10,
    http://example.com/1
    #EXTINF:10,
    http://example.com/2
  `);
  utils.parseFail(t, `
    #EXT-X-TARGETDURATION:10
    #EXTM3U
    #EXTINF:10,
    http://example.com/1
    #EXTINF:10,
    http://example.com/2
  `);
  // Master Playlist
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-STREAM-INF:BANDWIDTH=1280000
    http://example.com/low.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=2560000
    http://example.com/mid.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=7680000
    http://example.com/hi.m3u8
  `);
  utils.parseFail(t, `
    #EXT-X-STREAM-INF:BANDWIDTH=1280000
    http://example.com/low.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=2560000
    http://example.com/mid.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=7680000
    http://example.com/hi.m3u8
  `);
});


================================================
FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.2_EXT-X-VERSION.spec.js
================================================
const test = require('ava');
const utils = require('../../../../helpers/utils');

// A Playlist file MUST NOT contain more than one EXT-X-VERSION tag.
test('#EXT-X-VERSION_01', t => {
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:10
    #EXTINF:9.9,
    http://example.com/1
    #EXTINF:10.0,
    http://example.com/2
  `);
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:10
    #EXTINF:9.9,
    http://example.com/1
    #EXTINF:10.0,
    http://example.com/2
    #EXT-X-VERSION:4
  `);
});


================================================
FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.1_EXTINF.spec.js
================================================
const test = require('ava');
const utils = require('../../../../helpers/utils');

// This tag is REQUIRED for each Media Segment
test('#EXTINF_01', t => {
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXTINF:10,
    http://example.com/1
    #EXTINF:10,
    http://example.com/2
  `);
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXTINF:10,
    http://example.com/1
    http://example.com/2
  `);
});

// If the compatibility version number is less than 3,
// durations MUST be integers.
test('#EXTINF_02', t => {
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:10
    #EXTINF:9.9,
    http://example.com/1
  `);
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-VERSION:2
    #EXT-X-TARGETDURATION:10
    #EXTINF:9.9,
    http://example.com/1
  `);
});

// The remainder of the line following the comma is an optional human-
// readable informative title of the Media Segment expressed as raw
// UTF-8 text.
test('#EXTINF_03', t => {
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXTINF:10,abc
    http://example.com/1
  `);
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXTINF:10,${unescape(encodeURIComponent('\u3042'))}
    http://example.com/1
  `);
});


================================================
FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.2_EXT-X-BYTERANGE.spec.js
================================================
const test = require('ava');
const HLS = require('../../../../..');
const utils = require('../../../../helpers/utils');

// It applies only to the next URI line that follows it in the Playlist.
test('#EXT-X-BYTERANGE_01', t => {
  const playlist = HLS.parse(`
    #EXTM3U
    #EXT-X-VERSION:4
    #EXT-X-TARGETDURATION:10
    #EXT-X-BYTERANGE:100@200
    #EXTINF:10,
    http://example.com/1
    #EXTINF:10,
    http://example.com/2
  `);
  t.is(playlist.segments[0].byterange.offset, 200);
  t.is(playlist.segments[0].byterange.length, 100);
  t.falsy(playlist.segments[1].byterange);
});

// If o is not present, the sub-range begins at the next byte following
// the sub-range of the previous Media Segment.
test('#EXT-X-BYTERANGE_02', t => {
  const playlist = HLS.parse(`
    #EXTM3U
    #EXT-X-VERSION:4
    #EXT-X-TARGETDURATION:10
    #EXT-X-BYTERANGE:100@200
    #EXTINF:9.9,
    http://example.com/1
    #EXT-X-BYTERANGE:100
    #EXTINF:9.9,
    http://example.com/1
    #EXT-X-BYTERANGE:100
    #EXTINF:9.9,
    http://example.com/1
  `);
  t.is(playlist.segments[0].byterange.offset, 200);
  t.is(playlist.segments[1].byterange.offset, 300);
  t.is(playlist.segments[2].byterange.offset, 400);
});

// If o is not present, a previous Media Segment MUST appear in the
// Playlist file and MUST be a sub-range of the same media resource, or
// the Media Segment is undefined and the Playlist MUST be rejected.
test('#EXT-X-BYTERANGE_03', t => {
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-VERSION:4
    #EXT-X-TARGETDURATION:10
    #EXT-X-BYTERANGE:100
    #EXTINF:9.9,
    http://example.com/1
  `);
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-VERSION:4
    #EXT-X-TARGETDURATION:10
    #EXT-X-BYTERANGE:100@200
    #EXTINF:9.9,
    http://example.com/1
    #EXT-X-BYTERANGE:100
    #EXTINF:9.9,
    http://example.com/1
    #EXT-X-BYTERANGE:100
    #EXTINF:9.9,
    http://example.com/2
  `);
  utils.parsePass(t, `
    #EXTM3U
    #EXT-X-VERSION:4
    #EXT-X-TARGETDURATION:10
    #EXT-X-BYTERANGE:100@200
    #EXTINF:9.9,
    http://example.com/1
    #EXT-X-BYTERANGE:100
    #EXTINF:9.9,
    http://example.com/1
    #EXT-X-BYTERANGE:100@200
    #EXTINF:9.9,
    http://example.com/2
  `);
});

// Use of the EXT-X-BYTERANGE tag REQUIRES a compatibility version
// number of 4 or greater.
test('#EXT-X-BYTERANGE_04', t => {
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-TARGETDURATION:10
    #EXT-X-BYTERANGE:100@200
    #EXTINF:9.9,
    http://example.com/1
  `);
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-VERSION:4
    #EXT-X-TARGETDURATION:10
    #EXT-X-BYTERANGE:100@200
    #EXTINF:9.9,
    http://example.com/1
  `);
});

// EXT-X-BYTERANGE should come at end of segment.
test('#EXT-X-BYTERANGE_05', t => {
  t.is(
    utils.bothPass(t, `
        #EXTM3U
        #EXT-X-VERSION:4
        #EXT-X-TARGETDURATION:10
        #EXTINF:9.9,comment
        #EXT-X-BYTERANGE:100@200
        http://example.com/1
        #EXT-X-DISCONTINUITY
        #EXTINF:9.9,comment
        #EXT-X-BYTERANGE:100@200
        http://example.com/2
    `),
    utils.stripCommentsAndEmptyLines(`
        #EXTM3U
        #EXT-X-VERSION:4
        #EXT-X-TARGETDURATION:10
        #EXTINF:9.9,comment
        #EXT-X-BYTERANGE:100@200
        http://example.com/1
        #EXT-X-DISCONTINUITY
        #EXTINF:9.9,comment
        #EXT-X-BYTERANGE:100@200
        http://example.com/2
    `)
  );
});


================================================
FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.3_EXT-X-DISCONTINUITY.spec.js
================================================
const test = require('ava');
const HLS = require('../../../../..');

// The EXT-X-DISCONTINUITY tag indicates a discontinuity between the
// Media Segment that follows it and the one that preceded it.
test('#EXT-X-DISCONTINUITY_01', t => {
  const playlist = HLS.parse(`
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXTINF:10,
    http://example.com/1
    #EXT-X-DISCONTINUITY
    #EXTINF:10,
    http://example.com/2
    #EXTINF:10,
    http://example.com/3
  `);
  t.falsy(playlist.segments[0].discontinuity);
  t.true(playlist.segments[1].discontinuity);
  t.falsy(playlist.segments[2].discontinuity);
});


================================================
FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.4_EXT-X-KEY.spec.js
================================================
const test = require('ava');
const HLS = require('../../../../..');
const utils = require('../../../../helpers/utils');

// It applies to every Media Segment that appears between
// it and the next EXT-X-KEY tag in the Playlist file with the same
// KEYFORMAT attribute (or the end of the Playlist file).
test('#EXT-X-KEY_01', t => {
  let playlist;
  // Until the end of the Playlist file
  playlist = HLS.parse(`
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXTINF:10,
    http://example.com/1
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com"
    #EXTINF:10,
    http://example.com/2
    #EXTINF:10,
    http://example.com/3
  `);
  t.falsy(playlist.segments[0].key);
  t.truthy(playlist.segments[1].key);
  t.truthy(playlist.segments[2].key);
  // Until the next EXT-X-KEY tag in the Playlist file with the same
  // KEYFORMAT attribute
  playlist = HLS.parse(`
    #EXTM3U
    #EXT-X-VERSION:5
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity"
    #EXTINF:10,
    http://example.com/1
    #EXTINF:10,
    http://example.com/2
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity"
    #EXTINF:10,
    http://example.com/3
  `);
  t.is(playlist.segments[0].key.uri, 'http://example.com/key-1');
  t.is(playlist.segments[1].key.uri, 'http://example.com/key-1');
  t.is(playlist.segments[2].key.uri, 'http://example.com/key-2');
});

// METHOD: This attribute is REQUIRED.
test('#EXT-X-KEY_02', t => {
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:URI="http://example.com"
    #EXTINF:10,
    http://example.com/2
  `);
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com"
    #EXTINF:10,
    http://example.com/2
  `);
});

// If the encryption method is NONE, other attributes
// MUST NOT be present.
test('#EXT-X-KEY_03', t => {
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:METHOD=NONE,URI="http://example.com"
    #EXTINF:10,
    http://example.com/2
  `);
});

// URI: This attribute is REQUIRED unless the METHOD is NONE.
test('#EXT-X-KEY_04', t => {
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:METHOD=AES-128
    #EXTINF:10,
    http://example.com/2
  `);
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:METHOD=NONE
    #EXTINF:10,
    http://example.com/2
  `);
});

// Use of the IV attribute REQUIRES a compatibility version number of
// 2 or greater.
test('#EXT-X-KEY_05', t => {
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-VERSION:1
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100
    #EXTINF:10,
    http://example.com/2
  `);
  const playlist = utils.parsePass(t, `
    #EXTM3U
    #EXT-X-VERSION:2
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100
    #EXTINF:10,
    http://example.com/2
  `);
  t.is(playlist.segments[0].key.iv.length, 16);
});

// The tag place should be preserved
test('#EXT-X-KEY_06', t => {
  const sourceText = `
    #EXTM3U
    #EXT-X-VERSION:5
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity"
    #EXTINF:10,
    http://example.com/1
    #EXTINF:10,
    http://example.com/2
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity"
    #EXTINF:10,
    http://example.com/3
    #EXTINF:10,
    http://example.com/4
  `;
  const obj = HLS.parse(sourceText);
  const text = HLS.stringify(obj);
  t.is(text, utils.stripCommentsAndEmptyLines(sourceText));
});

// The same tag can appear multiple times
test('#EXT-X-KEY_07', t => {
  const sourceText = `
    #EXTM3U
    #EXT-X-VERSION:5
    #EXT-X-TARGETDURATION:10
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity"
    #EXTINF:10,
    http://example.com/1
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity"
    #EXTINF:10,
    http://example.com/2
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity"
    #EXTINF:10,
    http://example.com/3
    #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity"
    #EXTINF:10,
    http://example.com/4
  `;
  const obj = HLS.parse(sourceText);
  const text = HLS.stringify(obj);
  t.is(text, utils.stripCommentsAndEmptyLines(sourceText));
});


================================================
FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.5_EXT-X-MAP.spec.js
================================================
const test = require('ava');
const HLS = require('../../../../..');
const utils = require('../../../../helpers/utils');

// It applies to every Media Segment that appears after it in the
// Playlist until the next EXT-X-MAP tag or until the end of the
// playlist.
test('#EXT-X-MAP_01', t => {
  let playlist;
  // Until the end of the Playlist
  playlist = HLS.parse(`
    #EXTM3U
    #EXT-X-VERSION:6
    #EXT-X-TARGETDURATION:10
    #EXTINF:10,
    http://example.com/1
    #EXT-X-MAP:URI="http://example.com/map-1"
    #EXTINF:10,
    http://example.com/2
    #EXTINF:10,
    http://example.com/3
  `);
  t.falsy(playlist.segments[0].map);
  t.truthy(playlist.segments[1].map);
  t.truthy(playlist.segments[2].map);
  // Until the next EXT-X-MAP tag
  playlist = HLS.parse(`
    #EXTM3U
    #EXT-X-VERSION:6
    #EXT-X-TARGETDURATION:10
    #EXT-X-MAP:URI="http://example.com/map-1"
    #EXTINF:10,
    http://example.com/1
    #EXTINF:10,
    http://example.com/2
    #EXT-X-MAP:URI="http://example.com/map-2"
    #EXTINF:10,
    http://example.com/3
  `);
  t.is(playlist.segments[0].map.uri, 'http://example.com/map-1');
  t.is(playlist.segments[1].map.uri, 'http://example.com/map-1');
  t.is(playlist.segments[2].map.uri, 'http://example.com/map-2');
  HLS.stringify(playlist);
});

// URI: This attribute is REQUIRED.
test('#EXT-X-MAP_02', t => {
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-VERSION:6
    #EXT-X-TARGETDURATION:10
    #EXT-X-MAP:BYTERANGE="256@128"
    #EXTINF:10,
    http://example.com/1
  `);
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-VERSION:6
    #EXT-X-TARGETDURATION:10
    #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128"
    #EXTINF:10,
    http://example.com/1
  `);
});

// Use of the EXT-X-MAP tag in a Media Playlist that contains the
// EXT-X-I-FRAMES-ONLY tag REQUIRES a compatibility version number of 5
// or greater.
// URI: This attribute is REQUIRED.
test('#EXT-X-MAP_03', t => {
  utils.parseFail(t, `
    #EXTM3U
    #EXT-X-VERSION:4
    #EXT-X-TARGETDURATION:10
    #EXT-X-I-FRAMES-ONLY
    #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128"
    #EXTINF:10,
    http://example.com/1
  `);
  utils.bothPass(t, `
    #EXTM3U
    #EXT-X-VERSION:5
    #EXT-X-TARGETDURATION:10
    #EXT-X-I-FRAMES-ONLY
    #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128"
    #EXTINF:10,
    http://example.com/1
  `);
});

// Use of the EXT-X-MAP tag in a Media Playlist that DOES
// NOT contain the EXT-X-I-FRAMES-ONLY tag REQUIRES a compatibility
// version number of 6 or greater.
test('#EXT-X-MAP_04', t => {
Download .txt
gitextract_hu9vpdm4/

├── .github/
│   └── workflows/
│       ├── release.yml
│       └── tests.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── index.ts
├── package.json
├── parse.ts
├── stringify.ts
├── test/
│   ├── fixtures/
│   │   ├── m3u8/
│   │   │   ├── 8.1-Simple-Media-Playlist.m3u8
│   │   │   ├── 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.m3u8
│   │   │   ├── 8.11-EXT-X-CUE-OUT-Media-Playlist.m3u8
│   │   │   ├── 8.2-Live-Media-Playlist_using-HTTPS.m3u8
│   │   │   ├── 8.3-Playlist-with-encrypted-Media-Segments.m3u8
│   │   │   ├── 8.4-Master-Playlist.m3u8
│   │   │   ├── 8.5-Master-Playlist-with-I-Frames.m3u8
│   │   │   ├── 8.6-Master-Playlist-with-Alternative-audio.m3u8
│   │   │   ├── 8.7-Master-Playlist-with-Alternative-video.m3u8
│   │   │   ├── 8.8-Session-Data-in-a-Master-Playlist.m3u8
│   │   │   ├── 8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.m3u8
│   │   │   ├── Low-Latency_Example-01_Low-Latency_HLS_Playlist.m3u8
│   │   │   ├── Low-Latency_Example-02_Playlist_Delta_Update.m3u8
│   │   │   ├── Low-Latency_Example-03_Byterange-addressed_Parts-01.m3u8
│   │   │   ├── Low-Latency_Example-03_Byterange-addressed_Parts-02.m3u8
│   │   │   ├── Low-Latency_Example-03_Byterange-addressed_Parts-03.m3u8
│   │   │   ├── Multiple-rendition-groups.m3u8
│   │   │   ├── RedundantSegments.m3u8
│   │   │   ├── SCTE-35_01.m3u8
│   │   │   ├── SCTE-35_02.m3u8
│   │   │   ├── SCTE-35_03.m3u8
│   │   │   ├── SCTE-35_04.m3u8
│   │   │   ├── SCTE-35_05.m3u8
│   │   │   ├── SCTE-35_06.m3u8
│   │   │   ├── SCTE-35_07.m3u8
│   │   │   ├── Streaming-Examples_bipbop_16x9_variant.m3u8
│   │   │   └── Streaming-Examples_img_bipbop_adv_example_ts_master.m3u8
│   │   └── objects/
│   │       ├── 8.1-Simple-Media-Playlist.js
│   │       ├── 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js
│   │       ├── 8.11-EXT-X-CUE-OUT-Media-Playlist.js
│   │       ├── 8.2-Live-Media-Playlist_using-HTTPS.js
│   │       ├── 8.3-Playlist-with-encrypted-Media-Segments.js
│   │       ├── 8.4-Master-Playlist.js
│   │       ├── 8.5-Master-Playlist-with-I-Frames.js
│   │       ├── 8.6-Master-Playlist-with-Alternative-audio.js
│   │       ├── 8.7-Master-Playlist-with-Alternative-video.js
│   │       ├── 8.8-Session-Data-in-a-Master-Playlist.js
│   │       ├── 8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js
│   │       ├── Low-Latency_Example-01_Low-Latency_HLS_Playlist.js
│   │       ├── Low-Latency_Example-02_Playlist_Delta_Update.js
│   │       ├── Low-Latency_Example-03_Byterange-addressed_Parts-01.js
│   │       ├── Low-Latency_Example-03_Byterange-addressed_Parts-02.js
│   │       ├── Low-Latency_Example-03_Byterange-addressed_Parts-03.js
│   │       ├── Multiple-rendition-groups.js
│   │       ├── RedundantSegments.js
│   │       ├── SCTE-35_01.js
│   │       ├── SCTE-35_02.js
│   │       ├── SCTE-35_03.js
│   │       ├── SCTE-35_04.js
│   │       ├── SCTE-35_05.js
│   │       ├── SCTE-35_06.js
│   │       ├── SCTE-35_07.js
│   │       ├── Streaming-Examples_bipbop_16x9_variant.js
│   │       └── Streaming-Examples_img_bipbop_adv_example_ts_master.js
│   ├── helpers/
│   │   ├── fixtures.js
│   │   ├── matchers.js
│   │   └── utils.js
│   └── spec/
│       ├── 4_Playlists/
│       │   └── 4.3_Playlist-Tags/
│       │       ├── 4.3.1_Basic-Tags/
│       │       │   ├── 4.3.1.1_EXTM3U.spec.js
│       │       │   └── 4.3.1.2_EXT-X-VERSION.spec.js
│       │       ├── 4.3.2_Media-Segment-Tags/
│       │       │   ├── 4.3.2.1_EXTINF.spec.js
│       │       │   ├── 4.3.2.2_EXT-X-BYTERANGE.spec.js
│       │       │   ├── 4.3.2.3_EXT-X-DISCONTINUITY.spec.js
│       │       │   ├── 4.3.2.4_EXT-X-KEY.spec.js
│       │       │   ├── 4.3.2.5_EXT-X-MAP.spec.js
│       │       │   ├── 4.3.2.6_EXT-X-PROGRAM-DATE-TIME.spec.js
│       │       │   ├── 4.3.2.7_EXT-X-DATERANGE.spec.js
│       │       │   ├── 4.3.2_Media-Segment-Tags.spec.js
│       │       │   └── 4.4.4.7_EXT-X-GAP.spec.js
│       │       ├── 4.3.3_Media-Playlist-Tags/
│       │       │   ├── 4.3.3.1_EXT-X-TARGETDURATION.spec.js
│       │       │   ├── 4.3.3.2_EXT-X-MEDIA-SEQUENCE.spec.js
│       │       │   ├── 4.3.3.3_EXT-X-DISCONTINUITY-SEQUENCE.spec.js
│       │       │   ├── 4.3.3.4_EXT-X-ENDLIST.spec.js
│       │       │   ├── 4.3.3.5_EXT-X-PLAYLIST-TYPE.spec.js
│       │       │   ├── 4.3.3.6_EXT-X-I-FRAMES-ONLY.spec.js
│       │       │   ├── 4.3.3.7_EXT-X-CUE-OUT.spec.js
│       │       │   └── 4.3.3_Media-Playlist-Tags.spec.js
│       │       ├── 4.3.4_Master-Playlist-Tags/
│       │       │   ├── 4.3.4.1_EXT-X-MEDIA.spec.js
│       │       │   ├── 4.3.4.2_EXT-X-STREAM-INF.spec.js
│       │       │   ├── 4.3.4.2_EXT-X-STREAM-INF_2.spec.js
│       │       │   ├── 4.3.4.3_EXT-X-I-FRAME-STREAM-INF.spec.js
│       │       │   ├── 4.3.4.4_EXT-X-SESSION-DATA.spec.js
│       │       │   ├── 4.3.4.5_EXT-X-SESSION-KEY.spec.js
│       │       │   └── 4.3.4_Master-Playlist-Tags.spec.js
│       │       └── 4.3.5_Media-or-Master-Playlist-Tags/
│       │           ├── 4.3.5.1_EXT-X-INDEPENDENT-SEGMENTS.spec.js
│       │           └── 4.3.5.2_EXT-X-START.spec.js
│       ├── 7_Protocol-version-compatibility/
│       │   └── 7_EXT-X-VERSION.spec.js
│       ├── Apple-Low-Latency/
│       │   └── New_Media_Playlist_Tags_for_Low-Latency_HLS/
│       │       ├── 01_EXT-X-SERVER-CONTROL.spec.js
│       │       ├── 02_EXT-X-PART-INF.spec.js
│       │       ├── 03_EXT-X-PART.spec.js
│       │       ├── 04_EXT-X-PRELOAD-HINT.spec.js
│       │       ├── 05_EXT-X-RENDITION-REPORT.spec.js
│       │       └── 06_EXT-X-SKIP.spec.js
│       ├── Apple_HLS_Overview/
│       │   └── 02_Using_HLS.spec.js
│       ├── HLSJS-LHLS/
│       │   ├── 01_EXT-X-PREFETCH.spec.js
│       │   └── 02_EXT-X-PREFETCH-DISCONTINUITY.spec.js
│       ├── misc/
│       │   ├── multiple-rendition-groups.js
│       │   └── scte-35.spec.js
│       ├── parser.spec.js
│       ├── stringify.spec.js
│       └── utils.spec.js
├── tsconfig.json
├── types.ts
└── utils.ts
Download .txt
SYMBOL INDEX (209 symbols across 60 files)

FILE: parse.ts
  function unquote (line 25) | function unquote(str: string | undefined) {
  type TagCategory (line 29) | type TagCategory = 'Basic' | 'Segment' | 'MasterPlaylist' | 'MediaPlayli...
  function getTagCategory (line 31) | function getTagCategory(tagName: string): TagCategory {
  function parseEXTINF (line 83) | function parseEXTINF(param: string): ExtInfo {
  function parseBYTERANGE (line 88) | function parseBYTERANGE(param: string): Byterange {
  function parseResolution (line 93) | function parseResolution(str: string): Resolution {
  function parseAllowedCpc (line 98) | function parseAllowedCpc(str: string): AllowedCpc[] {
  function parseIV (line 116) | function parseIV(str: string): Uint8Array {
  function parseUserAttribute (line 124) | function parseUserAttribute(str: string): UserAttribute {
  function setCompatibleVersionOfKey (line 134) | function setCompatibleVersionOfKey(params: Record<string, any>, attribut...
  function parseAttributeList (line 143) | function parseAttributeList(param): Record<string, any> {
  function parseTagParam (line 213) | function parseTagParam(name: string, param): TagParam {
  function MIXEDTAGS (line 266) | function MIXEDTAGS() {
  function splitTag (line 270) | function splitTag(line: string): [string, string | null] {
  function parseRendition (line 278) | function parseRendition({attributes}: Tag): Rendition {
  function checkRedundantRendition (line 297) | function checkRedundantRendition(renditions, rendition): string {
  function addRendition (line 313) | function addRendition(variant, line, type) {
  function matchTypes (line 326) | function matchTypes(attrs, variant, params) {
  function parseVariant (line 337) | function parseVariant(lines, variantAttrs, uri: string, iFrameOnly: bool...
  function sameKey (line 378) | function sameKey(key1: Key, key2: Key): boolean {
  function parseMasterPlaylist (line 409) | function parseMasterPlaylist(lines: Line[], params: Record<string, any>)...
  function parseDateRange (line 504) | function parseDateRange(attributes) {
  function parseSegment (line 525) | function parseSegment(lines: Line[], uri: string, start: number, end: nu...
  function parsePrefetchSegment (line 646) | function parsePrefetchSegment(lines: Line[], uri: any, start: number, en...
  function parseMediaPlaylist (line 672) | function parseMediaPlaylist(lines: Line[], params: Record<string, any>):...
  function addSegment (line 836) | function addSegment(playlist: MediaPlaylist, segment: Segment, discontin...
  function checkDateRange (line 864) | function checkDateRange(segments: Segment[]) {
  function checkLowLatencyCompatibility (line 909) | function checkLowLatencyCompatibility({lowLatencyCompatibility, targetDu...
  function CHECKTAGCATEGORY (line 954) | function CHECKTAGCATEGORY(category: TagCategory, params: Record<string, ...
  type Tag (line 977) | type Tag = {
  function parseTag (line 984) | function parseTag(line: string, params: Record<string, any>): Tag | null {
  type Line (line 1001) | type Line = string | Tag;
  function lexicalParse (line 1003) | function lexicalParse(text: string, params: Record<string, any>): Line[] {
  function semanticParse (line 1034) | function semanticParse(lines: Line[], params: Record<string, any>): Mast...
  function parse (line 1052) | function parse(text: string): MasterPlaylist | MediaPlaylist {
  function mapTo (line 1069) | function mapTo<T extends object>(value: T | string): Partial<T> {

FILE: stringify.ts
  constant ALLOW_REDUNDANCY (line 18) | const ALLOW_REDUNDANCY = [
  constant SKIP_IF_REDUNDANT (line 29) | const SKIP_IF_REDUNDANT = [
  class LineArray (line 33) | class LineArray extends Array<string> {
    method constructor (line 36) | constructor(baseUri?: string) {
    method push (line 41) | override push(...elems: string[]) {
    method join (line 63) | override join(separator: string | undefined = ','): string {
  function buildDecimalFloatingNumber (line 73) | function buildDecimalFloatingNumber(num: number, fixed?: number) {
  function getNumberOfDecimalPlaces (line 82) | function getNumberOfDecimalPlaces(num: number) {
  function buildMasterPlaylist (line 91) | function buildMasterPlaylist(lines: LineArray, playlist: MasterPlaylist,...
  function buildContentSteeringServer (line 110) | function buildContentSteeringServer(contentSteering: ContentSteering) {
  function buildSessionData (line 118) | function buildSessionData(sessionData: SessionData) {
  function buildKey (line 131) | function buildKey(key: Key, isSessionKey?: boolean) {
  function buildVariant (line 152) | function buildVariant(lines: LineArray, variant: Variant) {
  function buildRendition (line 227) | function buildRendition(rendition: Rendition) {
  function buildMediaPlaylist (line 263) | function buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist, p...
  function buildSegment (line 345) | function buildSegment(lines: LineArray, segment: Segment, lastKey: strin...
  function buildMap (line 395) | function buildMap(map: MediaInitializationSection) {
  function buildByteRange (line 403) | function buildByteRange({offset, length}: Byterange) {
  function buildDateRange (line 407) | function buildDateRange(dateRange: DateRange) {
  function buildMarkers (line 446) | function buildMarkers(lines: LineArray, markers: SpliceInfo[]) {
  function buildParts (line 463) | function buildParts(lines: LineArray, parts: PartialSegment[]) {
  function buildDefines (line 496) | function buildDefines(define: Record<string, string>) {
  function stringify (line 504) | function stringify(playlist: MasterPlaylist | MediaPlaylist, postProcess...

FILE: test/fixtures/objects/8.1-Simple-Media-Playlist.js
  function createSegments (line 10) | function createSegments() {

FILE: test/fixtures/objects/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js
  function createSegments (line 10) | function createSegments() {

FILE: test/fixtures/objects/8.11-EXT-X-CUE-OUT-Media-Playlist.js
  function createSegments (line 10) | function createSegments() {

FILE: test/fixtures/objects/8.2-Live-Media-Playlist_using-HTTPS.js
  function createSegments (line 12) | function createSegments() {

FILE: test/fixtures/objects/8.3-Playlist-with-encrypted-Media-Segments.js
  function createSegments (line 15) | function createSegments() {

FILE: test/fixtures/objects/8.4-Master-Playlist.js
  function createVariants (line 7) | function createVariants() {

FILE: test/fixtures/objects/8.5-Master-Playlist-with-I-Frames.js
  function createVariants (line 7) | function createVariants() {

FILE: test/fixtures/objects/8.6-Master-Playlist-with-Alternative-audio.js
  function createRendition (line 5) | function createRendition() {
  function createVariants (line 41) | function createVariants() {

FILE: test/fixtures/objects/8.7-Master-Playlist-with-Alternative-video.js
  function createRendition (line 3) | function createRendition(groupId) {
  function createVariants (line 33) | function createVariants() {

FILE: test/fixtures/objects/8.8-Session-Data-in-a-Master-Playlist.js
  function createSetssionDataList (line 7) | function createSetssionDataList() {

FILE: test/fixtures/objects/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js
  function createRendition (line 5) | function createRendition() {
  function createVariants (line 44) | function createVariants() {

FILE: test/fixtures/objects/Low-Latency_Example-01_Low-Latency_HLS_Playlist.js
  function createSegments (line 13) | function createSegments() {
  function createRenditionReports (line 76) | function createRenditionReports() {
  function createParts1 (line 91) | function createParts1() {
  function createParts2 (line 103) | function createParts2() {
  function createParts3 (line 116) | function createParts3() {

FILE: test/fixtures/objects/Low-Latency_Example-02_Playlist_Delta_Update.js
  function createSegments (line 14) | function createSegments() {
  function createRenditionReports (line 54) | function createRenditionReports() {
  function createParts1 (line 69) | function createParts1() {
  function createParts2 (line 81) | function createParts2() {
  function createParts3 (line 94) | function createParts3() {

FILE: test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-01.js
  function createSegments (line 13) | function createSegments() {
  function createParts (line 36) | function createParts() {

FILE: test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-02.js
  function createSegments (line 13) | function createSegments() {
  function createParts (line 48) | function createParts() {

FILE: test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-03.js
  function createSegments (line 13) | function createSegments() {
  function createParts (line 55) | function createParts() {

FILE: test/fixtures/objects/RedundantSegments.js
  function createSegments (line 10) | function createSegments() {

FILE: test/fixtures/objects/SCTE-35_01.js
  function createSegments (line 11) | function createSegments() {

FILE: test/fixtures/objects/SCTE-35_02.js
  function createSegments (line 11) | function createSegments() {

FILE: test/fixtures/objects/SCTE-35_03.js
  function createSegments (line 11) | function createSegments() {

FILE: test/fixtures/objects/SCTE-35_04.js
  function createSegments (line 11) | function createSegments() {

FILE: test/fixtures/objects/SCTE-35_05.js
  function createSegments (line 11) | function createSegments() {

FILE: test/fixtures/objects/SCTE-35_06.js
  function createSegments (line 11) | function createSegments() {

FILE: test/fixtures/objects/SCTE-35_07.js
  function createSegments (line 10) | function createSegments() {

FILE: test/fixtures/objects/Streaming-Examples_bipbop_16x9_variant.js
  function createVariants (line 115) | function createVariants() {

FILE: test/fixtures/objects/Streaming-Examples_img_bipbop_adv_example_ts_master.js
  function createVariants (line 71) | function createVariants() {

FILE: test/helpers/matchers.js
  function removeSpaceFromLine (line 1) | function removeSpaceFromLine(line) {
  function strip (line 15) | function strip(playlist) {
  function equalPlaylist (line 31) | function equalPlaylist(t, expected, actual) {
  function notEqualPlaylist (line 40) | function notEqualPlaylist(t, expected, actual) {

FILE: test/helpers/utils.js
  constant HLS (line 1) | const HLS = require('../..');
  function parsePass (line 5) | function parsePass(t, text) {
  function stringifyPass (line 16) | function stringifyPass(t, obj) {
  function bothPass (line 27) | function bothPass(t, text) {
  function parseFail (line 32) | function parseFail(t, text) {
  function stringifyFail (line 41) | function stringifyFail(t, obj) {
  function stripSpaces (line 50) | function stripSpaces(text) {
  function stripCommentsAndEmptyLines (line 66) | function stripCommentsAndEmptyLines(text) {

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.2_EXT-X-BYTERANGE.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.3_EXT-X-DISCONTINUITY.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.4_EXT-X-KEY.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.5_EXT-X-MAP.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.6_EXT-X-PROGRAM-DATE-TIME.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.7_EXT-X-DATERANGE.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.4.4.7_EXT-X-GAP.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.2_EXT-X-MEDIA-SEQUENCE.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.3_EXT-X-DISCONTINUITY-SEQUENCE.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.5_EXT-X-PLAYLIST-TYPE.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.1_EXT-X-MEDIA.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.2_EXT-X-STREAM-INF.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.2_EXT-X-STREAM-INF_2.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.5_Media-or-Master-Playlist-Tags/4.3.5.1_EXT-X-INDEPENDENT-SEGMENTS.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.5_Media-or-Master-Playlist-Tags/4.3.5.2_EXT-X-START.spec.js
  constant HLS (line 2) | const HLS = require('../../../../..');

FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/01_EXT-X-SERVER-CONTROL.spec.js
  constant HLS (line 3) | const HLS = require('../../../..');

FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/02_EXT-X-PART-INF.spec.js
  constant HLS (line 3) | const HLS = require('../../../..');

FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/03_EXT-X-PART.spec.js
  constant HLS (line 3) | const HLS = require('../../../..');

FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/04_EXT-X-PRELOAD-HINT.spec.js
  constant HLS (line 3) | const HLS = require('../../../..');

FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/05_EXT-X-RENDITION-REPORT.spec.js
  constant HLS (line 3) | const HLS = require('../../../..');

FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/06_EXT-X-SKIP.spec.js
  constant HLS (line 3) | const HLS = require('../../../..');

FILE: test/spec/Apple_HLS_Overview/02_Using_HLS.spec.js
  constant HLS (line 2) | const HLS = require('../../..');

FILE: test/spec/HLSJS-LHLS/01_EXT-X-PREFETCH.spec.js
  constant HLS (line 3) | const HLS = require("../../..");

FILE: test/spec/HLSJS-LHLS/02_EXT-X-PREFETCH-DISCONTINUITY.spec.js
  constant HLS (line 3) | const HLS = require("../../..");

FILE: test/spec/misc/multiple-rendition-groups.js
  constant HLS (line 3) | const HLS = require("../../..");

FILE: test/spec/misc/scte-35.spec.js
  constant HLS (line 3) | const HLS = require("../../..");

FILE: test/spec/parser.spec.js
  constant HLS (line 3) | const HLS = require('../..');
  function buildMessage (line 18) | function buildMessage(propName, actual, expected) {
  function deepEqual (line 34) | function deepEqual(t, actual, expected) {
  function deepEqualVariant (line 132) | function deepEqualVariant(t, actual, expected) {
  function deepEqualSessionData (line 216) | function deepEqualSessionData(t, actual, expected) {
  function deepEqualKey (line 236) | function deepEqualKey(t, actual, expected) {
  function deepEqualSegment (line 266) | function deepEqualSegment(t, actual, expected) {
  function deepEqualRendition (line 322) | function deepEqualRendition(t, actual, expected) {
  function deepEqualMap (line 366) | function deepEqualMap(t, actual, expected) {
  function deepEqualDateRange (line 382) | function deepEqualDateRange(t, actual, expected) {

FILE: test/spec/stringify.spec.js
  constant HLS (line 4) | const HLS = require('../..');

FILE: types.ts
  type RenditionType (line 3) | type RenditionType = 'AUDIO' | 'VIDEO' | 'SUBTITLES' | 'CLOSED-CAPTIONS';
  class Rendition (line 5) | class Rendition {
    method constructor (line 20) | constructor({
  class Variant (line 53) | class Variant {
    method constructor (line 74) | constructor({
  class SessionData (line 119) | class SessionData {
    method constructor (line 125) | constructor({
  class Key (line 140) | class Key {
    method constructor (line 147) | constructor({
  class ContentSteering (line 165) | class ContentSteering {
    method constructor (line 169) | constructor({
  type Byterange (line 178) | type Byterange = {
  class MediaInitializationSection (line 183) | class MediaInitializationSection {
    method constructor (line 189) | constructor({
  class DateRange (line 203) | class DateRange {
    method constructor (line 214) | constructor({
  class SpliceInfo (line 240) | class SpliceInfo {
    method constructor (line 246) | constructor({
  type DataType (line 262) | type DataType = 'part' | 'playlist' | 'prefetch' | 'segment';
  class Data (line 264) | class Data {
    method constructor (line 267) | constructor(type: DataType) {
  class Playlist (line 273) | class Playlist extends Data {
    method constructor (line 282) | constructor({
  class MasterPlaylist (line 303) | class MasterPlaylist extends Playlist {
    method constructor (line 311) | constructor(params: Partial<MasterPlaylist> = {}) {
  type LowLatencyCompatibility (line 328) | type LowLatencyCompatibility = {
  class MediaPlaylist (line 335) | class MediaPlaylist extends Playlist {
    method constructor (line 352) | constructor(params: Partial<MediaPlaylist> = {}) {
  class Segment (line 387) | class Segment extends Data {
    method constructor (line 405) | constructor({
  class PartialSegment (line 444) | class PartialSegment extends Data {
    method constructor (line 452) | constructor({
  class PrefetchSegment (line 472) | class PrefetchSegment extends Data {
    method constructor (line 479) | constructor({
  class RenditionReport (line 496) | class RenditionReport {
    method constructor (line 501) | constructor({
  type AllowedCpc (line 531) | type AllowedCpc = {
  type ExtInfo (line 536) | type ExtInfo = {
  type Resolution (line 541) | type Resolution = {
  type TagParam (line 546) | type TagParam =
  type UserAttribute (line 554) | type UserAttribute = number | string | Uint8Array;
  type PostProcess (line 556) | type PostProcess = {

FILE: utils.ts
  type Options (line 2) | type Options = {
  function THROW (line 10) | function THROW(err: Error) {
  function ASSERT (line 20) | function ASSERT(msg: string, ...options: boolean[]) {
  function CONDITIONALASSERT (line 28) | function CONDITIONALASSERT(...options) {
  function PARAMCHECK (line 39) | function PARAMCHECK(...options) {
  function CONDITIONALPARAMCHECK (line 47) | function CONDITIONALPARAMCHECK(...options) {
  function INVALIDPLAYLIST (line 58) | function INVALIDPLAYLIST(msg: string) {
  function toNumber (line 62) | function toNumber(str: string, radix = 10) {
  function hexToByteSequence (line 73) | function hexToByteSequence(str: string): Uint8Array {
  function byteSequenceToHex (line 84) | function byteSequenceToHex(sequence: ArrayBuffer, start = 0, end = seque...
  function tryCatch (line 95) | function tryCatch<T>(body: () => T, errorHandler: (err: unknown) => T): T {
  function splitAt (line 103) | function splitAt(str: string, delimiter: string, index = 0): [string] | ...
  function trim (line 119) | function trim(str: string | undefined, char = ' ') {
  function splitByCommaWithPreservingQuotes (line 136) | function splitByCommaWithPreservingQuotes(str: string) {
  function camelify (line 164) | function camelify(str: string) {
  function formatDate (line 182) | function formatDate(date: Date) {
  function hasOwnProp (line 193) | function hasOwnProp(obj: object, propName: string): boolean {
  function setOptions (line 197) | function setOptions(newOptions: Partial<Options> = {}): void {
  function getOptions (line 201) | function getOptions(): Options {
Condensed preview — 113 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (363K chars).
[
  {
    "path": ".github/workflows/release.yml",
    "chars": 548,
    "preview": "name: release\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch: {}\nconcurrency:\n  group: ${{ github.workflow }"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 427,
    "preview": "name: HLS parser tests\non: [ push ]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-ve"
  },
  {
    "path": ".gitignore",
    "chars": 691,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscov"
  },
  {
    "path": ".npmignore",
    "chars": 736,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscov"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2020 Kuu Miyazaki\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "README.md",
    "chars": 26447,
    "preview": "[![HLS parser tests](https://github.com/kuu/hls-parser/actions/workflows/tests.yml/badge.svg)](https://github.com/kuu/hl"
  },
  {
    "path": "index.ts",
    "chars": 281,
    "preview": "/*! Copyright Kuu Miyazaki. SPDX-License-Identifier: MIT */\nimport { getOptions, setOptions } from './utils';\nimport par"
  },
  {
    "path": "package.json",
    "chars": 3973,
    "preview": "{\n  \"name\": \"hls-parser\",\n  \"version\": \"0.16.1\",\n  \"description\": \"A simple library to read/write HLS playlists\",\n  \"mai"
  },
  {
    "path": "parse.ts",
    "chars": 39500,
    "preview": "import * as utils from './utils';\nimport {\n  AllowedCpc,\n  ExtInfo,\n  Rendition,\n  Resolution,\n  TagParam,\n  UserAttribu"
  },
  {
    "path": "stringify.ts",
    "chars": 16105,
    "preview": "import * as utils from './utils';\nimport {\n  Byterange,\n  DateRange, Key,\n  MasterPlaylist,\n  MediaInitializationSection"
  },
  {
    "path": "test/fixtures/m3u8/8.1-Simple-Media-Playlist.m3u8",
    "chars": 213,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:10\n#EXTINF:9.009,\nhttp://media.example.com/first.ts\n#EXTINF:9.009,\nhttp:/"
  },
  {
    "path": "test/fixtures/m3u8/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.m3u8",
    "chars": 937,
    "preview": "# This example shows two EXT-X-DATERANGE tags that describe a single\n# Date Range, with a SCTE-35 \"out\" splice_insert() "
  },
  {
    "path": "test/fixtures/m3u8/8.11-EXT-X-CUE-OUT-Media-Playlist.m3u8",
    "chars": 240,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:10\n#EXTINF:9.009,\nhttp://media.example.com/first.ts\n#EXT-X-CUE-OUT:DURATI"
  },
  {
    "path": "test/fixtures/m3u8/8.2-Live-Media-Playlist_using-HTTPS.m3u8",
    "chars": 257,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:8\n#EXT-X-MEDIA-SEQUENCE:2680\n\n#EXTINF:7.975,\nhttps://priv.example.com/fil"
  },
  {
    "path": "test/fixtures/m3u8/8.3-Playlist-with-encrypted-Media-Segments.m3u8",
    "chars": 456,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:15\n#EXT-X-MEDIA-SEQUENCE:7794\n\n#EXT-X-KEY:METHOD=AES-128,URI=\"https://pri"
  },
  {
    "path": "test/fixtures/m3u8/8.4-Master-Playlist.m3u8",
    "chars": 458,
    "preview": "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000,CODECS=\"avc1.640029,mp4a.40.2\"\nhttp://example.com/"
  },
  {
    "path": "test/fixtures/m3u8/8.5-Master-Playlist-with-I-Frames.m3u8",
    "chars": 596,
    "preview": "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"avc1.640029,mp4a.40.2\"\nlow/audio-video.m3u8\n#EXT-X-I-FRAME-STREAM-IN"
  },
  {
    "path": "test/fixtures/m3u8/8.6-Master-Playlist-with-Alternative-audio.m3u8",
    "chars": 907,
    "preview": "# In this example, the CODECS attributes have been condensed for space.\n# A '\\' is used to indicate that the tag continu"
  },
  {
    "path": "test/fixtures/m3u8/8.7-Master-Playlist-with-Alternative-video.m3u8",
    "chars": 1835,
    "preview": "# This example shows 3 different video Renditions (Main, Centerfield\n# and Dugout), and 3 different Variant Streams (low"
  },
  {
    "path": "test/fixtures/m3u8/8.8-Session-Data-in-a-Master-Playlist.m3u8",
    "chars": 312,
    "preview": "# In this example, only the EXT-X-SESSION-DATA is shown:\n#EXTM3U\n\n#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\""
  },
  {
    "path": "test/fixtures/m3u8/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.m3u8",
    "chars": 1162,
    "preview": "# In this example, the CODECS attributes have been condensed for space.\n# A '\\' is used to indicate that the tag continu"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-01_Low-Latency_HLS_Playlist.m3u8",
    "chars": 2379,
    "preview": "#EXTM3U\n# This Playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_msn=273&_HLS_part=2\n#EXT-X-VERS"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-02_Playlist_Delta_Update.m3u8",
    "chars": 2320,
    "preview": "#EXTM3U\n# Following the example above, this Playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_ms"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-01.m3u8",
    "chars": 590,
    "preview": "#EXTM3U\n#EXT-X-VERSION:9\n#EXT-X-TARGETDURATION:4\n#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-02.m3u8",
    "chars": 697,
    "preview": "#EXTM3U\n#EXT-X-VERSION:9\n#EXT-X-TARGETDURATION:4\n#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-03.m3u8",
    "chars": 771,
    "preview": "#EXTM3U\n#EXT-X-VERSION:9\n#EXT-X-TARGETDURATION:4\n#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-"
  },
  {
    "path": "test/fixtures/m3u8/Multiple-rendition-groups.m3u8",
    "chars": 861,
    "preview": "#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-INDEPENDENT-SEGMENTS\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_high\",NAME=\"English\",DEFAULT="
  },
  {
    "path": "test/fixtures/m3u8/RedundantSegments.m3u8",
    "chars": 288,
    "preview": "#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-TARGETDURATION:10\n#EXTINF:9.009,\nhttp://media.example.com/first.ts\n#EXTINF:9.009,\nhttp:/"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_01.m3u8",
    "chars": 222,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:8\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:8.008,\n1.ts\n#EXT-X-CUE-OUT:DURATION=15"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_02.m3u8",
    "chars": 311,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:8\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:8.008,\n1.ts\n#EXT-X-CUE-OUT:DURATION=23"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_03.m3u8",
    "chars": 247,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:8\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:8.008,\n1.ts\n#EXT-X-CUE:DURATION=\"15.0\""
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_04.m3u8",
    "chars": 444,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:8\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:8.008,\n1.ts\n#EXT-OATCLS-SCTE35:/DA0AAA"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_05.m3u8",
    "chars": 383,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:8\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:8.008,\n1.ts\n#EXT-X-SCTE35:TYPE=0x34,DU"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_06.m3u8",
    "chars": 513,
    "preview": "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:8\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:8.008,\n1.ts\n#EXT-X-CUE-OUT:DURATION=23"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_07.m3u8",
    "chars": 892,
    "preview": "# This example shows two EXT-X-DATERANGE tags that describe a single\n# Date Range, with a SCTE-35 \"out\" splice_insert() "
  },
  {
    "path": "test/fixtures/m3u8/Streaming-Examples_bipbop_16x9_variant.m3u8",
    "chars": 3270,
    "preview": "#EXTM3U\n\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"bipbop_audio\",NAME=\"BipBop Audio 1\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"eng\""
  },
  {
    "path": "test/fixtures/m3u8/Streaming-Examples_img_bipbop_adv_example_ts_master.m3u8",
    "chars": 6284,
    "preview": "#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-INDEPENDENT-SEGMENTS\n\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",NAME=\"English\",DEFAULT=YES"
  },
  {
    "path": "test/fixtures/objects/8.1-Simple-Media-Playlist.js",
    "chars": 814,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 3,\n  target"
  },
  {
    "path": "test/fixtures/objects/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js",
    "chars": 1983,
    "preview": "const {MediaPlaylist, Segment, DateRange} = require('../../../types');\nconst utils = require('../../../utils');\n\nconst p"
  },
  {
    "path": "test/fixtures/objects/8.11-EXT-X-CUE-OUT-Media-Playlist.js",
    "chars": 876,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 3,\n  target"
  },
  {
    "path": "test/fixtures/objects/8.2-Live-Media-Playlist_using-HTTPS.js",
    "chars": 942,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst mediaSequenceBase = 2680;\n\nconst playlist = new Media"
  },
  {
    "path": "test/fixtures/objects/8.3-Playlist-with-encrypted-Media-Segments.js",
    "chars": 1393,
    "preview": "const {MediaPlaylist, Segment, Key} = require('../../../types');\n\nconst mediaSequenceBase = 7794;\n\nconst key1 = new Key("
  },
  {
    "path": "test/fixtures/objects/8.4-Master-Playlist.js",
    "chars": 867,
    "preview": "const {MasterPlaylist, Variant} = require('../../../types');\n\nconst playlist = new MasterPlaylist({\n  variants: createVa"
  },
  {
    "path": "test/fixtures/objects/8.5-Master-Playlist-with-I-Frames.js",
    "chars": 1146,
    "preview": "const {MasterPlaylist, Variant} = require('../../../types');\n\nconst playlist = new MasterPlaylist({\n  variants: createVa"
  },
  {
    "path": "test/fixtures/objects/8.6-Master-Playlist-with-Alternative-audio.js",
    "chars": 1668,
    "preview": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = createRendition();\n\nfunction"
  },
  {
    "path": "test/fixtures/objects/8.7-Master-Playlist-with-Alternative-video.js",
    "chars": 1424,
    "preview": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nfunction createRendition(groupId) {\n  const ren"
  },
  {
    "path": "test/fixtures/objects/8.8-Session-Data-in-a-Master-Playlist.js",
    "chars": 639,
    "preview": "const {MasterPlaylist, SessionData} = require('../../../types');\n\nconst playlist = new MasterPlaylist({\n  sessionDataLis"
  },
  {
    "path": "test/fixtures/objects/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js",
    "chars": 1941,
    "preview": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = createRendition();\n\nfunction"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-01_Low-Latency_HLS_Playlist.js",
    "chars": 3110,
    "preview": "const {MediaPlaylist, Segment, PartialSegment, MediaInitializationSection, RenditionReport} = require('../../../types');"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-02_Playlist_Delta_Update.js",
    "chars": 2480,
    "preview": "const {MediaPlaylist, Segment, PartialSegment, RenditionReport} = require('../../../types');\n\nconst playlist = new Media"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-01.js",
    "chars": 1460,
    "preview": "const {MediaPlaylist, Segment, PartialSegment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  vers"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-02.js",
    "chars": 1768,
    "preview": "const {MediaPlaylist, Segment, PartialSegment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  vers"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-03.js",
    "chars": 1935,
    "preview": "const {MediaPlaylist, Segment, PartialSegment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  vers"
  },
  {
    "path": "test/fixtures/objects/Multiple-rendition-groups.js",
    "chars": 1320,
    "preview": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = [\n  new Rendition({type: 'AU"
  },
  {
    "path": "test/fixtures/objects/RedundantSegments.js",
    "chars": 1034,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 4,\n  target"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_01.js",
    "chars": 1139,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_02.js",
    "chars": 1378,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_03.js",
    "chars": 1175,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_04.js",
    "chars": 1602,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_05.js",
    "chars": 1386,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_06.js",
    "chars": 2302,
    "preview": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_07.js",
    "chars": 1901,
    "preview": "const {MediaPlaylist, Segment, DateRange} = require('../../../types');\nconst utils = require('../../../utils');\n\nconst p"
  },
  {
    "path": "test/fixtures/objects/Streaming-Examples_bipbop_16x9_variant.js",
    "chars": 5249,
    "preview": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = {\n  bipbop_audio: [\n    new "
  },
  {
    "path": "test/fixtures/objects/Streaming-Examples_img_bipbop_adv_example_ts_master.js",
    "chars": 10331,
    "preview": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = {\n  aud1: [\n    new Renditio"
  },
  {
    "path": "test/helpers/fixtures.js",
    "chars": 553,
    "preview": "const path = require('node:path');\nconst fs = require('node:fs');\n\nconst fixtures = [];\nconst baseDir = path.join(__dirn"
  },
  {
    "path": "test/helpers/matchers.js",
    "chars": 1109,
    "preview": "function removeSpaceFromLine(line) {\n  let inside = false;\n  let str = '';\n  for (const ch of line) {\n    if (ch === '\"'"
  },
  {
    "path": "test/helpers/utils.js",
    "chars": 1666,
    "preview": "const HLS = require('../..');\n\nHLS.setOptions({strictMode: true});\n\nfunction parsePass(t, text) {\n  let obj;\n  try {\n   "
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.1_EXTM3U.spec.js",
    "chars": 1038,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// It MUST be the first line of every "
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.2_EXT-X-VERSION.spec.js",
    "chars": 560,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// A Playlist file MUST NOT contain mo"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.1_EXTINF.spec.js",
    "chars": 1277,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// This tag is REQUIRED for each Media"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.2_EXT-X-BYTERANGE.spec.js",
    "chars": 3417,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.3_EXT-X-DISCONTINUITY.spec.js",
    "chars": 610,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\n\n// The EXT-X-DISCONTINUITY tag indicates a disconti"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.4_EXT-X-KEY.spec.js",
    "chars": 4514,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.5_EXT-X-MAP.spec.js",
    "chars": 4178,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.6_EXT-X-PROGRAM-DATE-TIME.spec.js",
    "chars": 480,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\n\n// It applies only to the next Media Segment.\ntest("
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.7_EXT-X-DATERANGE.spec.js",
    "chars": 9948,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2_Media-Segment-Tags.spec.js",
    "chars": 817,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// A Media Segment tag MUST NOT appear"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.4.4.7_EXT-X-GAP.spec.js",
    "chars": 1164,
    "preview": "const test = require(\"ava\");\nconst HLS = require('../../../../..');\nconst utils = require(\"../../../../helpers/utils\");\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.1_EXT-X-TARGETDURATION.spec.js",
    "chars": 999,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// The EXTINF duration of each Media S"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.2_EXT-X-MEDIA-SEQUENCE.spec.js",
    "chars": 1232,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.3_EXT-X-DISCONTINUITY-SEQUENCE.spec.js",
    "chars": 1964,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.4_EXT-X-ENDLIST.spec.js",
    "chars": 1186,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// It MAY occur anywhere in the Media "
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.5_EXT-X-PLAYLIST-TYPE.spec.js",
    "chars": 968,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\n\n// #EXT-X-PLAYLIST-TYPE:<EVENT|VOD>\ntest('#EXT-X-PL"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.6_EXT-X-I-FRAMES-ONLY.spec.js",
    "chars": 612,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// Use of the EXT-X-I-FRAMES-ONLY REQU"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.7_EXT-X-CUE-OUT.spec.js",
    "chars": 630,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\ntest('#EXT-X-CUE-OUT_01', t => {\n  let"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3_Media-Playlist-Tags.spec.js",
    "chars": 1176,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// There MUST NOT be more than one Med"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.1_EXT-X-MEDIA.spec.js",
    "chars": 6264,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.2_EXT-X-STREAM-INF.spec.js",
    "chars": 8865,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.2_EXT-X-STREAM-INF_2.spec.js",
    "chars": 541,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.3_EXT-X-I-FRAME-STREAM-INF.spec.js",
    "chars": 425,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// Every EXT-X-I-FRAME-STREAM-INF tag "
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.4_EXT-X-SESSION-DATA.spec.js",
    "chars": 1692,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// DATA-ID attribute is REQUIRED.\ntest"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.5_EXT-X-SESSION-KEY.spec.js",
    "chars": 1087,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// The value of the METHOD attribute M"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4_Master-Playlist-Tags.spec.js",
    "chars": 578,
    "preview": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// Master Playlist Tags MUST NOT appea"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.5_Media-or-Master-Playlist-Tags/4.3.5.1_EXT-X-INDEPENDENT-SEGMENTS.spec.js",
    "chars": 1753,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.5_Media-or-Master-Playlist-Tags/4.3.5.2_EXT-X-START.spec.js",
    "chars": 2672,
    "preview": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n"
  },
  {
    "path": "test/spec/7_Protocol-version-compatibility/7_EXT-X-VERSION.spec.js",
    "chars": 7161,
    "preview": "const test = require('ava');\nconst utils = require('../../helpers/utils');\n\n// A Playlist that contains tags or attribut"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/01_EXT-X-SERVER-CONTROL.spec.js",
    "chars": 5350,
    "preview": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// CA"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/02_EXT-X-PART-INF.spec.js",
    "chars": 5295,
    "preview": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// EX"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/03_EXT-X-PART.spec.js",
    "chars": 8495,
    "preview": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// Al"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/04_EXT-X-PRELOAD-HINT.spec.js",
    "chars": 6775,
    "preview": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// TY"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/05_EXT-X-RENDITION-REPORT.spec.js",
    "chars": 3749,
    "preview": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// UR"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/06_EXT-X-SKIP.spec.js",
    "chars": 2791,
    "preview": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// SK"
  },
  {
    "path": "test/spec/Apple_HLS_Overview/02_Using_HLS.spec.js",
    "chars": 922,
    "preview": "const test = require('ava');\nconst HLS = require('../../..');\nconst utils = require('../../helpers/utils');\n\n// Starting"
  },
  {
    "path": "test/spec/HLSJS-LHLS/01_EXT-X-PREFETCH.spec.js",
    "chars": 7401,
    "preview": "const test = require(\"ava\");\nconst utils = require(\"../../helpers/utils\");\nconst HLS = require(\"../../..\");\n\ntest(\"#EXT-"
  },
  {
    "path": "test/spec/HLSJS-LHLS/02_EXT-X-PREFETCH-DISCONTINUITY.spec.js",
    "chars": 1591,
    "preview": "const test = require(\"ava\");\nconst utils = require(\"../../helpers/utils\");\nconst HLS = require(\"../../..\");\n\ntest(\"#EXT-"
  },
  {
    "path": "test/spec/misc/multiple-rendition-groups.js",
    "chars": 2199,
    "preview": "const test = require(\"ava\");\nconst utils = require(\"../../helpers/utils\");\nconst HLS = require(\"../../..\");\n\ntest(\"Multi"
  },
  {
    "path": "test/spec/misc/scte-35.spec.js",
    "chars": 3971,
    "preview": "const test = require(\"ava\");\nconst utils = require(\"../../helpers/utils\");\nconst HLS = require(\"../../..\");\n\ntest(\"#EXT-"
  },
  {
    "path": "test/spec/parser.spec.js",
    "chars": 16405,
    "preview": "const test = require('ava');\nconst fixtures = require('../helpers/fixtures');\nconst HLS = require('../..');\n\nHLS.setOpti"
  },
  {
    "path": "test/spec/stringify.spec.js",
    "chars": 8274,
    "preview": "const test = require('ava');\nconst fixtures = require('../helpers/fixtures');\nconst utils = require('../helpers/utils');"
  },
  {
    "path": "test/spec/utils.spec.js",
    "chars": 5410,
    "preview": "const test = require('ava');\nconst rewire = require('rewire');\nconst utils = require('../../utils');\n\nutils.setOptions({"
  },
  {
    "path": "tsconfig.json",
    "chars": 260,
    "preview": "{\n  \"extends\": \"@tsconfig/node18/tsconfig.json\",\n  \"module\": \"node\",\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"ch"
  },
  {
    "path": "types.ts",
    "chars": 13134,
    "preview": "import * as utils from './utils';\n\ntype RenditionType = 'AUDIO' | 'VIDEO' | 'SUBTITLES' | 'CLOSED-CAPTIONS';\n\nclass Rend"
  },
  {
    "path": "utils.ts",
    "chars": 5216,
    "preview": "\ntype Options = {\n  strictMode?: boolean,\n  allowClosedCaptionsNone?: boolean,\n  silent?: boolean\n};\n\nlet options: Optio"
  }
]

About this extraction

This page contains the full source code of the kuu/hls-parser GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 113 files (331.3 KB), approximately 114.6k tokens, and a symbol index with 209 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!