[
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch: {}\nconcurrency:\n  group: ${{ github.workflow }}\n  cancel-in-progress: false\npermissions:\n  id-token: write  # Required for OIDC\n  contents: read\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-node@v6\n        with:\n          node-version: lts/*\n          registry-url: 'https://registry.npmjs.org'\n      - run: npm ci\n      - run: npm run build --if-present\n      - run: npm test\n      - run: npm publish\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: HLS parser tests\non: [ push ]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        node-version: [ 'current', 'lts/*', 'lts/-1' ]\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Node ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n      - run: npm ci\n      - run: npm test\n      - run: npm run build\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Editors\n.idea/\n.vscode/\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Typescript intermediate files\ntsconfig.tsbuildinfo\n*.d.ts\n*.js\n!test/**/*.js\n\n# Dependency directories\nnode_modules\njspm_packages\n\n# Optional npm cache directory\n.npm\n\n# Optional REPL history\n.node_repl_history\n\ndist\n"
  },
  {
    "path": ".npmignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules\njspm_packages\n\n# Editors\n.idea/\n.vscode/\n\n# Optional npm cache directory\n.npm\n\n# Optional REPL history\n.node_repl_history\n\ntest\n.github/\n.node-version\n.travis.yml\n.npmignore\ndata-structure.png\ntsconfig.tsbuildinfo\nwebpack.config.js\nLICENSE\n*.ts\n!*.d.ts\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Kuu Miyazaki\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "[![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)\n[![Coverage Status](https://coveralls.io/repos/github/kuu/hls-parser/badge.svg?branch=master)](https://coveralls.io/github/kuu/hls-parser?branch=master)\n[![Known Vulnerabilities](https://snyk.io/test/github/kuu/hls-parser/badge.svg)](https://snyk.io/test/github/kuu/hls-parser)\n[![npm Downloads](https://img.shields.io/npm/dw/hls-parser.svg?style=flat-square)](https://npmjs.com/hls-parser)\n[![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)\n\n\n# hls-parser\n\nProvides 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))\n\n## Install\n[![NPM](https://nodei.co/npm/hls-parser.png?mini=true)](https://nodei.co/npm/hls-parser/)\n[![](https://data.jsdelivr.com/v1/package/npm/hls-parser/badge)](https://www.jsdelivr.com/package/npm/hls-parser?path=dist)\n\n## Usage\n```js\nimport { parse, types, stringify } from 'hls-parser';\n\n// Parse the playlist\nconst playlist = parse(textData);\n// You can access the playlist as a JS object\nif (playlist.isMasterPlaylist) {\n  // Master playlist\n} else {\n  // Media playlist\n}\n// Create a new playlist\nconst {MediaPlaylist, Segment} = types;\nconst obj = new MediaPlaylist({\n  targetDuration: 9,\n  playlistType: 'VOD',\n  segments: [\n    new Segment({\n      uri: 'low/1.m3u8',\n      duration: 9\n    })\n  ]\n});\n// Convert the object into a text\nstringify(obj);\n/*\n#EXTM3U\n#EXT-X-TARGETDURATION:9\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXTINF:9,\nlow/1.m3u8\n*/\n```\n\n## API\n\n### `HLS.parse(str)`\nConverts a text playlist into a structured JS object\n\n#### params\n| Name    | Type   | Required | Default | Description   |\n| ------- | ------ | -------- | ------- | ------------- |\n| 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) |\n\n#### return value\nAn instance of either `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.)\n\n### `HLS.stringify(obj, postProcess)`\nConverts a JS object into a plain text playlist\n\n#### params\n| Name    | Type   | Required | Default | Description   |\n| ------- | ------ | -------- | ------- | ------------- |\n| obj     | `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.)  | Yes      | N/A     | An object returned by `HLS.parse()` or a manually created object |\n| postProcess     | PostProcess  | No      | undefined | A function to be called for each segment or variant to manipulate the output. |\n\n##### `PostProcess`\n| Property         | Type          | Required | Default | Description   |\n| ---------------- | ------------- | -------- | ------- | ------------- |\n| `segmentProcessor` | (lines: string[], start: number, end: number, segment: Segment, i: number) => void | No      | undefined     | A function to manipulate the segment output.  |\n| `variantProcessor` | (lines: string[], start: number, end: number, variant: Variant, i: number) => void | No      | undefined     | A function to manipulate the variant output.  |\n\n\n#### return value\nA text data that conforms to [the HLS playlist spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.1)\n\n### `HLS.setOptions(obj)`\nUpdates the option values\n\n#### params\n| Name    | Type   | Required | Default | Description   |\n| ------- | ------ | -------- | ------- | ------------- |\n| obj     | Object | Yes      | {}     | An object holding option values which will be used to overwrite the internal option values.  |\n\n##### supported options\n| Name       | Type    | Default | Description   |\n| ---------- | ------- | ------- | ------------- |\n| `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.|\n| `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) |\n| `silent` | boolean | false   | If true, `console.error` will be suppressed.|\n\n### `HLS.getOptions()`\nRetrieves the current option values\n\n#### return value\nA cloned object containing the current option values\n\n### `HLS.types`\nAn object that holds all the classes described below.\n\n\n## Data format\nThis section describes the structure of the object returned by `parse()` method.\n\n![data structure](./data-structure.png)\n\n### `Data`\n| Property         | Type          | Required | Default | Description   |\n| ---------------- | ------------- | -------- | ------- | ------------- |\n| `type` | string     | Yes      | N/A     | Either `playlist` or `segment` or `part`}  |\n\n### `Playlist` (extends `Data`)\n| Property         | Type          | Required | Default | Description   |\n| ---------------- | ------------- | -------- | ------- | ------------- |\n| `isMasterPlaylist` | boolean     | Yes      | N/A     | `true` if this playlist is a master playlist  |\n| `uri`              | string | No      | undefined     | Playlist URL  |\n| `version`          | number | No       | undefined      | See [EXT-X-VERSION](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.1.2) |\n| `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) |\n| `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) |\n| `source` | string     | No      | undefined     | The unprocessed text of the playlist  |\n\n### `MasterPlaylist` (extends `Playlist`)\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `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)  |\n| `currentVariant`  | number   | No       | undefined | Array index that points to the chosen item in `variants` |\n| `sessionDataList` | [`SessionData`]  | No       | []        | See [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) |\n| `sessionKeyList`      | [`Key`]    | No       | [] | See [EXT-X-SESSION-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.5) |\n\n### `Variant`\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `uri`        | string  | Yes       | N/A        | URI of the variant playlist  |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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)  |\n| `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)  |\n| `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) |\n| `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 |\n\n### `Rendition`\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `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) |\n| `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)  |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n\n### `SessionData`\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `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) |\n| `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) |\n| `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)  |\n| `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) |\n\n### `MediaPlaylist` (extends `Playlist`)\n| Property                    | Type     | Required | Default   | Description   |\n| --------------------------- | -------- | -------- | --------- | ------------- |\n| `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) |\n| `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) |\n| `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) |\n| `endlist`                   | boolean | No       | false        | See [EXT-X-ENDLIST](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.4) |\n| `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) |\n| `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) |\n| `segments`                  | [`Segment`] | No       | []        | A list of available segments |\n| `prefetchSegments`          | [`PrefetchSegment`] | No       | []        | A list of available prefetch segments |\n| `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) |\n| `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) |\n| `renditionReports`  | [`RenditionReport`]   | No       | [] | Update status of the associated renditions |\n| `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) |\n\n### `Segment` (extends `Data`)\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `uri`        | string  | Yes*       | N/A        | URI of the media segment. *Not required if the segment contains `EXT-X-PRELOAD-HINT` tag |\n| `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 |\n| `title`  | string   | No       | undefined | See [EXTINF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.1) |\n| `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) |\n| `discontinuity`  | boolean   | No       | undefined | See [EXT-X-DISCONTINUITY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.3) |\n| `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) |\n| `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) |\n| `key`  | `Key`   | No       | undefined | See [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) |\n| `map`  | `MediaInitializationSection`   | No       | undefined | See [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) |\n| `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) |\n| `dateRange`  | `DateRange`   | No       | undefined | See [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) |\n| `markers`  | [`SpliceInfo`]   | No       | [] | SCTE-35 messages associated with this segment|\n| `parts`  | [`PartialSegment`]   | No       | [] | Partial Segments that constitute this segment |\n| `gap` | boolean | No | undefined | See [EXT-X-GAP](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.7) |\n\n### `PartialSegment` (extends `Data`)\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `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) |\n| `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) |\n| `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`|\n| `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) |\n| `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) |\n| `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) |\n\n### `PrefetchSegment` (extends `Data`)\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `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) |\n| `discontinuity`  | boolean   | No       | undefined | See [EXT-X-PREFETCH-DISCONTINUITY](https://github.com/video-dev/hlsjs-rfcs/blob/lhls-spec/proposals/0001-lhls.md) |\n| `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) |\n| `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) |\n\n### `Key`\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n\n### `MediaInitializationSection`\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `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) |\n| `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) |\n| `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) |\n\n### `DateRange`\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n| `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) |\n\n### `SpliceInfo`\nOnly `EXT-X-CUE-OUT` and `EXT-X-CUE-IN` tags are supported. Other SCTE-35-related tags are stored as raw (string) values.\n\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `type`  | string   | Yes       | N/A | {'OUT', 'IN', 'RAW'} |\n| `duration`        | number   | No       | undefined        | Required if the `type` is 'OUT' |\n| `tagName`        | string   | No       | undefined        | Holds the tag name if any unsupported tag are found. Required if the `type` is 'RAW' |\n| `value`        | string   | No       | undefined        | Holds a raw (string) value for the unsupported tag. |\n\n### `RenditionReport`\n| Property          | Type     | Required | Default   | Description   |\n| ----------------- | -------- | -------- | --------- | ------------- |\n| `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) |\n| `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) |\n| `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) |\n"
  },
  {
    "path": "index.ts",
    "content": "/*! Copyright Kuu Miyazaki. SPDX-License-Identifier: MIT */\nimport { getOptions, setOptions } from './utils';\nimport parse from './parse';\nimport stringify from './stringify';\nimport * as types from './types';\n\nexport {\n  parse,\n  stringify,\n  types,\n  getOptions,\n  setOptions\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"hls-parser\",\n  \"version\": \"0.16.1\",\n  \"description\": \"A simple library to read/write HLS playlists\",\n  \"main\": \"index.js\",\n  \"types\": \"index.d.ts\",\n  \"browser\": \"dist/hls-parser.min.js\",\n  \"scripts\": {\n    \"lint\": \"xo\",\n    \"type-check\": \"tsc --noEmit\",\n    \"audit\": \"npm audit --audit-level high\",\n    \"build\": \"rm -fR ./dist; tsc ; webpack --mode development ; webpack --mode production\",\n    \"test\": \"npm run lint && npm run build && npm run audit && ava --verbose\",\n    \"test-offline\": \"npm run lint && npm run build && ava --verbose\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/kuu/hls-parser.git\"\n  },\n  \"keywords\": [\n    \"HLS\",\n    \"media\",\n    \"video\",\n    \"audio\",\n    \"streaming\"\n  ],\n  \"author\": \"Kuu Miyazaki\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/kuu/hls-parser/issues\"\n  },\n  \"homepage\": \"https://github.com/kuu/hls-parser#readme\",\n  \"devDependencies\": {\n    \"@ava/typescript\": \"^6.0.0\",\n    \"@babel/core\": \"^7.28.4\",\n    \"@babel/eslint-parser\": \"^7.28.4\",\n    \"@babel/preset-env\": \"^7.28.3\",\n    \"@tsconfig/node18\": \"^18.2.4\",\n    \"ava\": \"^6.4.1\",\n    \"babel-loader\": \"^10.0.0\",\n    \"eslint-plugin-unicorn\": \"^61.0.2\",\n    \"rewire\": \"^9.0.1\",\n    \"terser-webpack-plugin\": \"^5.3.14\",\n    \"ts-loader\": \"^9.5.4\",\n    \"typescript\": \"^5.9.3\",\n    \"webpack\": \"^5.102.1\",\n    \"webpack-cli\": \"^6.0.1\",\n    \"xo\": \"^0.60.0\"\n  },\n  \"ava\": {\n    \"typescript\": {\n      \"compile\": \"tsc\",\n      \"extensions\": [\n        \"ts\",\n        \"js\"\n      ],\n      \"rewritePaths\": {}\n    }\n  },\n  \"xo\": {\n    \"esnext\": true,\n    \"space\": true,\n    \"rules\": {\n      \"arrow-body-style\": 0,\n      \"ava/no-ignored-test-files\": 0,\n      \"camelcase\": 0,\n      \"comma-dangle\": 0,\n      \"capitalized-comments\": 0,\n      \"dot-notation\": 0,\n      \"guard-for-in\": 0,\n      \"import/extensions\": 0,\n      \"import/no-dynamic-require\": 0,\n      \"new-cap\": 0,\n      \"no-bitwise\": 0,\n      \"no-cond-assign\": 0,\n      \"no-mixed-operators\": 0,\n      \"no-multi-assign\": 0,\n      \"no-use-extend-native/no-use-extend-native\": 0,\n      \"object-curly-newline\": 0,\n      \"operator-linebreak\": 0,\n      \"padding-line-between-statements\": 0,\n      \"quotes\": 0,\n      \"unicorn/catch-error-name\": 0,\n      \"unicorn/filename-case\": 0,\n      \"unicorn/no-lonely-if\": 0,\n      \"unicorn/no-useless-spread\": 0,\n      \"unicorn/no-zero-fractions\": 0,\n      \"unicorn/numeric-separators-style\": 0,\n      \"unicorn/prefer-code-point\": 0,\n      \"unicorn/prefer-module\": 0,\n      \"unicorn/prefer-switch\": 0,\n      \"unicorn/prevent-abbreviations\": 0,\n      \"unicorn/switch-case-braces\": 0\n    },\n    \"overrides\": [\n      {\n        \"files\": \"test/**/*.js\",\n        \"rules\": {\n          \"unicorn/no-array-push-push\": 0\n        }\n      },\n      {\n        \"files\": \"*.ts\",\n        \"rules\": {\n          \"n/file-extension-in-import\": 0,\n          \"@typescript-eslint/array-type\": 1,\n          \"@typescript-eslint/ban-types\": 1,\n          \"@typescript-eslint/comma-dangle\": 0,\n          \"@typescript-eslint/consistent-type-imports\": 0,\n          \"@typescript-eslint/dot-notation\": 0,\n          \"@typescript-eslint/member-delimiter-style\": 0,\n          \"@typescript-eslint/naming-convention\": 0,\n          \"@typescript-eslint/no-unsafe-call\": 0,\n          \"@typescript-eslint/no-unsafe-argument\": 0,\n          \"@typescript-eslint/no-unsafe-assignment\": 0,\n          \"@typescript-eslint/no-unsafe-return\": 0,\n          \"@typescript-eslint/object-curly-spacing\": 0,\n          \"@typescript-eslint/padding-line-between-statements\": 0,\n          \"@typescript-eslint/prefer-optional-chain\": 1,\n          \"@typescript-eslint/prefer-nullish-coalescing\": 0,\n          \"@typescript-eslint/quotes\": 0,\n          \"@typescript-eslint/restrict-template-expressions\": 0,\n          \"@typescript-eslint/restrict-plus-operands\": 0,\n          \"unicorn/prefer-export-from\": 0\n        }\n      }\n    ],\n    \"settings\": {\n      \"import/resolver\": {\n        \"node\": {}\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "parse.ts",
    "content": "import * as utils from './utils';\nimport {\n  AllowedCpc,\n  ExtInfo,\n  Rendition,\n  Resolution,\n  TagParam,\n  UserAttribute,\n  Variant,\n  SessionData,\n  Key,\n  MediaInitializationSection,\n  Byterange,\n  DateRange,\n  SpliceInfo,\n  MasterPlaylist,\n  MediaPlaylist,\n  Segment,\n  PartialSegment,\n  PrefetchSegment,\n  RenditionReport,\n  ContentSteering\n} from './types';\n\nfunction unquote(str: string | undefined) {\n  return utils.trim(str, '\"');\n}\n\ntype TagCategory = 'Basic' | 'Segment' | 'MasterPlaylist' | 'MediaPlaylist' | 'MediaorMasterPlaylist' | 'Unknown';\n\nfunction getTagCategory(tagName: string): TagCategory {\n  switch (tagName) {\n    case 'EXTM3U':\n    case 'EXT-X-VERSION':\n    case 'EXT-X-CONTENT-STEERING':\n      return 'Basic';\n    case 'EXTINF':\n    case 'EXT-X-BYTERANGE':\n    case 'EXT-X-DISCONTINUITY':\n    case 'EXT-X-PREFETCH-DISCONTINUITY':\n    case 'EXT-X-KEY':\n    case 'EXT-X-MAP':\n    case 'EXT-X-PROGRAM-DATE-TIME':\n    case 'EXT-X-DATERANGE':\n    case 'EXT-X-CUE-OUT':\n    case 'EXT-X-CUE-IN':\n    case 'EXT-X-CUE-OUT-CONT':\n    case 'EXT-X-CUE':\n    case 'EXT-OATCLS-SCTE35':\n    case 'EXT-X-ASSET':\n    case 'EXT-X-SCTE35':\n    case 'EXT-X-PART':\n    case 'EXT-X-PRELOAD-HINT':\n    case 'EXT-X-GAP':\n      return 'Segment';\n    case 'EXT-X-TARGETDURATION':\n    case 'EXT-X-MEDIA-SEQUENCE':\n    case 'EXT-X-DISCONTINUITY-SEQUENCE':\n    case 'EXT-X-ENDLIST':\n    case 'EXT-X-PLAYLIST-TYPE':\n    case 'EXT-X-I-FRAMES-ONLY':\n    case 'EXT-X-SERVER-CONTROL':\n    case 'EXT-X-PART-INF':\n    case 'EXT-X-PREFETCH':\n    case 'EXT-X-RENDITION-REPORT':\n    case 'EXT-X-SKIP':\n      return 'MediaPlaylist';\n    case 'EXT-X-MEDIA':\n    case 'EXT-X-STREAM-INF':\n    case 'EXT-X-I-FRAME-STREAM-INF':\n    case 'EXT-X-SESSION-DATA':\n    case 'EXT-X-SESSION-KEY':\n      return 'MasterPlaylist';\n    case 'EXT-X-INDEPENDENT-SEGMENTS':\n    case 'EXT-X-START':\n    case 'EXT-X-DEFINE':\n      return 'MediaorMasterPlaylist';\n    default:\n      return 'Unknown';\n  }\n}\n\nfunction parseEXTINF(param: string): ExtInfo {\n  const pair = utils.splitAt(param, ',') as [string, string];\n  return {duration: utils.toNumber(pair[0]), title: decodeURIComponent(escape(pair[1]))};\n}\n\nfunction parseBYTERANGE(param: string): Byterange {\n  const pair = utils.splitAt(param, '@');\n  return {length: utils.toNumber(pair[0]), offset: pair[1] ? utils.toNumber(pair[1]) : -1};\n}\n\nfunction parseResolution(str: string): Resolution {\n  const pair = utils.splitAt(str, 'x') as [string, string];\n  return {width: utils.toNumber(pair[0]), height: utils.toNumber(pair[1])};\n}\n\nfunction parseAllowedCpc(str: string): AllowedCpc[] {\n  const message = 'ALLOWED-CPC: Each entry must consit of KEYFORMAT and Content Protection Configuration';\n  const list = str.split(',');\n  if (list.length === 0) {\n    utils.INVALIDPLAYLIST(message);\n  }\n  const allowedCpcList: AllowedCpc[] = [];\n  for (const item of list) {\n    const [format, cpcText] = utils.splitAt(item, ':');\n    if (!format || !cpcText) {\n      utils.INVALIDPLAYLIST(message);\n      continue;\n    }\n    allowedCpcList.push({format, cpcList: cpcText.split('/')});\n  }\n  return allowedCpcList;\n}\n\nfunction parseIV(str: string): Uint8Array {\n  const iv = utils.hexToByteSequence(str);\n  if (iv.length !== 16) {\n    utils.INVALIDPLAYLIST('IV must be a 128-bit unsigned integer');\n  }\n  return iv;\n}\n\nfunction parseUserAttribute(str: string): UserAttribute {\n  if (str.startsWith('\"')) {\n    return unquote(str)!;\n  }\n  if (str.startsWith('0x') || str.startsWith('0X')) {\n    return utils.hexToByteSequence(str);\n  }\n  return utils.toNumber(str);\n}\n\nfunction setCompatibleVersionOfKey(params: Record<string, any>, attributes: Record<string, any>) {\n  if (attributes['IV'] && params.compatibleVersion < 2) {\n    params.compatibleVersion = 2;\n  }\n  if ((attributes['KEYFORMAT'] || attributes['KEYFORMATVERSIONS']) && params.compatibleVersion < 5) {\n    params.compatibleVersion = 5;\n  }\n}\n\nfunction parseAttributeList(param): Record<string, any> {\n  const attributes = {};\n  for (const item of utils.splitByCommaWithPreservingQuotes(param)) {\n    const [key, value] = utils.splitAt(item, '=');\n    const val = unquote(value)!;\n    switch (key) {\n      case 'URI':\n        attributes[key] = val;\n        break;\n      case 'START-DATE':\n      case 'END-DATE':\n        attributes[key] = new Date(val);\n        break;\n      case 'IV':\n        attributes[key] = parseIV(val);\n        break;\n      case 'BYTERANGE':\n        attributes[key] = parseBYTERANGE(val);\n        break;\n      case 'RESOLUTION':\n        attributes[key] = parseResolution(val);\n        break;\n      case 'ALLOWED-CPC':\n        attributes[key] = parseAllowedCpc(val);\n        break;\n      case 'END-ON-NEXT':\n      case 'DEFAULT':\n      case 'AUTOSELECT':\n      case 'FORCED':\n      case 'PRECISE':\n      case 'CAN-BLOCK-RELOAD':\n      case 'INDEPENDENT':\n      case 'GAP':\n        attributes[key] = val === 'YES';\n        break;\n      case 'DURATION':\n      case 'PLANNED-DURATION':\n      case 'BANDWIDTH':\n      case 'AVERAGE-BANDWIDTH':\n      case 'FRAME-RATE':\n      case 'TIME-OFFSET':\n      case 'CAN-SKIP-UNTIL':\n      case 'HOLD-BACK':\n      case 'PART-HOLD-BACK':\n      case 'PART-TARGET':\n      case 'BYTERANGE-START':\n      case 'BYTERANGE-LENGTH':\n      case 'LAST-MSN':\n      case 'LAST-PART':\n      case 'SKIPPED-SEGMENTS':\n      case 'SCORE':\n      case 'PROGRAM-ID':\n        attributes[key] = utils.toNumber(val);\n        break;\n      default:\n        if (key.startsWith('SCTE35-')) {\n          attributes[key] = utils.hexToByteSequence(val);\n        } else if (key.startsWith('X-')) {\n          attributes[key] = parseUserAttribute(value!);\n        } else {\n          if (key === 'VIDEO-RANGE' && val !== 'SDR' && val !== 'HLG' && val !== 'PQ') {\n            utils.INVALIDPLAYLIST(`VIDEO-RANGE: unknown value \"${val}\"`);\n          }\n          attributes[key] = val;\n        }\n    }\n  }\n  return attributes;\n}\n\nfunction parseTagParam(name: string, param): TagParam {\n  switch (name) {\n    case 'EXTM3U':\n    case 'EXT-X-DISCONTINUITY':\n    case 'EXT-X-ENDLIST':\n    case 'EXT-X-I-FRAMES-ONLY':\n    case 'EXT-X-INDEPENDENT-SEGMENTS':\n    case 'EXT-X-CUE-IN':\n    case 'EXT-X-GAP':\n      return [null, null];\n    case 'EXT-X-VERSION':\n    case 'EXT-X-TARGETDURATION':\n    case 'EXT-X-MEDIA-SEQUENCE':\n    case 'EXT-X-DISCONTINUITY-SEQUENCE':\n      return [utils.toNumber(param), null];\n    case 'EXT-X-CUE-OUT':\n      // For backwards compatibility: attributes list is optional,\n      // if only a number is found, use it as the duration\n      if (!Number.isNaN(Number(param))) {\n        return [utils.toNumber(param), null];\n      }\n      // If attributes are found, parse them out (i.e. DURATION)\n      return [null, parseAttributeList(param)];\n    case 'EXT-X-KEY':\n    case 'EXT-X-MAP':\n    case 'EXT-X-DATERANGE':\n    case 'EXT-X-MEDIA':\n    case 'EXT-X-STREAM-INF':\n    case 'EXT-X-I-FRAME-STREAM-INF':\n    case 'EXT-X-SESSION-DATA':\n    case 'EXT-X-SESSION-KEY':\n    case 'EXT-X-START':\n    case 'EXT-X-SERVER-CONTROL':\n    case 'EXT-X-PART-INF':\n    case 'EXT-X-PART':\n    case 'EXT-X-PRELOAD-HINT':\n    case 'EXT-X-RENDITION-REPORT':\n    case 'EXT-X-SKIP':\n    case 'EXT-X-DEFINE':\n      return [null, parseAttributeList(param)];\n    case 'EXTINF':\n      return [parseEXTINF(param), null];\n    case 'EXT-X-BYTERANGE':\n      return [parseBYTERANGE(param), null];\n    case 'EXT-X-PROGRAM-DATE-TIME':\n      return [new Date(param), null];\n    case 'EXT-X-PLAYLIST-TYPE':\n      return [param, null]; // <EVENT|VOD>\n    default:\n      return [param, null]; // Unknown tag\n  }\n}\n\nfunction MIXEDTAGS() {\n  utils.INVALIDPLAYLIST(`The file contains both media and master playlist tags.`);\n}\n\nfunction splitTag(line: string): [string, string | null] {\n  const index = line.indexOf(':');\n  if (index === -1) {\n    return [line.slice(1).trim(), null];\n  }\n  return [line.slice(1, index).trim(), line.slice(index + 1).trim()];\n}\n\nfunction parseRendition({attributes}: Tag): Rendition {\n  const rendition = new Rendition({\n    type: attributes['TYPE'],\n    uri: attributes['URI'],\n    groupId: attributes['GROUP-ID'],\n    language: attributes['LANGUAGE'],\n    assocLanguage: attributes['ASSOC-LANGUAGE'],\n    name: attributes['NAME'],\n    isDefault: attributes['DEFAULT'],\n    autoselect: attributes['AUTOSELECT'],\n    forced: attributes['FORCED'],\n    instreamId: attributes['INSTREAM-ID'],\n    characteristics: attributes['CHARACTERISTICS'],\n    channels: attributes['CHANNELS'],\n    pathwayId: attributes['PATHWAY-ID']\n  });\n  return rendition;\n}\n\nfunction checkRedundantRendition(renditions, rendition): string {\n  let defaultFound = false;\n  for (const item of renditions) {\n    if (item.name === rendition.name) {\n      return 'All EXT-X-MEDIA tags in the same Group MUST have different NAME attributes.';\n    }\n    if (item.isDefault) {\n      defaultFound = true;\n    }\n  }\n  if (defaultFound && rendition.isDefault) {\n    return 'EXT-X-MEDIA A Group MUST NOT have more than one member with a DEFAULT attribute of YES.';\n  }\n  return '';\n}\n\nfunction addRendition(variant, line, type) {\n  const rendition = parseRendition(line);\n  const renditions = variant[utils.camelify(type)];\n  const errorMessage = checkRedundantRendition(renditions, rendition);\n  if (errorMessage) {\n    utils.INVALIDPLAYLIST(errorMessage);\n  }\n  renditions.push(rendition);\n  if (rendition.isDefault) {\n    variant.currentRenditions[utils.camelify(type)] = renditions.length - 1;\n  }\n}\n\nfunction matchTypes(attrs, variant, params) {\n  for (const type of ['AUDIO', 'VIDEO', 'SUBTITLES', 'CLOSED-CAPTIONS']) {\n    if (type === 'CLOSED-CAPTIONS' && attrs[type] === 'NONE') {\n      params.isClosedCaptionsNone = true;\n      variant.closedCaptions = [];\n    } else if (attrs[type] && !variant[utils.camelify(type)].some(item => item.groupId === attrs[type])) {\n      utils.INVALIDPLAYLIST(`${type} attribute MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag whose TYPE attribute is ${type}.`);\n    }\n  }\n}\n\nfunction parseVariant(lines, variantAttrs, uri: string, iFrameOnly: boolean, params: Record<string, any>): Variant {\n  const variant = new Variant({\n    uri,\n    bandwidth: variantAttrs['BANDWIDTH'],\n    averageBandwidth: variantAttrs['AVERAGE-BANDWIDTH'],\n    score: variantAttrs['SCORE'],\n    codecs: variantAttrs['CODECS'],\n    resolution: variantAttrs['RESOLUTION'],\n    frameRate: variantAttrs['FRAME-RATE'],\n    hdcpLevel: variantAttrs['HDCP-LEVEL'],\n    allowedCpc: variantAttrs['ALLOWED-CPC'],\n    videoRange: variantAttrs['VIDEO-RANGE'],\n    stableVariantId: variantAttrs['STABLE-VARIANT-ID'],\n    pathwayId: variantAttrs['STABLE-PATHWAY-ID'],\n    programId: variantAttrs['PROGRAM-ID']\n  });\n  for (const line of lines) {\n    if (line.name === 'EXT-X-MEDIA') {\n      const renditionAttrs = line.attributes;\n      const renditionType = renditionAttrs['TYPE'];\n      if (!renditionType || !renditionAttrs['GROUP-ID']) {\n        utils.INVALIDPLAYLIST('EXT-X-MEDIA TYPE attribute is REQUIRED.');\n      }\n      if (variantAttrs[renditionType] === renditionAttrs['GROUP-ID']) {\n        addRendition(variant, line, renditionType);\n        if (renditionType === 'CLOSED-CAPTIONS') {\n          for (const {instreamId} of variant.closedCaptions) {\n            if (instreamId && instreamId.startsWith('SERVICE') && params.compatibleVersion < 7) {\n              params.compatibleVersion = 7;\n              break;\n            }\n          }\n        }\n      }\n    }\n  }\n  matchTypes(variantAttrs, variant, params);\n  variant.isIFrameOnly = iFrameOnly;\n  return variant;\n}\n\nfunction sameKey(key1: Key, key2: Key): boolean {\n  if (key1.method !== key2.method) {\n    return false;\n  }\n  if (key1.uri !== key2.uri) {\n    return false;\n  }\n  if (key1.iv) {\n    if (!key2.iv) {\n      return false;\n    }\n    if (key1.iv.byteLength !== key2.iv.byteLength) {\n      return false;\n    }\n    for (let i = 0; i < key1.iv.byteLength; i++) {\n      if (key1.iv[i] !== key2.iv[i]) {\n        return false;\n      }\n    }\n  } else if (key2.iv) {\n    return false;\n  }\n  if (key1.format !== key2.format) {\n    return false;\n  }\n  if (key1.formatVersion !== key2.formatVersion) {\n    return false;\n  }\n  return true;\n}\n\nfunction parseMasterPlaylist(lines: Line[], params: Record<string, any>): MasterPlaylist {\n  const playlist = new MasterPlaylist();\n  let variantIsScored = false;\n  for (const [index, line] of lines.entries()) {\n    const {name, value, attributes} = mapTo<Tag>(line);\n    if (name === 'EXT-X-VERSION') {\n      playlist.version = value;\n    } else if (name === 'EXT-X-CONTENT-STEERING-SERVER') {\n      const contentSteering = new ContentSteering({\n        serverUri: attributes['SERVER-URI'],\n        pathwayId: attributes['PATHWAY-ID']\n      });\n      playlist.contentSteering = contentSteering;\n    } else if (name === 'EXT-X-STREAM-INF') {\n      const uri = lines[index + 1];\n      if (typeof uri !== 'string' || uri.startsWith('#EXT')) {\n        utils.INVALIDPLAYLIST('EXT-X-STREAM-INF must be followed by a URI line');\n      }\n      const variant = parseVariant(lines, attributes, uri as string, false, params);\n      if (variant) {\n        if (typeof variant.score === 'number') {\n          variantIsScored = true;\n          if (variant.score < 0) {\n            utils.INVALIDPLAYLIST('SCORE attribute on EXT-X-STREAM-INF must be positive decimal-floating-point number.');\n          }\n        }\n        playlist.variants.push(variant);\n      }\n    } else if (name === 'EXT-X-I-FRAME-STREAM-INF') {\n      const variant = parseVariant(lines, attributes, attributes.URI, true, params);\n      if (variant) {\n        playlist.variants.push(variant);\n      }\n    } else if (name === 'EXT-X-SESSION-DATA') {\n      const sessionData = new SessionData({\n        id: attributes['DATA-ID'],\n        value: attributes['VALUE'],\n        uri: attributes['URI'],\n        language: attributes['LANGUAGE']\n      });\n      if (playlist.sessionDataList.some(item => item.id === sessionData.id && item.language === sessionData.language)) {\n        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.');\n      }\n      playlist.sessionDataList.push(sessionData);\n    } else if (name === 'EXT-X-SESSION-KEY') {\n      if (attributes['METHOD'] === 'NONE') {\n        utils.INVALIDPLAYLIST('EXT-X-SESSION-KEY: The value of the METHOD attribute MUST NOT be NONE');\n      }\n      const sessionKey = new Key({\n        method: attributes['METHOD'],\n        uri: attributes['URI'],\n        iv: attributes['IV'],\n        format: attributes['KEYFORMAT'],\n        formatVersion: attributes['KEYFORMATVERSIONS']\n      });\n      if (playlist.sessionKeyList.some(item => sameKey(item, sessionKey))) {\n        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.');\n      }\n      setCompatibleVersionOfKey(params, attributes);\n      playlist.sessionKeyList.push(sessionKey);\n    } else if (name === 'EXT-X-INDEPENDENT-SEGMENTS') {\n      if (playlist.independentSegments) {\n        utils.INVALIDPLAYLIST('EXT-X-INDEPENDENT-SEGMENTS tag MUST NOT appear more than once in a Playlist');\n      }\n      playlist.independentSegments = true;\n    } else if (name === 'EXT-X-START') {\n      if (playlist.start) {\n        utils.INVALIDPLAYLIST('EXT-X-START tag MUST NOT appear more than once in a Playlist');\n      }\n      if (typeof attributes['TIME-OFFSET'] !== 'number') {\n        utils.INVALIDPLAYLIST('EXT-X-START: TIME-OFFSET attribute is REQUIRED');\n      }\n      playlist.start = {offset: attributes['TIME-OFFSET'], precise: attributes['PRECISE'] || false};\n    } else if (name === 'EXT-X-DEFINE') {\n      playlist.defines ||= [];\n      playlist.defines.push(attributes);\n    }\n  }\n  if (variantIsScored) {\n    for (const variant of playlist.variants) {\n      if (typeof variant.score !== 'number') {\n        utils.INVALIDPLAYLIST('If any Variant Stream contains the SCORE attribute, then all Variant Streams in the Master Playlist SHOULD have a SCORE attribute');\n      }\n    }\n  }\n  if (params.isClosedCaptionsNone) {\n    for (const variant of playlist.variants) {\n      if (variant.closedCaptions.length > 0) {\n        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');\n      }\n    }\n  }\n  return playlist;\n}\n\nfunction parseDateRange(attributes) {\n  const attrs: Record<string, any> = {};\n  for (const key of Object.keys(attributes)) {\n    if (key.startsWith('SCTE35-') || key.startsWith('X-')) {\n      attrs[key] = attributes[key];\n    }\n  }\n  const dateRange = new DateRange({\n    id: attributes['ID'],\n    classId: attributes['CLASS'],\n    start: attributes['START-DATE'],\n    cue: attributes['CUE'],\n    end: attributes['END-DATE'],\n    duration: attributes['DURATION'],\n    plannedDuration: attributes['PLANNED-DURATION'],\n    endOnNext: attributes['END-ON-NEXT'],\n    attributes: attrs\n  });\n  return dateRange;\n}\n\nfunction parseSegment(lines: Line[], uri: string, start: number, end: number, mediaSequenceNumber: number, discontinuitySequence: number, params: Record<string, any>): Segment {\n  const segment = new Segment({uri, mediaSequenceNumber, discontinuitySequence});\n  let mapHint = false;\n  let partHint = false;\n  for (let i = start; i <= end; i++) {\n    const {name, value, attributes} = mapTo<Tag>(lines[i]);\n    if (name === 'EXTINF') {\n      if (!Number.isInteger(value.duration) && params.compatibleVersion < 3) {\n        params.compatibleVersion = 3;\n      }\n      if (Math.round(value.duration) > params.targetDuration) {\n        utils.INVALIDPLAYLIST('EXTINF duration, when rounded to the nearest integer, MUST be less than or equal to the target duration');\n      }\n      segment.duration = value.duration;\n      segment.title = value.title;\n    } else if (name === 'EXT-X-BYTERANGE') {\n      if (params.compatibleVersion < 4) {\n        params.compatibleVersion = 4;\n      }\n      segment.byterange = value;\n    } else if (name === 'EXT-X-DISCONTINUITY') {\n      if (segment.parts.length > 0) {\n        utils.INVALIDPLAYLIST('EXT-X-DISCONTINUITY must appear before the first EXT-X-PART tag of the Parent Segment.');\n      }\n      segment.discontinuity = true;\n    } else if (name === 'EXT-X-GAP') {\n      if (params.compatibleVersion < 8) {\n        params.compatibleVersion = 8;\n      }\n      segment.gap = true;\n    } else if (name === 'EXT-X-KEY') {\n      if (segment.parts.length > 0) {\n        utils.INVALIDPLAYLIST('EXT-X-KEY must appear before the first EXT-X-PART tag of the Parent Segment.');\n      }\n      setCompatibleVersionOfKey(params, attributes);\n      segment.key = new Key({\n        method: attributes['METHOD'],\n        uri: attributes['URI'],\n        iv: attributes['IV'],\n        format: attributes['KEYFORMAT'],\n        formatVersion: attributes['KEYFORMATVERSIONS']\n      });\n    } else if (name === 'EXT-X-MAP') {\n      if (segment.parts.length > 0) {\n        utils.INVALIDPLAYLIST('EXT-X-MAP must appear before the first EXT-X-PART tag of the Parent Segment.');\n      }\n      if (params.compatibleVersion < 5) {\n        params.compatibleVersion = 5;\n      }\n      params.hasMap = true;\n      segment.map = new MediaInitializationSection({\n        uri: attributes['URI'],\n        byterange: attributes['BYTERANGE']\n      });\n    } else if (name === 'EXT-X-PROGRAM-DATE-TIME') {\n      segment.programDateTime = value;\n    } else if (name === 'EXT-X-DATERANGE') {\n      segment.dateRange = parseDateRange(attributes);\n    } else if (name === 'EXT-X-CUE-OUT') {\n      segment.markers.push(new SpliceInfo({\n        type: 'OUT',\n        duration: (attributes && attributes.DURATION) || value\n      }));\n    } else if (name === 'EXT-X-CUE-IN') {\n      segment.markers.push(new SpliceInfo({\n        type: 'IN'\n      }));\n    } else if (\n      name === 'EXT-X-CUE-OUT-CONT' ||\n      name === 'EXT-X-CUE' ||\n      name === 'EXT-OATCLS-SCTE35' ||\n      name === 'EXT-X-ASSET' ||\n      name === 'EXT-X-SCTE35'\n    ) {\n      segment.markers.push(new SpliceInfo({\n        type: 'RAW',\n        tagName: name,\n        value\n      }));\n    } else if (name === 'EXT-X-PRELOAD-HINT' && !attributes['TYPE']) {\n      utils.INVALIDPLAYLIST('EXT-X-PRELOAD-HINT: TYPE attribute is mandatory');\n    } else if (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'PART' && partHint) {\n      utils.INVALIDPLAYLIST('Servers should not add more than one EXT-X-PRELOAD-HINT tag with the same TYPE attribute to a Playlist.');\n    } else if ((name === 'EXT-X-PART' || name === 'EXT-X-PRELOAD-HINT') && !attributes['URI']) {\n      utils.INVALIDPLAYLIST('EXT-X-PART / EXT-X-PRELOAD-HINT: URI attribute is mandatory');\n    } else if (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'MAP') {\n      if (mapHint) {\n        utils.INVALIDPLAYLIST('Servers should not add more than one EXT-X-PRELOAD-HINT tag with the same TYPE attribute to a Playlist.');\n      }\n      mapHint = true;\n      params.hasMap = true;\n      segment.map = new MediaInitializationSection({\n        hint: true,\n        uri: attributes['URI'],\n        byterange: {length: attributes['BYTERANGE-LENGTH'], offset: attributes['BYTERANGE-START'] || 0}\n      });\n    } else if (name === 'EXT-X-PART' || (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'PART')) {\n      if (name === 'EXT-X-PART' && !attributes['DURATION']) {\n        utils.INVALIDPLAYLIST('EXT-X-PART: DURATION attribute is mandatory');\n      }\n      if (name === 'EXT-X-PRELOAD-HINT') {\n        partHint = true;\n      }\n      const partialSegment = new PartialSegment({\n        hint: (name === 'EXT-X-PRELOAD-HINT'),\n        uri: attributes['URI'],\n        byterange: (name === 'EXT-X-PART' ? attributes['BYTERANGE'] : {length: attributes['BYTERANGE-LENGTH'], offset: attributes['BYTERANGE-START'] || 0}),\n        duration: attributes['DURATION'],\n        independent: attributes['INDEPENDENT'],\n        gap: attributes['GAP']\n      });\n      if (segment.gap && !partialSegment.gap) {\n        // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.2.1\n        utils.INVALIDPLAYLIST('Partial segments must have GAP=YES if they are in a gap (EXT-X-GAP)');\n      }\n      segment.parts.push(partialSegment);\n    }\n  }\n  return segment;\n}\n\nfunction parsePrefetchSegment(lines: Line[], uri: any, start: number, end: number, mediaSequenceNumber: number, discontinuitySequence: number, params: Record<string, any>): PrefetchSegment {\n  const segment = new PrefetchSegment({uri, mediaSequenceNumber, discontinuitySequence});\n  for (let i = start; i <= end; i++) {\n    const {name, attributes} = lines[i] as Tag;\n    if (name === 'EXTINF') {\n      utils.INVALIDPLAYLIST('A prefetch segment must not be advertised with an EXTINF tag.');\n    } else if (name === 'EXT-X-DISCONTINUITY') {\n      utils.INVALIDPLAYLIST('A prefetch segment must not be advertised with an EXT-X-DISCONTINUITY tag.');\n    } else if (name === 'EXT-X-PREFETCH-DISCONTINUITY') {\n      segment.discontinuity = true;\n    } else if (name === 'EXT-X-KEY') {\n      setCompatibleVersionOfKey(params, attributes);\n      segment.key = new Key({\n        method: attributes['METHOD'],\n        uri: attributes['URI'],\n        iv: attributes['IV'],\n        format: attributes['KEYFORMAT'],\n        formatVersion: attributes['KEYFORMATVERSIONS']\n      });\n    } else if (name === 'EXT-X-MAP') {\n      utils.INVALIDPLAYLIST('Prefetch segments must not be advertised with an EXT-X-MAP tag.');\n    }\n  }\n  return segment;\n}\n\nfunction parseMediaPlaylist(lines: Line[], params: Record<string, any>): MediaPlaylist {\n  const playlist = new MediaPlaylist();\n  let segmentStart = -1;\n  let mediaSequence = 0;\n  let discontinuityFound = false;\n  let prefetchFound = false;\n  let discontinuitySequence = 0;\n  let currentKey: Key | null = null;\n  let currentMap: MediaInitializationSection | null = null;\n  let containsParts = false;\n  for (const [index, line] of lines.entries()) {\n    const {name, value, attributes, category} = mapTo<Tag>(line);\n    if (category === 'Segment') {\n      if (segmentStart === -1) {\n        segmentStart = index;\n      }\n      if (name === 'EXT-X-DISCONTINUITY') {\n        discontinuityFound = true;\n      }\n      continue;\n    }\n    if (name === 'EXT-X-VERSION') {\n      if (playlist.version === undefined) {\n        playlist.version = value;\n      } else {\n        utils.INVALIDPLAYLIST('A Playlist file MUST NOT contain more than one EXT-X-VERSION tag.');\n      }\n    } else if (name === 'EXT-X-TARGETDURATION') {\n      playlist.targetDuration = params.targetDuration = value;\n    } else if (name === 'EXT-X-MEDIA-SEQUENCE') {\n      if (playlist.segments.length > 0) {\n        utils.INVALIDPLAYLIST('The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.');\n      }\n      playlist.mediaSequenceBase = mediaSequence = value;\n    } else if (name === 'EXT-X-DISCONTINUITY-SEQUENCE') {\n      if (playlist.segments.length > 0) {\n        utils.INVALIDPLAYLIST('The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.');\n      }\n      if (discontinuityFound) {\n        utils.INVALIDPLAYLIST('The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before any EXT-X-DISCONTINUITY tag.');\n      }\n      playlist.discontinuitySequenceBase = discontinuitySequence = value;\n    } else if (name === 'EXT-X-ENDLIST') {\n      playlist.endlist = true;\n    } else if (name === 'EXT-X-PLAYLIST-TYPE') {\n      playlist.playlistType = value;\n    } else if (name === 'EXT-X-I-FRAMES-ONLY') {\n      if (params.compatibleVersion < 4) {\n        params.compatibleVersion = 4;\n      }\n      playlist.isIFrame = true;\n    } else if (name === 'EXT-X-INDEPENDENT-SEGMENTS') {\n      if (playlist.independentSegments) {\n        utils.INVALIDPLAYLIST('EXT-X-INDEPENDENT-SEGMENTS tag MUST NOT appear more than once in a Playlist');\n      }\n      playlist.independentSegments = true;\n    } else if (name === 'EXT-X-START') {\n      if (playlist.start) {\n        utils.INVALIDPLAYLIST('EXT-X-START tag MUST NOT appear more than once in a Playlist');\n      }\n      if (typeof attributes['TIME-OFFSET'] !== 'number') {\n        utils.INVALIDPLAYLIST('EXT-X-START: TIME-OFFSET attribute is REQUIRED');\n      }\n      playlist.start = {offset: attributes['TIME-OFFSET'], precise: attributes['PRECISE'] || false};\n    } else if (name === 'EXT-X-SERVER-CONTROL') {\n      if (!attributes['CAN-BLOCK-RELOAD']) {\n        utils.INVALIDPLAYLIST('EXT-X-SERVER-CONTROL: CAN-BLOCK-RELOAD=YES is mandatory for Low-Latency HLS');\n      }\n      playlist.lowLatencyCompatibility = {\n        canBlockReload: attributes['CAN-BLOCK-RELOAD'],\n        canSkipUntil: attributes['CAN-SKIP-UNTIL'],\n        holdBack: attributes['HOLD-BACK'],\n        partHoldBack: attributes['PART-HOLD-BACK']\n      };\n    } else if (name === 'EXT-X-PART-INF') {\n      if (!attributes['PART-TARGET']) {\n        utils.INVALIDPLAYLIST('EXT-X-PART-INF: PART-TARGET attribute is mandatory');\n      }\n      playlist.partTargetDuration = attributes['PART-TARGET'];\n    } else if (name === 'EXT-X-RENDITION-REPORT') {\n      if (!attributes['URI']) {\n        utils.INVALIDPLAYLIST('EXT-X-RENDITION-REPORT: URI attribute is mandatory');\n      }\n      if (attributes['URI'].search(/^[a-z]+:/) === 0) {\n        utils.INVALIDPLAYLIST('EXT-X-RENDITION-REPORT: URI must be relative to the playlist uri');\n      }\n      playlist.renditionReports.push(new RenditionReport({\n        uri: attributes['URI'],\n        lastMSN: attributes['LAST-MSN'],\n        lastPart: attributes['LAST-PART']\n      }));\n    } else if (name === 'EXT-X-SKIP') {\n      if (!attributes['SKIPPED-SEGMENTS']) {\n        utils.INVALIDPLAYLIST('EXT-X-SKIP: SKIPPED-SEGMENTS attribute is mandatory');\n      }\n      if (params.compatibleVersion < 9) {\n        params.compatibleVersion = 9;\n      }\n      playlist.skip = attributes['SKIPPED-SEGMENTS'];\n      mediaSequence += playlist.skip;\n    } else if (name === 'EXT-X-PREFETCH') {\n      const segment = parsePrefetchSegment(lines, value, segmentStart === -1 ? index : segmentStart, index - 1, mediaSequence++, discontinuitySequence, params);\n      if (segment) {\n        if (segment.discontinuity) {\n          segment.discontinuitySequence++;\n          discontinuitySequence = segment.discontinuitySequence;\n        }\n        if (segment.key) {\n          currentKey = segment.key;\n        } else {\n          segment.key = currentKey;\n        }\n        playlist.prefetchSegments.push(segment);\n      }\n      prefetchFound = true;\n      segmentStart = -1;\n    } else if (name === 'EXT-X-DEFINE') {\n      playlist.defines ||= [];\n      playlist.defines.push(attributes);\n    } else if (name === 'EXT-X-DATERANGE') {\n      const dateRange = parseDateRange(attributes);\n      playlist.dateRanges.push(dateRange);\n    } else if (typeof line === 'string') {\n      // uri\n      if (segmentStart === -1) {\n        utils.INVALIDPLAYLIST('A URI line is not preceded by any segment tags');\n      }\n      if (!playlist.targetDuration) {\n        utils.INVALIDPLAYLIST('The EXT-X-TARGETDURATION tag is REQUIRED');\n      }\n      if (prefetchFound) {\n        utils.INVALIDPLAYLIST('These segments must appear after all complete segments.');\n      }\n      const segment = parseSegment(lines, line, segmentStart, index - 1, mediaSequence++, discontinuitySequence, params);\n      if (segment) {\n        [discontinuitySequence, currentKey, currentMap] = addSegment(playlist, segment, discontinuitySequence, currentKey!, currentMap!);\n        if (!containsParts && segment.parts.length > 0) {\n          containsParts = true;\n        }\n      }\n      segmentStart = -1;\n    }\n  }\n  if (segmentStart !== -1) {\n    const segment = parseSegment(lines, '', segmentStart, lines.length - 1, mediaSequence++, discontinuitySequence, params);\n    if (segment) {\n      const {parts} = segment;\n      if (parts.length > 0 && !playlist.endlist && !parts.at(-1)?.hint) {\n        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');\n      }\n      // @ts-expect-error TODO check if this is not a bug the third argument should be a discontinuitySequence\n      addSegment(playlist, segment, currentKey, currentMap);\n      if (!containsParts && segment.parts.length > 0) {\n        containsParts = true;\n      }\n    }\n  }\n  checkDateRange(playlist.segments);\n  if (playlist.lowLatencyCompatibility) {\n    checkLowLatencyCompatibility(playlist, containsParts);\n  }\n  return playlist;\n}\n\nfunction addSegment(playlist: MediaPlaylist, segment: Segment, discontinuitySequence: number, currentKey?: Key, currentMap?: MediaInitializationSection): [number, Key, MediaInitializationSection] {\n  const {discontinuity, key, map, byterange, uri} = segment;\n  if (discontinuity) {\n    segment.discontinuitySequence = discontinuitySequence + 1;\n  }\n  if (!key) {\n    segment.key = currentKey;\n  }\n  if (!map) {\n    segment.map = currentMap!;\n  }\n  if (byterange && byterange.offset === -1) {\n    const {segments} = playlist;\n    if (segments.length > 0) {\n      const prevSegment = segments.at(-1)!;\n      if (prevSegment.byterange && prevSegment.uri === uri) {\n        byterange.offset = prevSegment.byterange.offset + prevSegment.byterange.length;\n      } else {\n        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');\n      }\n    } else {\n      utils.INVALIDPLAYLIST('If offset of EXT-X-BYTERANGE is not present, a previous Media Segment MUST appear in the Playlist file');\n    }\n  }\n  playlist.segments.push(segment);\n  return [segment.discontinuitySequence, segment.key!, segment.map];\n}\n\nfunction checkDateRange(segments: Segment[]) {\n  const earliestDates = new Map();\n  const rangeList = new Map();\n  let hasDateRange = false;\n  let hasProgramDateTime = false;\n  for (let i = segments.length - 1; i >= 0; i--) {\n    const {programDateTime, dateRange} = segments[i];\n    if (programDateTime) {\n      hasProgramDateTime = true;\n    }\n    if (dateRange && dateRange.start) {\n      hasDateRange = true;\n      if (dateRange.endOnNext && (dateRange.end || dateRange.duration)) {\n        utils.INVALIDPLAYLIST('An EXT-X-DATERANGE tag with an END-ON-NEXT=YES attribute MUST NOT contain DURATION or END-DATE attributes.');\n      }\n      const start = dateRange.start.getTime();\n      const duration = dateRange.duration || 0;\n      if (dateRange.end && dateRange.duration) {\n        if ((start + duration * 1000) !== dateRange.end.getTime()) {\n          utils.INVALIDPLAYLIST('END-DATE MUST be equal to the value of the START-DATE attribute plus the value of the DURATION');\n        }\n      }\n      if (dateRange.endOnNext) {\n        dateRange.end = earliestDates.get(dateRange.classId);\n      }\n      earliestDates.set(dateRange.classId, dateRange.start);\n      const end = dateRange.end ? dateRange.end.getTime() : dateRange.start.getTime() + (dateRange.duration || 0) * 1000;\n      const range = rangeList.get(dateRange.classId);\n      if (range) {\n        for (const entry of range) {\n          if ((entry.start <= start && entry.end > start) || (entry.start >= start && entry.start < end)) {\n            utils.INVALIDPLAYLIST('DATERANGE tags with the same CLASS should not overlap');\n          }\n        }\n        range.push({start, end});\n      } else if (dateRange.classId) {\n        rangeList.set(dateRange.classId, [{start, end}]);\n      }\n    }\n  }\n  if (hasDateRange && !hasProgramDateTime) {\n    utils.INVALIDPLAYLIST('If a Playlist contains an EXT-X-DATERANGE tag, it MUST also contain at least one EXT-X-PROGRAM-DATE-TIME tag.');\n  }\n}\n\nfunction checkLowLatencyCompatibility({lowLatencyCompatibility, targetDuration, partTargetDuration, segments, renditionReports}: any, containsParts) {\n  const {canSkipUntil, holdBack, partHoldBack} = lowLatencyCompatibility;\n  if (canSkipUntil < targetDuration * 6) {\n    utils.INVALIDPLAYLIST('The Skip Boundary must be at least six times the EXT-X-TARGETDURATION.');\n  }\n  // Its value is a floating-point number of seconds and .\n  if (holdBack < targetDuration * 3) {\n    utils.INVALIDPLAYLIST('HOLD-BACK must be at least three times the EXT-X-TARGETDURATION.');\n  }\n  if (containsParts) {\n    if (partTargetDuration === undefined) {\n      utils.INVALIDPLAYLIST('EXT-X-PART-INF is required if a Playlist contains one or more EXT-X-PART tags');\n    }\n    if (partHoldBack === undefined) {\n      utils.INVALIDPLAYLIST('EXT-X-PART: PART-HOLD-BACK attribute is mandatory');\n    }\n    if (partHoldBack < partTargetDuration) {\n      utils.INVALIDPLAYLIST('PART-HOLD-BACK must be at least PART-TARGET');\n    }\n    for (const [segmentIndex, {parts}] of segments.entries()) {\n      if (parts.length > 0 && segmentIndex < segments.length - 3) {\n        utils.INVALIDPLAYLIST('Remove EXT-X-PART tags from the Playlist after they are greater than three target durations from the end of the Playlist.');\n      }\n      for (const [partIndex, {duration}] of parts.entries()) {\n        if (duration === undefined) {\n          continue;\n        }\n        if (duration > partTargetDuration) {\n          utils.INVALIDPLAYLIST('PART-TARGET is the maximum duration of any Partial Segment');\n        }\n        if (partIndex < parts.length - 1 && duration < partTargetDuration * 0.85) {\n          utils.INVALIDPLAYLIST('All Partial Segments except the last part of a segment must have a duration of at least 85% of PART-TARGET');\n        }\n      }\n    }\n  }\n  for (const report of renditionReports) {\n    const lastSegment = segments.at(-1);\n    report.lastMSN ??= lastSegment.mediaSequenceNumber;\n    if ((report.lastPart === null || report.lastPart === undefined) && lastSegment.parts.length > 0) {\n      report.lastPart = lastSegment.parts.length - 1;\n    }\n  }\n}\n\nfunction CHECKTAGCATEGORY(category: TagCategory, params: Record<string, any>) {\n  if (category === 'Segment' || category === 'MediaPlaylist') {\n    if (params.isMasterPlaylist === undefined) {\n      params.isMasterPlaylist = false;\n      return;\n    }\n    if (params.isMasterPlaylist) {\n      MIXEDTAGS();\n    }\n    return;\n  }\n  if (category === 'MasterPlaylist') {\n    if (params.isMasterPlaylist === undefined) {\n      params.isMasterPlaylist = true;\n      return;\n    }\n    if (params.isMasterPlaylist === false) {\n      MIXEDTAGS();\n    }\n  }\n  // category === 'Basic' or 'MediaorMasterPlaylist' or 'Unknown'\n}\n\ntype Tag = {\n  name: string;\n  category: TagCategory;\n  value: any;\n  attributes: any;\n};\n\nfunction parseTag(line: string, params: Record<string, any>): Tag | null {\n  const [name, param] = splitTag(line);\n  const category = getTagCategory(name);\n  CHECKTAGCATEGORY(category, params);\n  if (category === 'Unknown') {\n    return null;\n  }\n  if (category === 'MediaPlaylist' && name !== 'EXT-X-RENDITION-REPORT' && name !== 'EXT-X-PREFETCH') {\n    if (params.hash[name]) {\n      utils.INVALIDPLAYLIST('There MUST NOT be more than one Media Playlist tag of each type in any Media Playlist');\n    }\n    params.hash[name] = true;\n  }\n  const [value, attributes] = parseTagParam(name, param);\n  return {name, category, value, attributes};\n}\n\ntype Line = string | Tag;\n\nfunction lexicalParse(text: string, params: Record<string, any>): Line[] {\n  const lines: Line[] = [];\n  for (const l of text.split('\\n')) {\n    // V8 has garbage collection issues when cleaning up substrings split from strings greater\n    // than 13 characters so before we continue we need to safely copy over each line so that it\n    // doesn't hold any reference to the containing string.\n    const line = l.trim();\n    if (!line) {\n      // empty line\n      continue;\n    }\n    if (line.startsWith('#')) {\n      if (line.startsWith('#EXT')) {\n        // tag\n        const tag = parseTag(line, params);\n        if (tag) {\n          lines.push(tag);\n        }\n      }\n      // comment\n      continue;\n    }\n    // uri\n    lines.push(line);\n  }\n  if (lines.length === 0 || (lines[0] as Tag).name !== 'EXTM3U') {\n    utils.INVALIDPLAYLIST('The EXTM3U tag MUST be the first line.');\n  }\n  return lines;\n}\n\nfunction semanticParse(lines: Line[], params: Record<string, any>): MasterPlaylist | MediaPlaylist {\n  let playlist;\n  if (params.isMasterPlaylist) {\n    playlist = parseMasterPlaylist(lines, params);\n  } else {\n    playlist = parseMediaPlaylist(lines, params);\n    if (!playlist.isIFrame && params.hasMap && params.compatibleVersion < 6) {\n      params.compatibleVersion = 6;\n    }\n  }\n  if (params.compatibleVersion > 1) {\n    if (!playlist.version || playlist.version < params.compatibleVersion) {\n      utils.INVALIDPLAYLIST(`EXT-X-VERSION needs to be ${params.compatibleVersion} or higher.`);\n    }\n  }\n  return playlist;\n}\n\nfunction parse(text: string): MasterPlaylist | MediaPlaylist {\n  const params: Record<string, any> = {\n    version: undefined,\n    isMasterPlaylist: undefined,\n    hasMap: false,\n    targetDuration: 0,\n    compatibleVersion: 1,\n    isClosedCaptionsNone: false,\n    hash: {}\n  };\n\n  const lines = lexicalParse(text, params);\n  const playlist = semanticParse(lines, params);\n  playlist.source = text;\n  return playlist;\n}\n\nfunction mapTo<T extends object>(value: T | string): Partial<T> {\n  return typeof value === 'string' ? {} : value;\n}\n\nexport default parse;\n"
  },
  {
    "path": "stringify.ts",
    "content": "import * as utils from './utils';\nimport {\n  Byterange,\n  DateRange, Key,\n  MasterPlaylist,\n  MediaInitializationSection,\n  MediaPlaylist,\n  PartialSegment,\n  Rendition,\n  Segment,\n  SessionData,\n  SpliceInfo,\n  Variant,\n  PostProcess,\n  ContentSteering,\n} from './types';\n\nconst ALLOW_REDUNDANCY = [\n  '#EXTINF',\n  '#EXT-X-BYTERANGE',\n  '#EXT-X-DISCONTINUITY',\n  '#EXT-X-STREAM-INF',\n  '#EXT-X-CUE-OUT',\n  '#EXT-X-CUE-IN',\n  '#EXT-X-KEY',\n  '#EXT-X-MAP'\n];\n\nconst SKIP_IF_REDUNDANT = [\n  '#EXT-X-MEDIA'\n];\n\nclass LineArray extends Array<string> {\n  baseUri?: string;\n\n  constructor(baseUri?: string) {\n    super();\n    this.baseUri = baseUri;\n  }\n\n  override push(...elems: string[]) {\n    // redundancy check\n    for (const elem of elems) {\n      if (!elem.startsWith('#')) {\n        super.push(elem);\n        continue;\n      }\n      if (ALLOW_REDUNDANCY.some(item => elem.startsWith(item))) {\n        super.push(elem);\n        continue;\n      }\n      if (this.includes(elem)) {\n        if (SKIP_IF_REDUNDANT.some(item => elem.startsWith(item))) {\n          continue;\n        }\n        utils.INVALIDPLAYLIST(`Redundant item (${elem})`);\n      }\n      super.push(elem);\n    }\n    return this.length;\n  }\n\n  override join(separator: string | undefined = ','): string {\n    for (let i = this.length - 1; i >= 0; i--) {\n      if (!this[i]) {\n        this.splice(i, 1);\n      }\n    }\n    return super.join(separator);\n  }\n}\n\nfunction buildDecimalFloatingNumber(num: number, fixed?: number) {\n  let roundFactor = 1000;\n  if (fixed) {\n    roundFactor = 10 ** fixed;\n  }\n  const rounded = Math.round(num * roundFactor) / roundFactor;\n  return fixed ? rounded.toFixed(fixed) : rounded;\n}\n\nfunction getNumberOfDecimalPlaces(num: number) {\n  const str = num.toString(10);\n  const index = str.indexOf('.');\n  if (index === -1) {\n    return 0;\n  }\n  return str.length - index - 1;\n}\n\nfunction buildMasterPlaylist(lines: LineArray, playlist: MasterPlaylist, postProcess: PostProcess | undefined) {\n  if (playlist.contentSteering) {\n    lines.push(buildContentSteeringServer(playlist.contentSteering));\n  }\n  for (const sessionData of playlist.sessionDataList) {\n    lines.push(buildSessionData(sessionData));\n  }\n  for (const sessionKey of playlist.sessionKeyList) {\n    lines.push(buildKey(sessionKey, true));\n  }\n  for (const [i, variant] of playlist.variants.entries()) {\n    const base = lines.length;\n    buildVariant(lines, variant);\n    if (postProcess?.variantProcessor) {\n      postProcess.variantProcessor(lines, base, lines.length - 1, variant, i);\n    }\n  }\n}\n\nfunction buildContentSteeringServer(contentSteering: ContentSteering) {\n  const attrs = [\n    `SERVER-URI=\"${contentSteering.serverUri}\"`,\n    `PATHWAY-ID=\"${contentSteering.pathwayId}\"`\n  ];\n  return `#EXT-X-CONTENT-STEERING:${attrs.join(',')}`;\n}\n\nfunction buildSessionData(sessionData: SessionData) {\n  const attrs = [`DATA-ID=\"${sessionData.id}\"`];\n  if (sessionData.language) {\n    attrs.push(`LANGUAGE=\"${sessionData.language}\"`);\n  }\n  if (sessionData.value) {\n    attrs.push(`VALUE=\"${sessionData.value}\"`);\n  } else if (sessionData.uri) {\n    attrs.push(`URI=\"${sessionData.uri}\"`);\n  }\n  return `#EXT-X-SESSION-DATA:${attrs.join(',')}`;\n}\n\nfunction buildKey(key: Key, isSessionKey?: boolean) {\n  const name = isSessionKey ? '#EXT-X-SESSION-KEY' : '#EXT-X-KEY';\n  const attrs = [`METHOD=${key.method}`];\n  if (key.uri) {\n    attrs.push(`URI=\"${key.uri}\"`);\n  }\n  if (key.iv) {\n    if (key.iv.byteLength !== 16) {\n      utils.INVALIDPLAYLIST('IV must be a 128-bit unsigned integer');\n    }\n    attrs.push(`IV=${utils.byteSequenceToHex(key.iv)}`);\n  }\n  if (key.format) {\n    attrs.push(`KEYFORMAT=\"${key.format}\"`);\n  }\n  if (key.formatVersion) {\n    attrs.push(`KEYFORMATVERSIONS=\"${key.formatVersion}\"`);\n  }\n  return `${name}:${attrs.join(',')}`;\n}\n\nfunction buildVariant(lines: LineArray, variant: Variant) {\n  const name = variant.isIFrameOnly ? '#EXT-X-I-FRAME-STREAM-INF' : '#EXT-X-STREAM-INF';\n  const attrs = [`BANDWIDTH=${variant.bandwidth}`];\n  if (variant.averageBandwidth) {\n    attrs.push(`AVERAGE-BANDWIDTH=${variant.averageBandwidth}`);\n  }\n  if (variant.isIFrameOnly) {\n    attrs.push(`URI=\"${variant.uri}\"`);\n  }\n  if (variant.codecs) {\n    attrs.push(`CODECS=\"${variant.codecs}\"`);\n  }\n  if (variant.resolution) {\n    attrs.push(`RESOLUTION=${variant.resolution.width}x${variant.resolution.height}`);\n  }\n  if (variant.frameRate) {\n    attrs.push(`FRAME-RATE=${buildDecimalFloatingNumber(variant.frameRate, 3)}`);\n  }\n  if (variant.hdcpLevel) {\n    attrs.push(`HDCP-LEVEL=${variant.hdcpLevel}`);\n  }\n  if (variant.audio.length > 0) {\n    attrs.push(`AUDIO=\"${variant.audio[0].groupId}\"`);\n    for (const rendition of variant.audio) {\n      lines.push(buildRendition(rendition));\n    }\n  }\n  if (variant.video.length > 0) {\n    attrs.push(`VIDEO=\"${variant.video[0].groupId}\"`);\n    for (const rendition of variant.video) {\n      lines.push(buildRendition(rendition));\n    }\n  }\n  if (variant.subtitles.length > 0) {\n    attrs.push(`SUBTITLES=\"${variant.subtitles[0].groupId}\"`);\n    for (const rendition of variant.subtitles) {\n      lines.push(buildRendition(rendition));\n    }\n  }\n  if (utils.getOptions().allowClosedCaptionsNone && variant.closedCaptions.length === 0) {\n    attrs.push(`CLOSED-CAPTIONS=NONE`);\n  } else if (variant.closedCaptions.length > 0) {\n    attrs.push(`CLOSED-CAPTIONS=\"${variant.closedCaptions[0].groupId}\"`);\n    for (const rendition of variant.closedCaptions) {\n      lines.push((buildRendition(rendition)));\n    }\n  }\n  if (variant.score) {\n    attrs.push(`SCORE=${variant.score}`);\n  }\n  if (variant.allowedCpc) {\n    const list: string[] = [];\n    for (const {format, cpcList} of variant.allowedCpc) {\n      list.push(`${format}:${cpcList.join('/')}`);\n    }\n    attrs.push(`ALLOWED-CPC=\"${list.join(',')}\"`);\n  }\n  if (variant.videoRange) {\n    attrs.push(`VIDEO-RANGE=${variant.videoRange}`);\n  }\n  if (variant.stableVariantId) {\n    attrs.push(`STABLE-VARIANT-ID=\"${variant.stableVariantId}\"`);\n  }\n  if (variant.pathwayId) {\n    attrs.push(`PATHWAY-ID=\"${variant.pathwayId}\"`);\n  }\n  if (variant.programId) {\n    attrs.push(`PROGRAM-ID=${variant.programId}`);\n  }\n  lines.push(`${name}:${attrs.join(',')}`);\n  if (!variant.isIFrameOnly) {\n    lines.push(`${variant.uri}`);\n  }\n}\n\nfunction buildRendition(rendition: Rendition) {\n  const attrs = [\n    `TYPE=${rendition.type}`,\n    `GROUP-ID=\"${rendition.groupId}\"`,\n    `NAME=\"${rendition.name}\"`\n  ];\n  if (rendition.isDefault !== undefined) {\n    attrs.push(`DEFAULT=${rendition.isDefault ? 'YES' : 'NO'}`);\n  }\n  if (rendition.autoselect !== undefined) {\n    attrs.push(`AUTOSELECT=${rendition.autoselect ? 'YES' : 'NO'}`);\n  }\n  if (rendition.forced !== undefined) {\n    attrs.push(`FORCED=${rendition.forced ? 'YES' : 'NO'}`);\n  }\n  if (rendition.language) {\n    attrs.push(`LANGUAGE=\"${rendition.language}\"`);\n  }\n  if (rendition.assocLanguage) {\n    attrs.push(`ASSOC-LANGUAGE=\"${rendition.assocLanguage}\"`);\n  }\n  if (rendition.instreamId) {\n    attrs.push(`INSTREAM-ID=\"${rendition.instreamId}\"`);\n  }\n  if (rendition.characteristics) {\n    attrs.push(`CHARACTERISTICS=\"${rendition.characteristics}\"`);\n  }\n  if (rendition.channels) {\n    attrs.push(`CHANNELS=\"${rendition.channels}\"`);\n  }\n  if (rendition.uri) {\n    attrs.push(`URI=\"${rendition.uri}\"`);\n  }\n  return `#EXT-X-MEDIA:${attrs.join(',')}`;\n}\n\nfunction buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist, postProcess: PostProcess | undefined) {\n  let lastKey = '';\n  let lastMap = '';\n  let unclosedCueIn = false;\n\n  if (playlist.targetDuration) {\n    lines.push(`#EXT-X-TARGETDURATION:${playlist.targetDuration}`);\n  }\n  if (playlist.lowLatencyCompatibility) {\n    const {canBlockReload, canSkipUntil, holdBack, partHoldBack} = playlist.lowLatencyCompatibility;\n    const params: string[] = [];\n    params.push(`CAN-BLOCK-RELOAD=${canBlockReload ? 'YES' : 'NO'}`);\n    if (canSkipUntil !== undefined) {\n      params.push(`CAN-SKIP-UNTIL=${canSkipUntil}`);\n    }\n    if (holdBack !== undefined) {\n      params.push(`HOLD-BACK=${holdBack}`);\n    }\n    if (partHoldBack !== undefined) {\n      params.push(`PART-HOLD-BACK=${partHoldBack}`);\n    }\n    lines.push(`#EXT-X-SERVER-CONTROL:${params.join(',')}`);\n  }\n  if (playlist.partTargetDuration) {\n    lines.push(`#EXT-X-PART-INF:PART-TARGET=${playlist.partTargetDuration}`);\n  }\n  if (playlist.mediaSequenceBase) {\n    lines.push(`#EXT-X-MEDIA-SEQUENCE:${playlist.mediaSequenceBase}`);\n  }\n  if (playlist.discontinuitySequenceBase) {\n    lines.push(`#EXT-X-DISCONTINUITY-SEQUENCE:${playlist.discontinuitySequenceBase}`);\n  }\n  if (playlist.playlistType) {\n    lines.push(`#EXT-X-PLAYLIST-TYPE:${playlist.playlistType}`);\n  }\n  if (playlist.isIFrame) {\n    lines.push(`#EXT-X-I-FRAMES-ONLY`);\n  }\n  if (playlist.skip > 0) {\n    lines.push(`#EXT-X-SKIP:SKIPPED-SEGMENTS=${playlist.skip}`);\n  }\n  for (const dateRange of playlist.dateRanges) {\n    lines.push(buildDateRange(dateRange));\n  }\n  for (const [i, segment] of playlist.segments.entries()) {\n    const base = lines.length;\n    let markerType = '';\n    [lastKey, lastMap, markerType] = buildSegment(lines, segment, lastKey, lastMap, playlist.version);\n    if (markerType === 'OUT') {\n      unclosedCueIn = true;\n    } else if (markerType === 'IN' && unclosedCueIn) {\n      unclosedCueIn = false;\n    }\n    if (postProcess?.segmentProcessor) {\n      postProcess.segmentProcessor(lines, base, lines.length - 1, segment, i);\n    }\n  }\n  if (playlist.playlistType === 'VOD' && unclosedCueIn) {\n    lines.push('#EXT-X-CUE-IN');\n  }\n  if (playlist.prefetchSegments.length > 2) {\n    utils.INVALIDPLAYLIST('The server must deliver no more than two prefetch segments');\n  }\n  for (const segment of playlist.prefetchSegments) {\n    if (segment.discontinuity) {\n      lines.push(`#EXT-X-PREFETCH-DISCONTINUITY`);\n    }\n    lines.push(`#EXT-X-PREFETCH:${segment.uri}`);\n  }\n  if (playlist.endlist) {\n    lines.push(`#EXT-X-ENDLIST`);\n  }\n  for (const report of playlist.renditionReports) {\n    const params: string[] = [];\n    params.push(`URI=\"${report.uri}\"`, `LAST-MSN=${report.lastMSN}`);\n    if (report.lastPart !== undefined) {\n      params.push(`LAST-PART=${report.lastPart}`);\n    }\n    lines.push(`#EXT-X-RENDITION-REPORT:${params.join(',')}`);\n  }\n}\n\nfunction buildSegment(lines: LineArray, segment: Segment, lastKey: string, lastMap: string, version = 1) {\n  let hint = false;\n  let markerType = '';\n\n  if (segment.discontinuity) {\n    lines.push(`#EXT-X-DISCONTINUITY`);\n  }\n  if (segment.gap) {\n    lines.push(`#EXT-X-GAP`);\n  }\n  if (segment.key) {\n    const line = buildKey(segment.key);\n    if (line !== lastKey) {\n      lines.push(line);\n      lastKey = line;\n    }\n  }\n  if (segment.map) {\n    const line = buildMap(segment.map);\n    if (line !== lastMap) {\n      lines.push(line);\n      lastMap = line;\n    }\n  }\n  if (segment.programDateTime) {\n    lines.push(`#EXT-X-PROGRAM-DATE-TIME:${utils.formatDate(segment.programDateTime)}`);\n  }\n  if (segment.dateRange) {\n    lines.push(buildDateRange(segment.dateRange));\n  }\n  if (segment.markers.length > 0) {\n    markerType = buildMarkers(lines, segment.markers);\n  }\n  if (segment.parts.length > 0) {\n    hint = buildParts(lines, segment.parts);\n  }\n  if (hint) {\n    return [lastKey, lastMap];\n  }\n  if (typeof segment.duration === 'number' && !Number.isNaN(segment.duration)) {\n    const duration = version < 3 ? Math.round(segment.duration) : buildDecimalFloatingNumber(segment.duration, getNumberOfDecimalPlaces(segment.duration));\n    lines.push(`#EXTINF:${duration},${unescape(encodeURIComponent(segment.title || ''))}`);\n  }\n  if (segment.byterange) {\n    lines.push(`#EXT-X-BYTERANGE:${buildByteRange(segment.byterange)}`);\n  }\n  Array.prototype.push.call(lines, `${segment.uri}`); // URIs could be redundant when EXT-X-BYTERANGE is used\n  return [lastKey, lastMap, markerType];\n}\n\nfunction buildMap(map: MediaInitializationSection) {\n  const attrs = [`URI=\"${map.uri}\"`];\n  if (map.byterange) {\n    attrs.push(`BYTERANGE=\"${buildByteRange(map.byterange)}\"`);\n  }\n  return `#EXT-X-MAP:${attrs.join(',')}`;\n}\n\nfunction buildByteRange({offset, length}: Byterange) {\n  return `${length}@${offset}`;\n}\n\nfunction buildDateRange(dateRange: DateRange) {\n  const attrs = [\n    `ID=\"${dateRange.id}\"`\n  ];\n  if (dateRange.start) {\n    attrs.push(`START-DATE=\"${utils.formatDate(dateRange.start)}\"`);\n  }\n  if (dateRange.cue) {\n    attrs.push(`CUE=\"${dateRange.cue}\"`);\n  }\n  if (dateRange.end) {\n    attrs.push(`END-DATE=\"${utils.formatDate(dateRange.end)}\"`);\n  }\n  if (dateRange.duration) {\n    attrs.push(`DURATION=${dateRange.duration}`);\n  }\n  if (dateRange.plannedDuration) {\n    attrs.push(`PLANNED-DURATION=${dateRange.plannedDuration}`);\n  }\n  if (dateRange.classId) {\n    attrs.push(`CLASS=\"${dateRange.classId}\"`);\n  }\n  if (dateRange.endOnNext) {\n    attrs.push(`END-ON-NEXT=YES`);\n  }\n  for (const key of Object.keys(dateRange.attributes)) {\n    if (key.startsWith('X-')) {\n      if (typeof dateRange.attributes[key] === 'number') {\n        attrs.push(`${key}=${dateRange.attributes[key]}`);\n      } else {\n        attrs.push(`${key}=\"${dateRange.attributes[key]}\"`);\n      }\n    } else if (key.startsWith('SCTE35-')) {\n      attrs.push(`${key}=${utils.byteSequenceToHex(dateRange.attributes[key])}`);\n    }\n  }\n  return `#EXT-X-DATERANGE:${attrs.join(',')}`;\n}\n\nfunction buildMarkers(lines: LineArray, markers: SpliceInfo[]) {\n  let type = '';\n  for (const marker of markers) {\n    if (marker.type === 'OUT') {\n      type = 'OUT';\n      lines.push(`#EXT-X-CUE-OUT:DURATION=${marker.duration}`);\n    } else if (marker.type === 'IN') {\n      type = 'IN';\n      lines.push('#EXT-X-CUE-IN');\n    } else if (marker.type === 'RAW') {\n      const value = marker.value ? `:${marker.value}` : '';\n      lines.push(`#${marker.tagName}${value}`);\n    }\n  }\n  return type;\n}\n\nfunction buildParts(lines: LineArray, parts: PartialSegment[]) {\n  let hint = false;\n  for (const part of parts) {\n    if (part.hint) {\n      const params: string[] = [];\n      params.push('TYPE=PART', `URI=\"${part.uri}\"`);\n      if (part.byterange) {\n        const {offset, length} = part.byterange;\n        params.push(`BYTERANGE-START=${offset}`);\n        if (length) {\n          params.push(`BYTERANGE-LENGTH=${length}`);\n        }\n      }\n      lines.push(`#EXT-X-PRELOAD-HINT:${params.join(',')}`);\n      hint = true;\n    } else {\n      const params: string[] = [];\n      params.push(`DURATION=${part.duration}`, `URI=\"${part.uri}\"`);\n      if (part.byterange) {\n        params.push(`BYTERANGE=${buildByteRange(part.byterange)}`);\n      }\n      if (part.independent) {\n        params.push('INDEPENDENT=YES');\n      }\n      if (part.gap) {\n        params.push('GAP=YES');\n      }\n      lines.push(`#EXT-X-PART:${params.join(',')}`);\n    }\n  }\n  return hint;\n}\n\nfunction buildDefines(define: Record<string, string>) {\n  const attrs: string[] = [];\n  for (const attr in define) {\n    attrs.push(`${attr}=\"${define[attr]}\"`);\n  }\n  return `#EXT-X-DEFINE:${attrs.join(',')}`;\n}\n\nfunction stringify(playlist: MasterPlaylist | MediaPlaylist, postProcess?: PostProcess): string {\n  utils.PARAMCHECK(playlist);\n  utils.ASSERT('Not a playlist', playlist.type === 'playlist');\n  const lines = new LineArray(playlist.uri);\n  lines.push('#EXTM3U');\n  if (playlist.version) {\n    lines.push(`#EXT-X-VERSION:${playlist.version}`);\n  }\n  if (playlist.independentSegments) {\n    lines.push('#EXT-X-INDEPENDENT-SEGMENTS');\n  }\n  if (playlist.start) {\n    lines.push(`#EXT-X-START:TIME-OFFSET=${buildDecimalFloatingNumber(playlist.start.offset)}${playlist.start.precise ? ',PRECISE=YES' : ''}`);\n  }\n  if (playlist.defines) {\n    for (const session of playlist.defines) {\n      lines.push(buildDefines(session));\n    }\n  }\n  if (playlist.isMasterPlaylist) {\n    buildMasterPlaylist(lines, playlist, postProcess);\n  } else {\n    buildMediaPlaylist(lines, playlist, postProcess);\n  }\n  // console.log('<<<');\n  // console.log(lines.join('\\n'));\n  // console.log('>>>');\n  return lines.join('\\n');\n}\n\nexport default stringify;\n"
  },
  {
    "path": "test/fixtures/m3u8/8.1-Simple-Media-Playlist.m3u8",
    "content": "#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://media.example.com/second.ts\n#EXTINF:3.003,\nhttp://media.example.com/third.ts\n#EXT-X-ENDLIST\n"
  },
  {
    "path": "test/fixtures/m3u8/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.m3u8",
    "content": "# This example shows two EXT-X-DATERANGE tags that describe a single\n# Date Range, with a SCTE-35 \"out\" splice_insert() command that is\n# subsequently updated with an SCTE-35 \"in\" splice_insert() command.\n\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:30\n\n#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z\n#EXTINF:30,\nhttp://media.example.com/01.ts\n#EXTINF:30,\nhttp://media.example.com/02.ts\n#EXT-X-DATERANGE:ID=\"splice-6FFFFFF0\",START-DATE=\"2014-03-05T11:15:00.000Z\",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000\n#EXTINF:30,\nhttp://ads.example.com/ad-01.ts\n#EXTINF:30,\nhttp://ads.example.com/ad-02.ts\n#EXT-X-DATERANGE:ID=\"splice-6FFFFFF0\",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000\n#EXTINF:30,\nhttp://media.example.com/03.ts\n#EXTINF:3.003,\nhttp://media.example.com/04.ts\n"
  },
  {
    "path": "test/fixtures/m3u8/8.11-EXT-X-CUE-OUT-Media-Playlist.m3u8",
    "content": "#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:DURATION=15\n#EXTINF:9.009,\nhttp://media.example.com/second.ts\n#EXTINF:3.003,\nhttp://media.example.com/third.ts\n#EXT-X-ENDLIST\n"
  },
  {
    "path": "test/fixtures/m3u8/8.2-Live-Media-Playlist_using-HTTPS.m3u8",
    "content": "#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/fileSequence2680.ts\n#EXTINF:7.941,\nhttps://priv.example.com/fileSequence2681.ts\n#EXTINF:7.975,\nhttps://priv.example.com/fileSequence2682.ts\n"
  },
  {
    "path": "test/fixtures/m3u8/8.3-Playlist-with-encrypted-Media-Segments.m3u8",
    "content": "#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://priv.example.com/key.php?r=52\"\n\n#EXTINF:2.833,\nhttp://media.example.com/fileSequence52-A.ts\n#EXTINF:15,\nhttp://media.example.com/fileSequence52-B.ts\n#EXTINF:13.333,\nhttp://media.example.com/fileSequence52-C.ts\n\n#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=53\"\n\n#EXTINF:15,\nhttp://media.example.com/fileSequence53-A.ts\n"
  },
  {
    "path": "test/fixtures/m3u8/8.4-Master-Playlist.m3u8",
    "content": "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000,CODECS=\"avc1.640029,mp4a.40.2\"\nhttp://example.com/low.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000,CODECS=\"avc1.640029,mp4a.40.2\"\nhttp://example.com/mid.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000,CODECS=\"avc1.640029,mp4a.40.2\"\nhttp://example.com/hi.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\nhttp://example.com/audio-only.m3u8\n"
  },
  {
    "path": "test/fixtures/m3u8/8.5-Master-Playlist-with-I-Frames.m3u8",
    "content": "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"avc1.640029,mp4a.40.2\"\nlow/audio-video.m3u8\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\",CODECS=\"avc1.640029\"\n#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"avc1.640029,mp4a.40.2\"\nmid/audio-video.m3u8\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\",CODECS=\"avc1.640029\"\n#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"avc1.640029,mp4a.40.2\"\nhi/audio-video.m3u8\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\",CODECS=\"avc1.640029\"\n#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\naudio-only.m3u8\n"
  },
  {
    "path": "test/fixtures/m3u8/8.6-Master-Playlist-with-Alternative-audio.m3u8",
    "content": "# In this example, the CODECS attributes have been condensed for space.\n# A '\\' is used to indicate that the tag continues on the following\n# line with whitespace removed:\n\n#EXTM3U\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",URI=\"main/english-audio.m3u8\"\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Deutsch\",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE=\"de\",URI=\"main/german-audio.m3u8\"\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Commentary\",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE=\"en\",URI=\"commentary/audio-only.m3u8\"\n#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2\",AUDIO=\"aac\"\nlow/video-only.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"mp4a.40.2\",AUDIO=\"aac\"\nmid/video-only.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"mp4a.40.2\",AUDIO=\"aac\"\nhi/video-only.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\nmain/english-audio.m3u8\n"
  },
  {
    "path": "test/fixtures/m3u8/8.7-Master-Playlist-with-Alternative-video.m3u8",
    "content": "# This example shows 3 different video Renditions (Main, Centerfield\n# and Dugout), and 3 different Variant Streams (low, mid and high).  In\n# this example, clients that did not support the EXT-X-MEDIA tag and\n# the VIDEO attribute of the EXT-X-STREAM-INF tag would only be able to\n# play the video Rendition \"Main\".\n# Since the EXT-X-STREAM-INF tag has no AUDIO attribute, all video\n# Renditions would be required to contain the audio.\n#\n# In this example, the CODECS attributes have been condensed for space.\n# A '\\' is used to indicate that the tag continues on the following\n# line with whitespace removed:\n\n#EXTM3U\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Main\",DEFAULT=YES,URI=\"low/main/audio-video.m3u8\"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Centerfield\",DEFAULT=NO,URI=\"low/centerfield/audio-video.m3u8\"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Dugout\",DEFAULT=NO,URI=\"low/dugout/audio-video.m3u8\"\n\n#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"avc1.640029,mp4a.40.2\",VIDEO=\"low\"\nlow/main/audio-video.m3u8\n\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Main\",DEFAULT=NO,URI=\"mid/main/audio-video.m3u8\"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Centerfield\",DEFAULT=YES,URI=\"mid/centerfield/audio-video.m3u8\"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Dugout\",DEFAULT=NO,URI=\"mid/dugout/audio-video.m3u8\"\n\n#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"avc1.640029,mp4a.40.2\",VIDEO=\"mid\"\nmid/main/audio-video.m3u8\n\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Main\",DEFAULT=YES,URI=\"hi/main/audio-video.m3u8\"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Centerfield\",DEFAULT=NO,URI=\"hi/centerfield/audio-video.m3u8\"\n#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Dugout\",DEFAULT=NO,URI=\"hi/dugout/audio-video.m3u8\"\n\n#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"avc1.640029,mp4a.40.2\",VIDEO=\"hi\"\nhi/main/audio-video.m3u8\n"
  },
  {
    "path": "test/fixtures/m3u8/8.8-Session-Data-in-a-Master-Playlist.m3u8",
    "content": "# 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=\"lyrics.json\"\n\n#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\",VALUE=\"This is an example\"\n#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"es\",VALUE=\"Este es un ejemplo\"\n"
  },
  {
    "path": "test/fixtures/m3u8/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.m3u8",
    "content": "# In this example, the CODECS attributes have been condensed for space.\n# A '\\' is used to indicate that the tag continues on the following\n# line with whitespace removed:\n\n#EXTM3U\n#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\"\n#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\"\n#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\"\n#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2\",AUDIO=\"aac\"\nlow/video-only.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"mp4a.40.2\",AUDIO=\"aac\"\nmid/video-only.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"mp4a.40.2\",AUDIO=\"aac\"\nhi/video-only.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\nmain/english-audio.m3u8\n"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-01_Low-Latency_HLS_Playlist.m3u8",
    "content": "#EXTM3U\n# This Playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_msn=273&_HLS_part=2\n#EXT-X-VERSION:6\n#EXT-X-TARGETDURATION:4\n#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1\n#EXT-X-PART-INF:PART-TARGET=0.33334\n#EXT-X-MEDIA-SEQUENCE:266\n#EXT-X-MAP:URI=\"init.mp4\"\n#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z\n#EXTINF:4.00008,\nfileSequence266.mp4\n#EXTINF:4.00008,\nfileSequence267.mp4\n#EXTINF:4.00008,\nfileSequence268.mp4\n#EXTINF:4.00008,\nfileSequence269.mp4\n#EXTINF:4.00008,\nfileSequence270.mp4\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.0.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.1.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.2.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.3.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.4.mp4\",INDEPENDENT=YES\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.5.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.6.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.7.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.8.mp4\",INDEPENDENT=YES\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.9.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.10.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.11.mp4\"\n#EXTINF:4.00008,\nfileSequence271.mp4\n#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.a.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.b.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.c.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.d.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.e.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.f.mp4\",INDEPENDENT=YES\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.g.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.h.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.i.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.j.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.k.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.l.mp4\"\n#EXTINF:4.00008,\nfileSequence272.mp4\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart273.0.mp4\",INDEPENDENT=YES\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart273.1.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart273.2.mp4\"\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart273.3.mp4\"\n\n#EXT-X-RENDITION-REPORT:URI=\"../1M/waitForMSN.php\",LAST-MSN=273,LAST-PART=2\n#EXT-X-RENDITION-REPORT:URI=\"../4M/waitForMSN.php\",LAST-MSN=273,LAST-PART=1\n"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-02_Playlist_Delta_Update.m3u8",
    "content": "#EXTM3U\n# 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\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-BACK=1\n#EXT-X-PART-INF:PART-TARGET=0.33334\n#EXT-X-MEDIA-SEQUENCE:266\n#EXT-X-SKIP:SKIPPED-SEGMENTS=3\n#EXTINF:4.00008,\nfileSequence269.mp4\n#EXTINF:4.00008,\nfileSequence270.mp4\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.0.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.1.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.2.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.3.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.4.mp4\",INDEPENDENT=YES\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.5.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.6.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.7.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.8.mp4\",INDEPENDENT=YES\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.9.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.10.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart271.11.mp4\"\n#EXTINF:4.00008,\nfileSequence271.mp4\n#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.a.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.b.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.c.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.d.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.e.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.f.mp4\",INDEPENDENT=YES\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.g.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.h.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.i.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.j.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.k.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart272.l.mp4\"\n#EXTINF:4.00008,\nfileSequence272.mp4\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart273.0.mp4\",INDEPENDENT=YES\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart273.1.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart273.2.mp4\"\n#EXT-X-PART:DURATION=0.33334,URI=\"filePart273.3.mp4\"\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"filePart273.4.mp4\"\n\n#EXT-X-RENDITION-REPORT:URI=\"../1M/waitForMSN.php\",LAST-MSN=273,LAST-PART=3\n#EXT-X-RENDITION-REPORT:URI=\"../4M/waitForMSN.php\",LAST-MSN=273,LAST-PART=3\n"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-01.m3u8",
    "content": "#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-BACK=1.02\n#EXT-X-PART-INF:PART-TARGET=1.02\n#EXT-X-MEDIA-SEQUENCE:266\n#EXT-X-SKIP:SKIPPED-SEGMENTS=3\n#EXTINF:4.00008,\nfileSequence269.mp4\n#EXTINF:4.00008,\nfileSequence270.mp4\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=20000@0\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=23000@20000\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=18000@43000\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fileSequence271.mp4\",BYTERANGE-START=61000\n"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-02.m3u8",
    "content": "#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-BACK=1.02\n#EXT-X-PART-INF:PART-TARGET=1.02\n#EXT-X-MEDIA-SEQUENCE:266\n#EXT-X-SKIP:SKIPPED-SEGMENTS=3\n#EXTINF:4.00008,\nfileSequence269.mp4\n#EXTINF:4.00008,\nfileSequence270.mp4\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=20000@0\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=23000@20000\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=18000@43000\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=19000@61000\n#EXTINF:4.00008,\nfileSequence271.mp4\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fileSequence272.mp4\",BYTERANGE-START=0\n"
  },
  {
    "path": "test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-03.m3u8",
    "content": "#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-BACK=1.02\n#EXT-X-PART-INF:PART-TARGET=1.02\n#EXT-X-MEDIA-SEQUENCE:266\n#EXT-X-SKIP:SKIPPED-SEGMENTS=3\n#EXTINF:4.00008,\nfileSequence269.mp4\n#EXTINF:4.00008,\nfileSequence270.mp4\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=20000@0\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=23000@20000\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=18000@43000\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence271.mp4\",BYTERANGE=19000@61000\n#EXTINF:4.00008,\nfileSequence271.mp4\n#EXT-X-PART:DURATION=1.02,URI=\"fileSequence272.mp4\",BYTERANGE=21000@0\n#EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fileSequence272.mp4\",BYTERANGE-START=21000\n"
  },
  {
    "path": "test/fixtures/m3u8/Multiple-rendition-groups.m3u8",
    "content": "#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-INDEPENDENT-SEGMENTS\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_high\",NAME=\"English\",DEFAULT=YES,URI=\"aac_high_eng.m3u8\"\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_high\",NAME=\"Japanese\",DEFAULT=NO,URI=\"aac_high_jp.m3u8\"\n#EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO=\"aac_high\"\n1080p.m3u8\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_mid\",NAME=\"English\",DEFAULT=YES,URI=\"aac_mid_eng.m3u8\"\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_mid\",NAME=\"Japanese\",DEFAULT=NO,URI=\"aac_mid_jp.m3u8\"\n#EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO=\"aac_mid\"\n720p.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO=\"aac_mid\"\n540p.m3u8\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_low\",NAME=\"English\",DEFAULT=YES,URI=\"aac_low_eng.m3u8\"\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_low\",NAME=\"Japanese\",DEFAULT=NO,URI=\"aac_low_jp.m3u8\"\n#EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO=\"aac_low\"\n360p.m3u8"
  },
  {
    "path": "test/fixtures/m3u8/RedundantSegments.m3u8",
    "content": "#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://media.example.com/second.ts\n#EXTINF:9.009,\n#EXT-X-BYTERANGE:128@256\nhttp://media.example.com/second.ts\n#EXTINF:3.003,\nhttp://media.example.com/third.ts\n#EXT-X-ENDLIST\n"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_01.m3u8",
    "content": "#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\n#EXTINF:8,\n2.ts\n#EXTINF:7,\n3.ts\n#EXT-X-CUE-IN\n#EXTINF:8.008,\n4.ts\n#EXTINF:8.008,\n5.ts\n#EXT-X-ENDLIST\n"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_02.m3u8",
    "content": "#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\n#EXTINF:8,\n2.ts\n#EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23\n#EXTINF:8,\n3.ts\n#EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23\n#EXTINF:7,\n4.ts\n#EXT-X-CUE-IN\n#EXTINF:8.008,\n5.ts\n#EXT-X-ENDLIST\n"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_03.m3u8",
    "content": "#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\",ID=\"0\",TYPE=\"SpliceOut\",TIME=\"414.171\"\n#EXTINF:8,\n2.ts\n#EXTINF:7,\n3.ts\n#EXTINF:8.008,\n4.ts\n#EXTINF:8.008,\n5.ts\n#EXT-X-ENDLIST\n"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_04.m3u8",
    "content": "#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:/DA0AAAAAAAAAAAABQb+ADAQ6QAeAhxDVUVJQAAAO3/PAAEUrEoICAAAAAAg+2UBNAAANvrtoQ==\n#EXT-X-ASSET:CAID=0x0000000020FB6501\n#EXT-X-CUE-OUT:DURATION=15\n#EXTINF:8,\n2.ts\n#EXT-X-CUE-OUT-CONT:ElapsedTime=5.939,Duration=25.0,SCTE35=/DA0AAAA+…AAg+2UBNAAANvrtoQ==\n#EXTINF:7,\n3.ts\n#EXT-X-CUE-IN\n#EXTINF:8.008,\n4.ts\n#EXTINF:8.008,\n5.ts\n#EXT-X-ENDLIST\n"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_05.m3u8",
    "content": "#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,DURATION=15.0,CUE-OUT=YES,UPID=\"0x08:0x9425BC\",CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”pIViS5”\n#EXTINF:8,\n2.ts\n#EXTINF:7,\n3.ts\n#EXT-X-SCTE35:TYPE=0x35,CUE-IN=YES,CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”f6UrRd”\n#EXTINF:8.008,\n4.ts\n#EXTINF:8.008,\n5.ts\n#EXT-X-ENDLIST\n"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_06.m3u8",
    "content": "#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\n#EXTINF:8,\n2.ts\n#EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23\n#EXTINF:8,\n3.ts\n#EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23\n#EXTINF:7,\n4.ts\n#EXT-X-CUE-IN\n#EXTINF:8.008,\n5.ts\n#EXT-X-CUE-OUT:DURATION=23\n#EXTINF:8,\n6.ts\n#EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23\n#EXTINF:8,\n7.ts\n#EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23\n#EXTINF:7,\n8.ts\n#EXT-X-CUE-IN\n#EXTINF:8.008,\n9.ts\n#EXT-X-ENDLIST\n"
  },
  {
    "path": "test/fixtures/m3u8/SCTE-35_07.m3u8",
    "content": "# This example shows two EXT-X-DATERANGE tags that describe a single\n# Date Range, with a SCTE-35 \"out\" splice_insert() command that is\n# subsequently updated with an SCTE-35 \"in\" splice_insert() command.\n\n#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:30\n\n#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z\n#EXTINF:30,\nhttp://media.example.com/01.ts\n#EXTINF:30,\nhttp://media.example.com/02.ts\n#EXT-X-DATERANGE:ID=\"splice-6FFFFFF0\",START-DATE=\"2023-10-09T06:16:00.820Z\",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC30250001D1F7E25300FFF0140565239AA07FEFFE015C3F90FE00526362000101010000A7C1792D\n#EXTINF:30,\nhttp://ads.example.com/ad-01.ts\n#EXTINF:30,\nhttp://ads.example.com/ad-02.ts\n#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\n#EXTINF:30,\nhttp://media.example.com/03.ts\n#EXTINF:3.003,\nhttp://media.example.com/04.ts\n"
  },
  {
    "path": "test/fixtures/m3u8/Streaming-Examples_bipbop_16x9_variant.m3u8",
    "content": "#EXTM3U\n\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"bipbop_audio\",NAME=\"BipBop Audio 1\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"eng\"\n#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\"\n\n\n#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\"\n#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\"\n#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\"\n#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\"\n#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\"\n#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\"\n#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\"\n#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\"\n\n\n#EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS=\"mp4a.40.2, avc1.4d400d\",RESOLUTION=416x234,AUDIO=\"bipbop_audio\",SUBTITLES=\"subs\"\ngear1/prog_index.m3u8\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=28451,URI=\"gear1/iframe_index.m3u8\",CODECS=\"avc1.4d400d\"\n\n#EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS=\"mp4a.40.2, avc1.4d401e\",RESOLUTION=640x360,AUDIO=\"bipbop_audio\",SUBTITLES=\"subs\"\ngear2/prog_index.m3u8\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=181534,URI=\"gear2/iframe_index.m3u8\",CODECS=\"avc1.4d401e\"\n\n#EXT-X-STREAM-INF:BANDWIDTH=915905,CODECS=\"mp4a.40.2, avc1.4d401f\",RESOLUTION=960x540,AUDIO=\"bipbop_audio\",SUBTITLES=\"subs\"\ngear3/prog_index.m3u8\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=297056,URI=\"gear3/iframe_index.m3u8\",CODECS=\"avc1.4d401f\"\n\n#EXT-X-STREAM-INF:BANDWIDTH=1030138,CODECS=\"mp4a.40.2, avc1.4d401f\",RESOLUTION=1280x720,AUDIO=\"bipbop_audio\",SUBTITLES=\"subs\"\ngear4/prog_index.m3u8\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=339492,URI=\"gear4/iframe_index.m3u8\",CODECS=\"avc1.4d401f\"\n\n#EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS=\"mp4a.40.2, avc1.4d401f\",RESOLUTION=1920x1080,AUDIO=\"bipbop_audio\",SUBTITLES=\"subs\"\ngear5/prog_index.m3u8\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=669554,URI=\"gear5/iframe_index.m3u8\",CODECS=\"avc1.4d401f\"\n\n#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\",AUDIO=\"bipbop_audio\",SUBTITLES=\"subs\"\ngear0/prog_index.m3u8\n"
  },
  {
    "path": "test/fixtures/m3u8/Streaming-Examples_img_bipbop_adv_example_ts_master.m3u8",
    "content": "#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,AUTOSELECT=YES,LANGUAGE=\"en\",CHANNELS=\"2\",URI=\"a1/prog_index.m3u8\"\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"sub1\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE=\"en\",URI=\"s1/en/prog_index.m3u8\"\n#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc1\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",INSTREAM-ID=\"CC1\"\n\n#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\"\nv5/prog_index.m3u8\n#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\"\nv9/prog_index.m3u8\n#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\"\nv8/prog_index.m3u8\n#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\"\nv7/prog_index.m3u8\n#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\"\nv6/prog_index.m3u8\n#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\"\nv4/prog_index.m3u8\n#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\"\nv3/prog_index.m3u8\n#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\"\nv2/prog_index.m3u8\n\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",CHANNELS=\"6\",URI=\"a2/prog_index.m3u8\"\n\n#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\"\nv5/prog_index.m3u8\n#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\"\nv9/prog_index.m3u8\n#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\"\nv8/prog_index.m3u8\n#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\"\nv7/prog_index.m3u8\n#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\"\nv6/prog_index.m3u8\n#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\"\nv4/prog_index.m3u8\n#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\"\nv3/prog_index.m3u8\n#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\"\nv2/prog_index.m3u8\n\n#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud3\",NAME=\"English\",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\",CHANNELS=\"6\",URI=\"a3/prog_index.m3u8\"\n\n#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\"\nv5/prog_index.m3u8\n#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\"\nv9/prog_index.m3u8\n#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\"\nv8/prog_index.m3u8\n#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\"\nv7/prog_index.m3u8\n#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\"\nv6/prog_index.m3u8\n#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\"\nv4/prog_index.m3u8\n#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\"\nv3/prog_index.m3u8\n#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\"\nv2/prog_index.m3u8\n\n\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=186522,AVERAGE-BANDWIDTH=182077,URI=\"v7/iframe_index.m3u8\",CODECS=\"avc1.64002a\",RESOLUTION=1920x1080\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=133856,AVERAGE-BANDWIDTH=129936,URI=\"v6/iframe_index.m3u8\",CODECS=\"avc1.640020\",RESOLUTION=1280x720\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=98136,AVERAGE-BANDWIDTH=94286,URI=\"v5/iframe_index.m3u8\",CODECS=\"avc1.640020\",RESOLUTION=960x540\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=76704,AVERAGE-BANDWIDTH=74767,URI=\"v4/iframe_index.m3u8\",CODECS=\"avc1.64001e\",RESOLUTION=768x432\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=64078,AVERAGE-BANDWIDTH=62251,URI=\"v3/iframe_index.m3u8\",CODECS=\"avc1.64001e\",RESOLUTION=640x360\n#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=38728,AVERAGE-BANDWIDTH=37866,URI=\"v2/iframe_index.m3u8\",CODECS=\"avc1.640015\",RESOLUTION=480x270\n"
  },
  {
    "path": "test/fixtures/objects/8.1-Simple-Media-Playlist.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 3,\n  targetDuration: 10,\n  segments: createSegments(),\n  endlist: true\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'http://media.example.com/first.ts',\n    duration: 9.009,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/second.ts',\n    duration: 9.009,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/third.ts',\n    duration: 3.003,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js",
    "content": "const {MediaPlaylist, Segment, DateRange} = require('../../../types');\nconst utils = require('../../../utils');\n\nconst playlist = new MediaPlaylist({\n  version: 3,\n  targetDuration: 30,\n  segments: createSegments()\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'http://media.example.com/01.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0,\n    programDateTime: new Date('2014-03-05T11:14:00Z')\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/02.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://ads.example.com/ad-01.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0,\n    dateRange: new DateRange({\n      id: 'splice-6FFFFFF0',\n      start: new Date('2014-03-05T11:15:00Z'),\n      plannedDuration: 59.993,\n      attributes: {\n        'SCTE35-OUT': utils.hexToByteSequence('0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000')\n      }\n    })\n  }));\n  segments.push(new Segment({\n    uri: 'http://ads.example.com/ad-02.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 3,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/03.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 4,\n    discontinuitySequence: 0,\n    dateRange: new DateRange({\n      id: 'splice-6FFFFFF0',\n      duration: 59.993,\n      attributes: {\n        'SCTE35-IN': utils.hexToByteSequence('0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000')\n      }\n    })\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/04.ts',\n    duration: 3.003,\n    title: '',\n    mediaSequenceNumber: 5,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.11-EXT-X-CUE-OUT-Media-Playlist.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 3,\n  targetDuration: 10,\n  segments: createSegments(),\n  endlist: true\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'http://media.example.com/first.ts',\n    duration: 9.009,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/second.ts',\n    duration: 9.009,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'OUT',\n      duration: 15\n    }]\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/third.ts',\n    duration: 3.003,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.2-Live-Media-Playlist_using-HTTPS.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst mediaSequenceBase = 2680;\n\nconst playlist = new MediaPlaylist({\n  version: 3,\n  targetDuration: 8,\n  mediaSequenceBase,\n  segments: createSegments()\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'https://priv.example.com/fileSequence2680.ts',\n    duration: 7.975,\n    title: '',\n    mediaSequenceNumber: mediaSequenceBase + 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'https://priv.example.com/fileSequence2681.ts',\n    duration: 7.941,\n    title: '',\n    mediaSequenceNumber: mediaSequenceBase + 1,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'https://priv.example.com/fileSequence2682.ts',\n    duration: 7.975,\n    title: '',\n    mediaSequenceNumber: mediaSequenceBase + 2,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.3-Playlist-with-encrypted-Media-Segments.js",
    "content": "const {MediaPlaylist, Segment, Key} = require('../../../types');\n\nconst mediaSequenceBase = 7794;\n\nconst key1 = new Key({method: 'AES-128', uri: 'https://priv.example.com/key.php?r=52'});\nconst key2 = new Key({method: 'AES-128', uri: 'https://priv.example.com/key.php?r=53'});\n\nconst playlist = new MediaPlaylist({\n  version: 3,\n  targetDuration: 15,\n  mediaSequenceBase,\n  segments: createSegments()\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'http://media.example.com/fileSequence52-A.ts',\n    duration: 2.833,\n    title: '',\n    mediaSequenceNumber: mediaSequenceBase + 0,\n    discontinuitySequence: 0,\n    key: key1\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/fileSequence52-B.ts',\n    duration: 15.0,\n    title: '',\n    mediaSequenceNumber: mediaSequenceBase + 1,\n    discontinuitySequence: 0,\n    key: key1\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/fileSequence52-C.ts',\n    duration: 13.333,\n    title: '',\n    mediaSequenceNumber: mediaSequenceBase + 2,\n    discontinuitySequence: 0,\n    key: key1\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/fileSequence53-A.ts',\n    duration: 15.0,\n    title: '',\n    mediaSequenceNumber: mediaSequenceBase + 3,\n    discontinuitySequence: 0,\n    key: key2\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.4-Master-Playlist.js",
    "content": "const {MasterPlaylist, Variant} = require('../../../types');\n\nconst playlist = new MasterPlaylist({\n  variants: createVariants()\n});\n\nfunction createVariants() {\n  const variants = [];\n  variants.push(new Variant({\n    uri: 'http://example.com/low.m3u8',\n    bandwidth: 1280000,\n    averageBandwidth: 1000000,\n    codecs: 'avc1.640029,mp4a.40.2'\n  }));\n  variants.push(new Variant({\n    uri: 'http://example.com/mid.m3u8',\n    bandwidth: 2560000,\n    averageBandwidth: 2000000,\n    codecs: 'avc1.640029,mp4a.40.2'\n  }));\n  variants.push(new Variant({\n    uri: 'http://example.com/hi.m3u8',\n    bandwidth: 7680000,\n    averageBandwidth: 6000000,\n    codecs: 'avc1.640029,mp4a.40.2'\n  }));\n  variants.push(new Variant({\n    uri: 'http://example.com/audio-only.m3u8',\n    bandwidth: 65000,\n    codecs: 'mp4a.40.5'\n  }));\n  return variants;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.5-Master-Playlist-with-I-Frames.js",
    "content": "const {MasterPlaylist, Variant} = require('../../../types');\n\nconst playlist = new MasterPlaylist({\n  variants: createVariants()\n});\n\nfunction createVariants() {\n  const variants = [];\n  variants.push(new Variant({\n    uri: 'low/audio-video.m3u8',\n    bandwidth: 1280000,\n    codecs: 'avc1.640029,mp4a.40.2'\n  }));\n  variants.push(new Variant({\n    uri: 'low/iframe.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 86000,\n    codecs: 'avc1.640029'\n  }));\n  variants.push(new Variant({\n    uri: 'mid/audio-video.m3u8',\n    bandwidth: 2560000,\n    codecs: 'avc1.640029,mp4a.40.2'\n  }));\n  variants.push(new Variant({\n    uri: 'mid/iframe.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 150000,\n    codecs: 'avc1.640029'\n  }));\n  variants.push(new Variant({\n    uri: 'hi/audio-video.m3u8',\n    bandwidth: 7680000,\n    codecs: 'avc1.640029,mp4a.40.2'\n  }));\n  variants.push(new Variant({\n    uri: 'hi/iframe.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 550000,\n    codecs: 'avc1.640029'\n  }));\n  variants.push(new Variant({\n    uri: 'audio-only.m3u8',\n    bandwidth: 65000,\n    codecs: 'mp4a.40.5'\n  }));\n  return variants;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.6-Master-Playlist-with-Alternative-audio.js",
    "content": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = createRendition();\n\nfunction createRendition() {\n  const renditions = [];\n  renditions.push(new Rendition({\n    type: 'AUDIO',\n    uri: 'main/english-audio.m3u8',\n    groupId: 'aac',\n    language: 'en',\n    name: 'English',\n    isDefault: true,\n    autoselect: true\n  }));\n  renditions.push(new Rendition({\n    type: 'AUDIO',\n    uri: 'main/german-audio.m3u8',\n    groupId: 'aac',\n    language: 'de',\n    name: 'Deutsch',\n    isDefault: false,\n    autoselect: true\n  }));\n  renditions.push(new Rendition({\n    type: 'AUDIO',\n    uri: 'commentary/audio-only.m3u8',\n    groupId: 'aac',\n    language: 'en',\n    name: 'Commentary',\n    isDefault: false,\n    autoselect: false\n  }));\n  return renditions;\n}\n\nconst playlist = new MasterPlaylist({\n  variants: createVariants()\n});\n\nfunction createVariants() {\n  const variants = [];\n  variants.push(new Variant({\n    uri: 'low/video-only.m3u8',\n    bandwidth: 1280000,\n    codecs: 'mp4a.40.2',\n    audio: renditions,\n    currentRenditions: {audio: 0}\n  }));\n  variants.push(new Variant({\n    uri: 'mid/video-only.m3u8',\n    bandwidth: 2560000,\n    codecs: 'mp4a.40.2',\n    audio: renditions,\n    currentRenditions: {audio: 0}\n  }));\n  variants.push(new Variant({\n    uri: 'hi/video-only.m3u8',\n    bandwidth: 7680000,\n    codecs: 'mp4a.40.2',\n    audio: renditions,\n    currentRenditions: {audio: 0}\n  }));\n  variants.push(new Variant({\n    uri: 'main/english-audio.m3u8',\n    bandwidth: 65000,\n    codecs: 'mp4a.40.5',\n    audio: renditions,\n    currentRenditions: {audio: 0}\n  }));\n  return variants;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.7-Master-Playlist-with-Alternative-video.js",
    "content": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nfunction createRendition(groupId) {\n  const renditions = [];\n  renditions.push(new Rendition({\n    type: 'VIDEO',\n    uri: `${groupId}/main/audio-video.m3u8`,\n    groupId,\n    name: 'Main',\n    isDefault: !(groupId === 'mid')\n  }));\n  renditions.push(new Rendition({\n    type: 'VIDEO',\n    uri: `${groupId}/centerfield/audio-video.m3u8`,\n    groupId,\n    name: 'Centerfield',\n    isDefault: groupId === 'mid'\n  }));\n  renditions.push(new Rendition({\n    type: 'VIDEO',\n    uri: `${groupId}/dugout/audio-video.m3u8`,\n    groupId,\n    name: 'Dugout',\n    isDefault: false\n  }));\n  return renditions;\n}\n\nconst playlist = new MasterPlaylist({\n  variants: createVariants()\n});\n\nfunction createVariants() {\n  const variants = [];\n  variants.push(new Variant({\n    uri: 'low/main/audio-video.m3u8',\n    bandwidth: 1280000,\n    codecs: 'avc1.640029,mp4a.40.2',\n    video: createRendition('low'),\n    currentRenditions: {video: 0}\n  }));\n  variants.push(new Variant({\n    uri: 'mid/main/audio-video.m3u8',\n    bandwidth: 2560000,\n    codecs: 'avc1.640029,mp4a.40.2',\n    video: createRendition('mid'),\n    currentRenditions: {video: 1}\n  }));\n  variants.push(new Variant({\n    uri: 'hi/main/audio-video.m3u8',\n    bandwidth: 7680000,\n    codecs: 'avc1.640029,mp4a.40.2',\n    video: createRendition('hi')\n  }));\n  return variants;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.8-Session-Data-in-a-Master-Playlist.js",
    "content": "const {MasterPlaylist, SessionData} = require('../../../types');\n\nconst playlist = new MasterPlaylist({\n  sessionDataList: createSetssionDataList()\n});\n\nfunction createSetssionDataList() {\n  const setssionDataList = [];\n  setssionDataList.push(new SessionData({\n    id: 'com.example.lyrics',\n    uri: 'lyrics.json'\n  }));\n  setssionDataList.push(new SessionData({\n    id: 'com.example.title',\n    language: 'en',\n    value: 'This is an example'\n  }));\n  setssionDataList.push(new SessionData({\n    id: 'com.example.title',\n    language: 'es',\n    value: 'Este es un ejemplo'\n  }));\n  return setssionDataList;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js",
    "content": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = createRendition();\n\nfunction createRendition() {\n  const renditions = [];\n  renditions.push(new Rendition({\n    type: 'AUDIO',\n    uri: 'main/english-audio.m3u8',\n    groupId: 'aac',\n    language: 'en',\n    name: 'English',\n    isDefault: true,\n    autoselect: true,\n    characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read'\n  }));\n  renditions.push(new Rendition({\n    type: 'AUDIO',\n    uri: 'main/german-audio.m3u8',\n    groupId: 'aac',\n    language: 'de',\n    name: 'Deutsch',\n    isDefault: false,\n    autoselect: true,\n    characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read'\n  }));\n  renditions.push(new Rendition({\n    type: 'AUDIO',\n    uri: 'commentary/audio-only.m3u8',\n    groupId: 'aac',\n    language: 'en',\n    name: 'Commentary',\n    isDefault: false,\n    autoselect: false,\n    characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read'\n  }));\n  return renditions;\n}\n\nconst playlist = new MasterPlaylist({\n  variants: createVariants()\n});\n\nfunction createVariants() {\n  const variants = [];\n  variants.push(new Variant({\n    uri: 'low/video-only.m3u8',\n    bandwidth: 1280000,\n    codecs: 'mp4a.40.2',\n    audio: renditions,\n    currentRenditions: {audio: 0}\n  }));\n  variants.push(new Variant({\n    uri: 'mid/video-only.m3u8',\n    bandwidth: 2560000,\n    codecs: 'mp4a.40.2',\n    audio: renditions,\n    currentRenditions: {audio: 0}\n  }));\n  variants.push(new Variant({\n    uri: 'hi/video-only.m3u8',\n    bandwidth: 7680000,\n    codecs: 'mp4a.40.2',\n    audio: renditions,\n    currentRenditions: {audio: 0}\n  }));\n  variants.push(new Variant({\n    uri: 'main/english-audio.m3u8',\n    bandwidth: 65000,\n    codecs: 'mp4a.40.5',\n    audio: renditions,\n    currentRenditions: {audio: 0}\n  }));\n  return variants;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-01_Low-Latency_HLS_Playlist.js",
    "content": "const {MediaPlaylist, Segment, PartialSegment, MediaInitializationSection, RenditionReport} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 6,\n  targetDuration: 4,\n  mediaSequenceBase: 266,\n  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24, partHoldBack: 1},\n  partTargetDuration: 0.33334,\n  segments: createSegments(),\n  renditionReports: createRenditionReports()\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'fileSequence266.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 266,\n    discontinuitySequence: 0,\n    programDateTime: new Date('2019-02-14T02:13:36.106Z'),\n    map: new MediaInitializationSection({uri: 'init.mp4'})\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence267.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 267,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence268.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 268,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence269.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 269,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence270.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 270,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence271.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 271,\n    discontinuitySequence: 0,\n    parts: createParts1()\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence272.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 272,\n    discontinuitySequence: 0,\n    programDateTime: new Date('2019-02-14T02:14:00.106Z'),\n    parts: createParts2()\n  }));\n  segments.push(new Segment({\n    mediaSequenceNumber: 273,\n    parts: createParts3()\n  }));\n  return segments;\n}\n\nfunction createRenditionReports() {\n  const reports = [];\n  reports.push(new RenditionReport({\n    uri: '../1M/waitForMSN.php',\n    lastMSN: 273,\n    lastPart: 2\n  }));\n  reports.push(new RenditionReport({\n    uri: '../4M/waitForMSN.php',\n    lastMSN: 273,\n    lastPart: 1\n  }));\n  return reports;\n}\n\nfunction createParts1() {\n  const parts = [];\n  for (let i = 0; i < 12; i++) {\n    parts.push(new PartialSegment({\n      uri: `filePart271.${i}.mp4`,\n      duration: 0.33334,\n      independent: (i === 4 || i === 8)\n    }));\n  }\n  return parts;\n}\n\nfunction createParts2() {\n  const parts = [];\n  const aCode = 'a'.charCodeAt(0);\n  for (let i = 0; i < 12; i++) {\n    parts.push(new PartialSegment({\n      uri: `filePart272.${String.fromCharCode(aCode + i)}.mp4`,\n      duration: 0.33334,\n      independent: (i === 5)\n    }));\n  }\n  return parts;\n}\n\nfunction createParts3() {\n  const parts = [];\n  for (let i = 0; i < 4; i++) {\n    parts.push(new PartialSegment({\n      uri: `filePart273.${i}.mp4`,\n      duration: 0.33334,\n      independent: (i === 0),\n      hint: (i === 3)\n    }));\n  }\n  return parts;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-02_Playlist_Delta_Update.js",
    "content": "const {MediaPlaylist, Segment, PartialSegment, RenditionReport} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 9,\n  targetDuration: 4,\n  mediaSequenceBase: 266,\n  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.0},\n  partTargetDuration: 0.33334,\n  skip: 3,\n  segments: createSegments(),\n  renditionReports: createRenditionReports()\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'fileSequence269.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 269,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence270.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 270,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence271.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 271,\n    discontinuitySequence: 0,\n    parts: createParts1()\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence272.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 272,\n    discontinuitySequence: 0,\n    programDateTime: new Date('2019-02-14T02:14:00.106Z'),\n    parts: createParts2()\n  }));\n  segments.push(new Segment({\n    mediaSequenceNumber: 273,\n    parts: createParts3()\n  }));\n  return segments;\n}\n\nfunction createRenditionReports() {\n  const reports = [];\n  reports.push(new RenditionReport({\n    uri: '../1M/waitForMSN.php',\n    lastMSN: 273,\n    lastPart: 3\n  }));\n  reports.push(new RenditionReport({\n    uri: '../4M/waitForMSN.php',\n    lastMSN: 273,\n    lastPart: 3\n  }));\n  return reports;\n}\n\nfunction createParts1() {\n  const parts = [];\n  for (let i = 0; i < 12; i++) {\n    parts.push(new PartialSegment({\n      uri: `filePart271.${i}.mp4`,\n      duration: 0.33334,\n      independent: (i === 4 || i === 8)\n    }));\n  }\n  return parts;\n}\n\nfunction createParts2() {\n  const parts = [];\n  const aCode = 'a'.charCodeAt(0);\n  for (let i = 0; i < 12; i++) {\n    parts.push(new PartialSegment({\n      uri: `filePart272.${String.fromCharCode(aCode + i)}.mp4`,\n      duration: 0.33334,\n      independent: (i === 5)\n    }));\n  }\n  return parts;\n}\n\nfunction createParts3() {\n  const parts = [];\n  for (let i = 0; i < 5; i++) {\n    parts.push(new PartialSegment({\n      uri: `filePart273.${i}.mp4`,\n      duration: 0.33334,\n      independent: (i === 0),\n      hint: (i === 4)\n    }));\n  }\n  return parts;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-01.js",
    "content": "const {MediaPlaylist, Segment, PartialSegment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 9,\n  targetDuration: 4,\n  mediaSequenceBase: 266,\n  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02},\n  partTargetDuration: 1.02,\n  skip: 3,\n  segments: createSegments()\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'fileSequence269.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 269,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence270.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 270,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    mediaSequenceNumber: 271,\n    parts: createParts()\n  }));\n  return segments;\n}\n\nfunction createParts() {\n  const parts = [];\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 0, length: 20000}\n  }));\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 20000, length: 23000}\n  }));\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 43000, length: 18000}\n  }));\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    byterange: {offset: 61000},\n    hint: true\n  }));\n  return parts;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-02.js",
    "content": "const {MediaPlaylist, Segment, PartialSegment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 9,\n  targetDuration: 4,\n  mediaSequenceBase: 266,\n  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02},\n  partTargetDuration: 1.02,\n  skip: 3,\n  segments: createSegments()\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'fileSequence269.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 269,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence270.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 270,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence271.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 271,\n    discontinuitySequence: 0,\n    parts: createParts()\n  }));\n  segments.push(new Segment({\n    mediaSequenceNumber: 272,\n    parts: [new PartialSegment({\n      uri: 'fileSequence272.mp4',\n      byterange: {offset: 0},\n      hint: true\n    })]\n  }));\n  return segments;\n}\n\nfunction createParts() {\n  const parts = [];\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 0, length: 20000}\n  }));\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 20000, length: 23000}\n  }));\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 43000, length: 18000}\n  }));\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 61000, length: 19000}\n  }));\n  return parts;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-03.js",
    "content": "const {MediaPlaylist, Segment, PartialSegment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 9,\n  targetDuration: 4,\n  mediaSequenceBase: 266,\n  lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02},\n  partTargetDuration: 1.02,\n  skip: 3,\n  segments: createSegments()\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'fileSequence269.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 269,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence270.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 270,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'fileSequence271.mp4',\n    duration: 4.00008,\n    title: '',\n    mediaSequenceNumber: 271,\n    discontinuitySequence: 0,\n    parts: createParts()\n  }));\n  segments.push(new Segment({\n    mediaSequenceNumber: 272,\n    parts: [\n      new PartialSegment({\n        uri: 'fileSequence272.mp4',\n        duration: 1.02,\n        byterange: {offset: 0, length: 21000}\n      }),\n      new PartialSegment({\n        uri: 'fileSequence272.mp4',\n        byterange: {offset: 21000},\n        hint: true\n      })\n    ]\n  }));\n  return segments;\n}\n\nfunction createParts() {\n  const parts = [];\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 0, length: 20000}\n  }));\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 20000, length: 23000}\n  }));\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 43000, length: 18000}\n  }));\n  parts.push(new PartialSegment({\n    uri: 'fileSequence271.mp4',\n    duration: 1.02,\n    byterange: {offset: 61000, length: 19000}\n  }));\n  return parts;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/Multiple-rendition-groups.js",
    "content": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = [\n  new Rendition({type: 'AUDIO', groupId: 'aac_high', name: 'English', isDefault: true, uri: 'aac_high_eng.m3u8'}),\n  new Rendition({type: 'AUDIO', groupId: 'aac_high', name: 'Japanese', isDefault: false, uri: 'aac_high_jp.m3u8'}),\n  new Rendition({type: 'AUDIO', groupId: 'aac_mid', name: 'English', isDefault: true, uri: 'aac_mid_eng.m3u8'}),\n  new Rendition({type: 'AUDIO', groupId: 'aac_mid', name: 'Japanese', isDefault: false, uri: 'aac_mid_jp.m3u8'}),\n  new Rendition({type: 'AUDIO', groupId: 'aac_low', name: 'English', isDefault: true, uri: 'aac_low_eng.m3u8'}),\n  new Rendition({type: 'AUDIO', groupId: 'aac_low', name: 'Japanese', isDefault: false, uri: 'aac_low_jp.m3u8'}),\n];\nconst variants = [\n  {uri: '1080p.m3u8', bandwidth: 6000000, audioId: 'aac_high'},\n  {uri: '720p.m3u8', bandwidth: 3000000, audioId: 'aac_mid'},\n  {uri: '540p.m3u8', bandwidth: 1500000, audioId: 'aac_mid'},\n  {uri: '360p.m3u8', bandwidth: 1000000, audioId: 'aac_low'},\n].map(\n  ({uri, bandwidth, audioId}) => new Variant({\n    uri, bandwidth, audio: renditions.filter(({groupId}) => groupId === audioId)\n  })\n);\n\nconst playlist = new MasterPlaylist({\n  version: 4,\n  independentSegments: true,\n  variants,\n});\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/RedundantSegments.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  version: 4,\n  targetDuration: 10,\n  segments: createSegments(),\n  endlist: true\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'http://media.example.com/first.ts',\n    duration: 9.009,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/second.ts',\n    duration: 9.009,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/second.ts',\n    byterange: {offset: 256, length: 128},\n    duration: 9.009,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/third.ts',\n    duration: 3.003,\n    title: '',\n    mediaSequenceNumber: 3,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_01.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',\n  version: 3,\n  targetDuration: 8,\n  segments: createSegments(),\n  endlist: true\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: '1.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '2.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'OUT',\n      duration: 15.0\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '3.ts',\n    duration: 7,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '4.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 3,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'IN'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '5.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 4,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_02.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',\n  version: 3,\n  targetDuration: 8,\n  segments: createSegments(),\n  endlist: true\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: '1.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '2.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'OUT',\n      duration: 23.0\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '3.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-CUE-OUT-CONT',\n      value: 'ElapsedTime=8,Duration=23'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '4.ts',\n    duration: 7,\n    title: '',\n    mediaSequenceNumber: 3,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-CUE-OUT-CONT',\n      value: 'ElapsedTime=16,Duration=23'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '5.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 4,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'IN'\n    }]\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_03.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',\n  version: 3,\n  targetDuration: 8,\n  segments: createSegments(),\n  endlist: true\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: '1.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '2.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-CUE',\n      value: 'DURATION=\"15.0\",ID=\"0\",TYPE=\"SpliceOut\",TIME=\"414.171\"'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '3.ts',\n    duration: 7,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '4.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 3,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '5.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 4,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_04.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',\n  version: 3,\n  targetDuration: 8,\n  segments: createSegments(),\n  endlist: true\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: '1.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '2.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0,\n    markers: [\n      {\n        type: 'RAW',\n        tagName: 'EXT-OATCLS-SCTE35',\n        value: '/DA0AAAAAAAAAAAABQb+ADAQ6QAeAhxDVUVJQAAAO3/PAAEUrEoICAAAAAAg+2UBNAAANvrtoQ=='\n      },\n      {\n        type: 'RAW',\n        tagName: 'EXT-X-ASSET',\n        value: 'CAID=0x0000000020FB6501'\n      },\n      {\n        type: 'OUT',\n        duration: 15.0\n      }\n    ]\n  }));\n  segments.push(new Segment({\n    uri: '3.ts',\n    duration: 7,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-CUE-OUT-CONT',\n      value: 'ElapsedTime=5.939,Duration=25.0,SCTE35=/DA0AAAA+…AAg+2UBNAAANvrtoQ=='\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '4.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 3,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'IN'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '5.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 4,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_05.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',\n  version: 3,\n  targetDuration: 8,\n  segments: createSegments(),\n  endlist: true\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: '1.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '2.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-SCTE35',\n      value: 'TYPE=0x34,DURATION=15.0,CUE-OUT=YES,UPID=\"0x08:0x9425BC\",CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”pIViS5”'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '3.ts',\n    duration: 7,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '4.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 3,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-SCTE35',\n      value: 'TYPE=0x35,CUE-IN=YES,CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”f6UrRd”'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '5.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 4,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_06.js",
    "content": "const {MediaPlaylist, Segment} = require('../../../types');\n\nconst playlist = new MediaPlaylist({\n  playlistType: 'VOD',\n  version: 3,\n  targetDuration: 8,\n  segments: createSegments(),\n  endlist: true\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: '1.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: '2.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'OUT',\n      duration: 23.0\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '3.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-CUE-OUT-CONT',\n      value: 'ElapsedTime=8,Duration=23'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '4.ts',\n    duration: 7,\n    title: '',\n    mediaSequenceNumber: 3,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-CUE-OUT-CONT',\n      value: 'ElapsedTime=16,Duration=23'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '5.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 4,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'IN'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '6.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 5,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'OUT',\n      duration: 23.0\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '7.ts',\n    duration: 8,\n    title: '',\n    mediaSequenceNumber: 6,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-CUE-OUT-CONT',\n      value: 'ElapsedTime=8,Duration=23'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '8.ts',\n    duration: 7,\n    title: '',\n    mediaSequenceNumber: 7,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'RAW',\n      tagName: 'EXT-X-CUE-OUT-CONT',\n      value: 'ElapsedTime=16,Duration=23'\n    }]\n  }));\n  segments.push(new Segment({\n    uri: '9.ts',\n    duration: 8.008,\n    title: '',\n    mediaSequenceNumber: 8,\n    discontinuitySequence: 0,\n    markers: [{\n      type: 'IN'\n    }]\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/SCTE-35_07.js",
    "content": "const {MediaPlaylist, Segment, DateRange} = require('../../../types');\nconst utils = require('../../../utils');\n\nconst playlist = new MediaPlaylist({\n  version: 3,\n  targetDuration: 30,\n  segments: createSegments()\n});\n\nfunction createSegments() {\n  const segments = [];\n  segments.push(new Segment({\n    uri: 'http://media.example.com/01.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 0,\n    discontinuitySequence: 0,\n    programDateTime: new Date('2014-03-05T11:14:00Z')\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/02.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 1,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://ads.example.com/ad-01.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 2,\n    discontinuitySequence: 0,\n    dateRange: new DateRange({\n      id: 'splice-6FFFFFF0',\n      start: new Date('2023-10-09T06:16:00.820Z'),\n      plannedDuration: 59.993,\n      attributes: {\n        'SCTE35-OUT': utils.hexToByteSequence('0xFC30250001D1F7E25300FFF0140565239AA07FEFFE015C3F90FE00526362000101010000A7C1792D')\n      }\n    })\n  }));\n  segments.push(new Segment({\n    uri: 'http://ads.example.com/ad-02.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 3,\n    discontinuitySequence: 0\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/03.ts',\n    duration: 30,\n    title: '',\n    mediaSequenceNumber: 4,\n    discontinuitySequence: 0,\n    dateRange: new DateRange({\n      id: 'splice-6FFFFFF0',\n      start: new Date('2023-10-09T06:16:00.820Z'),\n      end: new Date('2023-10-09T06:17:01.514Z'),\n      duration: 60.694\n    })\n  }));\n  segments.push(new Segment({\n    uri: 'http://media.example.com/04.ts',\n    duration: 3.003,\n    title: '',\n    mediaSequenceNumber: 5,\n    discontinuitySequence: 0\n  }));\n  return segments;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/Streaming-Examples_bipbop_16x9_variant.js",
    "content": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = {\n  bipbop_audio: [\n    new Rendition({\n      type: 'AUDIO',\n      groupId: 'bipbop_audio',\n      language: 'eng',\n      name: 'BipBop Audio 1',\n      autoselect: true,\n      isDefault: true\n    }),\n    new Rendition({\n      type: 'AUDIO',\n      uri: 'alternate_audio_aac/prog_index.m3u8',\n      groupId: 'bipbop_audio',\n      language: 'eng',\n      name: 'BipBop Audio 2',\n      autoselect: false,\n      isDefault: false\n    })\n  ],\n  subs: [\n    new Rendition({\n      type: 'SUBTITLES',\n      uri: 'subtitles/eng/prog_index.m3u8',\n      groupId: 'subs',\n      language: 'en',\n      name: 'English',\n      autoselect: true,\n      isDefault: true,\n      forced: false,\n      characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound'\n    }),\n    new Rendition({\n      type: 'SUBTITLES',\n      uri: 'subtitles/eng_forced/prog_index.m3u8',\n      groupId: 'subs',\n      language: 'en',\n      name: 'English (Forced)',\n      autoselect: false,\n      isDefault: false,\n      forced: true\n    }),\n    new Rendition({\n      type: 'SUBTITLES',\n      uri: 'subtitles/fra/prog_index.m3u8',\n      groupId: 'subs',\n      language: 'fr',\n      name: 'Français',\n      autoselect: true,\n      isDefault: false,\n      forced: false,\n      characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound'\n    }),\n    new Rendition({\n      type: 'SUBTITLES',\n      uri: 'subtitles/fra_forced/prog_index.m3u8',\n      groupId: 'subs',\n      language: 'fr',\n      name: 'Français (Forced)',\n      autoselect: false,\n      isDefault: false,\n      forced: true\n    }),\n    new Rendition({\n      type: 'SUBTITLES',\n      uri: 'subtitles/spa/prog_index.m3u8',\n      groupId: 'subs',\n      language: 'es',\n      name: 'Español',\n      autoselect: true,\n      isDefault: false,\n      forced: false,\n      characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound'\n    }),\n    new Rendition({\n      type: 'SUBTITLES',\n      uri: 'subtitles/spa_forced/prog_index.m3u8',\n      groupId: 'subs',\n      language: 'es',\n      name: 'Español (Forced)',\n      autoselect: false,\n      isDefault: false,\n      forced: true\n    }),\n    new Rendition({\n      type: 'SUBTITLES',\n      uri: 'subtitles/jpn/prog_index.m3u8',\n      groupId: 'subs',\n      language: 'ja',\n      name: '日本語',\n      autoselect: true,\n      isDefault: false,\n      forced: false,\n      characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound'\n    }),\n    new Rendition({\n      type: 'SUBTITLES',\n      uri: 'subtitles/jpn_forced/prog_index.m3u8',\n      groupId: 'subs',\n      language: 'ja',\n      name: '日本語 (Forced)',\n      autoselect: false,\n      isDefault: false,\n      forced: true\n    })\n  ]\n};\n\nconst playlist = new MasterPlaylist({\n  variants: createVariants()\n});\n\nfunction createVariants() {\n  const variants = [];\n  variants.push(new Variant({\n    uri: 'gear1/prog_index.m3u8',\n    bandwidth: 263851,\n    codecs: 'mp4a.40.2, avc1.4d400d',\n    resolution: {width: 416, height: 234},\n    audio: renditions.bipbop_audio,\n    subtitles: renditions.subs\n  }));\n  variants.push(new Variant({\n    uri: 'gear1/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 28451,\n    codecs: 'avc1.4d400d'\n  }));\n  variants.push(new Variant({\n    uri: 'gear2/prog_index.m3u8',\n    bandwidth: 577610,\n    codecs: 'mp4a.40.2, avc1.4d401e',\n    resolution: {width: 640, height: 360},\n    audio: renditions.bipbop_audio,\n    subtitles: renditions.subs\n  }));\n  variants.push(new Variant({\n    uri: 'gear2/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 181534,\n    codecs: 'avc1.4d401e'\n  }));\n  variants.push(new Variant({\n    uri: 'gear3/prog_index.m3u8',\n    bandwidth: 915905,\n    codecs: 'mp4a.40.2, avc1.4d401f',\n    resolution: {width: 960, height: 540},\n    audio: renditions.bipbop_audio,\n    subtitles: renditions.subs\n  }));\n  variants.push(new Variant({\n    uri: 'gear3/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 297056,\n    codecs: 'avc1.4d401f'\n  }));\n  variants.push(new Variant({\n    uri: 'gear4/prog_index.m3u8',\n    bandwidth: 1030138,\n    codecs: 'mp4a.40.2, avc1.4d401f',\n    resolution: {width: 1280, height: 720},\n    audio: renditions.bipbop_audio,\n    subtitles: renditions.subs\n  }));\n  variants.push(new Variant({\n    uri: 'gear4/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 339492,\n    codecs: 'avc1.4d401f'\n  }));\n  variants.push(new Variant({\n    uri: 'gear5/prog_index.m3u8',\n    bandwidth: 1924009,\n    codecs: 'mp4a.40.2, avc1.4d401f',\n    resolution: {width: 1920, height: 1080},\n    audio: renditions.bipbop_audio,\n    subtitles: renditions.subs\n  }));\n  variants.push(new Variant({\n    uri: 'gear5/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 669554,\n    codecs: 'avc1.4d401f'\n  }));\n  variants.push(new Variant({\n    uri: 'gear0/prog_index.m3u8',\n    bandwidth: 41457,\n    codecs: 'mp4a.40.2',\n    audio: renditions.bipbop_audio,\n    subtitles: renditions.subs\n  }));\n  return variants;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/fixtures/objects/Streaming-Examples_img_bipbop_adv_example_ts_master.js",
    "content": "const {MasterPlaylist, Variant, Rendition} = require('../../../types');\n\nconst renditions = {\n  aud1: [\n    new Rendition({\n      type: 'AUDIO',\n      uri: 'a1/prog_index.m3u8',\n      groupId: 'aud1',\n      language: 'en',\n      name: 'English',\n      autoselect: true,\n      isDefault: true,\n      channels: '2'\n    })\n  ],\n  aud2: [\n    new Rendition({\n      type: 'AUDIO',\n      uri: 'a2/prog_index.m3u8',\n      groupId: 'aud2',\n      language: 'en',\n      name: 'English',\n      autoselect: true,\n      isDefault: true,\n      channels: '6'\n    })\n  ],\n  aud3: [\n    new Rendition({\n      type: 'AUDIO',\n      uri: 'a3/prog_index.m3u8',\n      groupId: 'aud3',\n      language: 'en',\n      name: 'English',\n      autoselect: true,\n      isDefault: true,\n      channels: '6'\n    })\n  ],\n  cc1: [\n    new Rendition({\n      type: 'CLOSED-CAPTIONS',\n      groupId: 'cc1',\n      language: 'en',\n      name: 'English',\n      autoselect: true,\n      isDefault: true,\n      instreamId: 'CC1'\n    })\n  ],\n  sub1: [\n    new Rendition({\n      type: 'SUBTITLES',\n      uri: 's1/en/prog_index.m3u8',\n      groupId: 'sub1',\n      language: 'en',\n      name: 'English',\n      autoselect: true,\n      isDefault: true,\n      forced: false\n    })\n  ]\n};\n\nconst playlist = new MasterPlaylist({\n  version: 6,\n  independentSegments: true,\n  variants: createVariants()\n});\n\nfunction createVariants() {\n  const variants = [];\n  variants.push(new Variant({\n    uri: 'v5/prog_index.m3u8',\n    bandwidth: 2227464,\n    averageBandwidth: 2218327,\n    codecs: 'avc1.640020,mp4a.40.2',\n    resolution: {width: 960, height: 540},\n    frameRate: 60.0,\n    audio: renditions.aud1,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v9/prog_index.m3u8',\n    bandwidth: 8178040,\n    averageBandwidth: 8144656,\n    codecs: 'avc1.64002a,mp4a.40.2',\n    resolution: {width: 1920, height: 1080},\n    frameRate: 60.0,\n    audio: renditions.aud1,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v8/prog_index.m3u8',\n    bandwidth: 6453202,\n    averageBandwidth: 6307144,\n    codecs: 'avc1.64002a,mp4a.40.2',\n    resolution: {width: 1920, height: 1080},\n    frameRate: 60.0,\n    audio: renditions.aud1,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v7/prog_index.m3u8',\n    bandwidth: 5054232,\n    averageBandwidth: 4775338,\n    codecs: 'avc1.64002a,mp4a.40.2',\n    resolution: {width: 1920, height: 1080},\n    frameRate: 60.0,\n    audio: renditions.aud1,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v6/prog_index.m3u8',\n    bandwidth: 3289288,\n    averageBandwidth: 3240596,\n    codecs: 'avc1.640020,mp4a.40.2',\n    resolution: {width: 1280, height: 720},\n    frameRate: 60.0,\n    audio: renditions.aud1,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v4/prog_index.m3u8',\n    bandwidth: 1296989,\n    averageBandwidth: 1292926,\n    codecs: 'avc1.64001e,mp4a.40.2',\n    resolution: {width: 768, height: 432},\n    frameRate: 30.0,\n    audio: renditions.aud1,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v3/prog_index.m3u8',\n    bandwidth: 922242,\n    averageBandwidth: 914722,\n    codecs: 'avc1.64001e,mp4a.40.2',\n    resolution: {width: 640, height: 360},\n    frameRate: 30.0,\n    audio: renditions.aud1,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v2/prog_index.m3u8',\n    bandwidth: 553010,\n    averageBandwidth: 541239,\n    codecs: 'avc1.640015,mp4a.40.2',\n    resolution: {width: 480, height: 270},\n    frameRate: 30.0,\n    audio: renditions.aud1,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v5/prog_index.m3u8',\n    bandwidth: 2448841,\n    averageBandwidth: 2439704,\n    codecs: 'avc1.640020,ac-3',\n    resolution: {width: 960, height: 540},\n    frameRate: 60.0,\n    audio: renditions.aud2,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v9/prog_index.m3u8',\n    bandwidth: 8399417,\n    averageBandwidth: 8366033,\n    codecs: 'avc1.64002a,ac-3',\n    resolution: {width: 1920, height: 1080},\n    frameRate: 60.0,\n    audio: renditions.aud2,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v8/prog_index.m3u8',\n    bandwidth: 6674579,\n    averageBandwidth: 6528521,\n    codecs: 'avc1.64002a,ac-3',\n    resolution: {width: 1920, height: 1080},\n    frameRate: 60.0,\n    audio: renditions.aud2,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v7/prog_index.m3u8',\n    bandwidth: 5275609,\n    averageBandwidth: 4996715,\n    codecs: 'avc1.64002a,ac-3',\n    resolution: {width: 1920, height: 1080},\n    frameRate: 60.0,\n    audio: renditions.aud2,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v6/prog_index.m3u8',\n    bandwidth: 3510665,\n    averageBandwidth: 3461973,\n    codecs: 'avc1.640020,ac-3',\n    resolution: {width: 1280, height: 720},\n    frameRate: 60.0,\n    audio: renditions.aud2,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v4/prog_index.m3u8',\n    bandwidth: 1518366,\n    averageBandwidth: 1514303,\n    codecs: 'avc1.64001e,ac-3',\n    resolution: {width: 768, height: 432},\n    frameRate: 30.0,\n    audio: renditions.aud2,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v3/prog_index.m3u8',\n    bandwidth: 1143619,\n    averageBandwidth: 1136099,\n    codecs: 'avc1.64001e,ac-3',\n    resolution: {width: 640, height: 360},\n    frameRate: 30.0,\n    audio: renditions.aud2,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v2/prog_index.m3u8',\n    bandwidth: 774387,\n    averageBandwidth: 762616,\n    codecs: 'avc1.640015,ac-3',\n    resolution: {width: 480, height: 270},\n    frameRate: 30.0,\n    audio: renditions.aud2,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v5/prog_index.m3u8',\n    bandwidth: 2256841,\n    averageBandwidth: 2247704,\n    codecs: 'avc1.640020,ec-3',\n    resolution: {width: 960, height: 540},\n    frameRate: 60.0,\n    audio: renditions.aud3,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v9/prog_index.m3u8',\n    bandwidth: 8207417,\n    averageBandwidth: 8174033,\n    codecs: 'avc1.64002a,ec-3',\n    resolution: {width: 1920, height: 1080},\n    frameRate: 60.0,\n    audio: renditions.aud3,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v8/prog_index.m3u8',\n    bandwidth: 6482579,\n    averageBandwidth: 6336521,\n    codecs: 'avc1.64002a,ec-3',\n    resolution: {width: 1920, height: 1080},\n    frameRate: 60.0,\n    audio: renditions.aud3,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v7/prog_index.m3u8',\n    bandwidth: 5083609,\n    averageBandwidth: 4804715,\n    codecs: 'avc1.64002a,ec-3',\n    resolution: {width: 1920, height: 1080},\n    frameRate: 60.0,\n    audio: renditions.aud3,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v6/prog_index.m3u8',\n    bandwidth: 3318665,\n    averageBandwidth: 3269973,\n    codecs: 'avc1.640020,ec-3',\n    resolution: {width: 1280, height: 720},\n    frameRate: 60.0,\n    audio: renditions.aud3,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v4/prog_index.m3u8',\n    bandwidth: 1326366,\n    averageBandwidth: 1322303,\n    codecs: 'avc1.64001e,ec-3',\n    resolution: {width: 768, height: 432},\n    frameRate: 30.0,\n    audio: renditions.aud3,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v3/prog_index.m3u8',\n    bandwidth: 951619,\n    averageBandwidth: 944099,\n    codecs: 'avc1.64001e,ec-3',\n    resolution: {width: 640, height: 360},\n    frameRate: 30.0,\n    audio: renditions.aud3,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v2/prog_index.m3u8',\n    bandwidth: 582387,\n    averageBandwidth: 570616,\n    codecs: 'avc1.640015,ec-3',\n    resolution: {width: 480, height: 270},\n    frameRate: 30.0,\n    audio: renditions.aud3,\n    subtitles: renditions.sub1,\n    closedCaptions: renditions.cc1\n  }));\n  variants.push(new Variant({\n    uri: 'v7/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 186522,\n    averageBandwidth: 182077,\n    codecs: 'avc1.64002a',\n    resolution: {width: 1920, height: 1080}\n  }));\n  variants.push(new Variant({\n    uri: 'v6/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 133856,\n    averageBandwidth: 129936,\n    codecs: 'avc1.640020',\n    resolution: {width: 1280, height: 720}\n  }));\n  variants.push(new Variant({\n    uri: 'v5/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 98136,\n    averageBandwidth: 94286,\n    codecs: 'avc1.640020',\n    resolution: {width: 960, height: 540}\n  }));\n  variants.push(new Variant({\n    uri: 'v4/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 76704,\n    averageBandwidth: 74767,\n    codecs: 'avc1.64001e',\n    resolution: {width: 768, height: 432}\n  }));\n  variants.push(new Variant({\n    uri: 'v3/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 64078,\n    averageBandwidth: 62251,\n    codecs: 'avc1.64001e',\n    resolution: {width: 640, height: 360}\n  }));\n  variants.push(new Variant({\n    uri: 'v2/iframe_index.m3u8',\n    isIFrameOnly: true,\n    bandwidth: 38728,\n    averageBandwidth: 37866,\n    codecs: 'avc1.640015',\n    resolution: {width: 480, height: 270}\n  }));\n  return variants;\n}\n\nmodule.exports = playlist;\n"
  },
  {
    "path": "test/helpers/fixtures.js",
    "content": "const path = require('node:path');\nconst fs = require('node:fs');\n\nconst fixtures = [];\nconst baseDir = path.join(__dirname, '../fixtures/m3u8');\nconst filenames = fs.readdirSync(baseDir);\n\nfor (const filename of filenames) {\n  if (filename.endsWith('.m3u8')) {\n    const name = path.basename(filename, '.m3u8');\n    const filepath = path.join(baseDir, filename);\n    const m3u8 = fs.readFileSync(filepath, 'utf8');\n    const object = require(`../fixtures/objects/${name}.js`);\n    fixtures.push({name, m3u8, object});\n  }\n}\n\nmodule.exports = fixtures;\n"
  },
  {
    "path": "test/helpers/matchers.js",
    "content": "function removeSpaceFromLine(line) {\n  let inside = false;\n  let str = '';\n  for (const ch of line) {\n    if (ch === '\"') {\n      inside = !inside;\n    } else if (!inside && ch === ' ') {\n      continue;\n    }\n    str += ch;\n  }\n  return str;\n}\n\nfunction strip(playlist) {\n  playlist = playlist.trim();\n  const filtered = [];\n  for (let line of playlist.split('\\n')) {\n    line = removeSpaceFromLine(line);\n    if (line.startsWith('#')) {\n      if (line.startsWith('#EXT')) {\n        filtered.push(line);\n      }\n    } else {\n      filtered.push(line);\n    }\n  }\n  return filtered.join('\\n');\n}\n\nfunction equalPlaylist(t, expected, actual) {\n  expected &&= strip(expected);\n  actual &&= strip(actual);\n  if (expected === actual) {\n    return t.pass();\n  }\n  t.fail(`expected=\"${expected}\", actual=\"${actual}\"`);\n}\n\nfunction notEqualPlaylist(t, expected, actual) {\n  expected &&= strip(expected);\n  actual &&= strip(actual);\n  if (expected === actual) {\n    t.fail(`expected=\"${expected}\", actual=\"${actual}\"`);\n    return t.fail();\n  }\n  t.pass();\n}\n\nmodule.exports = {\n  equalPlaylist,\n  notEqualPlaylist\n};\n"
  },
  {
    "path": "test/helpers/utils.js",
    "content": "const HLS = require('../..');\n\nHLS.setOptions({strictMode: true});\n\nfunction parsePass(t, text) {\n  let obj;\n  try {\n    obj = HLS.parse(text);\n  } catch (err) {\n    t.fail(err.stack);\n  }\n  t.truthy(obj);\n  return obj;\n}\n\nfunction stringifyPass(t, obj) {\n  let text;\n  try {\n    text = HLS.stringify(obj);\n  } catch (err) {\n    t.fail(err.stack);\n  }\n  t.truthy(text);\n  return text;\n}\n\nfunction bothPass(t, text) {\n  const obj = parsePass(t, text);\n  return stringifyPass(t, obj);\n}\n\nfunction parseFail(t, text) {\n  try {\n    HLS.parse(text);\n  } catch (err) {\n    return t.truthy(err);\n  }\n  t.fail('HLS.parse() did not fail');\n}\n\nfunction stringifyFail(t, obj) {\n  try {\n    HLS.stringify(obj);\n  } catch (err) {\n    return t.truthy(err);\n  }\n  t.fail('HLS.stringify() did not fail');\n}\n\nfunction stripSpaces(text) {\n  const chars = [];\n  let insideDoubleQuotes = false;\n  for (const ch of text) {\n    if (ch === '\"') {\n      insideDoubleQuotes = !insideDoubleQuotes;\n    } else if (ch === ' ') {\n      if (!insideDoubleQuotes) {\n        continue;\n      }\n    }\n    chars.push(ch);\n  }\n  return chars.join('');\n}\n\nfunction stripCommentsAndEmptyLines(text) {\n  const lines = [];\n  for (const l of text.split('\\n')) {\n    const line = l.trim();\n    if (!line) {\n      // empty line\n      continue;\n    }\n    if (line.startsWith('#')) {\n      if (line.startsWith('#EXT')) {\n        // tag\n        lines.push(stripSpaces(line));\n      }\n      // comment\n      continue;\n    }\n    // uri\n    lines.push(line);\n  }\n  return lines.join('\\n');\n}\n\nmodule.exports = {\n  parsePass,\n  stringifyPass,\n  bothPass,\n  parseFail,\n  stringifyFail,\n  stripCommentsAndEmptyLines\n};\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.1_EXTM3U.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// It MUST be the first line of every Media Playlist and\n// every Master Playlist.\ntest('#EXTM3U-01', t => {\n  // Media Playlist\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXT-X-TARGETDURATION:10\n    #EXTM3U\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  // Master Playlist\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    http://example.com/low.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=2560000\n    http://example.com/mid.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=7680000\n    http://example.com/hi.m3u8\n  `);\n  utils.parseFail(t, `\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    http://example.com/low.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=2560000\n    http://example.com/mid.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=7680000\n    http://example.com/hi.m3u8\n  `);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.2_EXT-X-VERSION.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// A Playlist file MUST NOT contain more than one EXT-X-VERSION tag.\ntest('#EXT-X-VERSION_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXTINF:10.0,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXTINF:10.0,\n    http://example.com/2\n    #EXT-X-VERSION:4\n  `);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.1_EXTINF.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// This tag is REQUIRED for each Media Segment\ntest('#EXTINF_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    http://example.com/2\n  `);\n});\n\n// If the compatibility version number is less than 3,\n// durations MUST be integers.\ntest('#EXTINF_02', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9.9,\n    http://example.com/1\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:2\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9.9,\n    http://example.com/1\n  `);\n});\n\n// The remainder of the line following the comma is an optional human-\n// readable informative title of the Media Segment expressed as raw\n// UTF-8 text.\ntest('#EXTINF_03', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,abc\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,${unescape(encodeURIComponent('\\u3042'))}\n    http://example.com/1\n  `);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.2_EXT-X-BYTERANGE.spec.js",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// It applies only to the next URI line that follows it in the Playlist.\ntest('#EXT-X-BYTERANGE_01', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-BYTERANGE:100@200\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(playlist.segments[0].byterange.offset, 200);\n  t.is(playlist.segments[0].byterange.length, 100);\n  t.falsy(playlist.segments[1].byterange);\n});\n\n// If o is not present, the sub-range begins at the next byte following\n// the sub-range of the previous Media Segment.\ntest('#EXT-X-BYTERANGE_02', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-BYTERANGE:100@200\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXT-X-BYTERANGE:100\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXT-X-BYTERANGE:100\n    #EXTINF:9.9,\n    http://example.com/1\n  `);\n  t.is(playlist.segments[0].byterange.offset, 200);\n  t.is(playlist.segments[1].byterange.offset, 300);\n  t.is(playlist.segments[2].byterange.offset, 400);\n});\n\n// If o is not present, a previous Media Segment MUST appear in the\n// Playlist file and MUST be a sub-range of the same media resource, or\n// the Media Segment is undefined and the Playlist MUST be rejected.\ntest('#EXT-X-BYTERANGE_03', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-BYTERANGE:100\n    #EXTINF:9.9,\n    http://example.com/1\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-BYTERANGE:100@200\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXT-X-BYTERANGE:100\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXT-X-BYTERANGE:100\n    #EXTINF:9.9,\n    http://example.com/2\n  `);\n  utils.parsePass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-BYTERANGE:100@200\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXT-X-BYTERANGE:100\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXT-X-BYTERANGE:100@200\n    #EXTINF:9.9,\n    http://example.com/2\n  `);\n});\n\n// Use of the EXT-X-BYTERANGE tag REQUIRES a compatibility version\n// number of 4 or greater.\ntest('#EXT-X-BYTERANGE_04', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-BYTERANGE:100@200\n    #EXTINF:9.9,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-BYTERANGE:100@200\n    #EXTINF:9.9,\n    http://example.com/1\n  `);\n});\n\n// EXT-X-BYTERANGE should come at end of segment.\ntest('#EXT-X-BYTERANGE_05', t => {\n  t.is(\n    utils.bothPass(t, `\n        #EXTM3U\n        #EXT-X-VERSION:4\n        #EXT-X-TARGETDURATION:10\n        #EXTINF:9.9,comment\n        #EXT-X-BYTERANGE:100@200\n        http://example.com/1\n        #EXT-X-DISCONTINUITY\n        #EXTINF:9.9,comment\n        #EXT-X-BYTERANGE:100@200\n        http://example.com/2\n    `),\n    utils.stripCommentsAndEmptyLines(`\n        #EXTM3U\n        #EXT-X-VERSION:4\n        #EXT-X-TARGETDURATION:10\n        #EXTINF:9.9,comment\n        #EXT-X-BYTERANGE:100@200\n        http://example.com/1\n        #EXT-X-DISCONTINUITY\n        #EXTINF:9.9,comment\n        #EXT-X-BYTERANGE:100@200\n        http://example.com/2\n    `)\n  );\n});\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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\n\n// The EXT-X-DISCONTINUITY tag indicates a discontinuity between the\n// Media Segment that follows it and the one that preceded it.\ntest('#EXT-X-DISCONTINUITY_01', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-DISCONTINUITY\n    #EXTINF:10,\n    http://example.com/2\n    #EXTINF:10,\n    http://example.com/3\n  `);\n  t.falsy(playlist.segments[0].discontinuity);\n  t.true(playlist.segments[1].discontinuity);\n  t.falsy(playlist.segments[2].discontinuity);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.4_EXT-X-KEY.spec.js",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// It applies to every Media Segment that appears between\n// it and the next EXT-X-KEY tag in the Playlist file with the same\n// KEYFORMAT attribute (or the end of the Playlist file).\ntest('#EXT-X-KEY_01', t => {\n  let playlist;\n  // Until the end of the Playlist file\n  playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com/2\n    #EXTINF:10,\n    http://example.com/3\n  `);\n  t.falsy(playlist.segments[0].key);\n  t.truthy(playlist.segments[1].key);\n  t.truthy(playlist.segments[2].key);\n  // Until the next EXT-X-KEY tag in the Playlist file with the same\n  // KEYFORMAT attribute\n  playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com/key-1\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com/key-2\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com/3\n  `);\n  t.is(playlist.segments[0].key.uri, 'http://example.com/key-1');\n  t.is(playlist.segments[1].key.uri, 'http://example.com/key-1');\n  t.is(playlist.segments[2].key.uri, 'http://example.com/key-2');\n});\n\n// METHOD: This attribute is REQUIRED.\ntest('#EXT-X-KEY_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com/2\n  `);\n});\n\n// If the encryption method is NONE, other attributes\n// MUST NOT be present.\ntest('#EXT-X-KEY_03', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=NONE,URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com/2\n  `);\n});\n\n// URI: This attribute is REQUIRED unless the METHOD is NONE.\ntest('#EXT-X-KEY_04', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=NONE\n    #EXTINF:10,\n    http://example.com/2\n  `);\n});\n\n// Use of the IV attribute REQUIRES a compatibility version number of\n// 2 or greater.\ntest('#EXT-X-KEY_05', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:1\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221100\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  const playlist = utils.parsePass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:2\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221100\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(playlist.segments[0].key.iv.length, 16);\n});\n\n// The tag place should be preserved\ntest('#EXT-X-KEY_06', t => {\n  const sourceText = `\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com/key-1\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com/key-2\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com/3\n    #EXTINF:10,\n    http://example.com/4\n  `;\n  const obj = HLS.parse(sourceText);\n  const text = HLS.stringify(obj);\n  t.is(text, utils.stripCommentsAndEmptyLines(sourceText));\n});\n\n// The same tag can appear multiple times\ntest('#EXT-X-KEY_07', t => {\n  const sourceText = `\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com/key-1\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com/key-2\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com/key-1\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com/3\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com/key-2\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com/4\n  `;\n  const obj = HLS.parse(sourceText);\n  const text = HLS.stringify(obj);\n  t.is(text, utils.stripCommentsAndEmptyLines(sourceText));\n});\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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// It applies to every Media Segment that appears after it in the\n// Playlist until the next EXT-X-MAP tag or until the end of the\n// playlist.\ntest('#EXT-X-MAP_01', t => {\n  let playlist;\n  // Until the end of the Playlist\n  playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-MAP:URI=\"http://example.com/map-1\"\n    #EXTINF:10,\n    http://example.com/2\n    #EXTINF:10,\n    http://example.com/3\n  `);\n  t.falsy(playlist.segments[0].map);\n  t.truthy(playlist.segments[1].map);\n  t.truthy(playlist.segments[2].map);\n  // Until the next EXT-X-MAP tag\n  playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MAP:URI=\"http://example.com/map-1\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-MAP:URI=\"http://example.com/map-2\"\n    #EXTINF:10,\n    http://example.com/3\n  `);\n  t.is(playlist.segments[0].map.uri, 'http://example.com/map-1');\n  t.is(playlist.segments[1].map.uri, 'http://example.com/map-1');\n  t.is(playlist.segments[2].map.uri, 'http://example.com/map-2');\n  HLS.stringify(playlist);\n});\n\n// URI: This attribute is REQUIRED.\ntest('#EXT-X-MAP_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MAP:BYTERANGE=\"256@128\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MAP:URI=\"http://example.com/map-1\",BYTERANGE=\"256@128\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// Use of the EXT-X-MAP tag in a Media Playlist that contains the\n// EXT-X-I-FRAMES-ONLY tag REQUIRES a compatibility version number of 5\n// or greater.\n// URI: This attribute is REQUIRED.\ntest('#EXT-X-MAP_03', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-I-FRAMES-ONLY\n    #EXT-X-MAP:URI=\"http://example.com/map-1\",BYTERANGE=\"256@128\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-I-FRAMES-ONLY\n    #EXT-X-MAP:URI=\"http://example.com/map-1\",BYTERANGE=\"256@128\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// Use of the EXT-X-MAP tag in a Media Playlist that DOES\n// NOT contain the EXT-X-I-FRAMES-ONLY tag REQUIRES a compatibility\n// version number of 6 or greater.\ntest('#EXT-X-MAP_04', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MAP:URI=\"http://example.com/map-1\",BYTERANGE=\"256@128\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MAP:URI=\"http://example.com/map-1\",BYTERANGE=\"256@128\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// The tag place should be preserved\ntest('#EXT-X-MAP_05', t => {\n  const sourceText = `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MAP:URI=\"http://example.com/map-1\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-MAP:URI=\"http://example.com/map-2\"\n    #EXTINF:10,\n    http://example.com/3\n    #EXTINF:10,\n    http://example.com/4\n  `;\n  const obj = HLS.parse(sourceText);\n  const text = HLS.stringify(obj);\n  t.is(text, utils.stripCommentsAndEmptyLines(sourceText));\n});\n\n// The same tag can appear multiple times\ntest('#EXT-X-MAP_06', t => {\n  const sourceText = `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MAP:URI=\"http://example.com/map-1\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-MAP:URI=\"http://example.com/map-2\"\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-MAP:URI=\"http://example.com/map-1\"\n    #EXTINF:10,\n    http://example.com/3\n    #EXT-X-MAP:URI=\"http://example.com/map-2\"\n    #EXTINF:10,\n    http://example.com/4\n  `;\n  const obj = HLS.parse(sourceText);\n  const text = HLS.stringify(obj);\n  t.is(text, utils.stripCommentsAndEmptyLines(sourceText));\n});\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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\n\n// It applies only to the next Media Segment.\ntest('#EXT-X-PROGRAM-DATE-TIME_01', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.truthy(playlist.segments[0].programDateTime);\n  t.falsy(playlist.segments[1].programDateTime);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.7_EXT-X-DATERANGE.spec.js",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// ID attribute is REQUIRED.\ntest('#EXT-X-DATERANGE_01', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\"\n\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// START-DATE attribute is REQUIRED.\n//   * removed because START-DATE is omitted in case of 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags\n/*\ntest('#EXT-X-DATERANGE_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n*/\n\n// START-DATE attribute is not REQUIRED\ntest('#EXT-X-DATERANGE_02', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// END-DATE MUST be equal to or later than the value of the\n// START-DATE attribute.\ntest('#EXT-X-DATERANGE_03', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",END-DATE=\"2010-02-19T14:54:22Z\",ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",END-DATE=\"2010-02-19T14:54:23Z\",ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// DURATION MUST NOT be negative.\ntest('#EXT-X-DATERANGE_04', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",DURATION=-180.0,ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",DURATION=180.0,ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// PLANNED-DURATION MUST NOT be negative.\ntest('#EXT-X-DATERANGE_05', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",PLANNED-DURATION=-180.0,ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",PLANNED-DURATION=180.0,ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// X-<client-attribute>\n// The attribute value MUST be a quoted-string,\n// a hexadecimal-sequence, or a decimal-floating-point.\ntest('#EXT-X-DATERANGE_06', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",ID=\"ads\",X-STR=\"abc\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",ID=\"ads\",X-BYTE=0xFFFEFDFC\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:23Z\",ID=\"ads\",X-FLOAT=0.999\n    #EXTINF:10,\n    http://example.com/3\n  `);\n  t.is(playlist.segments[0].dateRange.attributes['X-STR'], 'abc');\n  t.deepEqual(playlist.segments[1].dateRange.attributes['X-BYTE'], new Uint8Array([255, 254, 253, 252]));\n  t.is(playlist.segments[2].dateRange.attributes['X-FLOAT'], 0.999);\n});\n\n// END-ON-NEXT attribute indicates that the end of the range containing it\n// is equal to the START-DATE of its Following Range. The Following Range is\n// the Date Range of the same CLASS that has the earliest START-DATE\n// after the START-DATE of the range in question.\ntest('#EXT-X-DATERANGE_07', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",ID=\"ads\",CLASS=\"A\",END-ON-NEXT=YES\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:55:00Z\",ID=\"ads\",CLASS=\"B\",END-ON-NEXT=YES\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:56:00Z\",ID=\"ads\",CLASS=\"A\",END-ON-NEXT=YES\n    #EXTINF:10,\n    http://example.com/3\n  `);\n  t.true(playlist.segments[0].dateRange.endOnNext);\n  t.not(playlist.segments[0].dateRange.end.getTime(), playlist.segments[1].dateRange.start.getTime());\n  t.is(playlist.segments[0].dateRange.end.getTime(), playlist.segments[2].dateRange.start.getTime());\n});\n\n// An EXT-X-DATERANGE tag with an END-ON-NEXT=YES attribute MUST have a\n// CLASS attribute.\ntest('#EXT-X-DATERANGE_08', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",ID=\"ads\",END-ON-NEXT=YES\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",ID=\"ads\",CLASS=\"A\",END-ON-NEXT=YES\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// Other EXT-X-DATERANGE tags with the same CLASS\n// attribute MUST NOT specify Date Ranges that overlap.\ntest('#EXT-X-DATERANGE_09', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",DURATION=60.0,ID=\"ads\",CLASS=\"A\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:55:00Z\",DURATION=61.0,ID=\"ads\",CLASS=\"A\"\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:56:00Z\",DURATION=60.0,ID=\"ads\",CLASS=\"A\"\n    #EXTINF:10,\n    http://example.com/3\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",DURATION=60.0,ID=\"ads\",CLASS=\"A\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:55:00Z\",DURATION=60.0,ID=\"ads\",CLASS=\"A\"\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:56:00Z\",DURATION=60.0,ID=\"ads\",CLASS=\"A\"\n    #EXTINF:10,\n    http://example.com/3\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",DURATION=60.0,ID=\"ads\",CLASS=\"A\"\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:55:00Z\",DURATION=61.0,ID=\"ads\",CLASS=\"B\"\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:56:00Z\",DURATION=60.0,ID=\"ads\",CLASS=\"A\"\n    #EXTINF:10,\n    http://example.com/3\n  `);\n});\n\n// An EXT-X-DATERANGE tag with an END-ON-NEXT=YES attribute MUST NOT\n// contain DURATION or END-DATE attributes.\ntest('#EXT-X-DATERANGE_11', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",END-DATE=\"2010-02-19T14:55:00Z\",ID=\"ads\",CLASS=\"A\",END-ON-NEXT=YES\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",DURATION=120.0,ID=\"ads\",CLASS=\"A\",END-ON-NEXT=YES\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",ID=\"ads\",CLASS=\"A\",END-ON-NEXT=YES\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// If a Playlist contains an EXT-X-DATERANGE tag, it MUST also contain\n// at least one EXT-X-PROGRAM-DATE-TIME tag.\ntest('#EXT-X-DATERANGE_12', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",ID=\"ads\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n\n// If a Date Range contains both a DURATION attribute and an END-DATE\n// attribute, the value of the END-DATE attribute MUST be equal to the\n// value of the START-DATE attribute plus the value of the DURATION\n// attribute.\ntest('#EXT-X-DATERANGE_13', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",ID=\"ads\",DURATION=60.0,END-DATE=\"2010-02-19T14:56:00Z\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z\n    #EXT-X-DATERANGE:START-DATE=\"2010-02-19T14:54:00Z\",ID=\"ads\",DURATION=60.0,END-DATE=\"2010-02-19T14:55:00Z\"\n    #EXTINF:10,\n    http://example.com/1\n  `);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2_Media-Segment-Tags.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// A Media Segment tag MUST NOT appear in a Master Playlist.  Clients\n// MUST reject Playlists that contain both Media Segment Tags and Master\n// Playlist tags.\ntest('Media-Segment-Tags', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    http://example.com/low.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=2560000\n    http://example.com/mid.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=7680000\n    http://example.com/hi.m3u8\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    http://example.com/low.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=2560000\n    http://example.com/mid.m3u8\n    #EXT-X-DISCONTINUITY\n    #EXT-X-STREAM-INF:BANDWIDTH=7680000\n    http://example.com/hi.m3u8\n  `);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.4.4.7_EXT-X-GAP.spec.js",
    "content": "const test = require(\"ava\");\nconst HLS = require('../../../../..');\nconst utils = require(\"../../../../helpers/utils\");\nconst {equalPlaylist} = require(\"../../../../helpers/matchers\");\n\n// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.7\n\ntest('#EXT-X-TAG_01', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:8\n    #EXT-X-GAP\n    1.ts\n  `);\n  utils.parsePass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:8\n    #EXT-X-TARGETDURATION:5\n    #EXT-X-GAP\n    #EXTINF:4,\n    1.ts\n  `);\n});\n\ntest('#EXT-X-TAG_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:8\n    #EXT-X-TARGETDURATION:5\n    #EXT-X-GAP\n    #EXT-X-PART:DURATION=2,URI=\"1.ts\"\n    #EXT-X-ENDLIST\n  `);\n  utils.parsePass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:8\n    #EXT-X-TARGETDURATION:5\n    #EXT-X-GAP\n    #EXT-X-PART:DURATION=2,URI=\"1.ts\",GAP=YES\n    #EXT-X-ENDLIST\n  `);\n});\n\ntest('#EXT-X-TAG_03', t => {\n  const txt = `\n    #EXTM3U\n    #EXT-X-VERSION:8\n    #EXT-X-TARGETDURATION:5\n    #EXT-X-GAP\n    #EXTINF:4,\n    1.ts\n  `;\n  const playlist = HLS.parse(txt);\n  t.truthy(playlist.segments[0].gap);\n  equalPlaylist(t, txt, HLS.stringify(playlist));\n});\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",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// The EXTINF duration of each Media Segment in the Playlist\n// file, when rounded to the nearest integer, MUST be less than or equal\n// to the target duration\ntest('#EXT-X-TARGETDURATION_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXTINF:10.4,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9.9,\n    http://example.com/1\n    #EXTINF:10.5,\n    http://example.com/2\n  `);\n});\n\n// The EXT-X-TARGETDURATION tag is REQUIRED.\ntest('#EXT-X-TARGETDURATION_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n});\n"
  },
  {
    "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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// If the Media Playlist file does not contain an EXT-X-MEDIA-SEQUENCE\n// tag then the Media Sequence Number of the first Media Segment in the\n// Media Playlist SHALL be considered to be 0.\ntest('#EXT-X-MEDIA-SEQUENCE_01', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(playlist.mediaSequenceBase, 0);\n});\n\n// The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media\n// Segment in the Playlist.\ntest('#EXT-X-MEDIA-SEQUENCE_02', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MEDIA-SEQUENCE:20\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    http://example.com/1\n    #EXT-X-MEDIA-SEQUENCE:20\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    #EXT-X-MEDIA-SEQUENCE:20\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n});\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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// If the Media Playlist does not contain an EXT-X-DISCONTINUITY-\n// SEQUENCE tag, then the Discontinuity Sequence Number of the first\n// Media Segment in the Playlist SHALL be considered to be 0.\ntest('#EXT-X-DISCONTINUITY-SEQUENCE_01', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-DISCONTINUITY\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(playlist.discontinuitySequenceBase, 0);\n});\n\n// The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before the first\n// Media Segment in the Playlist.\ntest('#EXT-X-DISCONTINUITY-SEQUENCE_02', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-DISCONTINUITY-SEQUENCE:20\n    #EXTINF:9,\n    http://example.com/1\n    #EXT-X-DISCONTINUITY\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    http://example.com/1\n    #EXT-X-DISCONTINUITY-SEQUENCE:20\n    #EXT-X-DISCONTINUITY\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    #EXT-X-DISCONTINUITY-SEQUENCE:20\n    http://example.com/1\n    #EXT-X-DISCONTINUITY\n    #EXTINF:10,\n    http://example.com/2\n  `);\n});\n\n// The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before any\n// EXT-X-DISCONTINUITY tag.\ntest('#EXT-X-DISCONTINUITY-SEQUENCE_03', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-DISCONTINUITY\n    #EXT-X-DISCONTINUITY-SEQUENCE:20\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-DISCONTINUITY-SEQUENCE:20\n    #EXT-X-DISCONTINUITY\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n});\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",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// It MAY occur anywhere in the Media Playlist file.\ntest('#EXT-X-ENDLIST_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-ENDLIST\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-ENDLIST\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    #EXT-X-ENDLIST\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    http://example.com/1\n    #EXT-X-ENDLIST\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    #EXT-X-ENDLIST\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-ENDLIST\n  `);\n});\n"
  },
  {
    "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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\n\n// #EXT-X-PLAYLIST-TYPE:<EVENT|VOD>\ntest('#EXT-X-PLAYLIST-TYPE_01', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PLAYLIST-TYPE:EVENT\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(playlist.playlistType, 'EVENT');\n});\n\n// #EXT-X-PLAYLIST-TYPE:<EVENT|VOD>\ntest('#EXT-X-PLAYLIST-TYPE_02', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PLAYLIST-TYPE:VOD\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(playlist.playlistType, 'VOD');\n});\n\n// #EXT-X-PLAYLIST-TYPE:<EVENT|VOD>\ntest('#EXT-X-PLAYLIST-TYPE_03', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(playlist.playlistType, undefined);\n});\n"
  },
  {
    "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",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// Use of the EXT-X-I-FRAMES-ONLY REQUIRES a compatibility version\n// number of 4 or greater.\ntest('#EXT-X-I-FRAMES-ONLY_01', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-VERSION:3\n    #EXT-X-I-FRAMES-ONLY\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-VERSION:4\n    #EXT-X-I-FRAMES-ONLY\n    #EXTINF:9,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n});\n"
  },
  {
    "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",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\ntest('#EXT-X-CUE-OUT_01', t => {\n  let obj = utils.parsePass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-VERSION:3\n    #EXTINF:9,\n    http://example.com/1\n    #EXT-X-CUE-OUT:30\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(obj.segments[1].markers[0].duration, 30);\n  obj = utils.parsePass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-VERSION:3\n    #EXTINF:9,\n    http://example.com/1\n    #EXT-X-CUE-OUT:DURATION=30\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(obj.segments[1].markers[0].duration, 30);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3_Media-Playlist-Tags.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// There MUST NOT be more than one Media Playlist tag of each type in\n// any Media Playlist.\ntest('Media-Playlist-Tags', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/2\n  `);\n});\n\n// A Media Playlist Tag MUST NOT appear in a Master Playlist\ntest('Media-Segment-Tags', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    http://example.com/low.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=2560000\n    http://example.com/mid.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=7680000\n    http://example.com/hi.m3u8\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    http://example.com/low.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=2560000\n    http://example.com/mid.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=7680000\n    http://example.com/hi.m3u8\n    #EXT-X-ENDLIST\n  `);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.1_EXT-X-MEDIA.spec.js",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// TYPE attribute is REQUIRED.\ntest('#EXT-X-MEDIA_01', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"audio\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:GROUP-ID=\"audio\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"audio\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n});\n\n// TYPE attribute: valid strings are AUDIO, VIDEO,\n// SUBTITLES and CLOSED-CAPTIONS.\ntest('#EXT-X-MEDIA_02', t => {\n  let playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n  t.is(playlist.variants[0].audio[0].type, 'AUDIO');\n  playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/video/en.m3u8\"\n  `);\n  t.is(playlist.variants[0].video[0].type, 'VIDEO');\n  playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/subtitles/en.m3u8\"\n  `);\n  t.is(playlist.variants[0].subtitles[0].type, 'SUBTITLES');\n  playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,INSTREAM-ID=\"CC1\"\n  `);\n  t.is(playlist.variants[0].closedCaptions[0].type, 'CLOSED-CAPTIONS');\n});\n\n// If the TYPE is CLOSED-CAPTIONS, the URI\n//  attribute MUST NOT be present.\ntest('#EXT-X-MEDIA_03', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,INSTREAM-ID=\"CC1\",URI=\"/audio/en.m3u8\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,INSTREAM-ID=\"CC1\"\n  `);\n});\n\n// GROUP-ID attribute is REQUIRED.\ntest('#EXT-X-MEDIA_04', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,NAME=\"en\",DEFAULT=YES\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES\"\n  `);\n});\n\n// NAME attribute is REQUIRED.\ntest('#EXT-X-MEDIA_05', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",DEFAULT=YES\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES\"\n  `);\n});\n\n// The FORCED attribute MUST NOT be present unless the\n// TYPE is SUBTITLES.\ntest('#EXT-X-MEDIA_06', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,FORCED=YES\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/subtitles/en.m3u8\",FORCED=YES\n  `);\n});\n\n// INSTREAM-ID attribute is REQUIRED if the TYPE attribute is CLOSED-CAPTIONS\ntest('#EXT-X-MEDIA_07', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,INSTREAM-ID=\"CC1\"\n  `);\n});\n\n// All EXT-X-MEDIA tags in the same Group MUST have different NAME attributes.\ntest('#EXT-X-MEDIA_08', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"ja\",URI=\"/audio/ja.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"ja\",URI=\"/audio/ja.m3u8\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"ja\",URI=\"/audio/ja.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"fr\",URI=\"/audio/fr.m3u8\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"ja\",URI=\"/audio/ja.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test2\",NAME=\"ja\",URI=\"/audio/ja.m3u8\"\n  `);\n});\n\n// A Group MUST NOT have more than one member with a DEFAULT attribute of YES.\ntest('#EXT-X-MEDIA_09', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"ja\",URI=\"/audio/ja.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"fr\",DEFAULT=YES,URI=\"/audio/fr.m3u8\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"ja\",URI=\"/audio/ja.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"fr\",URI=\"/audio/fr.m3u8\"\n  `);\n});\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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// The URI line is REQUIRED\ntest('#EXT-X-STREAM-INF_01', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n  `);\n});\n\n// Every EXT-X-STREAM-INF tag MUST include the BANDWIDTH attribute.\ntest('#EXT-X-STREAM-INF_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1280000\n    /video/main.m3u8\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n  `);\n});\n\n// RESOLUTION The value is a decimal-resolution:\n//  two decimal-integers separated by the \"x\"\n//  character.  The first integer is a horizontal pixel dimension\n//  (width); the second is a vertical pixel dimension (height).\ntest('#EXT-X-STREAM-INF_03', t => {\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,RESOLUTION=123x456\n    /video/main.m3u8\n  `);\n  t.deepEqual(playlist.variants[0].resolution, {width: 123, height: 456});\n});\n\n// AUDIO attribute MUST match the value of the\n// GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Master\n// Playlist whose TYPE attribute is AUDIO.\ntest('#EXT-X-STREAM-INF_04', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test1\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n});\n\n// VIDEO MUST match the value of the\n// GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Master\n// Playlist whose TYPE attribute is VIDEO.\ntest('#EXT-X-STREAM-INF_05', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"test1\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n});\n\n// SUBTITLES MUST match the value of the\n// GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Master\n// Playlist whose TYPE attribute is SUBTITLES.\ntest('#EXT-X-STREAM-INF_06', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"test1\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n});\n\n// CLOSED-CAPTIONS: it MUST match the value of the\n// GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist\n// whose TYPE attribute is CLOSED-CAPTIONS\ntest('#EXT-X-STREAM-INF_07', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"test1\",NAME=\"en\",DEFAULT=YES,INSTREAM-ID=\"CC1\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,INSTREAM-ID=\"CC1\"\n  `);\n});\n\n// CLOSED-CAPTIONS: The value can be either a quoted-string or an enumerated-string with the value NONE.\ntest('#EXT-X-STREAM-INF_07-01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE\n    /video/main.m3u8\n  `);\n  const playlist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,INSTREAM-ID=\"CC1\"\n  `);\n  t.is(playlist.variants[0].closedCaptions.length, 0);\n});\n\n// CLOSED-CAPTIONS: If the value is the enumerated-string value NONE,\n// all EXT-X-STREAM-INF tags MUST have this attribute with a value of NONE,\n// indicating that there are no closed captions in any Variant Stream in the Master Playlist.\ntest('#EXT-X-STREAM-INF_07-02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE\n    /video/main.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=2040000,CLOSED-CAPTIONS=\"test\"\n    /video/high.m3u8\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,INSTREAM-ID=\"CC1\"\n  `);\n});\n\n/*\ntest('#EXT-X-STREAM-INF_07-03', t => {\n  const sourceText = `\n  #EXTM3U\n  #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE\n  /video/main.m3u8\n  #EXT-X-STREAM-INF:BANDWIDTH=2040000,CLOSED-CAPTIONS=NONE\n  /video/high.m3u8\n  `;\n  HLS.setOptions({allowClosedCaptionsNone: true});\n  const obj = HLS.parse(sourceText);\n  const text = HLS.stringify(obj);\n  t.is(text, utils.stripCommentsAndEmptyLines(sourceText));\n});\n*/\n\n// The URI attribute of the EXT-X-MEDIA tag is REQUIRED if the media\n// type is SUBTITLES\ntest('#EXT-X-STREAM-INF_08', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES=\"test\"\n    /video/main.m3u8\n    #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"test\",NAME=\"en\",DEFAULT=YES,URI=\"/audio/en.m3u8\"\n  `);\n});\n\n// SCORE: The value is a positive decimal-floating-point number.\ntest('#EXT-X-STREAM-INF_09', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,SCORE=-0.5\n    /video/main.m3u8\n  `);\n  const expected = `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,SCORE=0.5\n    /video/main.m3u8\n  `;\n  const actual = utils.bothPass(t, expected);\n  t.is(actual, utils.stripCommentsAndEmptyLines(expected));\n});\n\n// The SCORE attribute is OPTIONAL, but if any Variant Stream\n// contains the SCORE attribute, then all Variant Streams in the\n// Master Playlist SHOULD have a SCORE attribute.\ntest('#EXT-X-STREAM-INF_10', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"avc1.640029,mp4a.40.2\",SCORE=0.5\n    low/main/audio-video.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"avc1.640029,mp4a.40.2\"\n    mid/main/audio-video.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"avc1.640029,mp4a.40.2\"\n    hi/main/audio-video.m3u8\n  `);\n  const expected = `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"avc1.640029,mp4a.40.2\",SCORE=0.5\n    low/main/audio-video.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"avc1.640029,mp4a.40.2\",SCORE=0.3\n    mid/main/audio-video.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"avc1.640029,mp4a.40.2\",SCORE=0.1\n    hi/main/audio-video.m3u8\n  `;\n  const actual = utils.bothPass(t, expected);\n  t.is(actual, utils.stripCommentsAndEmptyLines(expected));\n});\n\n// ALLOWED-CPC: Its value is a quoted-string containing\n// a comma-separated list of entries.  Each entry consists\n// of a KEYFORMAT attribute value followed by a colon character (:)\n// followed by a sequence of Content Protection Configuration (CPC)\n// Labels separated by slash (/) characters.  Each CPC Label is a\n// string containing characters from the set [A..Z], [0..9], and '-'.\ntest('#EXT-X-STREAM-INF_11', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,ALLOWED-CPC=\"abc\"\n    /video/main.m3u8\n  `);\n  const expected = `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,ALLOWED-CPC=\"com.example.drm1:SMART-TV/PC,com.example.drm2:HW\"\n    /video/main.m3u8\n  `;\n  const actual = utils.bothPass(t, expected);\n  t.is(actual, utils.stripCommentsAndEmptyLines(expected));\n});\n\n// VIDEO-RANGE: The value is an enumerated-string; valid strings are SDR, HLG and PQ.\ntest('#EXT-X-STREAM-INF_12', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO-RANGE=abc\n    /video/main.m3u8\n  `);\n  const expected = `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO-RANGE=HLG\n    /video/main.m3u8\n  `;\n  const actual = utils.bothPass(t, expected);\n  t.is(actual, utils.stripCommentsAndEmptyLines(expected));\n});\n\n// STABLE-VARIANT-ID: The value is a quoted-string which is\n// a stable identifier for the URI within the Master Playlist.\ntest('#EXT-X-STREAM-INF_13', t => {\n  const expected = `\n    #EXTM3U\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000,STABLE-VARIANT-ID=\"abc\"\n    /video/main.m3u8\n  `;\n  const actual = utils.bothPass(t, expected);\n  t.is(actual, utils.stripCommentsAndEmptyLines(expected));\n});\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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\ntest('#EXT-X-STREAM-INF_07-03', t => {\n  const sourceText = `\n  #EXTM3U\n  #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE\n  /video/main.m3u8\n  #EXT-X-STREAM-INF:BANDWIDTH=2040000,CLOSED-CAPTIONS=NONE\n  /video/high.m3u8\n  `;\n  HLS.setOptions({allowClosedCaptionsNone: true});\n  const obj = HLS.parse(sourceText);\n  const text = HLS.stringify(obj);\n  t.is(text, utils.stripCommentsAndEmptyLines(sourceText));\n});\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",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// Every EXT-X-I-FRAME-STREAM-INF tag MUST include a BANDWIDTH attribute\n// and a URI attribute.\ntest('#EXT-X-I-FRAME-STREAM-INF_01', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1280000\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1280000,URI=/video/main.m3u8\n  `);\n});\n"
  },
  {
    "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",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// DATA-ID attribute is REQUIRED.\ntest('#EXT-X-SESSION-DATA_01', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-SESSION-DATA:LANGUAGE=\"en\",VALUE=\"This is an example\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\",VALUE=\"This is an example\"\n  `);\n});\n\n// Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI\n// attribute, but not both.\ntest('#EXT-X-SESSION-DATA_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\",VALUE=\"This is an example\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\",URI=\"/data/title.json\"\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\",VALUE=\"This is an example\",URI=\"/data/title.json\"\n  `);\n});\n\n// A Playlist MUST NOT contain more than one EXT-X-SESSION-DATA tag\n// with the same DATA-ID attribute and the same LANGUAGE attribute.\ntest('#EXT-X-SESSION-DATA_03', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\",VALUE=\"This is an example\"\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\",VALUE=\"This is an example\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\",VALUE=\"This is an example\"\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"ja\",VALUE=\"This is an example\"\n  `);\n});\n"
  },
  {
    "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",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// The value of the METHOD attribute MUST NOT be NONE\ntest('#EXT-X-SESSION-KEY_01', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-SESSION-KEY:METHOD=NONE\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://example.com\"\n  `);\n});\n\n// A Master Playlist MUST NOT contain more than one EXT-X-SESSION-KEY\n// tag with the same METHOD, URI, IV, KEYFORMAT, and KEYFORMATVERSIONS\n// attribute values.\ntest('#EXT-X-SESSION-KEY_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:2\n    #EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221100\n    #EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221100\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:2\n    #EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221100\n    #EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221101\n  `);\n});\n"
  },
  {
    "path": "test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4_Master-Playlist-Tags.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../../helpers/utils');\n\n// Master Playlist Tags MUST NOT appear in a Media Playlist\ntest('Master-Playlist-Tags', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",LANGUAGE=\"en\",VALUE=\"This is an example\"\n  `);\n});\n"
  },
  {
    "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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// The tags in this section can appear in either Master Playlists or\n// Media Playlists.\ntest('#EXT-X-INDEPENDENT-SEGMENTS_01', t => {\n  const mediaPlaylist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-INDEPENDENT-SEGMENTS\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.true(mediaPlaylist.independentSegments);\n  const masterPlaylist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-INDEPENDENT-SEGMENTS\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=640000\n    /video/low.m3u8\n  `);\n  t.true(masterPlaylist.independentSegments);\n});\n\n// These tags MUST NOT appear more than once in a Playlist.  If a tag\n// appears more than once, clients MUST reject the playlist.\ntest('#EXT-X-INDEPENDENT-SEGMENTS_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-INDEPENDENT-SEGMENTS\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-INDEPENDENT-SEGMENTS\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-INDEPENDENT-SEGMENTS\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-INDEPENDENT-SEGMENTS\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=640000\n    /video/low.m3u8\n    #EXT-X-INDEPENDENT-SEGMENTS\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-INDEPENDENT-SEGMENTS\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=640000\n    /video/low.m3u8\n  `);\n});\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",
    "content": "const test = require('ava');\nconst HLS = require('../../../../..');\nconst utils = require('../../../../helpers/utils');\n\n// The tags in this section can appear in either Master Playlists or\n// Media Playlists.\ntest('#EXT-X-START_01', t => {\n  const mediaPlaylist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  t.is(mediaPlaylist.start.offset, -10);\n  t.true(mediaPlaylist.start.precise);\n  const masterPlaylist = HLS.parse(`\n    #EXTM3U\n    #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=640000\n    /video/low.m3u8\n  `);\n  t.is(masterPlaylist.start.offset, -10);\n  t.true(masterPlaylist.start.precise);\n});\n\n// These tags MUST NOT appear more than once in a Playlist.  If a tag\n// appears more than once, clients MUST reject the playlist.\ntest('#EXT-X-START_02', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-START:TIME-OFFSET=-10\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n    #EXT-X-START:TIME-OFFSET=-20\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-START:TIME-OFFSET=-10\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-START:TIME-OFFSET=-10\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=640000\n    /video/low.m3u8\n    #EXT-X-START:TIME-OFFSET=-20\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-START:TIME-OFFSET=-10\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=640000\n    /video/low.m3u8\n  `);\n});\n\n// TIME-OFFSET attribute is REQUIRED.\ntest('#EXT-X-START_03', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-START:PRECISE=YES\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com/1\n    #EXTINF:10,\n    http://example.com/2\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-START:PRECISE=YES\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=640000\n    /video/low.m3u8\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES\n    #EXT-X-STREAM-INF:BANDWIDTH=1280000\n    /video/main.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=640000\n    /video/low.m3u8\n  `);\n});\n"
  },
  {
    "path": "test/spec/7_Protocol-version-compatibility/7_EXT-X-VERSION.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../helpers/utils');\n\n// A Playlist that contains tags or attributes that are not compatible\n// with protocol version 1 MUST include an EXT-X-VERSION tag.\ntest('#EXT-X-VERSION_02', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://example.com\"\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221100\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:2\n    #EXT-X-SESSION-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221100\n  `);\n});\n\n// A Media Playlist MUST indicate a EXT-X-VERSION of 2 or higher if it\n// contains:\n// - The IV attribute of the EXT-X-KEY tag.\ntest('#EXT-X-VERSION_03', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:1\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:1\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221100\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:2\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\",IV=0xFFEEDDCCBBAA99887766554433221100\n    #EXTINF:10,\n    http://example.com\n  `);\n});\n\n// A Media Playlist MUST indicate a EXT-X-VERSION of 3 or higher if it\n// contains:\n// - Floating-point EXTINF duration values.\ntest('#EXT-X-VERSION_04', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:2\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:2\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9.9,\n    http://example.com\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:9.9,\n    http://example.com\n  `);\n});\n\n// A Media Playlist MUST indicate a EXT-X-VERSION of 4 or higher if it\n// contains:\n// - The EXT-X-BYTERANGE tag.\ntest('#EXT-X-VERSION_05', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-BYTERANGE:256@100\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-BYTERANGE:256@100\n    #EXTINF:10,\n    http://example.com\n  `);\n});\n\n// A Media Playlist MUST indicate a EXT-X-VERSION of 4 or higher if it\n// contains:\n// - The EXT-X-I-FRAMES-ONLY tag.\ntest('#EXT-X-VERSION_06', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-I-FRAMES-ONLY\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-I-FRAMES-ONLY\n  `);\n});\n\n// A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it\n// contains:\n// - The KEYFORMAT attributes of the EXT-X-KEY tag.\ntest('#EXT-X-VERSION_07', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\",KEYFORMAT=\"identity\"\n    #EXTINF:10,\n    http://example.com\n  `);\n});\n\n// A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it\n// contains:\n// - The KEYFORMATVERSIONS attributes of the EXT-X-KEY tag.\ntest('#EXT-X-VERSION_08', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\",KEYFORMATVERSIONS=\"1\"\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\",KEYFORMATVERSIONS=\"1\"\n    #EXTINF:10,\n    http://example.com  `);\n});\n\n// A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it\n// contains:\n// - The EXT-X-MAP tag.\ntest('#EXT-X-VERSION_09', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-I-FRAMES-ONLY\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-I-FRAMES-ONLY\n    #EXT-X-MAP:URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-I-FRAMES-ONLY\n    #EXT-X-MAP:URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com\n  `);\n});\n\n// A Media Playlist MUST indicate a EXT-X-VERSION of 6 or higher if it\n// contains:\n// - The EXT-X-MAP tag in a Media Playlist that does not contain EXT-X-I-FRAMES-ONLY.\ntest('#EXT-X-VERSION_10', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-I-FRAMES-ONLY\n    #EXT-X-MAP:URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:5\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MAP:URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-MAP:URI=\"http://example.com\"\n    #EXTINF:10,\n    http://example.com\n  `);\n});\n\n// A Master Playlist MUST indicate a EXT-X-VERSION of 7 or higher if it\n// contains:\n// - \"SERVICE\" values for the INSTREAM-ID attribute of the EXT-X-MEDIA tag.\ntest('#EXT-X-VERSION_11', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc\",NAME=\"JP\",INSTREAM-ID=\"CC1\"\n    #EXT-X-STREAM-INF:BANDWIDTH=500000,CLOSED-CAPTIONS=\"cc\"\n    http://example.com\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc\",NAME=\"JP\",INSTREAM-ID=\"SERVICE1\"\n    #EXT-X-STREAM-INF:BANDWIDTH=500000,CLOSED-CAPTIONS=\"cc\"\n    http://example.com\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:7\n    #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID=\"cc\",NAME=\"JP\",INSTREAM-ID=\"SERVICE1\"\n    #EXT-X-STREAM-INF:BANDWIDTH=500000,CLOSED-CAPTIONS=\"cc\"\n    http://example.com\n  `);\n});\n\n// A Media Playlist MUST indicate a EXT-X-VERSION of 8 or higher if it\n// contains:\n// - the \"EXT-X-GAP\" tag.\ntest('#EXT-X-VERSION_12', t => {\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:1\n    #EXTINF:5\n    #EXT-X-GAP\n    http://example.com\n  `);\n});\n"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/01_EXT-X-SERVER-CONTROL.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// CAN-BLOCK-RELOAD=YES: ...\n// It is mandatory for Low-Latency HLS.\ntest('#EXT-X-SERVER-CONTROL_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=12.0\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n});\n\n// CAN-SKIP-UNTIL=<seconds>: (optional)\ntest('#EXT-X-SERVER-CONTROL_02', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n});\n\n// CAN-SKIP-UNTIL=<seconds>: ...\n// The Skip Boundary must be at least six times the EXT-X-TARGETDURATION.\ntest('#EXT-X-SERVER-CONTROL_03', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=11.9\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n});\n\n// HOLD-BACK=<seconds>: (optional)\ntest('#EXT-X-SERVER-CONTROL_04', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=6.0\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n});\n\n// HOLD-BACK=<seconds>: ...\n// Its value is a floating-point number of seconds and must be at least\n// three times the EXT-X-TARGETDURATION.\ntest('#EXT-X-SERVER-CONTROL_05', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=6.0\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=5.9\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n  `);\n});\n\n// PART-HOLD-BACK=<seconds>: ...\n// It is mandatory if the Playlist contains EXT-X-PART tags.\ntest('#EXT-X-SERVER-CONTROL_06', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n});\n\n// PART-HOLD-BACK=<seconds>: ...\n// This attribute's value is a floating-point number of seconds and must be\n// at least PART-TARGET.\ntest('#EXT-X-SERVER-CONTROL_07', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.19\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n});\n\ntest('#EXT-X-SERVER-CONTROL_08', t => {\n  const {lowLatencyCompatibility} = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n\n  t.truthy(lowLatencyCompatibility);\n  t.is(lowLatencyCompatibility.canBlockReload, true);\n  t.is(lowLatencyCompatibility.canSkipUntil, 12);\n  t.is(lowLatencyCompatibility.holdBack, 6);\n  t.is(lowLatencyCompatibility.partHoldBack, 0.2);\n});\n"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/02_EXT-X-PART-INF.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// EXT-X-PART-INF provides information about HLS Partial Segments in the Playlist. It is\n// required if a Playlist contains one or more EXT-X-PART tags.\ntest('#EXT-X-PART-INF_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n});\n\n// PART-TARGET=<s>: (mandatory)\ntest('#EXT-X-PART-INF_02', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n});\n\n// PART-TARGET=<s>: (mandatory) Indicates the part target duration in floating-point seconds\n// and is the maximum duration of any Partial Segment.\ntest('#EXT-X-PART-INF_03', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXT-X-PART:DURATION=0.17,URI=\"fs240.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs240.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PART:DURATION=0.17,URI=\"fs240.mp4\",BYTERANGE=20000@40000\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.17\n    #EXT-X-PART:DURATION=0.17,URI=\"fs240.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs240.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PART:DURATION=0.17,URI=\"fs240.mp4\",BYTERANGE=20000@40000\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n});\n\n// All Partial Segments except the last part of a segment\n// must have a duration of at least 85% of PART-TARGET.\ntest('#EXT-X-PART-INF_04', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=23000@20000\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=18000@43000\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@61000\n    #EXT-X-PART:DURATION=0.05,URI=\"fs241.mp4\",BYTERANGE=10000@81000\n    #EXTINF:2,\n    fs241.mp4\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=23000@20000\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=18000@43000\n    #EXT-X-PART:DURATION=0.16,URI=\"fs241.mp4\",BYTERANGE=20000@61000\n    #EXT-X-PART:DURATION=0.05,URI=\"fs241.mp4\",BYTERANGE=10000@81000\n    #EXTINF:2,\n    fs241.mp4\n  `);\n});\n\ntest('#EXT-X-PART-INF_05', t => {\n  const {partTargetDuration} = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n\n  t.is(partTargetDuration, 0.2);\n});\n"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/03_EXT-X-PART.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// All Media Segment tags except for EXT-X-DATERANGE, EXT-X-BYTERANGE,\n// and EXT-X-GAP that are applied to a Parent Segment must appear before\n// the first EXT-X-PART tag of the Parent Segment. These tags include\n// EXT-X-MAP, EXT-X-DISCONTINUITY, and EXT-X-KEY.\ntest('#EXT-X-PART_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-MAP:URI=\"http://example.com/map-1\"\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n    #EXT-X-MAP:URI=\"http://example.com/map-1\"\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-DISCONTINUITY\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n    #EXT-X-DISCONTINUITY\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\"\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\"\n  `);\n});\n\n// Remove EXT-X-PART tags from the Playlist after they are greater than\n// three target durations from the end of the Playlist. Partial Segments\n// are useful for navigating close to the live edge, after which their\n// presence does not justify the increase in the Playlist size and the\n// responsibility of retaining the parallel Partial Segment stream on the server.\ntest('#EXT-X-PART_02', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:1\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.9\n    #EXT-X-PART-INF:PART-TARGET=0.4\n    #EXTINF:1,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.34,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.34,URI=\"fs241.mp4\",BYTERANGE=23000@20000\n    #EXT-X-PART:DURATION=0.4,URI=\"fs241.mp4\",BYTERANGE=18000@43000\n    #EXTINF:1,\n    fs241.mp4\n    #EXT-X-PART:DURATION=0.34,URI=\"fs242.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.34,URI=\"fs242.mp4\",BYTERANGE=23000@20000\n    #EXT-X-PART:DURATION=0.4,URI=\"fs242.mp4\",BYTERANGE=18000@43000\n    #EXTINF:1,\n    fs242.mp4\n    #EXT-X-PART:DURATION=0.34,URI=\"fs243.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.34,URI=\"fs243.mp4\",BYTERANGE=23000@20000\n    #EXT-X-PART:DURATION=0.4,URI=\"fs243.mp4\",BYTERANGE=18000@43000\n    #EXTINF:1,\n    fs243.mp4\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:1\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.9\n    #EXT-X-PART-INF:PART-TARGET=0.4\n    #EXTINF:1,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.34,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.34,URI=\"fs241.mp4\",BYTERANGE=23000@20000\n    #EXT-X-PART:DURATION=0.4,URI=\"fs241.mp4\",BYTERANGE=18000@43000\n    #EXTINF:1,\n    fs241.mp4\n    #EXT-X-PART:DURATION=0.34,URI=\"fs242.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.34,URI=\"fs242.mp4\",BYTERANGE=23000@20000\n    #EXT-X-PART:DURATION=0.4,URI=\"fs242.mp4\",BYTERANGE=18000@43000\n    #EXTINF:1,\n    fs242.mp4\n    #EXT-X-PART:DURATION=0.34,URI=\"fs243.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.34,URI=\"fs243.mp4\",BYTERANGE=23000@20000\n    #EXT-X-PART:DURATION=0.4,URI=\"fs243.mp4\",BYTERANGE=18000@43000\n    #EXTINF:1,\n    fs243.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs244.mp4\",BYTERANGE-START=0,BYTERANGE-LENGTH=20000\n  `);\n});\n\n// DURATION=<s>: (mandatory) Indicates the duration of the Partial Segment\n// in floating-point seconds.\ntest('#EXT-X-PART_03', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000\n  `);\n});\n\n// URI=<url>: (mandatory) Indicates the URI for the Partial Segment.\ntest('#EXT-X-PART_04', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.17,BYTERANGE=20000@0\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000\n  `);\n});\n\ntest('#EXT-X-PART_05', t => {\n  const {segments} = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0,INDEPENDENT=YES,GAP=YES\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000,INDEPENDENT=YES,GAP=YES\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n\n  t.is(segments.length, 2);\n  const {parts} = segments[1];\n  t.is(parts.length, 3);\n  let offset = 0;\n  const length = 20000;\n  for (const [index, part] of parts.entries()) {\n    t.is(part.uri, 'fs241.mp4');\n    t.deepEqual(part.byterange, {offset, length});\n    offset += length;\n    if (index < 2) {\n      t.is(part.duration, 0.2);\n      t.is(part.independent, true);\n      t.is(part.gap, true);\n    }\n  }\n});\n\n// EXTINF can be ommitted\ntest('#EXT-X-PART_06', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.17,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-ENDLIST\n  `);\n});\n"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/04_EXT-X-PRELOAD-HINT.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// TYPE=<hint-type>: (mandatory)\ntest('#EXT-X-PRELOAD-HINT_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:URI=\"fs241.mp4\",BYTERANGE-LENGTH=20000\n  `);\n});\n\n// If hint-type is PART, the resource is an upcoming Partial Segment.\ntest('#EXT-X-PRELOAD-HINT_02', t => {\n  const {segments} = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n\n  t.is(segments.length, 2);\n  const {parts} = segments[1];\n  t.is(parts.length, 3);\n  let offset = 0;\n  const length = 20000;\n  for (const [index, part] of parts.entries()) {\n    t.is(part.uri, 'fs241.mp4');\n    t.deepEqual(part.byterange, {offset, length});\n    offset += length;\n    if (index === 2) {\n      t.true(part.hint);\n    } else {\n      t.false(part.hint);\n    }\n  }\n});\n\n// If hint-type is MAP, the resource is an upcoming Media Initialization Section.\ntest('#EXT-X-PRELOAD-HINT_03', t => {\n  const {segments} = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXT-X-MAP:URI=\"map-0\"\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-LENGTH=20000\n    #EXT-X-PRELOAD-HINT:TYPE=MAP,URI=\"map-1\"\n\n  `);\n\n  t.is(segments.length, 2);\n  for (const [index, {map}] of segments.entries()) {\n    t.is(map.uri, `map-${index}`);\n  }\n});\n\n// URI=<uri>: (mandatory)\ntest('#EXT-X-PRELOAD-HINT_04', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\"\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART\n  `);\n});\n\n// BYTERANGE-START=<n>: ... Its absence implies a value of 0.\ntest('#EXT-X-PRELOAD-HINT_05', t => {\n  const {segments} = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-LENGTH=20000\n  `);\n\n  const {parts} = segments[1];\n  t.is(parts[0].byterange.offset, 0);\n});\n\n// If the Playlist contains EXT-X-PART tags and does not contain an EXT-X-ENDLIST tag,\n// the Playlist must contain an EXT-X-PRELOAD-HINT tag with a TYPE=PART attribute\n// to hint the URI of the next EXT-X-PART tag that is expected to be added to the\n// Playlist (and its byte range, if applicable).\ntest('#EXT-X-PRELOAD-HINT_06', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-ENDLIST\n  `);\n});\n\n// Servers should not add more than one EXT-X-PRELOAD-HINT\n// tag with the same TYPE attribute to a Playlist.\ntest('#EXT-X-PRELOAD-HINT_07', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-LENGTH=20000\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-LENGTH=20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000\n  `);\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXT-X-MAP:URI=\"map-0\"\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-LENGTH=20000\n    #EXT-X-PRELOAD-HINT:TYPE=MAP,URI=\"map-1\"\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:6\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXT-X-MAP:URI=\"map-0\"\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-LENGTH=20000\n    #EXT-X-PRELOAD-HINT:TYPE=MAP,URI=\"map-1\"\n    #EXT-X-PRELOAD-HINT:TYPE=MAP,URI=\"map-2\"\n  `);\n});\n"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/05_EXT-X-RENDITION-REPORT.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// URI=<uri>: (mandatory)\ntest('#EXT-X-RENDITION-REPORT_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-RENDITION-REPORT:URI=\"mid.m3u8\",LAST-MSN=1999\n    #EXT-X-RENDITION-REPORT:URI=\"high.m3u8\",LAST-MSN=1999\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-RENDITION-REPORT:LAST-MSN=1999\n    #EXT-X-RENDITION-REPORT:LAST-MSN=1999\n  `);\n});\n\n// URI=<uri>: (mandatory) ... It must be relative to the URI of the Media Playlist\n// containing the EXT-X-RENDITION-REPORT tag.\ntest('#EXT-X-RENDITION-REPORT_02', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-RENDITION-REPORT:URI=\"mid.m3u8\",LAST-MSN=1999\n    #EXT-X-RENDITION-REPORT:URI=\"high.m3u8\",LAST-MSN=1999\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-RENDITION-REPORT:URI=\"https://example.com/mid.m3u8\",LAST-MSN=1999\n    #EXT-X-RENDITION-REPORT:URI=\"https://example.com/high.m3u8\",LAST-MSN=1999\n  `);\n});\n\n// A server may omit adding an attribute to an EXT-X-RENDITION-REPORT\n// tag — even a mandatory attribute — if its value is the same as that\n// of the Rendition Report of the Media Playlist to which the EXT-X-RENDITION-REPORT\n// tag is being added. This step reduces the size of the Rendition Report.\ntest('#EXT-X-RENDITION-REPORT_03', t => {\n  const {renditionReports} = HLS.parse(`\n    #EXTM3U\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-MEDIA-SEQUENCE:1990\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXTINF:2,\n    fs240.mp4\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@0\n    #EXT-X-PART:DURATION=0.2,URI=\"fs241.mp4\",BYTERANGE=20000@20000\n    #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"fs241.mp4\",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000\n    #EXT-X-RENDITION-REPORT:URI=\"main-0.m3u8\"\n    #EXT-X-RENDITION-REPORT:URI=\"main-1.m3u8\"\n  `);\n\n  t.is(renditionReports.length, 2);\n  for (const [index, report] of renditionReports.entries()) {\n    t.is(report.uri, `main-${index}.m3u8`);\n    t.is(report.lastMSN, 1991);\n    t.is(report.lastPart, 2);\n  }\n});\n\n// Handle 0-indexed segment parts in rendition reports\ntest('#EXT-X-RENDITION-REPORT_04', t => {\n  const {renditionReports} = HLS.parse(`\n  #EXTM3U\n  #EXT-X-VERSION:6\n  #EXT-X-TARGETDURATION:3\n  #EXT-X-SERVER-CONTROL:PART-HOLD-BACK=3.150000,CAN-BLOCK-RELOAD=YES\n  #EXT-X-PART-INF:PART-TARGET=1\n  #EXT-X-PROGRAM-DATE-TIME:2022-08-12T15:53:22Z\n  media_b128000_cmaf_a_6.mp4\n  #EXT-X-PROGRAM-DATE-TIME:2022-08-12T15:53:31Z\n  #EXT-X-PART:DURATION=1,INDEPENDENT=YES,URI=\"media_b128000_cmaf_a_7_p0.mp4\"\n  #EXT-X-PRELOAD-HINT:TYPE=PART,URI=\"media_b128000_cmaf_a_7_p1.mp4\"\n  #EXT-X-RENDITION-REPORT:URI=\"chunklist_b56000_cmaf_a.m3u8?max_segments=10\",LAST-MSN=7,LAST-PART=0\n  #EXT-X-RENDITION-REPORT:URI=\"chunklist_b256000_cmaf_a.m3u8?max_segments=10\",LAST-MSN=7,LAST-PART=0\n  `);\n\n  t.is(renditionReports.length, 2);\n  for (const [index, report] of renditionReports.entries()) {\n    console.log(index, report);\n    t.is(report.lastMSN, 7);\n    t.is(report.lastPart, 0);\n  }\n});\n"
  },
  {
    "path": "test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/06_EXT-X-SKIP.spec.js",
    "content": "const test = require('ava');\nconst utils = require('../../../helpers/utils');\nconst HLS = require('../../../..');\n\n// SKIPPED-SEGMENTS=<N>: (mandatory)\ntest('#EXT-X-SKIP_01', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:9\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXT-X-SKIP:SKIPPED-SEGMENTS=20\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n    #EXTINF:2,\n    fs242.mp4\n    #EXTINF:2,\n    fs243.mp4\n    #EXTINF:2,\n    fs244.mp4\n    #EXTINF:2,\n    fs245.mp4\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:9\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXT-X-SKIP:NUM=20\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n    #EXTINF:2,\n    fs242.mp4\n    #EXTINF:2,\n    fs243.mp4\n    #EXTINF:2,\n    fs244.mp4\n    #EXTINF:2,\n    fs245.mp4\n  `);\n});\n\n// SKIPPED-SEGMENTS=<N>: (mandatory) Indicates how many\n// Media Segments were replaced by the EXT-X-SKIP tag,\n// along with their associated tags.\ntest('#EXT-X-SKIP_02', t => {\n  const {skip, segments} = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:9\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-MEDIA-SEQUENCE:9000\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXT-X-SKIP:SKIPPED-SEGMENTS=20\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n    #EXTINF:2,\n    fs242.mp4\n    #EXTINF:2,\n    fs243.mp4\n    #EXTINF:2,\n    fs244.mp4\n    #EXTINF:2,\n    fs245.mp4\n  `);\n\n  t.is(skip, 20);\n  t.is(segments[0].mediaSequenceNumber, 9020);\n});\n\n// A Playlist containing an EXT-X-SKIP tag must have\n// an EXT-X-VERSION tag with a value of nine or higher.\ntest('#EXT-X-SKIP_03', t => {\n  utils.bothPass(t, `\n    #EXTM3U\n    #EXT-X-VERSION:9\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXT-X-SKIP:SKIPPED-SEGMENTS=20\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n    #EXTINF:2,\n    fs242.mp4\n    #EXTINF:2,\n    fs243.mp4\n    #EXTINF:2,\n    fs244.mp4\n    #EXTINF:2,\n    fs245.mp4\n  `);\n  utils.parseFail(t, `\n    #EXTM3U\n    #EXT-X-VERSION:8\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2\n    #EXT-X-PART-INF:PART-TARGET=0.2\n    #EXT-X-SKIP:SKIPPED-SEGMENTS=20\n    #EXTINF:2,\n    fs240.mp4\n    #EXTINF:2,\n    fs241.mp4\n    #EXTINF:2,\n    fs242.mp4\n    #EXTINF:2,\n    fs243.mp4\n    #EXTINF:2,\n    fs244.mp4\n    #EXTINF:2,\n    fs245.mp4\n  `);\n});\n"
  },
  {
    "path": "test/spec/Apple_HLS_Overview/02_Using_HLS.spec.js",
    "content": "const test = require('ava');\nconst HLS = require('../../..');\nconst utils = require('../../helpers/utils');\n\n// Starting with iOS 3.1, if the client is unable to reload the index file for a stream (due to a 404 error, for example),\n// the client attempts to switch to an alternate stream.\ntest('Redundant_Streams_01', t => {\n  const sourceText = `\n  #EXTM3U\n  #EXT-X-STREAM-INF:BANDWIDTH=200000, RESOLUTION=720x480\n  http://ALPHA.mycompany.com/lo/prog_index.m3u8\n  #EXT-X-STREAM-INF:BANDWIDTH=200000, RESOLUTION=720x480\n  http://BETA.mycompany.com/lo/prog_index.m3u8\n\n  #EXT-X-STREAM-INF:BANDWIDTH=500000, RESOLUTION=1920x1080\n  http://ALPHA.mycompany.com/md/prog_index.m3u8\n  #EXT-X-STREAM-INF:BANDWIDTH=500000, RESOLUTION=1920x1080\n  http://BETA.mycompany.com/md/prog_index.m3u8\n  `;\n  const obj = HLS.parse(sourceText);\n  const text = HLS.stringify(obj);\n  t.is(text, utils.stripCommentsAndEmptyLines(sourceText));\n});\n"
  },
  {
    "path": "test/spec/HLSJS-LHLS/01_EXT-X-PREFETCH.spec.js",
    "content": "const test = require(\"ava\");\nconst utils = require(\"../../helpers/utils\");\nconst HLS = require(\"../../..\");\n\ntest(\"#EXT-X-PREFETCH_01\", t => {\n  utils.bothPass(\n    t,\n    `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-MEDIA-SEQUENCE: 0\n    #EXT-X-DISCONTINUITY-SEQUENCE: 0\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/0.ts\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/1.ts\n\n    #EXT-X-PREFETCH:https://foo.com/bar/2.ts\n    #EXT-X-PREFETCH:https://foo.com/bar/3.ts\n  `\n  );\n});\n\ntest(\"#EXT-X-PREFETCH_02\", t => {\n  const parsed = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-MEDIA-SEQUENCE: 0\n    #EXT-X-DISCONTINUITY-SEQUENCE: 0\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/0.ts\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/1.ts\n\n    #EXT-X-PREFETCH:https://foo.com/bar/2.ts\n    #EXT-X-PREFETCH:https://foo.com/bar/3.ts\n  `);\n  const {prefetchSegments} = parsed;\n\n  t.is(prefetchSegments.length, 2);\n  t.is(prefetchSegments[0].uri, \"https://foo.com/bar/2.ts\");\n  t.is(prefetchSegments[1].uri, \"https://foo.com/bar/3.ts\");\n\n  const stringified = HLS.stringify(parsed);\n\n  t.true(stringified.includes('#EXT-X-PREFETCH:https://foo.com/bar/2.ts'));\n  t.true(stringified.includes('#EXT-X-PREFETCH:https://foo.com/bar/3.ts'));\n});\n\n// If delivering a low-latency stream, the server must deliver at least one\n// prefetch segment, but no more than two.\ntest(\"#EXT-X-PREFETCH_03\", t => {\n  const parsed = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-MEDIA-SEQUENCE: 0\n    #EXT-X-DISCONTINUITY-SEQUENCE: 0\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/0.ts\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/1.ts\n\n    #EXT-X-PREFETCH:https://foo.com/bar/2.ts\n    #EXT-X-PREFETCH:https://foo.com/bar/3.ts\n    #EXT-X-PREFETCH:https://foo.com/bar/4.ts\n  `);\n  const {prefetchSegments} = parsed;\n  t.is(prefetchSegments.length, 3);\n\n  try {\n    HLS.stringify(parsed);\n    t.fail('The server must deliver no more than two prefetch segments');\n  } catch {\n    t.pass();\n  }\n});\n\n// These segments must appear after all complete segments.\ntest(\"#EXT-X-PREFETCH_04\", t => {\n  try {\n    HLS.parse(`\n      #EXTM3U\n      #EXT-X-VERSION:3\n      #EXT-X-TARGETDURATION:2\n      #EXT-X-MEDIA-SEQUENCE: 0\n      #EXT-X-DISCONTINUITY-SEQUENCE: 0\n      #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z\n      #EXTINF:2.000\n      https://foo.com/bar/0.ts\n      #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z\n      #EXTINF:2.000\n      https://foo.com/bar/1.ts\n\n      #EXT-X-PREFETCH:https://foo.com/bar/2.ts\n      #EXT-X-PREFETCH:https://foo.com/bar/3.ts\n\n      #EXTINF:2.000\n      https://foo.com/bar/4.ts\n    `);\n    t.fail('Prefetch segments must appear after all complete segments');\n  } catch {\n    t.pass();\n  }\n});\n\n// A prefetch segment's Discontinuity Sequence Number is the value of the\n// EXT-X-DISCONTINUITY-SEQUENCE tag (or zero if none) plus the number of\n// EXT-X-DISCONTINUITY and EXT-X-PREFETCH-DISCONTINUITY tags in the Playlist\n// preceding the URI line of the segment.\ntest(\"#EXT-X-PREFETCH_05\", t => {\n  const parsed = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-MEDIA-SEQUENCE: 0\n    #EXT-X-DISCONTINUITY-SEQUENCE: 100\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/0.ts\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/1.ts\n    #EXT-X-DISCONTINUITY\n    #EXTINF:2.000\n    https://foo.com/bar/1.ts\n\n    #EXT-X-PREFETCH-DISCONTINUITY\n    #EXT-X-PREFETCH:https://foo.com/bar/5.ts\n    #EXT-X-PREFETCH:https://foo.com/bar/6.ts\n  `);\n  const {prefetchSegments} = parsed;\n  t.is(prefetchSegments[1].discontinuitySequence, 102);\n});\n\n// If a prefetch segment is the first segment in a manifest, its Media Sequence\n// Number is either 0, or declared in the Playlist.\n// The Media Sequence Number of every other prefetch segment is equal to the\n// Media Sequence Number of the complete segment or prefetch segment that\n// precedes it plus one.\ntest(\"#EXT-X-PREFETCH_06\", t => {\n  let parsed;\n  parsed = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:2\n\n    #EXT-X-PREFETCH:https://foo.com/bar/5.ts\n    #EXT-X-PREFETCH:https://foo.com/bar/6.ts\n  `);\n  t.is(parsed.prefetchSegments[0].mediaSequenceNumber, 0);\n  t.is(parsed.prefetchSegments[1].mediaSequenceNumber, 1);\n\n  parsed = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-MEDIA-SEQUENCE: 100\n\n    #EXT-X-PREFETCH:https://foo.com/bar/5.ts\n    #EXT-X-PREFETCH:https://foo.com/bar/6.ts\n  `);\n  t.is(parsed.prefetchSegments[0].mediaSequenceNumber, 100);\n  t.is(parsed.prefetchSegments[1].mediaSequenceNumber, 101);\n});\n\n// A prefetch segment must not be advertised with an EXTINF tag. The duration of\n// a prefetch segment must be equal to or less than what is specified by the\n// EXT-X-TARGETDURATION tag.\ntest(\"#EXT-X-PREFETCH_07\", t => {\n  try {\n    HLS.parse(`\n      #EXTM3U\n      #EXT-X-VERSION:3\n      #EXT-X-TARGETDURATION:2\n\n      #EXTINF:2.000\n      #EXT-X-PREFETCH:https://foo.com/bar/5.ts\n      #EXTINF:2.000\n      #EXT-X-PREFETCH:https://foo.com/bar/6.ts\n    `);\n    t.fail('A prefetch segment must not be advertised with an EXTINF tag');\n  } catch {\n    t.pass();\n  }\n});\n\n// A prefetch segment must not be advertised with an EXT-X-DISCONTINUITY tag.\n// To insert a discontinuity just for prefetch segments, the server must insert\n// the EXT-X-PREFETCH-DISCONTINUITY tag before the newest EXT-X-PREFETCH tag of\n// the new discontinuous range.\ntest(\"#EXT-X-PREFETCH_08\", t => {\n  try {\n    HLS.parse(`\n      #EXTM3U\n      #EXT-X-VERSION:3\n      #EXT-X-TARGETDURATION:2\n\n      #EXT-X-DISCONTINUITY\n      #EXT-X-PREFETCH:https://foo.com/bar/5.ts\n      #EXT-X-PREFETCH:https://foo.com/bar/6.ts\n    `);\n    t.fail('A prefetch segment must not be advertised with an EXT-X-DISCONTINUITY tag');\n  } catch {\n    t.pass();\n  }\n});\n\n// Prefetch segments must not be advertised with an EXT-X-MAP tag.\ntest(\"#EXT-X-PREFETCH_09\", t => {\n  try {\n    HLS.parse(`\n      #EXTM3U\n      #EXT-X-VERSION:3\n      #EXT-X-TARGETDURATION:2\n\n      #EXT-X-MAP:URI=\"http://example.com/map-1\"\n      #EXT-X-PREFETCH:https://foo.com/bar/5.ts\n      #EXT-X-PREFETCH:https://foo.com/bar/6.ts\n    `);\n    t.fail('Prefetch segments must not be advertised with an EXT-X-MAP tag');\n  } catch {\n    t.pass();\n  }\n});\n\n// Prefetch segments may be advertised with an EXT-X-KEY tag. The key itself\n// must be complete; the server must not expect the client to progressively stream keys.\ntest(\"#EXT-X-PREFETCH_10\", t => {\n  const parsed = HLS.parse(`\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:2\n\n    #EXT-X-KEY:METHOD=AES-128,URI=\"http://example.com\"\n    #EXT-X-PREFETCH:https://foo.com/bar/5.ts\n    #EXT-X-PREFETCH:https://foo.com/bar/6.ts\n  `);\n  const {prefetchSegments} = parsed;\n  t.truthy(prefetchSegments[0].key);\n  t.is(prefetchSegments[0].key.uri, 'http://example.com');\n  t.truthy(prefetchSegments[1].key);\n  t.is(prefetchSegments[1].key.uri, 'http://example.com');\n});\n"
  },
  {
    "path": "test/spec/HLSJS-LHLS/02_EXT-X-PREFETCH-DISCONTINUITY.spec.js",
    "content": "const test = require(\"ava\");\nconst utils = require(\"../../helpers/utils\");\nconst HLS = require(\"../../..\");\n\ntest(\"#EXT-X-PREFETCH-DISCONTINUITY_01\", t => {\n  const text = `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-MEDIA-SEQUENCE: 0\n    #EXT-X-DISCONTINUITY-SEQUENCE: 0\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/0.ts\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/1.ts\n\n    #EXT-X-PREFETCH-DISCONTINUITY\n    #EXT-X-PREFETCH:https://foo.com/bar/5.ts\n    #EXT-X-PREFETCH:https://foo.com/bar/6.ts\n  `;\n  utils.bothPass(t, text);\n  const {prefetchSegments} = HLS.parse(text);\n  t.is(prefetchSegments.length, 2);\n  t.true(prefetchSegments[0].discontinuity);\n  t.falsy(prefetchSegments[1].discontinuity);\n});\n\ntest(\"#EXT-X-PREFETCH-DISCONTINUITY_02\", t => {\n  const text = `\n    #EXTM3U\n    #EXT-X-VERSION:3\n    #EXT-X-TARGETDURATION:2\n    #EXT-X-MEDIA-SEQUENCE: 1\n    #EXT-X-DISCONTINUITY-SEQUENCE: 0\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/1.ts\n    #EXT-X-DISCONTINUITY\n    #EXT-X-PROGRAM-DATE-TIME:2018-09-05T21:59:10.531Z\n    #EXTINF:2.000\n    https://foo.com/bar/5.ts\n\n    #EXT-X-PREFETCH:https://foo.com/bar/6.ts\n    #EXT-X-PREFETCH-DISCONTINUITY\n    #EXT-X-PREFETCH:https://foo.com/bar/9.ts\n  `;\n  utils.bothPass(t, text);\n  const {prefetchSegments} = HLS.parse(text);\n  t.is(prefetchSegments.length, 2);\n  t.falsy(prefetchSegments[0].discontinuity);\n  t.true(prefetchSegments[1].discontinuity);\n});\n"
  },
  {
    "path": "test/spec/misc/multiple-rendition-groups.js",
    "content": "const test = require(\"ava\");\nconst utils = require(\"../../helpers/utils\");\nconst HLS = require(\"../../..\");\n\ntest(\"Multiple-Rendition-Groups_01\", t => {\n  const shouldRead = `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-INDEPENDENT-SEGMENTS\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_high\",NAME=\"English\",DEFAULT=YES,URI=\"aac_high_eng.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_high\",NAME=\"Japanese\",DEFAULT=NO,URI=\"aac_high_jp.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_mid\",NAME=\"English\",DEFAULT=YES,URI=\"aac_mid_eng.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_mid\",NAME=\"Japanese\",DEFAULT=NO,URI=\"aac_mid_jp.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_low\",NAME=\"English\",DEFAULT=YES,URI=\"aac_low_eng.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_low\",NAME=\"Japanese\",DEFAULT=NO,URI=\"aac_low_jp.m3u8\"\n    #EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO=\"aac_high\"\n    1080p.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO=\"aac_mid\"\n    720p.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO=\"aac_mid\"\n    540p.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO=\"aac_low\"\n    360p.m3u8\n  `;\n\n  const playlist = HLS.parse(shouldRead);\n\n  const shouldWrite = `\n    #EXTM3U\n    #EXT-X-VERSION:4\n    #EXT-X-INDEPENDENT-SEGMENTS\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_high\",NAME=\"English\",DEFAULT=YES,URI=\"aac_high_eng.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_high\",NAME=\"Japanese\",DEFAULT=NO,URI=\"aac_high_jp.m3u8\"\n    #EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO=\"aac_high\"\n    1080p.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_mid\",NAME=\"English\",DEFAULT=YES,URI=\"aac_mid_eng.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_mid\",NAME=\"Japanese\",DEFAULT=NO,URI=\"aac_mid_jp.m3u8\"\n    #EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO=\"aac_mid\"\n    720p.m3u8\n    #EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO=\"aac_mid\"\n    540p.m3u8\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_low\",NAME=\"English\",DEFAULT=YES,URI=\"aac_low_eng.m3u8\"\n    #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac_low\",NAME=\"Japanese\",DEFAULT=NO,URI=\"aac_low_jp.m3u8\"\n    #EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO=\"aac_low\"\n    360p.m3u8\n  `;\n\n  t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(shouldWrite));\n});\n"
  },
  {
    "path": "test/spec/misc/scte-35.spec.js",
    "content": "const test = require(\"ava\");\nconst utils = require(\"../../helpers/utils\");\nconst HLS = require(\"../../..\");\n\ntest(\"#EXT-X-CUE-IN_01\", t => {\n  const {MediaPlaylist, Segment} = HLS.types;\n\n  const segments = [...Array.from({length: 3})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10}));\n  segments[0].discontinuity = true;\n  segments[0].markers.push({type: 'OUT', duration: 30});\n\n  const playlist = new MediaPlaylist({\n    targetDuration: 10,\n    segments\n  });\n\n  // For live media playlist, unclosed CUE-OUT is allowed.\n  const expected = `\n      #EXTM3U\n      #EXT-X-TARGETDURATION:10\n      #EXT-X-DISCONTINUITY\n      #EXT-X-CUE-OUT:DURATION=30\n      #EXTINF:10,\n      https://example.com/0.ts\n      #EXTINF:10,\n      https://example.com/1.ts\n      #EXTINF:10,\n      https://example.com/2.ts\n  `;\n\n  t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected));\n});\n\ntest(\"#EXT-X-CUE-IN_02\", t => {\n  const {MediaPlaylist, Segment} = HLS.types;\n\n  const segments = [...Array.from({length: 3})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10}));\n  segments[0].discontinuity = true;\n  segments[0].markers.push({type: 'OUT', duration: 30});\n\n  const playlist = new MediaPlaylist({\n    playlistType: 'VOD',\n    targetDuration: 10,\n    segments\n  });\n\n  // For VOD media playlist, unclosed CUE-OUT is not allowed.\n  // CUE-IN will be added.\n  const expected = `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PLAYLIST-TYPE:VOD\n    #EXT-X-DISCONTINUITY\n    #EXT-X-CUE-OUT:DURATION=30\n    #EXTINF:10,\n    https://example.com/0.ts\n    #EXTINF:10,\n    https://example.com/1.ts\n    #EXTINF:10,\n    https://example.com/2.ts\n    #EXT-X-CUE-IN\n  `;\n\n  t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected));\n});\n\ntest(\"#EXT-X-CUE-IN_03\", t => {\n  const {MediaPlaylist, Segment} = HLS.types;\n\n  const segments = [...Array.from({length: 6})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10}));\n  segments[0].markers.push({type: 'OUT', duration: 20});\n  segments[2].markers.push({type: 'IN'});\n  segments[4].markers.push({type: 'OUT', duration: 20});\n\n  const playlist = new MediaPlaylist({\n    playlistType: 'EVENT',\n    targetDuration: 10,\n    segments\n  });\n\n  // For live media playlist, unclosed CUE-OUT is allowed.\n  const expected = `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PLAYLIST-TYPE:EVENT\n    #EXT-X-CUE-OUT:DURATION=20\n    #EXTINF:10,\n    https://example.com/0.ts\n    #EXTINF:10,\n    https://example.com/1.ts\n    #EXT-X-CUE-IN\n    #EXTINF:10,\n    https://example.com/2.ts\n    #EXTINF:10,\n    https://example.com/3.ts\n    #EXT-X-CUE-OUT:DURATION=20\n    #EXTINF:10,\n    https://example.com/4.ts\n    #EXTINF:10,\n    https://example.com/5.ts\n  `;\n\n  t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected));\n});\n\ntest(\"#EXT-X-CUE-IN_04\", t => {\n  const {MediaPlaylist, Segment} = HLS.types;\n\n  const segments = [...Array.from({length: 6})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10}));\n  segments[0].markers.push({type: 'OUT', duration: 20});\n  segments[2].markers.push({type: 'IN'});\n  segments[4].markers.push({type: 'OUT', duration: 20});\n\n  const playlist = new MediaPlaylist({\n    playlistType: 'VOD',\n    targetDuration: 10,\n    segments\n  });\n\n  // For VOD media playlist, unclosed CUE-OUT is not allowed.\n  // CUE-IN will be added.\n  const expected = `\n    #EXTM3U\n    #EXT-X-TARGETDURATION:10\n    #EXT-X-PLAYLIST-TYPE:VOD\n    #EXT-X-CUE-OUT:DURATION=20\n    #EXTINF:10,\n    https://example.com/0.ts\n    #EXTINF:10,\n    https://example.com/1.ts\n    #EXT-X-CUE-IN\n    #EXTINF:10,\n    https://example.com/2.ts\n    #EXTINF:10,\n    https://example.com/3.ts\n    #EXT-X-CUE-OUT:DURATION=20\n    #EXTINF:10,\n    https://example.com/4.ts\n    #EXTINF:10,\n    https://example.com/5.ts\n    #EXT-X-CUE-IN\n  `;\n\n  t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected));\n});\n\n"
  },
  {
    "path": "test/spec/parser.spec.js",
    "content": "const test = require('ava');\nconst fixtures = require('../helpers/fixtures');\nconst HLS = require('../..');\n\nHLS.setOptions({strictMode: true});\n\nconst {Playlist} = HLS.types;\n\nfor (const {name, m3u8, object} of fixtures) {\n  test(name, t => {\n    const result = HLS.parse(m3u8);\n    if (result.source === m3u8 && deepEqual(t, result, object)) {\n      t.pass();\n    }\n  });\n}\n\nfunction buildMessage(propName, actual, expected) {\n  if (actual && typeof actual === 'object') {\n    actual = JSON.parse(actual);\n  }\n  if (expected && typeof expected === 'object') {\n    expected = JSON.parse(expected);\n  }\n  return `\n${propName} does not match.\nexpected:\n${expected}\nactual:\n${actual}\n`;\n}\n\nfunction deepEqual(t, actual, expected) {\n  if (!expected) {\n    return;\n  }\n  let errorMessage;\n  if (actual instanceof Playlist === false) {\n    return t.fail('The result is not an instance of Playlist');\n  }\n  if (actual.isMasterPlaylist !== expected.isMasterPlaylist) {\n    return t.fail(buildMessage('Playlist.isMasterPlaylist', actual.isMasterPlaylist, expected.isMasterPlaylist));\n  }\n  if (expected.uri) {\n    if (!actual.uri || actual.uri.href !== expected.uri.href) {\n      return t.fail(buildMessage('Playlist.uri', actual.uri, expected.uri));\n    }\n  }\n  if (actual.version !== expected.version) {\n    return t.fail(buildMessage('Playlist.version', actual.version, expected.version));\n  }\n  if (actual.independentSegments !== expected.independentSegments) {\n    return t.fail(buildMessage('Playlist.independentSegments', actual.independentSegments, expected.independentSegments));\n  }\n  if (actual.offset !== expected.offset) {\n    return t.fail(buildMessage('Playlist.offset', actual.offset, expected.offset));\n  }\n  if (expected.isMasterPlaylist) {\n    // MasterPlaylist\n    if (expected.variants) {\n      if (!actual.variants || actual.variants.length !== expected.variants.length) {\n        return t.fail(buildMessage('Playlist.variants', actual.variants, expected.variants));\n      }\n      for (const [index, actualVariant] of actual.variants.entries()) {\n        if (errorMessage = deepEqualVariant(t, actualVariant, expected.variants[index])) {\n          return t.fail(errorMessage);\n        }\n      }\n    }\n    if (actual.currentVariant !== expected.currentVariant) {\n      return t.fail(buildMessage('MasterPlaylist.currentVariant', actual.currentVariant, expected.currentVariant));\n    }\n\n    if (expected.sessionDataList) {\n      if (!actual.sessionDataList || actual.sessionDataList.length !== expected.sessionDataList.length) {\n        return t.fail(buildMessage('MasterPlaylist.sessionDataList', actual.sessionDataList, expected.sessionDataList));\n      }\n      for (const [index, actualSessionData] of actual.sessionDataList.entries()) {\n        if (errorMessage = deepEqualSessionData(t, actualSessionData, expected.sessionDataList[index])) {\n          return t.fail(errorMessage);\n        }\n      }\n    }\n    if (expected.sessionKeyList) {\n      if (!actual.sessionKeyList || actual.sessionKeyList.length !== expected.sessionKeyList.length) {\n        return t.fail(buildMessage('MasterPlaylist.sessionKeyList', actual.sessionKeyList, expected.sessionKeyList));\n      }\n      for (const [index, actualSessionKey] of actual.sessionKeyList.entries()) {\n        if (errorMessage = deepEqualKey(t, actualSessionKey, expected.sessionKeyList[index])) {\n          return t.fail(errorMessage);\n        }\n      }\n    }\n  } else {\n    // MediaPlaylist\n    if (actual.targetDuration !== expected.targetDuration) {\n      return t.fail(buildMessage('MediaPlaylist.targetDuration', actual.targetDuration, expected.targetDuration));\n    }\n    if (actual.mediaSequenceBase !== expected.mediaSequenceBase) {\n      return t.fail(buildMessage('MediaPlaylist.mediaSequenceBase', actual.mediaSequenceBase, expected.mediaSequenceBase));\n    }\n    if (actual.discontinuitySequenceBase !== expected.discontinuitySequenceBase) {\n      return t.fail(buildMessage('MediaPlaylist.discontinuitySequenceBase', actual.discontinuitySequenceBase, expected.discontinuitySequenceBase));\n    }\n    if (actual.endlist !== expected.endlist) {\n      return t.fail(buildMessage('MediaPlaylist.endlist', actual.endlist, expected.endlist));\n    }\n    if (actual.playlistType !== expected.playlistType) {\n      return t.fail(buildMessage('MediaPlaylist.playlistType', actual.playlistType, expected.playlistType));\n    }\n    if (actual.isIFrame !== expected.isIFrame) {\n      return t.fail(buildMessage('MediaPlaylist.isIFrame', actual.isIFrame, expected.isIFrame));\n    }\n    if (expected.segments) {\n      if (!actual.segments || actual.segments.length !== expected.segments.length) {\n        return t.fail(buildMessage('MediaPlaylist.segments', actual.segments, expected.segments));\n      }\n      for (const [index, actualSegment] of actual.segments.entries()) {\n        if (errorMessage = deepEqualSegment(t, actualSegment, expected.segments[index])) {\n          return t.fail(errorMessage);\n        }\n      }\n    }\n    if (actual.hash !== expected.hash) {\n      return t.fail(buildMessage('MediaPlaylist.hash', actual.hash, expected.hash));\n    }\n  }\n  return true;\n}\n\nfunction deepEqualVariant(t, actual, expected) {\n  if (!expected) {\n    return;\n  }\n  let errorMessage;\n  if (expected.uri) {\n    if (!actual.uri || actual.uri.href !== expected.uri.href) {\n      return t.fail(buildMessage('Variant.uri', actual.uri, expected.uri));\n    }\n  }\n  if (actual.isIFrameOnly !== expected.isIFrameOnly) {\n    return t.fail(buildMessage('Variant.isIFrameOnly', actual.isIFrameOnly, expected.isIFrameOnly));\n  }\n  if (actual.bandwidth !== expected.bandwidth) {\n    return t.fail(buildMessage('Variant.bandwidth', actual.bandwidth, expected.bandwidth));\n  }\n  if (actual.averageBandwidth !== expected.averageBandwidth) {\n    return t.fail(buildMessage('Variant.averageBandwidth', actual.averageBandwidth, expected.averageBandwidth));\n  }\n  if (actual.codecs !== expected.codecs) {\n    return t.fail(buildMessage('Variant.codecs', actual.codecs, expected.codecs));\n  }\n  if (expected.resolution) {\n    if (!actual.resolution || actual.resolution.width !== expected.resolution.width || actual.resolution.height !== expected.resolution.height) {\n      return t.fail(buildMessage('Variant.resolution', actual.resolution, expected.resolution));\n    }\n  }\n  if (actual.frameRate !== expected.frameRate) {\n    return t.fail(buildMessage('Variant.frameRate', actual.frameRate, expected.frameRate));\n  }\n  if (actual.hdcpLevel !== expected.hdcpLevel) {\n    return t.fail(buildMessage('Variant.hdcpLevel', actual.hdcpLevel, expected.hdcpLevel));\n  }\n  if (expected.audio) {\n    if (!actual.audio || actual.audio.length !== expected.audio.length) {\n      return t.fail(buildMessage('Variant.audio', actual.audio, expected.audio));\n    }\n    for (const [index, actualRendition] of actual.audio.entries()) {\n      if (errorMessage = deepEqualRendition(t, actualRendition, expected.audio[index])) {\n        return t.fail(errorMessage);\n      }\n    }\n  }\n  if (expected.video) {\n    if (!actual.video || actual.video.length !== expected.video.length) {\n      return t.fail(buildMessage('Variant.video', actual.video, expected.video));\n    }\n    for (const [index, actualRendition] of actual.video.entries()) {\n      if (errorMessage = deepEqualRendition(t, actualRendition, expected.video[index])) {\n        return t.fail(errorMessage);\n      }\n    }\n  }\n  if (expected.subtitles) {\n    if (!actual.subtitles || actual.subtitles.length !== expected.subtitles.length) {\n      return t.fail(buildMessage('Variant.subtitles', actual.subtitles, expected.subtitles));\n    }\n    for (const [index, actualRendition] of actual.subtitles.entries()) {\n      if (errorMessage = deepEqualRendition(t, actualRendition, expected.subtitles[index])) {\n        return t.fail(errorMessage);\n      }\n    }\n  }\n  if (expected.closedCaptions) {\n    if (!actual.closedCaptions || actual.closedCaptions.length !== expected.closedCaptions.length) {\n      return t.fail(buildMessage('Variant.closedCaptions', actual.closedCaptions, expected.closedCaptions));\n    }\n    for (const [index, actualRendition] of actual.closedCaptions.entries()) {\n      if (errorMessage = deepEqualRendition(t, actualRendition, expected.closedCaptions[index])) {\n        return t.fail(errorMessage);\n      }\n    }\n  }\n  if (expected.currentRenditions) {\n    const expectedCurrentRenditions = expected.currentRenditions;\n    const actualCurrentRenditions = actual.currentRenditions;\n    for (const key of Object.keys(expectedCurrentRenditions)) {\n      if (actualCurrentRenditions[key] !== expectedCurrentRenditions[key]) {\n        return t.fail(buildMessage('Variant.currentRenditions', actualCurrentRenditions[key], expectedCurrentRenditions[key]));\n      }\n    }\n  }\n}\n\nfunction deepEqualSessionData(t, actual, expected) {\n  if (!expected) {\n    return;\n  }\n  if (actual.id !== expected.id) {\n    return t.fail(buildMessage('SessionData.id', actual.id, expected.id));\n  }\n  if (actual.value !== expected.value) {\n    return t.fail(buildMessage('SessionData.value', actual.value, expected.value));\n  }\n  if (expected.uri) {\n    if (!actual.uri || actual.uri.href !== expected.uri.href) {\n      return t.fail(buildMessage('SessionData.uri', actual.uri, expected.uri));\n    }\n  }\n  if (actual.language !== expected.language) {\n    return t.fail(buildMessage('SessionData.language', actual.language, expected.language));\n  }\n}\n\nfunction deepEqualKey(t, actual, expected) {\n  if (!expected) {\n    return;\n  }\n  if (actual.method !== expected.method) {\n    return t.fail(buildMessage('Key.method', actual.method, expected.method));\n  }\n  if (expected.uri) {\n    if (!actual.uri || actual.uri.href !== expected.uri.href) {\n      return t.fail(buildMessage('Key.uri', actual.uri, expected.uri));\n    }\n  }\n  if (expected.iv) {\n    if (!actual.iv || actual.iv.length !== expected.iv.length) {\n      return t.fail(buildMessage('Key.iv', actual.iv, expected.iv));\n    }\n    for (let i = 0; i < actual.iv.length; i++) {\n      if (actual.iv[i] !== expected.iv[i]) {\n        return t.fail(buildMessage('Key.iv', actual.iv, expected.iv));\n      }\n    }\n  }\n  if (actual.format !== expected.format) {\n    return t.fail(buildMessage('Key.format', actual.format, expected.format));\n  }\n  if (actual.formatVersion !== expected.formatVersion) {\n    return t.fail(buildMessage('Key.formatVersion', actual.formatVersion, expected.formatVersion));\n  }\n}\n\nfunction deepEqualSegment(t, actual, expected) {\n  if (!expected) {\n    return;\n  }\n  let errorMessage;\n  if (expected.uri) {\n    if (!actual.uri || actual.uri.href !== expected.uri.href) {\n      return t.fail(buildMessage('Segment.uri', actual.uri, expected.uri));\n    }\n  }\n  if (expected.data) {\n    if (!actual.data || actual.data.length !== expected.data.length) {\n      return t.fail(buildMessage('Segment.data', actual.data, expected.data));\n    }\n    for (let i = 0; i < actual.data.length; i++) {\n      if (actual.data[i] !== expected.data[i]) {\n        return t.fail(buildMessage('Segment.data', actual.data, expected.data));\n      }\n    }\n  }\n  if (actual.duration !== expected.duration) {\n    return t.fail(buildMessage('Segment.duration', actual.duration, expected.duration));\n  }\n  if (actual.title !== expected.title) {\n    return t.fail(buildMessage('Segment.title', actual.title, expected.title));\n  }\n  if (expected.byterange) {\n    if (!actual.byterange || actual.byterange.length !== expected.byterange.length || actual.byterange.offset !== expected.byterange.offset) {\n      return t.fail(buildMessage('Segment.byterange', actual.byterange, expected.byterange));\n    }\n  }\n  if (actual.discontinuity !== expected.discontinuity) {\n    return t.fail(buildMessage('Segment.discontinuity', actual.discontinuity, expected.discontinuity));\n  }\n  if (actual.mediaSequenceNumber !== expected.mediaSequenceNumber) {\n    return t.fail(buildMessage('Segment.mediaSequenceNumber', actual.mediaSequenceNumber, expected.mediaSequenceNumber));\n  }\n  if (actual.discontinuitySequence !== expected.discontinuitySequence) {\n    return t.fail(buildMessage('Segment.discontinuitySequence', actual.discontinuitySequence, expected.discontinuitySequence));\n  }\n  if (errorMessage = deepEqualKey(t, actual.key, expected.key)) {\n    return t.fail(errorMessage);\n  }\n  if (errorMessage = deepEqualMap(t, actual.map, expected.map)) {\n    return t.fail(errorMessage);\n  }\n  if (expected.programDateTime) {\n    if (!actual.programDateTime || actual.programDateTime.getTime() !== expected.programDateTime.getTime()) {\n      return t.fail(buildMessage('Segment.programDateTime', actual.programDateTime, expected.programDateTime));\n    }\n  }\n  if (errorMessage = deepEqualDateRange(t, actual.dateRange, expected.dateRange)) {\n    return t.fail(errorMessage);\n  }\n}\n\nfunction deepEqualRendition(t, actual, expected) {\n  if (!expected) {\n    return;\n  }\n  if (actual.type !== expected.type) {\n    return t.fail(buildMessage('Rendition.type', actual.type, expected.type));\n  }\n  if (expected.uri) {\n    if (!actual.uri || actual.uri.href !== expected.uri.href) {\n      return t.fail(buildMessage('Rendition.uri', actual.uri, expected.uri));\n    }\n  }\n  if (actual.groupId !== expected.groupId) {\n    return t.fail(buildMessage('Rendition.groupId', actual.groupId, expected.groupId));\n  }\n  if (actual.language !== expected.language) {\n    return t.fail(buildMessage('Rendition.language', actual.language, expected.language));\n  }\n  if (actual.assocLanguage !== expected.assocLanguage) {\n    return t.fail(buildMessage('Rendition.assocLanguage', actual.assocLanguage, expected.assocLanguage));\n  }\n  if (actual.name !== expected.name) {\n    return t.fail(buildMessage('Rendition.name', actual.name, expected.name));\n  }\n  if (actual.isDefault !== expected.isDefault) {\n    return t.fail(buildMessage('Rendition.isDefault', actual.isDefault, expected.isDefault));\n  }\n  if (actual.autoselect !== expected.autoselect) {\n    return t.fail(buildMessage('Rendition.autoselect', actual.autoselect, expected.autoselect));\n  }\n  if (actual.forced !== expected.forced) {\n    return t.fail(buildMessage('Rendition.forced', actual.forced, expected.forced));\n  }\n  if (actual.instreamId !== expected.instreamId) {\n    return t.fail(buildMessage('Rendition.instreamId', actual.instreamId, expected.instreamId));\n  }\n  if (actual.characteristics !== expected.characteristics) {\n    return t.fail(buildMessage('Rendition.characteristics', actual.characteristics, expected.characteristics));\n  }\n  if (actual.channels !== expected.channels) {\n    return t.fail(buildMessage('Rendition.channels', actual.channels, expected.channels));\n  }\n}\n\nfunction deepEqualMap(t, actual, expected) {\n  if (!expected) {\n    return;\n  }\n  if (expected.uri) {\n    if (!actual.uri || actual.uri.href !== expected.uri.href) {\n      return t.fail(buildMessage('MediaInitializationSection.uri', actual.uri, expected.uri));\n    }\n  }\n  if (expected.byterange) {\n    if (!actual.byterange || actual.byterange.length !== expected.byterange.length || actual.byterange.offset !== expected.byterange.offset) {\n      return t.fail(buildMessage('MediaInitializationSection.byterange', actual.byterange, expected.byterange));\n    }\n  }\n}\n\nfunction deepEqualDateRange(t, actual, expected) {\n  if (!expected) {\n    return;\n  }\n  if (actual.id !== expected.id) {\n    return t.fail(buildMessage('DateRange.id', actual.id, expected.id));\n  }\n  if (actual.classId !== expected.classId) {\n    return t.fail(buildMessage('DateRange.classId', actual.classId, expected.classId));\n  }\n  if (expected.start) {\n    if (!actual.start || actual.start.getTime() !== expected.start.getTime()) {\n      return t.fail(buildMessage('DateRange.start', actual.start, expected.start));\n    }\n  }\n  if (expected.end) {\n    if (!actual.end || actual.end.getTime() !== expected.end.getTime()) {\n      return t.fail(buildMessage('DateRange.end', actual.end, expected.end));\n    }\n  }\n  if (actual.duration !== expected.duration) {\n    return t.fail(buildMessage('DateRange.duration', actual.duration, expected.duration));\n  }\n  if (actual.plannedDuration !== expected.plannedDuration) {\n    return t.fail(buildMessage('DateRange.plannedDuration', actual.plannedDuration, expected.plannedDuration));\n  }\n  if (actual.endOnNext !== expected.endOnNext) {\n    return t.fail(buildMessage('DateRange.endOnNext', actual.endOnNext, expected.endOnNext));\n  }\n}\n"
  },
  {
    "path": "test/spec/stringify.spec.js",
    "content": "const test = require('ava');\nconst fixtures = require('../helpers/fixtures');\nconst utils = require('../helpers/utils');\nconst HLS = require('../..');\n\nHLS.setOptions({strictMode: true});\n\nfor (const {name, m3u8, object} of fixtures) {\n  test(name, t => {\n    const result = HLS.stringify(object);\n    t.is(result, utils.stripCommentsAndEmptyLines(m3u8));\n  });\n}\n\ntest('stringify.postProcess.segment.add', t => {\n  const obj = HLS.parse(`\n  #EXTM3U\n  #EXT-X-VERSION:3\n  #EXT-X-TARGETDURATION:6\n  #EXTINF:6.006,\n  http://media.example.com/01.ts\n  #EXTINF:6.006,\n  http://media.example.com/02.ts\n  #EXTINF:6.006,\n  http://ads.example.com/ad-01.ts\n  #EXTINF:6.006,\n  http://ads.example.com/ad-02.ts\n  #EXTINF:6.006,\n  http://media.example.com/03.ts\n  #EXTINF:3.003,\n  http://media.example.com/04.ts\n  `);\n  const expected = `\n  #EXTM3U\n  #EXT-X-VERSION:3\n  #EXT-X-TARGETDURATION:6\n  #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z\n  #EXTINF:6.006,\n  http://media.example.com/01.ts\n  #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z\n  #EXTINF:6.006,\n  http://media.example.com/02.ts\n  #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z\n  #EXTINF:6.006,\n  http://ads.example.com/ad-01.ts\n  #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z\n  #EXTINF:6.006,\n  http://ads.example.com/ad-02.ts\n  #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z\n  #EXTINF:6.006,\n  http://media.example.com/03.ts\n  #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z\n  #EXTINF:3.003,\n  http://media.example.com/04.ts\n  `;\n  let time = new Date('2014-03-05T11:14:00.000Z').getTime();\n  const segmentProcessor = (lines, start, end, segment) => {\n    let hasPdt = false;\n    for (let i = start; i <= end; i++) {\n      if (lines[i].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {\n        hasPdt = true;\n        break;\n      }\n    }\n    if (!hasPdt) {\n      lines.splice(start, 0, `#EXT-X-MY-PROGRAM-DATE-TIME:${new Date(Math.round(time)).toISOString()}`);\n    }\n    time += segment.duration * 1000;\n  };\n  const result = HLS.stringify(obj, {segmentProcessor});\n  t.is(result, utils.stripCommentsAndEmptyLines(expected));\n});\n\ntest('stringify.postProcess.segment.delete', t => {\n  const obj = HLS.parse(`\n  #EXTM3U\n  #EXT-X-VERSION:3\n  #EXT-X-TARGETDURATION:6\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z\n  #EXTINF:6.006,\n  http://media.example.com/01.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z\n  #EXTINF:6.006,\n  http://media.example.com/02.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z\n  #EXT-X-DATERANGE:ID=\"splice-6FFFFFF0\",START-DATE=\"2014-03-05T11:15:00.000Z\",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000\n  #EXTINF:6.006,\n  http://ads.example.com/ad-01.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z\n  #EXTINF:6.006,\n  http://ads.example.com/ad-02.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z\n  #EXT-X-DATERANGE:ID=\"splice-6FFFFFF0\",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000\n  #EXTINF:6.006,\n  http://media.example.com/03.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z\n  #EXTINF:3.003,\n  http://media.example.com/04.ts\n  `);\n  const expected = `\n  #EXTM3U\n  #EXT-X-VERSION:3\n  #EXT-X-TARGETDURATION:6\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z\n  #EXTINF:6.006,\n  http://media.example.com/01.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z\n  #EXTINF:6.006,\n  http://media.example.com/02.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z\n  #EXTINF:6.006,\n  http://ads.example.com/ad-01.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z\n  #EXTINF:6.006,\n  http://ads.example.com/ad-02.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z\n  #EXTINF:6.006,\n  http://media.example.com/03.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z\n  #EXTINF:3.003,\n  http://media.example.com/04.ts\n  `;\n  const segmentProcessor = (lines, start, end) => {\n    for (let i = start; i <= end; i++) {\n      const line = lines[i];\n      if (line.startsWith('#EXT-X-DATERANGE')) {\n        lines[i] = '';\n      }\n    }\n  };\n  const result = HLS.stringify(obj, {segmentProcessor});\n  t.is(result, utils.stripCommentsAndEmptyLines(expected));\n});\n\ntest('stringify.postProcess.segment.update', t => {\n  const obj = HLS.parse(`\n  #EXTM3U\n  #EXT-X-VERSION:3\n  #EXT-X-TARGETDURATION:6\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z\n  #EXTINF:6.006,\n  http://media.example.com/01.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z\n  #EXTINF:6.006,\n  http://media.example.com/02.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z\n  #EXT-X-DATERANGE:ID=\"splice-6FFFFFF0\",START-DATE=\"2014-03-05T11:15:00.000Z\",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000\n  #EXTINF:6.006,\n  http://ads.example.com/ad-01.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z\n  #EXTINF:6.006,\n  http://ads.example.com/ad-02.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z\n  #EXT-X-DATERANGE:ID=\"splice-6FFFFFF0\",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000\n  #EXTINF:6.006,\n  http://media.example.com/03.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z\n  #EXTINF:3.003,\n  http://media.example.com/04.ts\n  `);\n  const expected = `\n  #EXTM3U\n  #EXT-X-VERSION:3\n  #EXT-X-TARGETDURATION:6\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z\n  #EXTINF:6.006,\n  http://media.example.com/01.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z\n  #EXTINF:6.006,\n  http://media.example.com/02.ts\n  <b>#EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z\n  #EXT-X-DATERANGE:ID=\"splice-6FFFFFF0\",START-DATE=\"2014-03-05T11:15:00.000Z\",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000\n  #EXTINF:6.006,\n  http://ads.example.com/ad-01.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z\n  #EXTINF:6.006,\n  http://ads.example.com/ad-02.ts</b>\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z\n  #EXT-X-DATERANGE:ID=\"splice-6FFFFFF0\",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000\n  #EXTINF:6.006,\n  http://media.example.com/03.ts\n  #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z\n  #EXTINF:3.003,\n  http://media.example.com/04.ts\n  `;\n  const segmentProcessor = (lines, start, end) => {\n    for (let i = start; i <= end; i++) {\n      const line = lines[i];\n      if (line.startsWith('#EXT-X-DATERANGE')) {\n        if (line.includes('PLANNED-DURATION')) {\n          lines[start] = `<b>${lines[start]}`;\n        } else if (start > 0) {\n          lines[start - 1] = `${lines[start - 1]}</b>`;\n        }\n      }\n    }\n  };\n  const result = HLS.stringify(obj, {segmentProcessor});\n  t.is(result, utils.stripCommentsAndEmptyLines(expected));\n});\n\ntest('stringify.postProcess.variant.update', t => {\n  const obj = HLS.parse(`\n  #EXTM3U\n  #EXT-X-STREAM-INF:BANDWIDTH=1280000\n  http://example.com/low.m3u8\n  #EXT-X-STREAM-INF:BANDWIDTH=2560000\n  http://example.com/mid.m3u8\n  #EXT-X-STREAM-INF:BANDWIDTH=7680000\n  http://example.com/hi.m3u8\n`);\n  const expected = `\n  #EXTM3U\n  #EXT-X-STREAM-INF:BANDWIDTH=1280000,MY-RESOLUTION=1280x720\n  http://example.com/low.m3u8\n  #EXT-X-STREAM-INF:BANDWIDTH=2560000,MY-RESOLUTION=1920x1080\n  http://example.com/mid.m3u8\n  #EXT-X-STREAM-INF:BANDWIDTH=7680000,MY-RESOLUTION=3840x2160\n  http://example.com/hi.m3u8\n`;\n  const variantProcessor = (lines, start, end, {bandwidth}) => {\n    for (let i = start; i <= end; i++) {\n      const line = lines[i];\n      if (line.startsWith('#EXT-X-STREAM-INF')) {\n        let resolution = '640x360';\n        if (bandwidth >= 1000000 && bandwidth < 2000000) {\n          resolution = '1280x720';\n        } else if (bandwidth >= 2000000 && bandwidth < 3000000) {\n          resolution = '1920x1080';\n        } else if (bandwidth >= 3000000) {\n          resolution = '3840x2160';\n        }\n        lines[i] = `${line},MY-RESOLUTION=${resolution}`;\n      }\n    }\n  };\n  const result = HLS.stringify(obj, {variantProcessor});\n  t.is(result, utils.stripCommentsAndEmptyLines(expected));\n});\n"
  },
  {
    "path": "test/spec/utils.spec.js",
    "content": "const test = require('ava');\nconst rewire = require('rewire');\nconst utils = require('../../utils');\n\nutils.setOptions({strictMode: true});\n\ntest('utils.THROW', t => {\n  try {\n    utils.THROW(new Error('abc'));\n  } catch (err) {\n    t.truthy(err);\n    t.is(err.message, 'abc');\n  }\n});\n\ntest('utils.ASSERT', t => {\n  utils.ASSERT('No error occurred', 1, 2, 3);\n  try {\n    utils.ASSERT('Error occurred', 1, 2, false);\n  } catch (err) {\n    t.truthy(err);\n    t.is(err.message, 'Error occurred : Failed at [2]');\n  }\n});\n\ntest('utils.CONDITIONALASSERT', t => {\n  utils.CONDITIONALASSERT([true, 1], [true, 2], [true, 3]);\n  utils.CONDITIONALASSERT([false, 0], [false, 1], [false, 2]);\n  try {\n    utils.CONDITIONALASSERT([false, 0], [true, 1], [true, 0]);\n  } catch (err) {\n    t.truthy(err);\n    t.is(err.message, 'Conditional Assert : Failed at [2]');\n  }\n});\n\ntest('utils.PARAMCHECK', t => {\n  utils.PARAMCHECK(1, 2, 3);\n  try {\n    utils.PARAMCHECK(1, 2, undefined);\n  } catch (err) {\n    t.truthy(err);\n    t.is(err.message, 'Param Check : Failed at [2]');\n  }\n});\n\ntest('utils.CONDITIONALPARAMCHECK', t => {\n  utils.CONDITIONALPARAMCHECK([true, 1], [true, 2], [true, 3]);\n  utils.CONDITIONALPARAMCHECK([false, undefined], [false, 1], [false, 2]);\n  try {\n    utils.CONDITIONALPARAMCHECK([false, undefined], [true, 1], [true, undefined]);\n  } catch (err) {\n    t.truthy(err);\n    t.is(err.message, 'Conditional Param Check : Failed at [2]');\n  }\n});\n\ntest('utils.toNumber', t => {\n  t.is(utils.toNumber('123'), 123);\n  t.is(utils.toNumber(123), 123);\n  t.is(utils.toNumber('abc'), 0);\n  t.is(utils.toNumber('8bc'), 8);\n});\n\ntest('utils.hexToByteSequence', t => {\n  t.deepEqual(utils.hexToByteSequence('0x000000'), new Uint8Array([0, 0, 0]));\n  t.deepEqual(utils.hexToByteSequence('0xFFFFFF'), new Uint8Array([255, 255, 255]));\n  t.deepEqual(utils.hexToByteSequence('FFFFFF'), new Uint8Array([255, 255, 255]));\n});\n\ntest('utils.byteSequenceToHex', t => {\n  t.is(utils.byteSequenceToHex(new Uint8Array([0, 0, 0])), '0x000000');\n  t.is(utils.byteSequenceToHex(new Uint8Array([255, 255, 255])), '0xFFFFFF');\n  t.is(utils.byteSequenceToHex(new Uint8Array([255, 255, 256])), '0xFFFF00');\n});\n\ntest('utils.tryCatch', t => {\n  let result = utils.tryCatch(\n    () => {\n      return 1;\n    },\n    () => {\n      return 0;\n    }\n  );\n  t.is(result, 1);\n  result = utils.tryCatch(\n    () => {\n      return JSON.parse('{{');\n    },\n    () => {\n      return 0;\n    }\n  );\n  t.is(result, 0);\n  t.throws(() => {\n    utils.tryCatch(\n      () => {\n        return JSON.parse('{{');\n      },\n      () => {\n        return JSON.parse('}}');\n      }\n    );\n  });\n});\n\ntest('utils.splitAt', t => {\n  t.deepEqual(utils.splitAt('a=1', '='), ['a', '1']);\n  t.deepEqual(utils.splitAt('a=1=2', '='), ['a', '1=2']);\n  t.deepEqual(utils.splitAt('a=1=2=3', '='), ['a', '1=2=3']);\n  t.deepEqual(utils.splitAt('a=1=2=3', '=', 0), ['a', '1=2=3']);\n  t.deepEqual(utils.splitAt('a=1=2=3', '=', 1), ['a=1', '2=3']);\n  t.deepEqual(utils.splitAt('a=1=2=3', '=', 2), ['a=1=2', '3']);\n  t.deepEqual(utils.splitAt('a=1=2=3', '=', -1), ['a=1=2', '3']);\n});\n\ntest('utils.trim', t => {\n  t.is(utils.trim(' abc '), 'abc');\n  t.is(utils.trim(' abc ', ' '), 'abc');\n  t.is(utils.trim('\"abc\"', '\"'), 'abc');\n  t.is(utils.trim('abc:', ':'), 'abc');\n  t.is(utils.trim('abc'), 'abc');\n  t.is(utils.trim(' \"abc\" ', '\"'), 'abc');\n});\n\ntest('utils.splitWithPreservingQuotes', t => {\n  t.deepEqual(utils.splitByCommaWithPreservingQuotes('abc=123, def=\"4,5,6\", ghi=78=9, jkl=\"abc\\'123\\'def\"'), ['abc=123', 'def=\"4,5,6\"', 'ghi=78=9', 'jkl=\"abc\\'123\\'def\"']);\n});\n\ntest('utls.camelify', t => {\n  const props = ['caption', 'Caption', 'captioN', 'CAPTION', 'closed-captions', 'closed_captions', 'CLOSED-CAPTIONS'];\n  const results = ['caption', 'caption', 'caption', 'caption', 'closedCaptions', 'closedCaptions', 'closedCaptions'];\n  t.deepEqual(props.map(p => utils.camelify(p)), results);\n});\n\ntest('utils.formatDate', t => {\n  const DATE = '2014-03-05T11:15:00.000Z';\n  t.is(utils.formatDate(new Date(DATE)), DATE);\n  const LOCALDATE = '2000-01-01T08:59:59.999+09:00';\n  const UTC = '1999-12-31T23:59:59.999Z';\n  t.is(utils.formatDate(new Date(LOCALDATE)), UTC);\n});\n\ntest('utils.setOptions/getOptions', t => {\n  const params = {a: 1, b: 'b', c: [1, 2, 3], strictMode: true};\n  utils.setOptions(params);\n  t.deepEqual(params, utils.getOptions());\n  params.strictMode = false;\n  t.notDeepEqual(params, utils.getOptions());\n  t.is(utils.getOptions().strictMode, true);\n});\n\ntest('utils.THROW.strictMode', t => {\n  const message = 'Error Message';\n  utils.setOptions({strictMode: false});\n  try {\n    utils.THROW({message});\n  } catch {\n    t.fail();\n  }\n  utils.setOptions({strictMode: true});\n  try {\n    utils.THROW({message});\n    t.fail();\n  } catch (e) {\n    t.is(e.message, message);\n  }\n  t.pass();\n});\n\ntest('utils.THROW.silent', t => {\n  let silent = false;\n  const utils = rewire('../../utils');\n  const errorHandler = msg => {\n    if (silent) {\n      t.is(msg, 'end');\n    } else {\n      t.is(msg, message);\n    }\n  };\n  utils.__set__({\n    console: {\n      error: errorHandler,\n      log: console.log\n    }\n  });\n  const message = 'Error Message';\n  utils.setOptions({strictMode: false});\n  utils.THROW({message});\n  silent = true;\n  utils.setOptions({silent});\n  utils.THROW({message});\n  console.error('end');\n  utils.setOptions({strictMode: true});\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@tsconfig/node18/tsconfig.json\",\n  \"module\": \"node\",\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"declaration\": true,\n    \"noImplicitAny\": false\n  },\n  \"exclude\": [\n    \"webpack.config.js\",\n    \"dist\",\n    \"test\"\n  ]\n}\n"
  },
  {
    "path": "types.ts",
    "content": "import * as utils from './utils';\n\ntype RenditionType = 'AUDIO' | 'VIDEO' | 'SUBTITLES' | 'CLOSED-CAPTIONS';\n\nclass Rendition {\n  type: RenditionType;\n  uri?: string;\n  groupId: string;\n  language?: string;\n  assocLanguage?: string;\n  name: string;\n  isDefault: boolean;\n  autoselect: boolean;\n  forced: boolean;\n  instreamId?: string;\n  characteristics?: string;\n  channels?: string;\n  pathwayId?: string;\n\n  constructor({\n    type, // required\n    uri, // required if type='SUBTITLES'\n    groupId, // required\n    language,\n    assocLanguage,\n    name, // required\n    isDefault,\n    autoselect,\n    forced,\n    instreamId, // required if type=CLOSED-CAPTIONS\n    characteristics,\n    channels,\n    pathwayId\n  }: Rendition) {\n    utils.PARAMCHECK(type, groupId, name);\n    utils.CONDITIONALASSERT([type === 'SUBTITLES', uri], [type === 'CLOSED-CAPTIONS', instreamId], [type === 'CLOSED-CAPTIONS', !uri], [forced, type === 'SUBTITLES']);\n    this.type = type;\n    this.uri = uri;\n    this.groupId = groupId;\n    this.language = language;\n    this.assocLanguage = assocLanguage;\n    this.name = name;\n    this.isDefault = isDefault;\n    this.autoselect = autoselect;\n    this.forced = forced;\n    this.instreamId = instreamId;\n    this.characteristics = characteristics;\n    this.channels = channels;\n    this.pathwayId = pathwayId;\n  }\n}\n\nclass Variant {\n  uri: string;\n  isIFrameOnly?: boolean;\n  bandwidth: number;\n  averageBandwidth?: number;\n  score: number;\n  codecs?: string;\n  resolution?: Resolution;\n  frameRate?: number;\n  hdcpLevel?: string;\n  allowedCpc: AllowedCpc[];\n  videoRange: 'SDR' | 'HLG' | 'PQ';\n  stableVariantId: string;\n  pathwayId: string;\n  programId: any;\n  audio: (Rendition & {type: 'AUDIO'})[];\n  video: (Rendition & {type: 'VIDEO'})[];\n  subtitles: (Rendition & {type: 'SUBTITLES'})[];\n  closedCaptions: (Rendition & {type: 'CLOSED-CAPTIONS'})[];\n  currentRenditions: { audio: number; video: number; subtitles: number; closedCaptions: number; };\n\n  constructor({\n    uri, // required\n    isIFrameOnly = false,\n    bandwidth, // required\n    averageBandwidth,\n    score,\n    codecs, // required?\n    resolution,\n    frameRate,\n    hdcpLevel,\n    allowedCpc,\n    videoRange,\n    stableVariantId,\n    pathwayId,\n    programId,\n    audio = [],\n    video = [],\n    subtitles = [],\n    closedCaptions = [],\n    currentRenditions = {audio: 0, video: 0, subtitles: 0, closedCaptions: 0}\n  }: any) {\n    // utils.PARAMCHECK(uri, bandwidth, codecs);\n    utils.PARAMCHECK(uri, bandwidth); // the spec states that CODECS is required but not true in the real world\n    this.uri = uri;\n    this.isIFrameOnly = isIFrameOnly;\n    this.bandwidth = bandwidth;\n    this.averageBandwidth = averageBandwidth;\n    this.score = score;\n    this.codecs = codecs;\n    this.resolution = resolution;\n    this.frameRate = frameRate;\n    this.hdcpLevel = hdcpLevel;\n    this.allowedCpc = allowedCpc;\n    this.videoRange = videoRange;\n    this.stableVariantId = stableVariantId;\n    this.pathwayId = pathwayId;\n    this.programId = programId;\n    this.audio = audio;\n    this.video = video;\n    this.subtitles = subtitles;\n    this.closedCaptions = closedCaptions;\n    this.currentRenditions = currentRenditions;\n  }\n}\n\nclass SessionData {\n  id: string;\n  value?: string;\n  uri?: string;\n  language?: string;\n\n  constructor({\n    id, // required\n    value,\n    uri,\n    language\n  }: SessionData) {\n    utils.PARAMCHECK(id, value || uri);\n    utils.ASSERT('SessionData cannot have both value and uri, shoud be either.', !(value && uri));\n    this.id = id;\n    this.value = value;\n    this.uri = uri;\n    this.language = language;\n  }\n}\n\nclass Key {\n  method: string;\n  uri?: string;\n  iv?: ArrayBuffer;\n  format?: string;\n  formatVersion?: string;\n\n  constructor({\n    method, // required\n    uri, // required unless method=NONE\n    iv,\n    format,\n    formatVersion\n  }: Key) {\n    utils.PARAMCHECK(method);\n    utils.CONDITIONALPARAMCHECK([method !== 'NONE', uri]);\n    utils.CONDITIONALASSERT([method === 'NONE', !(uri || iv || format || formatVersion)]);\n    this.method = method;\n    this.uri = uri;\n    this.iv = iv;\n    this.format = format;\n    this.formatVersion = formatVersion;\n  }\n}\n\nclass ContentSteering {\n  serverUri: string;\n  pathwayId: string;\n\n  constructor({\n    serverUri,\n    pathwayId\n  }: ContentSteering) {\n    this.serverUri = serverUri;\n    this.pathwayId = pathwayId;\n  }\n}\n\nexport type Byterange = {\n  length: number;\n  offset: number;\n};\n\nclass MediaInitializationSection {\n  hint: boolean;\n  uri: string;\n  mimeType?: string;\n  byterange?: Byterange;\n\n  constructor({\n    hint = false,\n    uri, // required\n    mimeType,\n    byterange\n  }: Partial<MediaInitializationSection> & {uri: string}) {\n    utils.PARAMCHECK(uri);\n    this.hint = hint;\n    this.uri = uri;\n    this.mimeType = mimeType;\n    this.byterange = byterange;\n  }\n}\n\nclass DateRange {\n  id: string;\n  classId?: string;\n  start?: Date;\n  cue?: string;\n  end?: Date;\n  duration?: number;\n  plannedDuration?: number;\n  endOnNext?: boolean;\n  attributes: Record<string, any>;\n\n  constructor({\n    id, // required\n    classId, // required if endOnNext is true\n    start,\n    cue,\n    end,\n    duration,\n    plannedDuration,\n    endOnNext,\n    attributes = {}\n  }: DateRange) {\n    utils.PARAMCHECK(id);\n    utils.CONDITIONALPARAMCHECK([endOnNext === true, classId]);\n    utils.CONDITIONALASSERT([end, start], [end, start! <= end!], [duration, duration! >= 0], [plannedDuration, plannedDuration! >= 0]);\n    this.id = id;\n    this.classId = classId;\n    this.start = start;\n    this.cue = cue;\n    this.end = end;\n    this.duration = duration;\n    this.plannedDuration = plannedDuration;\n    this.endOnNext = endOnNext;\n    this.attributes = attributes;\n  }\n}\n\nclass SpliceInfo {\n  type: string;\n  duration?: number;\n  tagName?: string;\n  value?: string;\n\n  constructor({\n    type, // required\n    duration, // required if the type is 'OUT'\n    tagName, // required if the type is 'RAW'\n    value\n  }: SpliceInfo) {\n    utils.PARAMCHECK(type);\n    utils.CONDITIONALPARAMCHECK([type === 'OUT', duration]);\n    utils.CONDITIONALPARAMCHECK([type === 'RAW', tagName]);\n    this.type = type;\n    this.duration = duration;\n    this.tagName = tagName;\n    this.value = value;\n  }\n}\n\ntype DataType = 'part' | 'playlist' | 'prefetch' | 'segment';\n\nclass Data {\n  type: DataType;\n\n  constructor(type: DataType) {\n    utils.PARAMCHECK(type);\n    this.type = type;\n  }\n}\n\nclass Playlist extends Data {\n  isMasterPlaylist: boolean;\n  uri?: string;\n  version?: number;\n  independentSegments: boolean;\n  start?: { offset: number; precise: boolean };\n  source?: string;\n  defines?: Record<string, string>[];\n\n  constructor({\n    isMasterPlaylist, // required\n    uri,\n    version,\n    independentSegments = false,\n    start,\n    source,\n    defines,\n  }: Partial<Playlist> & { isMasterPlaylist: boolean }) {\n    super('playlist');\n    utils.PARAMCHECK(isMasterPlaylist);\n    this.isMasterPlaylist = isMasterPlaylist;\n    this.uri = uri;\n    this.version = version;\n    this.independentSegments = independentSegments;\n    this.start = start;\n    this.source = source;\n    this.defines = defines;\n  }\n}\n\nclass MasterPlaylist extends Playlist {\n  declare isMasterPlaylist: true;\n  variants: Variant[];\n  currentVariant?: number;\n  sessionDataList: SessionData[];\n  sessionKeyList: Key[];\n  contentSteering?: ContentSteering;\n\n  constructor(params: Partial<MasterPlaylist> = {}) {\n    super({...params, isMasterPlaylist: true});\n    const {\n      variants = [],\n      currentVariant,\n      sessionDataList = [],\n      sessionKeyList = [],\n      contentSteering = undefined\n    } = params;\n    this.variants = variants;\n    this.currentVariant = currentVariant;\n    this.sessionDataList = sessionDataList;\n    this.sessionKeyList = sessionKeyList;\n    this.contentSteering = contentSteering;\n  }\n}\n\ntype LowLatencyCompatibility = {\n  canBlockReload: boolean,\n  canSkipUntil: number,\n  holdBack: number,\n  partHoldBack: number,\n};\n\nclass MediaPlaylist extends Playlist {\n  declare isMasterPlaylist: false;\n  targetDuration: number;\n  mediaSequenceBase?: number;\n  discontinuitySequenceBase?: number;\n  endlist: boolean;\n  playlistType?: 'EVENT' | 'VOD';\n  isIFrame?: boolean;\n  dateRanges: DateRange[];\n  segments: Segment[];\n  prefetchSegments: PrefetchSegment[];\n  lowLatencyCompatibility?: LowLatencyCompatibility;\n  partTargetDuration?: number;\n  renditionReports: RenditionReport[];\n  skip: number;\n  hash?: Record<string, any>;\n\n  constructor(params: Partial<MediaPlaylist> = {}) {\n    super({...params, isMasterPlaylist: false});\n    const {\n      targetDuration,\n      mediaSequenceBase = 0,\n      discontinuitySequenceBase = 0,\n      endlist = false,\n      playlistType,\n      isIFrame,\n      dateRanges = [],\n      segments = [],\n      prefetchSegments = [],\n      lowLatencyCompatibility,\n      partTargetDuration,\n      renditionReports = [],\n      skip = 0,\n      hash\n    } = params;\n    this.targetDuration = targetDuration!;\n    this.mediaSequenceBase = mediaSequenceBase;\n    this.discontinuitySequenceBase = discontinuitySequenceBase;\n    this.endlist = endlist;\n    this.playlistType = playlistType;\n    this.isIFrame = isIFrame;\n    this.dateRanges = dateRanges;\n    this.segments = segments;\n    this.prefetchSegments = prefetchSegments;\n    this.lowLatencyCompatibility = lowLatencyCompatibility;\n    this.partTargetDuration = partTargetDuration;\n    this.renditionReports = renditionReports;\n    this.skip = skip;\n    this.hash = hash;\n  }\n}\n\nclass Segment extends Data {\n  uri: string;\n  mimeType: string;\n  data: any;\n  duration: number;\n  title?: string;\n  byterange: Byterange;\n  discontinuity?: boolean;\n  mediaSequenceNumber: number;\n  discontinuitySequence: number;\n  key?: Key;\n  map: MediaInitializationSection;\n  programDateTime?: Date;\n  dateRange?: DateRange;\n  markers: SpliceInfo[];\n  parts: PartialSegment[];\n  gap?: boolean;\n\n  constructor({\n    uri,\n    mimeType,\n    data,\n    duration,\n    title,\n    byterange,\n    discontinuity,\n    mediaSequenceNumber = 0,\n    discontinuitySequence = 0,\n    key,\n    map,\n    programDateTime,\n    dateRange,\n    markers = [],\n    parts = [],\n    gap\n  }: any) {\n    super('segment');\n    // utils.PARAMCHECK(uri, mediaSequenceNumber, discontinuitySequence);\n    this.uri = uri;\n    this.mimeType = mimeType;\n    this.data = data;\n    this.duration = duration;\n    this.title = title;\n    this.byterange = byterange;\n    this.discontinuity = discontinuity;\n    this.mediaSequenceNumber = mediaSequenceNumber;\n    this.discontinuitySequence = discontinuitySequence;\n    this.key = key;\n    this.map = map;\n    this.programDateTime = programDateTime;\n    this.dateRange = dateRange;\n    this.markers = markers;\n    this.parts = parts;\n    this.gap = gap;\n  }\n}\n\nclass PartialSegment extends Data {\n  hint: boolean;\n  uri: string;\n  duration?: number;\n  independent?: boolean;\n  byterange?: Byterange;\n  gap?: boolean;\n\n  constructor({\n    hint = false,\n    uri, // required\n    duration,\n    independent,\n    byterange,\n    gap\n  }: Omit<PartialSegment, 'type'>) {\n    super('part');\n    utils.PARAMCHECK(uri);\n    this.hint = hint;\n    this.uri = uri;\n    this.duration = duration;\n    this.independent = independent;\n    this.duration = duration;\n    this.byterange = byterange;\n    this.gap = gap;\n  }\n}\n\nclass PrefetchSegment extends Data {\n  uri: string;\n  discontinuity?: boolean;\n  mediaSequenceNumber: number;\n  discontinuitySequence: number;\n  key?: Key | null;\n\n  constructor({\n    uri, // required\n    discontinuity,\n    mediaSequenceNumber = 0,\n    discontinuitySequence = 0,\n    key\n  }: Omit<PrefetchSegment, 'type'>) {\n    super('prefetch');\n    utils.PARAMCHECK(uri);\n    this.uri = uri;\n    this.discontinuity = discontinuity;\n    this.mediaSequenceNumber = mediaSequenceNumber;\n    this.discontinuitySequence = discontinuitySequence;\n    this.key = key;\n  }\n}\n\nclass RenditionReport {\n  uri: string;\n  lastMSN?: number;\n  lastPart: number;\n\n  constructor({\n    uri, // required\n    lastMSN,\n    lastPart\n  }: RenditionReport) {\n    utils.PARAMCHECK(uri);\n    this.uri = uri;\n    this.lastMSN = lastMSN;\n    this.lastPart = lastPart;\n  }\n}\n\nexport {\n  Rendition,\n  Variant,\n  SessionData,\n  Key,\n  MediaInitializationSection,\n  DateRange,\n  SpliceInfo,\n  Playlist,\n  MasterPlaylist,\n  MediaPlaylist,\n  Segment,\n  PartialSegment,\n  PrefetchSegment,\n  RenditionReport,\n  ContentSteering\n};\n\nexport type AllowedCpc = {\n  format: string;\n  cpcList: string[];\n};\n\nexport type ExtInfo = {\n  duration: number;\n  title: string;\n};\n\nexport type Resolution = {\n  width: number;\n  height: number;\n};\n\nexport type TagParam =\n    | [ null, null ]\n    | [ number, null ]\n    | [ null, Record<string, any> ]\n    | [ ExtInfo, null ]\n    | [ Byterange, null ]\n    | [ Date, null ];\n\nexport type UserAttribute = number | string | Uint8Array;\n\nexport type PostProcess = {\n  segmentProcessor?: ((lines: string[], start: number, end: number, segment: Segment, i: number) => void);\n  variantProcessor?: ((lines: string[], start: number, end: number, variant: Variant, i: number) => void);\n};\n"
  },
  {
    "path": "utils.ts",
    "content": "\ntype Options = {\n  strictMode?: boolean,\n  allowClosedCaptionsNone?: boolean,\n  silent?: boolean\n};\n\nlet options: Options = {};\n\nfunction THROW(err: Error) {\n  if (!options.strictMode) {\n    if (!options.silent) {\n      console.error(err.message);\n    }\n    return;\n  }\n  throw err;\n}\n\nfunction ASSERT(msg: string, ...options: boolean[]) {\n  for (const [index, param] of options.entries()) {\n    if (!param) {\n      THROW(new Error(`${msg} : Failed at [${index}]`));\n    }\n  }\n}\n\nfunction CONDITIONALASSERT(...options) {\n  for (const [index, [cond, param]] of options.entries()) {\n    if (!cond) {\n      continue;\n    }\n    if (!param) {\n      THROW(new Error(`Conditional Assert : Failed at [${index}]`));\n    }\n  }\n}\n\nfunction PARAMCHECK(...options) {\n  for (const [index, param] of options.entries()) {\n    if (param === undefined) {\n      THROW(new Error(`Param Check : Failed at [${index}]`));\n    }\n  }\n}\n\nfunction CONDITIONALPARAMCHECK(...options) {\n  for (const [index, [cond, param]] of options.entries()) {\n    if (!cond) {\n      continue;\n    }\n    if (param === undefined) {\n      THROW(new Error(`Conditional Param Check : Failed at [${index}]`));\n    }\n  }\n}\n\nfunction INVALIDPLAYLIST(msg: string) {\n  THROW(new Error(`Invalid Playlist : ${msg}`));\n}\n\nfunction toNumber(str: string, radix = 10) {\n  if (typeof str === 'number') {\n    return str;\n  }\n  const num = radix === 10 ? Number.parseFloat(str) : Number.parseInt(str, radix);\n  if (Number.isNaN(num)) {\n    return 0;\n  }\n  return num;\n}\n\nfunction hexToByteSequence(str: string): Uint8Array {\n  if (str.startsWith('0x') || str.startsWith('0X')) {\n    str = str.slice(2);\n  }\n  const numArray = new Uint8Array(str.length / 2);\n  for (let i = 0; i < str.length; i += 2) {\n    numArray[i / 2] = Number.parseInt(str.slice(i, i + 2), 16);\n  }\n  return numArray;\n}\n\nfunction byteSequenceToHex(sequence: ArrayBuffer, start = 0, end = sequence.byteLength) {\n  if (end <= start) {\n    THROW(new Error(`end must be larger than start : start=${start}, end=${end}`));\n  }\n  const array: string[] = [];\n  for (let i = start; i < end; i++) {\n    array.push(`0${(sequence[i] & 0xFF).toString(16).toUpperCase()}`.slice(-2));\n  }\n  return `0x${array.join('')}`;\n}\n\nfunction tryCatch<T>(body: () => T, errorHandler: (err: unknown) => T): T {\n  try {\n    return body();\n  } catch (err) {\n    return errorHandler(err);\n  }\n}\n\nfunction splitAt(str: string, delimiter: string, index = 0): [string] | [string, string] {\n  let lastDelimiterPos = -1;\n  for (let i = 0, j = 0; i < str.length; i++) {\n    if (str[i] === delimiter) {\n      if (j++ === index) {\n        return [str.slice(0, i), str.slice(i + 1)];\n      }\n      lastDelimiterPos = i;\n    }\n  }\n  if (lastDelimiterPos !== -1) {\n    return [str.slice(0, lastDelimiterPos), str.slice(lastDelimiterPos + 1)];\n  }\n  return [str];\n}\n\nfunction trim(str: string | undefined, char = ' ') {\n  if (!str) {\n    return str;\n  }\n  str = str.trim();\n  if (char === ' ') {\n    return str;\n  }\n  if (str.startsWith(char)) {\n    str = str.slice(1);\n  }\n  if (str.endsWith(char)) {\n    str = str.slice(0, -1);\n  }\n  return str;\n}\n\nfunction splitByCommaWithPreservingQuotes(str: string) {\n  const list: string[] = [];\n  let doParse = true;\n  let start = 0;\n  const prevQuotes: string[] = [];\n  for (let i = 0; i < str.length; i++) {\n    const curr = str[i];\n    if (doParse && curr === ',') {\n      list.push(str.slice(start, i).trim());\n      start = i + 1;\n      continue;\n    }\n    if (curr === '\"' || curr === '\\'') {\n      if (doParse) {\n        prevQuotes.push(curr);\n        doParse = false;\n      } else if (curr === prevQuotes.at(-1)) {\n        prevQuotes.pop();\n        doParse = true;\n      } else {\n        prevQuotes.push(curr);\n      }\n    }\n  }\n  list.push(str.slice(start).trim());\n  return list;\n}\n\nfunction camelify(str: string) {\n  const array: string[] = [];\n  let nextUpper = false;\n  for (const ch of str) {\n    if (ch === '-' || ch === '_') {\n      nextUpper = true;\n      continue;\n    }\n    if (nextUpper) {\n      array.push(ch.toUpperCase());\n      nextUpper = false;\n      continue;\n    }\n    array.push(ch.toLowerCase());\n  }\n  return array.join('');\n}\n\nfunction formatDate(date: Date) {\n  const YYYY = date.getUTCFullYear();\n  const MM = ('0' + (date.getUTCMonth() + 1)).slice(-2);\n  const DD = ('0' + date.getUTCDate()).slice(-2);\n  const hh = ('0' + date.getUTCHours()).slice(-2);\n  const mm = ('0' + date.getUTCMinutes()).slice(-2);\n  const ss = ('0' + date.getUTCSeconds()).slice(-2);\n  const msc = ('00' + date.getUTCMilliseconds()).slice(-3);\n  return `${YYYY}-${MM}-${DD}T${hh}:${mm}:${ss}.${msc}Z`;\n}\n\nfunction hasOwnProp(obj: object, propName: string): boolean {\n  return Object.hasOwn(obj, propName);\n}\n\nfunction setOptions(newOptions: Partial<Options> = {}): void {\n  options = Object.assign(options, newOptions);\n}\n\nfunction getOptions(): Options {\n  return Object.assign({}, options);\n}\n\nexport {\n  THROW,\n  ASSERT,\n  CONDITIONALASSERT,\n  PARAMCHECK,\n  CONDITIONALPARAMCHECK,\n  INVALIDPLAYLIST,\n  toNumber,\n  hexToByteSequence,\n  byteSequenceToHex,\n  tryCatch,\n  splitAt,\n  trim,\n  splitByCommaWithPreservingQuotes,\n  camelify,\n  formatDate,\n  hasOwnProp,\n  setOptions,\n  getOptions\n};\n"
  }
]