Repository: kuu/hls-parser Branch: master Commit: 837d023c721a Files: 113 Total size: 331.3 KB Directory structure: gitextract_hu9vpdm4/ ├── .github/ │ └── workflows/ │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.ts ├── package.json ├── parse.ts ├── stringify.ts ├── test/ │ ├── fixtures/ │ │ ├── m3u8/ │ │ │ ├── 8.1-Simple-Media-Playlist.m3u8 │ │ │ ├── 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.m3u8 │ │ │ ├── 8.11-EXT-X-CUE-OUT-Media-Playlist.m3u8 │ │ │ ├── 8.2-Live-Media-Playlist_using-HTTPS.m3u8 │ │ │ ├── 8.3-Playlist-with-encrypted-Media-Segments.m3u8 │ │ │ ├── 8.4-Master-Playlist.m3u8 │ │ │ ├── 8.5-Master-Playlist-with-I-Frames.m3u8 │ │ │ ├── 8.6-Master-Playlist-with-Alternative-audio.m3u8 │ │ │ ├── 8.7-Master-Playlist-with-Alternative-video.m3u8 │ │ │ ├── 8.8-Session-Data-in-a-Master-Playlist.m3u8 │ │ │ ├── 8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.m3u8 │ │ │ ├── Low-Latency_Example-01_Low-Latency_HLS_Playlist.m3u8 │ │ │ ├── Low-Latency_Example-02_Playlist_Delta_Update.m3u8 │ │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-01.m3u8 │ │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-02.m3u8 │ │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-03.m3u8 │ │ │ ├── Multiple-rendition-groups.m3u8 │ │ │ ├── RedundantSegments.m3u8 │ │ │ ├── SCTE-35_01.m3u8 │ │ │ ├── SCTE-35_02.m3u8 │ │ │ ├── SCTE-35_03.m3u8 │ │ │ ├── SCTE-35_04.m3u8 │ │ │ ├── SCTE-35_05.m3u8 │ │ │ ├── SCTE-35_06.m3u8 │ │ │ ├── SCTE-35_07.m3u8 │ │ │ ├── Streaming-Examples_bipbop_16x9_variant.m3u8 │ │ │ └── Streaming-Examples_img_bipbop_adv_example_ts_master.m3u8 │ │ └── objects/ │ │ ├── 8.1-Simple-Media-Playlist.js │ │ ├── 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js │ │ ├── 8.11-EXT-X-CUE-OUT-Media-Playlist.js │ │ ├── 8.2-Live-Media-Playlist_using-HTTPS.js │ │ ├── 8.3-Playlist-with-encrypted-Media-Segments.js │ │ ├── 8.4-Master-Playlist.js │ │ ├── 8.5-Master-Playlist-with-I-Frames.js │ │ ├── 8.6-Master-Playlist-with-Alternative-audio.js │ │ ├── 8.7-Master-Playlist-with-Alternative-video.js │ │ ├── 8.8-Session-Data-in-a-Master-Playlist.js │ │ ├── 8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js │ │ ├── Low-Latency_Example-01_Low-Latency_HLS_Playlist.js │ │ ├── Low-Latency_Example-02_Playlist_Delta_Update.js │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-01.js │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-02.js │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-03.js │ │ ├── Multiple-rendition-groups.js │ │ ├── RedundantSegments.js │ │ ├── SCTE-35_01.js │ │ ├── SCTE-35_02.js │ │ ├── SCTE-35_03.js │ │ ├── SCTE-35_04.js │ │ ├── SCTE-35_05.js │ │ ├── SCTE-35_06.js │ │ ├── SCTE-35_07.js │ │ ├── Streaming-Examples_bipbop_16x9_variant.js │ │ └── Streaming-Examples_img_bipbop_adv_example_ts_master.js │ ├── helpers/ │ │ ├── fixtures.js │ │ ├── matchers.js │ │ └── utils.js │ └── spec/ │ ├── 4_Playlists/ │ │ └── 4.3_Playlist-Tags/ │ │ ├── 4.3.1_Basic-Tags/ │ │ │ ├── 4.3.1.1_EXTM3U.spec.js │ │ │ └── 4.3.1.2_EXT-X-VERSION.spec.js │ │ ├── 4.3.2_Media-Segment-Tags/ │ │ │ ├── 4.3.2.1_EXTINF.spec.js │ │ │ ├── 4.3.2.2_EXT-X-BYTERANGE.spec.js │ │ │ ├── 4.3.2.3_EXT-X-DISCONTINUITY.spec.js │ │ │ ├── 4.3.2.4_EXT-X-KEY.spec.js │ │ │ ├── 4.3.2.5_EXT-X-MAP.spec.js │ │ │ ├── 4.3.2.6_EXT-X-PROGRAM-DATE-TIME.spec.js │ │ │ ├── 4.3.2.7_EXT-X-DATERANGE.spec.js │ │ │ ├── 4.3.2_Media-Segment-Tags.spec.js │ │ │ └── 4.4.4.7_EXT-X-GAP.spec.js │ │ ├── 4.3.3_Media-Playlist-Tags/ │ │ │ ├── 4.3.3.1_EXT-X-TARGETDURATION.spec.js │ │ │ ├── 4.3.3.2_EXT-X-MEDIA-SEQUENCE.spec.js │ │ │ ├── 4.3.3.3_EXT-X-DISCONTINUITY-SEQUENCE.spec.js │ │ │ ├── 4.3.3.4_EXT-X-ENDLIST.spec.js │ │ │ ├── 4.3.3.5_EXT-X-PLAYLIST-TYPE.spec.js │ │ │ ├── 4.3.3.6_EXT-X-I-FRAMES-ONLY.spec.js │ │ │ ├── 4.3.3.7_EXT-X-CUE-OUT.spec.js │ │ │ └── 4.3.3_Media-Playlist-Tags.spec.js │ │ ├── 4.3.4_Master-Playlist-Tags/ │ │ │ ├── 4.3.4.1_EXT-X-MEDIA.spec.js │ │ │ ├── 4.3.4.2_EXT-X-STREAM-INF.spec.js │ │ │ ├── 4.3.4.2_EXT-X-STREAM-INF_2.spec.js │ │ │ ├── 4.3.4.3_EXT-X-I-FRAME-STREAM-INF.spec.js │ │ │ ├── 4.3.4.4_EXT-X-SESSION-DATA.spec.js │ │ │ ├── 4.3.4.5_EXT-X-SESSION-KEY.spec.js │ │ │ └── 4.3.4_Master-Playlist-Tags.spec.js │ │ └── 4.3.5_Media-or-Master-Playlist-Tags/ │ │ ├── 4.3.5.1_EXT-X-INDEPENDENT-SEGMENTS.spec.js │ │ └── 4.3.5.2_EXT-X-START.spec.js │ ├── 7_Protocol-version-compatibility/ │ │ └── 7_EXT-X-VERSION.spec.js │ ├── Apple-Low-Latency/ │ │ └── New_Media_Playlist_Tags_for_Low-Latency_HLS/ │ │ ├── 01_EXT-X-SERVER-CONTROL.spec.js │ │ ├── 02_EXT-X-PART-INF.spec.js │ │ ├── 03_EXT-X-PART.spec.js │ │ ├── 04_EXT-X-PRELOAD-HINT.spec.js │ │ ├── 05_EXT-X-RENDITION-REPORT.spec.js │ │ └── 06_EXT-X-SKIP.spec.js │ ├── Apple_HLS_Overview/ │ │ └── 02_Using_HLS.spec.js │ ├── HLSJS-LHLS/ │ │ ├── 01_EXT-X-PREFETCH.spec.js │ │ └── 02_EXT-X-PREFETCH-DISCONTINUITY.spec.js │ ├── misc/ │ │ ├── multiple-rendition-groups.js │ │ └── scte-35.spec.js │ ├── parser.spec.js │ ├── stringify.spec.js │ └── utils.spec.js ├── tsconfig.json ├── types.ts └── utils.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: push: branches: - main workflow_dispatch: {} concurrency: group: ${{ github.workflow }} cancel-in-progress: false permissions: id-token: write # Required for OIDC contents: read jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: lts/* registry-url: 'https://registry.npmjs.org' - run: npm ci - run: npm run build --if-present - run: npm test - run: npm publish ================================================ FILE: .github/workflows/tests.yml ================================================ name: HLS parser tests on: [ push ] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [ 'current', 'lts/*', 'lts/-1' ] steps: - uses: actions/checkout@v4 - name: Setup Node ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm test - run: npm run build ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Editors .idea/ .vscode/ # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Typescript intermediate files tsconfig.tsbuildinfo *.d.ts *.js !test/**/*.js # Dependency directories node_modules jspm_packages # Optional npm cache directory .npm # Optional REPL history .node_repl_history dist ================================================ FILE: .npmignore ================================================ # Logs logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules jspm_packages # Editors .idea/ .vscode/ # Optional npm cache directory .npm # Optional REPL history .node_repl_history test .github/ .node-version .travis.yml .npmignore data-structure.png tsconfig.tsbuildinfo webpack.config.js LICENSE *.ts !*.d.ts ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Kuu Miyazaki Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![HLS parser tests](https://github.com/kuu/hls-parser/actions/workflows/tests.yml/badge.svg)](https://github.com/kuu/hls-parser/actions/workflows/tests.yml) [![Coverage Status](https://coveralls.io/repos/github/kuu/hls-parser/badge.svg?branch=master)](https://coveralls.io/github/kuu/hls-parser?branch=master) [![Known Vulnerabilities](https://snyk.io/test/github/kuu/hls-parser/badge.svg)](https://snyk.io/test/github/kuu/hls-parser) [![npm Downloads](https://img.shields.io/npm/dw/hls-parser.svg?style=flat-square)](https://npmjs.com/hls-parser) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) # hls-parser Provides synchronous functions to read/write HLS playlists (conforms to [the HLS spec rev.23](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23), [the Apple Low-Latency Spec rev. 2020/02/05](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification), and [HLS.js's Low-Latency spec](https://github.com/video-dev/hlsjs-rfcs/blob/lhls-spec/proposals/0001-lhls.md)) ## Install [![NPM](https://nodei.co/npm/hls-parser.png?mini=true)](https://nodei.co/npm/hls-parser/) [![](https://data.jsdelivr.com/v1/package/npm/hls-parser/badge)](https://www.jsdelivr.com/package/npm/hls-parser?path=dist) ## Usage ```js import { parse, types, stringify } from 'hls-parser'; // Parse the playlist const playlist = parse(textData); // You can access the playlist as a JS object if (playlist.isMasterPlaylist) { // Master playlist } else { // Media playlist } // Create a new playlist const {MediaPlaylist, Segment} = types; const obj = new MediaPlaylist({ targetDuration: 9, playlistType: 'VOD', segments: [ new Segment({ uri: 'low/1.m3u8', duration: 9 }) ] }); // Convert the object into a text stringify(obj); /* #EXTM3U #EXT-X-TARGETDURATION:9 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:9, low/1.m3u8 */ ``` ## API ### `HLS.parse(str)` Converts a text playlist into a structured JS object #### params | Name | Type | Required | Default | Description | | ------- | ------ | -------- | ------- | ------------- | | str | string | Yes | N/A | A text data that conforms to [the HLS playlist spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.1) | #### return value An instance of either `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.) ### `HLS.stringify(obj, postProcess)` Converts a JS object into a plain text playlist #### params | Name | Type | Required | Default | Description | | ------- | ------ | -------- | ------- | ------------- | | obj | `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.) | Yes | N/A | An object returned by `HLS.parse()` or a manually created object | | postProcess | PostProcess | No | undefined | A function to be called for each segment or variant to manipulate the output. | ##### `PostProcess` | Property | Type | Required | Default | Description | | ---------------- | ------------- | -------- | ------- | ------------- | | `segmentProcessor` | (lines: string[], start: number, end: number, segment: Segment, i: number) => void | No | undefined | A function to manipulate the segment output. | | `variantProcessor` | (lines: string[], start: number, end: number, variant: Variant, i: number) => void | No | undefined | A function to manipulate the variant output. | #### return value A text data that conforms to [the HLS playlist spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.1) ### `HLS.setOptions(obj)` Updates the option values #### params | Name | Type | Required | Default | Description | | ------- | ------ | -------- | ------- | ------------- | | obj | Object | Yes | {} | An object holding option values which will be used to overwrite the internal option values. | ##### supported options | Name | Type | Default | Description | | ---------- | ------- | ------- | ------------- | | `strictMode` | boolean | false | If true, the function throws an error when `parse`/`stringify` failed. If false, the function just logs the error and continues to run.| | `allowClosedCaptionsNone` | boolean | false | If true, `CLOSED-CAPTIONS` attribute on the `EXT-X-STREAM-INF` tag will be set to the enumerated-string value NONE when there are no closed-captions. See [CLOSED-CAPTIONS](https://tools.ietf.org/html/rfc8216#section-4.3.4.2) | | `silent` | boolean | false | If true, `console.error` will be suppressed.| ### `HLS.getOptions()` Retrieves the current option values #### return value A cloned object containing the current option values ### `HLS.types` An object that holds all the classes described below. ## Data format This section describes the structure of the object returned by `parse()` method. ![data structure](./data-structure.png) ### `Data` | Property | Type | Required | Default | Description | | ---------------- | ------------- | -------- | ------- | ------------- | | `type` | string | Yes | N/A | Either `playlist` or `segment` or `part`} | ### `Playlist` (extends `Data`) | Property | Type | Required | Default | Description | | ---------------- | ------------- | -------- | ------- | ------------- | | `isMasterPlaylist` | boolean | Yes | N/A | `true` if this playlist is a master playlist | | `uri` | string | No | undefined | Playlist URL | | `version` | number | No | undefined | See [EXT-X-VERSION](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.1.2) | | `independentSegments` | boolean | No | false | See [EXT-X-INDEPENDENT-SEGMENTS](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.5.1) | | `start` | object({offset: number, precise: boolean}) | No | undefined | See [EXT-X-START](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.5.2) | | `source` | string | No | undefined | The unprocessed text of the playlist | ### `MasterPlaylist` (extends `Playlist`) | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `variants` | [`Variant`] | No | [] | See [ EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) and [EXT-X-I-FRAME-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.3) | | `currentVariant` | number | No | undefined | Array index that points to the chosen item in `variants` | | `sessionDataList` | [`SessionData`] | No | [] | See [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) | | `sessionKeyList` | [`Key`] | No | [] | See [EXT-X-SESSION-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.5) | ### `Variant` | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `uri` | string | Yes | N/A | URI of the variant playlist | | `isIFrameOnly` | boolean | No | undefined | `true` if the variant is an I-frame media playlist. See [EXT-X-I-FRAME-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.3) | | `bandwidth` | number | Yes | N/A | See BANDWIDTH attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `averageBandwidth` | number | No | undefined | See AVERAGE-BANDWIDTH attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `score` | number | No | undefined | See SCORE attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.6.2) | | `codecs` | string | No | undefined | See CODECS attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `resolution` | object ({width: number, height: number}) | No | undefined | See RESOLUTION attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `frameRate` | number | No | undefined | See FRAME-RATE attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `hdcpLevel` | string | No | undefined | See HDCP-LEVEL attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `allowedCpc` | [object ({format: string, cpcList: [string]})] | No | undefined | See ALLOWED-CPC attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.6.2) | | `videoRange` | string {"SDR","HLG","PQ"} | No | undefined | See VIDEO-RANGE attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.6.2) | | `stableVariantId` | string | No | undefined | See STABLE-VARIANT-ID attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-4.4.6.2) | | `audio` | [`Rendition`(type='AUDIO')] | No | [] | See AUDIO attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `video` | [`Rendition`(type='VIDEO')] | No | [] | See VIDEO attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `subtitles` | [`Rendition`(type='SUBTITLES')] | No | [] | See SUBTITLES attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `closedCaptions` | [`Rendition`(type='CLOSED-CAPTIONS')] | No | [] | See CLOSED-CAPTIONS attribute in [EXT-X-STREAM-INF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.2) | | `currentRenditions` | object ({audio: number, video: number, subtitles: number, closedCaptions: number}) | No | {} | A hash object that contains array indices that points to the chosen `Rendition` for each type | ### `Rendition` | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `type` | string | Yes | N/A | See TYPE attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `uri` | string | No | undefined | See URI attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `groupId` | string | Yes | N/A | See GROUP-ID attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `language` | string | No | undefined | See LANGUAGE attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `assocLanguage` | string | No | undefined | See ASSOC-LANGUAGE attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `name` | string | Yes | N/A | See NAME attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `isDefault` | boolean | No | false | See DEFAULT attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `autoselect` | boolean | No | false | See AUTOSELECT attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `forced` | boolean | No | false | See FORCED attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `instreamId` | string | No | undefined | See INSTREAM-ID attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `characteristics` | string | No | undefined | See CHARACTERISTICS attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | | `channels` | string | No | undefined | See CHANNELS attribute in [EXT-X-MEDIA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.1) | ### `SessionData` | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `id` | string | Yes | N/A | See DATA-ID attribute in [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) | | `value` | string | No | undefined | See VALUE attribute in [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) | | `uri` | string | No | undefined | See URI attribute in [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) | | `language` | string | No | undefined | See LANGUAGE attribute in [EXT-X-SESSION-DATA](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.4.4) | ### `MediaPlaylist` (extends `Playlist`) | Property | Type | Required | Default | Description | | --------------------------- | -------- | -------- | --------- | ------------- | | `targetDuration` | number | Yes | N/A | See [EXT-X-TARGETDURATION](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1) | | `mediaSequenceBase` | number | No | 0 | See [EXT-X-MEDIA-SEQUENCE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.2) | | `discontinuitySequenceBase` | number | No | 0 | See [EXT-X-DISCONTINUITY-SEQUENCE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.3) | | `endlist` | boolean | No | false | See [EXT-X-ENDLIST](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.4) | | `playlistType` | string | No | undefined | See [EXT-X-PLAYLIST-TYPE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.5) | | `isIFrame` | boolean | No | undefined | See [EXT-X-I-FRAMES-ONLY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.6) | | `segments` | [`Segment`] | No | [] | A list of available segments | | `prefetchSegments` | [`PrefetchSegment`] | No | [] | A list of available prefetch segments | | `lowLatencyCompatibility` | object ({canBlockReload: boolean, canSkipUntil: number, holdBack: number, partHoldBack: number}) | No | undefined | See `CAN-BLOCK-RELOAD`, `CAN-SKIP-UNTIL`, `HOLD-BACK`, and `PART-HOLD-BACK` attributes in [EXT-X-SERVER-CONTROL](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3281374) | | `partTargetDuration` | number | No* | undefined | *Required if the playlist contains one or more `EXT-X-PART` tags. See [EXT-X-PART-INF](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282434) | | `renditionReports` | [`RenditionReport`] | No | [] | Update status of the associated renditions | | `skip` | number | No | 0 | See [EXT-X-SKIP](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282433) | ### `Segment` (extends `Data`) | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `uri` | string | Yes* | N/A | URI of the media segment. *Not required if the segment contains `EXT-X-PRELOAD-HINT` tag | | `duration` | number | Yes* | N/A | See [EXTINF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.1) *Not required if the segment contains `EXT-X-PRELOAD-HINT` tag | | `title` | string | No | undefined | See [EXTINF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.1) | | `byterange` | object ({length: number, offset: number}) | No | undefined | See [EXT-X-BYTERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2) | | `discontinuity` | boolean | No | undefined | See [EXT-X-DISCONTINUITY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.3) | | `mediaSequenceNumber` | number | No | 0 | See the description about 'Media Sequence Number' in [3. Media Segments](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#page-5) | | `discontinuitySequence` | number | No | 0 | See the description about 'Discontinuity Sequence Number' in [6.2.1. General Server Responsibilities](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.1) | | `key` | `Key` | No | undefined | See [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) | | `map` | `MediaInitializationSection` | No | undefined | See [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) | | `programDateTime` | `Date` | No | undefined | See [EXT-X-PROGRAM-DATE-TIME](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.6) | | `dateRange` | `DateRange` | No | undefined | See [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | | `markers` | [`SpliceInfo`] | No | [] | SCTE-35 messages associated with this segment| | `parts` | [`PartialSegment`] | No | [] | Partial Segments that constitute this segment | | `gap` | boolean | No | undefined | See [EXT-X-GAP](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.7) | ### `PartialSegment` (extends `Data`) | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `hint` | boolean | No | false | `true` indicates a hinted resource (`TYPE=PART`) See [EXT-X-PRELOAD-HINT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3526694) | | `uri` | string | Yes | N/A | See `URI` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) | | `duration` | number | Yes* | N/A | See `DURATION` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) *Not required if `hint` is `true`| | `independent` | boolean | No | undefined | See `INDEPENDENT` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) | | `byterange` | object ({length: number, offset: number}) | No | undefined | See `BYTERANGE` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) | | `gap` | boolean | No | undefined | See `GAP` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) | ### `PrefetchSegment` (extends `Data`) | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `uri` | string | Yes | N/A | See value of [EXT-X-PREFETCH](https://github.com/video-dev/hlsjs-rfcs/blob/lhls-spec/proposals/0001-lhls.md) | | `discontinuity` | boolean | No | undefined | See [EXT-X-PREFETCH-DISCONTINUITY](https://github.com/video-dev/hlsjs-rfcs/blob/lhls-spec/proposals/0001-lhls.md) | | `mediaSequenceNumber` | number | No | 0 | See the description about 'Media Sequence Number' in [3. Media Segments](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#page-5) | | `discontinuitySequence` | number | No | 0 | See the description about 'Discontinuity Sequence Number' in [6.2.1. General Server Responsibilities](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.1) | ### `Key` | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `method` | string | Yes | N/A | See METHOD attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) | | `uri` | string | No | undefined | See URI attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) | | `iv` | `ArrayBuffer`(length=16) | No | undefined | See IV attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) | | `format` | string | No | undefined | See KEYFORMAT attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) | | `formatVersion` | string | No | undefined | See KEYFORMATVERSIONS attribute in [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) | ### `MediaInitializationSection` | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `hint` | boolean | No | false | `true` indicates a hinted resource (`TYPE=MAP`) See [EXT-X-PRELOAD-HINT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3526694) | | `uri` | string | Yes | N/A | See URI attribute in [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) | | `byterange` | object ({length: number, offset: number}) | No | undefined | See BYTERANGE attribute in [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) | ### `DateRange` | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `id` | string | Yes | N/A | See ID attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | | `classId` | string | No | undefined | See CLASS attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | | `start` | `Date` | No | undefined | See START-DATE attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | | `end` | `Date` | No | undefined | See END-DATE attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | | `duration` | number | No | undefined | See DURATION attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | | `plannedDuration` | number | No | undefined | See PLANNED-DURATION attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | | `endOnNext` | boolean | No | undefined | See END-ON-NEXT attribute in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | | `attributes` | object | No | {} | A hash object that holds SCTE35 attributes and user defined attributes. See SCTE35-* and X- attributes in [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | ### `SpliceInfo` Only `EXT-X-CUE-OUT` and `EXT-X-CUE-IN` tags are supported. Other SCTE-35-related tags are stored as raw (string) values. | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `type` | string | Yes | N/A | {'OUT', 'IN', 'RAW'} | | `duration` | number | No | undefined | Required if the `type` is 'OUT' | | `tagName` | string | No | undefined | Holds the tag name if any unsupported tag are found. Required if the `type` is 'RAW' | | `value` | string | No | undefined | Holds a raw (string) value for the unsupported tag. | ### `RenditionReport` | Property | Type | Required | Default | Description | | ----------------- | -------- | -------- | --------- | ------------- | | `uri` | string | Yes | N/A | See `URI` attribute in [EXT-X-RENDITION-REPORT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282435) | | `lastMSN` | number | No | undefined | See `LAST-MSN` attribute in [EXT-X-RENDITION-REPORT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282435) | | `lastPart` | number | No | undefined | See `LAST-PART` attribute in [EXT-X-RENDITION-REPORT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282435) | ================================================ FILE: index.ts ================================================ /*! Copyright Kuu Miyazaki. SPDX-License-Identifier: MIT */ import { getOptions, setOptions } from './utils'; import parse from './parse'; import stringify from './stringify'; import * as types from './types'; export { parse, stringify, types, getOptions, setOptions }; ================================================ FILE: package.json ================================================ { "name": "hls-parser", "version": "0.16.1", "description": "A simple library to read/write HLS playlists", "main": "index.js", "types": "index.d.ts", "browser": "dist/hls-parser.min.js", "scripts": { "lint": "xo", "type-check": "tsc --noEmit", "audit": "npm audit --audit-level high", "build": "rm -fR ./dist; tsc ; webpack --mode development ; webpack --mode production", "test": "npm run lint && npm run build && npm run audit && ava --verbose", "test-offline": "npm run lint && npm run build && ava --verbose" }, "repository": { "type": "git", "url": "git+https://github.com/kuu/hls-parser.git" }, "keywords": [ "HLS", "media", "video", "audio", "streaming" ], "author": "Kuu Miyazaki", "license": "MIT", "bugs": { "url": "https://github.com/kuu/hls-parser/issues" }, "homepage": "https://github.com/kuu/hls-parser#readme", "devDependencies": { "@ava/typescript": "^6.0.0", "@babel/core": "^7.28.4", "@babel/eslint-parser": "^7.28.4", "@babel/preset-env": "^7.28.3", "@tsconfig/node18": "^18.2.4", "ava": "^6.4.1", "babel-loader": "^10.0.0", "eslint-plugin-unicorn": "^61.0.2", "rewire": "^9.0.1", "terser-webpack-plugin": "^5.3.14", "ts-loader": "^9.5.4", "typescript": "^5.9.3", "webpack": "^5.102.1", "webpack-cli": "^6.0.1", "xo": "^0.60.0" }, "ava": { "typescript": { "compile": "tsc", "extensions": [ "ts", "js" ], "rewritePaths": {} } }, "xo": { "esnext": true, "space": true, "rules": { "arrow-body-style": 0, "ava/no-ignored-test-files": 0, "camelcase": 0, "comma-dangle": 0, "capitalized-comments": 0, "dot-notation": 0, "guard-for-in": 0, "import/extensions": 0, "import/no-dynamic-require": 0, "new-cap": 0, "no-bitwise": 0, "no-cond-assign": 0, "no-mixed-operators": 0, "no-multi-assign": 0, "no-use-extend-native/no-use-extend-native": 0, "object-curly-newline": 0, "operator-linebreak": 0, "padding-line-between-statements": 0, "quotes": 0, "unicorn/catch-error-name": 0, "unicorn/filename-case": 0, "unicorn/no-lonely-if": 0, "unicorn/no-useless-spread": 0, "unicorn/no-zero-fractions": 0, "unicorn/numeric-separators-style": 0, "unicorn/prefer-code-point": 0, "unicorn/prefer-module": 0, "unicorn/prefer-switch": 0, "unicorn/prevent-abbreviations": 0, "unicorn/switch-case-braces": 0 }, "overrides": [ { "files": "test/**/*.js", "rules": { "unicorn/no-array-push-push": 0 } }, { "files": "*.ts", "rules": { "n/file-extension-in-import": 0, "@typescript-eslint/array-type": 1, "@typescript-eslint/ban-types": 1, "@typescript-eslint/comma-dangle": 0, "@typescript-eslint/consistent-type-imports": 0, "@typescript-eslint/dot-notation": 0, "@typescript-eslint/member-delimiter-style": 0, "@typescript-eslint/naming-convention": 0, "@typescript-eslint/no-unsafe-call": 0, "@typescript-eslint/no-unsafe-argument": 0, "@typescript-eslint/no-unsafe-assignment": 0, "@typescript-eslint/no-unsafe-return": 0, "@typescript-eslint/object-curly-spacing": 0, "@typescript-eslint/padding-line-between-statements": 0, "@typescript-eslint/prefer-optional-chain": 1, "@typescript-eslint/prefer-nullish-coalescing": 0, "@typescript-eslint/quotes": 0, "@typescript-eslint/restrict-template-expressions": 0, "@typescript-eslint/restrict-plus-operands": 0, "unicorn/prefer-export-from": 0 } } ], "settings": { "import/resolver": { "node": {} } } } } ================================================ FILE: parse.ts ================================================ import * as utils from './utils'; import { AllowedCpc, ExtInfo, Rendition, Resolution, TagParam, UserAttribute, Variant, SessionData, Key, MediaInitializationSection, Byterange, DateRange, SpliceInfo, MasterPlaylist, MediaPlaylist, Segment, PartialSegment, PrefetchSegment, RenditionReport, ContentSteering } from './types'; function unquote(str: string | undefined) { return utils.trim(str, '"'); } type TagCategory = 'Basic' | 'Segment' | 'MasterPlaylist' | 'MediaPlaylist' | 'MediaorMasterPlaylist' | 'Unknown'; function getTagCategory(tagName: string): TagCategory { switch (tagName) { case 'EXTM3U': case 'EXT-X-VERSION': case 'EXT-X-CONTENT-STEERING': return 'Basic'; case 'EXTINF': case 'EXT-X-BYTERANGE': case 'EXT-X-DISCONTINUITY': case 'EXT-X-PREFETCH-DISCONTINUITY': case 'EXT-X-KEY': case 'EXT-X-MAP': case 'EXT-X-PROGRAM-DATE-TIME': case 'EXT-X-DATERANGE': case 'EXT-X-CUE-OUT': case 'EXT-X-CUE-IN': case 'EXT-X-CUE-OUT-CONT': case 'EXT-X-CUE': case 'EXT-OATCLS-SCTE35': case 'EXT-X-ASSET': case 'EXT-X-SCTE35': case 'EXT-X-PART': case 'EXT-X-PRELOAD-HINT': case 'EXT-X-GAP': return 'Segment'; case 'EXT-X-TARGETDURATION': case 'EXT-X-MEDIA-SEQUENCE': case 'EXT-X-DISCONTINUITY-SEQUENCE': case 'EXT-X-ENDLIST': case 'EXT-X-PLAYLIST-TYPE': case 'EXT-X-I-FRAMES-ONLY': case 'EXT-X-SERVER-CONTROL': case 'EXT-X-PART-INF': case 'EXT-X-PREFETCH': case 'EXT-X-RENDITION-REPORT': case 'EXT-X-SKIP': return 'MediaPlaylist'; case 'EXT-X-MEDIA': case 'EXT-X-STREAM-INF': case 'EXT-X-I-FRAME-STREAM-INF': case 'EXT-X-SESSION-DATA': case 'EXT-X-SESSION-KEY': return 'MasterPlaylist'; case 'EXT-X-INDEPENDENT-SEGMENTS': case 'EXT-X-START': case 'EXT-X-DEFINE': return 'MediaorMasterPlaylist'; default: return 'Unknown'; } } function parseEXTINF(param: string): ExtInfo { const pair = utils.splitAt(param, ',') as [string, string]; return {duration: utils.toNumber(pair[0]), title: decodeURIComponent(escape(pair[1]))}; } function parseBYTERANGE(param: string): Byterange { const pair = utils.splitAt(param, '@'); return {length: utils.toNumber(pair[0]), offset: pair[1] ? utils.toNumber(pair[1]) : -1}; } function parseResolution(str: string): Resolution { const pair = utils.splitAt(str, 'x') as [string, string]; return {width: utils.toNumber(pair[0]), height: utils.toNumber(pair[1])}; } function parseAllowedCpc(str: string): AllowedCpc[] { const message = 'ALLOWED-CPC: Each entry must consit of KEYFORMAT and Content Protection Configuration'; const list = str.split(','); if (list.length === 0) { utils.INVALIDPLAYLIST(message); } const allowedCpcList: AllowedCpc[] = []; for (const item of list) { const [format, cpcText] = utils.splitAt(item, ':'); if (!format || !cpcText) { utils.INVALIDPLAYLIST(message); continue; } allowedCpcList.push({format, cpcList: cpcText.split('/')}); } return allowedCpcList; } function parseIV(str: string): Uint8Array { const iv = utils.hexToByteSequence(str); if (iv.length !== 16) { utils.INVALIDPLAYLIST('IV must be a 128-bit unsigned integer'); } return iv; } function parseUserAttribute(str: string): UserAttribute { if (str.startsWith('"')) { return unquote(str)!; } if (str.startsWith('0x') || str.startsWith('0X')) { return utils.hexToByteSequence(str); } return utils.toNumber(str); } function setCompatibleVersionOfKey(params: Record, attributes: Record) { if (attributes['IV'] && params.compatibleVersion < 2) { params.compatibleVersion = 2; } if ((attributes['KEYFORMAT'] || attributes['KEYFORMATVERSIONS']) && params.compatibleVersion < 5) { params.compatibleVersion = 5; } } function parseAttributeList(param): Record { const attributes = {}; for (const item of utils.splitByCommaWithPreservingQuotes(param)) { const [key, value] = utils.splitAt(item, '='); const val = unquote(value)!; switch (key) { case 'URI': attributes[key] = val; break; case 'START-DATE': case 'END-DATE': attributes[key] = new Date(val); break; case 'IV': attributes[key] = parseIV(val); break; case 'BYTERANGE': attributes[key] = parseBYTERANGE(val); break; case 'RESOLUTION': attributes[key] = parseResolution(val); break; case 'ALLOWED-CPC': attributes[key] = parseAllowedCpc(val); break; case 'END-ON-NEXT': case 'DEFAULT': case 'AUTOSELECT': case 'FORCED': case 'PRECISE': case 'CAN-BLOCK-RELOAD': case 'INDEPENDENT': case 'GAP': attributes[key] = val === 'YES'; break; case 'DURATION': case 'PLANNED-DURATION': case 'BANDWIDTH': case 'AVERAGE-BANDWIDTH': case 'FRAME-RATE': case 'TIME-OFFSET': case 'CAN-SKIP-UNTIL': case 'HOLD-BACK': case 'PART-HOLD-BACK': case 'PART-TARGET': case 'BYTERANGE-START': case 'BYTERANGE-LENGTH': case 'LAST-MSN': case 'LAST-PART': case 'SKIPPED-SEGMENTS': case 'SCORE': case 'PROGRAM-ID': attributes[key] = utils.toNumber(val); break; default: if (key.startsWith('SCTE35-')) { attributes[key] = utils.hexToByteSequence(val); } else if (key.startsWith('X-')) { attributes[key] = parseUserAttribute(value!); } else { if (key === 'VIDEO-RANGE' && val !== 'SDR' && val !== 'HLG' && val !== 'PQ') { utils.INVALIDPLAYLIST(`VIDEO-RANGE: unknown value "${val}"`); } attributes[key] = val; } } } return attributes; } function parseTagParam(name: string, param): TagParam { switch (name) { case 'EXTM3U': case 'EXT-X-DISCONTINUITY': case 'EXT-X-ENDLIST': case 'EXT-X-I-FRAMES-ONLY': case 'EXT-X-INDEPENDENT-SEGMENTS': case 'EXT-X-CUE-IN': case 'EXT-X-GAP': return [null, null]; case 'EXT-X-VERSION': case 'EXT-X-TARGETDURATION': case 'EXT-X-MEDIA-SEQUENCE': case 'EXT-X-DISCONTINUITY-SEQUENCE': return [utils.toNumber(param), null]; case 'EXT-X-CUE-OUT': // For backwards compatibility: attributes list is optional, // if only a number is found, use it as the duration if (!Number.isNaN(Number(param))) { return [utils.toNumber(param), null]; } // If attributes are found, parse them out (i.e. DURATION) return [null, parseAttributeList(param)]; case 'EXT-X-KEY': case 'EXT-X-MAP': case 'EXT-X-DATERANGE': case 'EXT-X-MEDIA': case 'EXT-X-STREAM-INF': case 'EXT-X-I-FRAME-STREAM-INF': case 'EXT-X-SESSION-DATA': case 'EXT-X-SESSION-KEY': case 'EXT-X-START': case 'EXT-X-SERVER-CONTROL': case 'EXT-X-PART-INF': case 'EXT-X-PART': case 'EXT-X-PRELOAD-HINT': case 'EXT-X-RENDITION-REPORT': case 'EXT-X-SKIP': case 'EXT-X-DEFINE': return [null, parseAttributeList(param)]; case 'EXTINF': return [parseEXTINF(param), null]; case 'EXT-X-BYTERANGE': return [parseBYTERANGE(param), null]; case 'EXT-X-PROGRAM-DATE-TIME': return [new Date(param), null]; case 'EXT-X-PLAYLIST-TYPE': return [param, null]; // default: return [param, null]; // Unknown tag } } function MIXEDTAGS() { utils.INVALIDPLAYLIST(`The file contains both media and master playlist tags.`); } function splitTag(line: string): [string, string | null] { const index = line.indexOf(':'); if (index === -1) { return [line.slice(1).trim(), null]; } return [line.slice(1, index).trim(), line.slice(index + 1).trim()]; } function parseRendition({attributes}: Tag): Rendition { const rendition = new Rendition({ type: attributes['TYPE'], uri: attributes['URI'], groupId: attributes['GROUP-ID'], language: attributes['LANGUAGE'], assocLanguage: attributes['ASSOC-LANGUAGE'], name: attributes['NAME'], isDefault: attributes['DEFAULT'], autoselect: attributes['AUTOSELECT'], forced: attributes['FORCED'], instreamId: attributes['INSTREAM-ID'], characteristics: attributes['CHARACTERISTICS'], channels: attributes['CHANNELS'], pathwayId: attributes['PATHWAY-ID'] }); return rendition; } function checkRedundantRendition(renditions, rendition): string { let defaultFound = false; for (const item of renditions) { if (item.name === rendition.name) { return 'All EXT-X-MEDIA tags in the same Group MUST have different NAME attributes.'; } if (item.isDefault) { defaultFound = true; } } if (defaultFound && rendition.isDefault) { return 'EXT-X-MEDIA A Group MUST NOT have more than one member with a DEFAULT attribute of YES.'; } return ''; } function addRendition(variant, line, type) { const rendition = parseRendition(line); const renditions = variant[utils.camelify(type)]; const errorMessage = checkRedundantRendition(renditions, rendition); if (errorMessage) { utils.INVALIDPLAYLIST(errorMessage); } renditions.push(rendition); if (rendition.isDefault) { variant.currentRenditions[utils.camelify(type)] = renditions.length - 1; } } function matchTypes(attrs, variant, params) { for (const type of ['AUDIO', 'VIDEO', 'SUBTITLES', 'CLOSED-CAPTIONS']) { if (type === 'CLOSED-CAPTIONS' && attrs[type] === 'NONE') { params.isClosedCaptionsNone = true; variant.closedCaptions = []; } else if (attrs[type] && !variant[utils.camelify(type)].some(item => item.groupId === attrs[type])) { utils.INVALIDPLAYLIST(`${type} attribute MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag whose TYPE attribute is ${type}.`); } } } function parseVariant(lines, variantAttrs, uri: string, iFrameOnly: boolean, params: Record): Variant { const variant = new Variant({ uri, bandwidth: variantAttrs['BANDWIDTH'], averageBandwidth: variantAttrs['AVERAGE-BANDWIDTH'], score: variantAttrs['SCORE'], codecs: variantAttrs['CODECS'], resolution: variantAttrs['RESOLUTION'], frameRate: variantAttrs['FRAME-RATE'], hdcpLevel: variantAttrs['HDCP-LEVEL'], allowedCpc: variantAttrs['ALLOWED-CPC'], videoRange: variantAttrs['VIDEO-RANGE'], stableVariantId: variantAttrs['STABLE-VARIANT-ID'], pathwayId: variantAttrs['STABLE-PATHWAY-ID'], programId: variantAttrs['PROGRAM-ID'] }); for (const line of lines) { if (line.name === 'EXT-X-MEDIA') { const renditionAttrs = line.attributes; const renditionType = renditionAttrs['TYPE']; if (!renditionType || !renditionAttrs['GROUP-ID']) { utils.INVALIDPLAYLIST('EXT-X-MEDIA TYPE attribute is REQUIRED.'); } if (variantAttrs[renditionType] === renditionAttrs['GROUP-ID']) { addRendition(variant, line, renditionType); if (renditionType === 'CLOSED-CAPTIONS') { for (const {instreamId} of variant.closedCaptions) { if (instreamId && instreamId.startsWith('SERVICE') && params.compatibleVersion < 7) { params.compatibleVersion = 7; break; } } } } } } matchTypes(variantAttrs, variant, params); variant.isIFrameOnly = iFrameOnly; return variant; } function sameKey(key1: Key, key2: Key): boolean { if (key1.method !== key2.method) { return false; } if (key1.uri !== key2.uri) { return false; } if (key1.iv) { if (!key2.iv) { return false; } if (key1.iv.byteLength !== key2.iv.byteLength) { return false; } for (let i = 0; i < key1.iv.byteLength; i++) { if (key1.iv[i] !== key2.iv[i]) { return false; } } } else if (key2.iv) { return false; } if (key1.format !== key2.format) { return false; } if (key1.formatVersion !== key2.formatVersion) { return false; } return true; } function parseMasterPlaylist(lines: Line[], params: Record): MasterPlaylist { const playlist = new MasterPlaylist(); let variantIsScored = false; for (const [index, line] of lines.entries()) { const {name, value, attributes} = mapTo(line); if (name === 'EXT-X-VERSION') { playlist.version = value; } else if (name === 'EXT-X-CONTENT-STEERING-SERVER') { const contentSteering = new ContentSteering({ serverUri: attributes['SERVER-URI'], pathwayId: attributes['PATHWAY-ID'] }); playlist.contentSteering = contentSteering; } else if (name === 'EXT-X-STREAM-INF') { const uri = lines[index + 1]; if (typeof uri !== 'string' || uri.startsWith('#EXT')) { utils.INVALIDPLAYLIST('EXT-X-STREAM-INF must be followed by a URI line'); } const variant = parseVariant(lines, attributes, uri as string, false, params); if (variant) { if (typeof variant.score === 'number') { variantIsScored = true; if (variant.score < 0) { utils.INVALIDPLAYLIST('SCORE attribute on EXT-X-STREAM-INF must be positive decimal-floating-point number.'); } } playlist.variants.push(variant); } } else if (name === 'EXT-X-I-FRAME-STREAM-INF') { const variant = parseVariant(lines, attributes, attributes.URI, true, params); if (variant) { playlist.variants.push(variant); } } else if (name === 'EXT-X-SESSION-DATA') { const sessionData = new SessionData({ id: attributes['DATA-ID'], value: attributes['VALUE'], uri: attributes['URI'], language: attributes['LANGUAGE'] }); if (playlist.sessionDataList.some(item => item.id === sessionData.id && item.language === sessionData.language)) { utils.INVALIDPLAYLIST('A Playlist MUST NOT contain more than one EXT-X-SESSION-DATA tag with the same DATA-ID attribute and the same LANGUAGE attribute.'); } playlist.sessionDataList.push(sessionData); } else if (name === 'EXT-X-SESSION-KEY') { if (attributes['METHOD'] === 'NONE') { utils.INVALIDPLAYLIST('EXT-X-SESSION-KEY: The value of the METHOD attribute MUST NOT be NONE'); } const sessionKey = new Key({ method: attributes['METHOD'], uri: attributes['URI'], iv: attributes['IV'], format: attributes['KEYFORMAT'], formatVersion: attributes['KEYFORMATVERSIONS'] }); if (playlist.sessionKeyList.some(item => sameKey(item, sessionKey))) { utils.INVALIDPLAYLIST('A Master Playlist MUST NOT contain more than one EXT-X-SESSION-KEY tag with the same METHOD, URI, IV, KEYFORMAT, and KEYFORMATVERSIONS attribute values.'); } setCompatibleVersionOfKey(params, attributes); playlist.sessionKeyList.push(sessionKey); } else if (name === 'EXT-X-INDEPENDENT-SEGMENTS') { if (playlist.independentSegments) { utils.INVALIDPLAYLIST('EXT-X-INDEPENDENT-SEGMENTS tag MUST NOT appear more than once in a Playlist'); } playlist.independentSegments = true; } else if (name === 'EXT-X-START') { if (playlist.start) { utils.INVALIDPLAYLIST('EXT-X-START tag MUST NOT appear more than once in a Playlist'); } if (typeof attributes['TIME-OFFSET'] !== 'number') { utils.INVALIDPLAYLIST('EXT-X-START: TIME-OFFSET attribute is REQUIRED'); } playlist.start = {offset: attributes['TIME-OFFSET'], precise: attributes['PRECISE'] || false}; } else if (name === 'EXT-X-DEFINE') { playlist.defines ||= []; playlist.defines.push(attributes); } } if (variantIsScored) { for (const variant of playlist.variants) { if (typeof variant.score !== 'number') { utils.INVALIDPLAYLIST('If any Variant Stream contains the SCORE attribute, then all Variant Streams in the Master Playlist SHOULD have a SCORE attribute'); } } } if (params.isClosedCaptionsNone) { for (const variant of playlist.variants) { if (variant.closedCaptions.length > 0) { utils.INVALIDPLAYLIST('If there is a variant with CLOSED-CAPTIONS attribute of NONE, all EXT-X-STREAM-INF tags MUST have this attribute with a value of NONE'); } } } return playlist; } function parseDateRange(attributes) { const attrs: Record = {}; for (const key of Object.keys(attributes)) { if (key.startsWith('SCTE35-') || key.startsWith('X-')) { attrs[key] = attributes[key]; } } const dateRange = new DateRange({ id: attributes['ID'], classId: attributes['CLASS'], start: attributes['START-DATE'], cue: attributes['CUE'], end: attributes['END-DATE'], duration: attributes['DURATION'], plannedDuration: attributes['PLANNED-DURATION'], endOnNext: attributes['END-ON-NEXT'], attributes: attrs }); return dateRange; } function parseSegment(lines: Line[], uri: string, start: number, end: number, mediaSequenceNumber: number, discontinuitySequence: number, params: Record): Segment { const segment = new Segment({uri, mediaSequenceNumber, discontinuitySequence}); let mapHint = false; let partHint = false; for (let i = start; i <= end; i++) { const {name, value, attributes} = mapTo(lines[i]); if (name === 'EXTINF') { if (!Number.isInteger(value.duration) && params.compatibleVersion < 3) { params.compatibleVersion = 3; } if (Math.round(value.duration) > params.targetDuration) { utils.INVALIDPLAYLIST('EXTINF duration, when rounded to the nearest integer, MUST be less than or equal to the target duration'); } segment.duration = value.duration; segment.title = value.title; } else if (name === 'EXT-X-BYTERANGE') { if (params.compatibleVersion < 4) { params.compatibleVersion = 4; } segment.byterange = value; } else if (name === 'EXT-X-DISCONTINUITY') { if (segment.parts.length > 0) { utils.INVALIDPLAYLIST('EXT-X-DISCONTINUITY must appear before the first EXT-X-PART tag of the Parent Segment.'); } segment.discontinuity = true; } else if (name === 'EXT-X-GAP') { if (params.compatibleVersion < 8) { params.compatibleVersion = 8; } segment.gap = true; } else if (name === 'EXT-X-KEY') { if (segment.parts.length > 0) { utils.INVALIDPLAYLIST('EXT-X-KEY must appear before the first EXT-X-PART tag of the Parent Segment.'); } setCompatibleVersionOfKey(params, attributes); segment.key = new Key({ method: attributes['METHOD'], uri: attributes['URI'], iv: attributes['IV'], format: attributes['KEYFORMAT'], formatVersion: attributes['KEYFORMATVERSIONS'] }); } else if (name === 'EXT-X-MAP') { if (segment.parts.length > 0) { utils.INVALIDPLAYLIST('EXT-X-MAP must appear before the first EXT-X-PART tag of the Parent Segment.'); } if (params.compatibleVersion < 5) { params.compatibleVersion = 5; } params.hasMap = true; segment.map = new MediaInitializationSection({ uri: attributes['URI'], byterange: attributes['BYTERANGE'] }); } else if (name === 'EXT-X-PROGRAM-DATE-TIME') { segment.programDateTime = value; } else if (name === 'EXT-X-DATERANGE') { segment.dateRange = parseDateRange(attributes); } else if (name === 'EXT-X-CUE-OUT') { segment.markers.push(new SpliceInfo({ type: 'OUT', duration: (attributes && attributes.DURATION) || value })); } else if (name === 'EXT-X-CUE-IN') { segment.markers.push(new SpliceInfo({ type: 'IN' })); } else if ( name === 'EXT-X-CUE-OUT-CONT' || name === 'EXT-X-CUE' || name === 'EXT-OATCLS-SCTE35' || name === 'EXT-X-ASSET' || name === 'EXT-X-SCTE35' ) { segment.markers.push(new SpliceInfo({ type: 'RAW', tagName: name, value })); } else if (name === 'EXT-X-PRELOAD-HINT' && !attributes['TYPE']) { utils.INVALIDPLAYLIST('EXT-X-PRELOAD-HINT: TYPE attribute is mandatory'); } else if (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'PART' && partHint) { utils.INVALIDPLAYLIST('Servers should not add more than one EXT-X-PRELOAD-HINT tag with the same TYPE attribute to a Playlist.'); } else if ((name === 'EXT-X-PART' || name === 'EXT-X-PRELOAD-HINT') && !attributes['URI']) { utils.INVALIDPLAYLIST('EXT-X-PART / EXT-X-PRELOAD-HINT: URI attribute is mandatory'); } else if (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'MAP') { if (mapHint) { utils.INVALIDPLAYLIST('Servers should not add more than one EXT-X-PRELOAD-HINT tag with the same TYPE attribute to a Playlist.'); } mapHint = true; params.hasMap = true; segment.map = new MediaInitializationSection({ hint: true, uri: attributes['URI'], byterange: {length: attributes['BYTERANGE-LENGTH'], offset: attributes['BYTERANGE-START'] || 0} }); } else if (name === 'EXT-X-PART' || (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'PART')) { if (name === 'EXT-X-PART' && !attributes['DURATION']) { utils.INVALIDPLAYLIST('EXT-X-PART: DURATION attribute is mandatory'); } if (name === 'EXT-X-PRELOAD-HINT') { partHint = true; } const partialSegment = new PartialSegment({ hint: (name === 'EXT-X-PRELOAD-HINT'), uri: attributes['URI'], byterange: (name === 'EXT-X-PART' ? attributes['BYTERANGE'] : {length: attributes['BYTERANGE-LENGTH'], offset: attributes['BYTERANGE-START'] || 0}), duration: attributes['DURATION'], independent: attributes['INDEPENDENT'], gap: attributes['GAP'] }); if (segment.gap && !partialSegment.gap) { // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.2.1 utils.INVALIDPLAYLIST('Partial segments must have GAP=YES if they are in a gap (EXT-X-GAP)'); } segment.parts.push(partialSegment); } } return segment; } function parsePrefetchSegment(lines: Line[], uri: any, start: number, end: number, mediaSequenceNumber: number, discontinuitySequence: number, params: Record): PrefetchSegment { const segment = new PrefetchSegment({uri, mediaSequenceNumber, discontinuitySequence}); for (let i = start; i <= end; i++) { const {name, attributes} = lines[i] as Tag; if (name === 'EXTINF') { utils.INVALIDPLAYLIST('A prefetch segment must not be advertised with an EXTINF tag.'); } else if (name === 'EXT-X-DISCONTINUITY') { utils.INVALIDPLAYLIST('A prefetch segment must not be advertised with an EXT-X-DISCONTINUITY tag.'); } else if (name === 'EXT-X-PREFETCH-DISCONTINUITY') { segment.discontinuity = true; } else if (name === 'EXT-X-KEY') { setCompatibleVersionOfKey(params, attributes); segment.key = new Key({ method: attributes['METHOD'], uri: attributes['URI'], iv: attributes['IV'], format: attributes['KEYFORMAT'], formatVersion: attributes['KEYFORMATVERSIONS'] }); } else if (name === 'EXT-X-MAP') { utils.INVALIDPLAYLIST('Prefetch segments must not be advertised with an EXT-X-MAP tag.'); } } return segment; } function parseMediaPlaylist(lines: Line[], params: Record): MediaPlaylist { const playlist = new MediaPlaylist(); let segmentStart = -1; let mediaSequence = 0; let discontinuityFound = false; let prefetchFound = false; let discontinuitySequence = 0; let currentKey: Key | null = null; let currentMap: MediaInitializationSection | null = null; let containsParts = false; for (const [index, line] of lines.entries()) { const {name, value, attributes, category} = mapTo(line); if (category === 'Segment') { if (segmentStart === -1) { segmentStart = index; } if (name === 'EXT-X-DISCONTINUITY') { discontinuityFound = true; } continue; } if (name === 'EXT-X-VERSION') { if (playlist.version === undefined) { playlist.version = value; } else { utils.INVALIDPLAYLIST('A Playlist file MUST NOT contain more than one EXT-X-VERSION tag.'); } } else if (name === 'EXT-X-TARGETDURATION') { playlist.targetDuration = params.targetDuration = value; } else if (name === 'EXT-X-MEDIA-SEQUENCE') { if (playlist.segments.length > 0) { utils.INVALIDPLAYLIST('The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.'); } playlist.mediaSequenceBase = mediaSequence = value; } else if (name === 'EXT-X-DISCONTINUITY-SEQUENCE') { if (playlist.segments.length > 0) { utils.INVALIDPLAYLIST('The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before the first Media Segment in the Playlist.'); } if (discontinuityFound) { utils.INVALIDPLAYLIST('The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before any EXT-X-DISCONTINUITY tag.'); } playlist.discontinuitySequenceBase = discontinuitySequence = value; } else if (name === 'EXT-X-ENDLIST') { playlist.endlist = true; } else if (name === 'EXT-X-PLAYLIST-TYPE') { playlist.playlistType = value; } else if (name === 'EXT-X-I-FRAMES-ONLY') { if (params.compatibleVersion < 4) { params.compatibleVersion = 4; } playlist.isIFrame = true; } else if (name === 'EXT-X-INDEPENDENT-SEGMENTS') { if (playlist.independentSegments) { utils.INVALIDPLAYLIST('EXT-X-INDEPENDENT-SEGMENTS tag MUST NOT appear more than once in a Playlist'); } playlist.independentSegments = true; } else if (name === 'EXT-X-START') { if (playlist.start) { utils.INVALIDPLAYLIST('EXT-X-START tag MUST NOT appear more than once in a Playlist'); } if (typeof attributes['TIME-OFFSET'] !== 'number') { utils.INVALIDPLAYLIST('EXT-X-START: TIME-OFFSET attribute is REQUIRED'); } playlist.start = {offset: attributes['TIME-OFFSET'], precise: attributes['PRECISE'] || false}; } else if (name === 'EXT-X-SERVER-CONTROL') { if (!attributes['CAN-BLOCK-RELOAD']) { utils.INVALIDPLAYLIST('EXT-X-SERVER-CONTROL: CAN-BLOCK-RELOAD=YES is mandatory for Low-Latency HLS'); } playlist.lowLatencyCompatibility = { canBlockReload: attributes['CAN-BLOCK-RELOAD'], canSkipUntil: attributes['CAN-SKIP-UNTIL'], holdBack: attributes['HOLD-BACK'], partHoldBack: attributes['PART-HOLD-BACK'] }; } else if (name === 'EXT-X-PART-INF') { if (!attributes['PART-TARGET']) { utils.INVALIDPLAYLIST('EXT-X-PART-INF: PART-TARGET attribute is mandatory'); } playlist.partTargetDuration = attributes['PART-TARGET']; } else if (name === 'EXT-X-RENDITION-REPORT') { if (!attributes['URI']) { utils.INVALIDPLAYLIST('EXT-X-RENDITION-REPORT: URI attribute is mandatory'); } if (attributes['URI'].search(/^[a-z]+:/) === 0) { utils.INVALIDPLAYLIST('EXT-X-RENDITION-REPORT: URI must be relative to the playlist uri'); } playlist.renditionReports.push(new RenditionReport({ uri: attributes['URI'], lastMSN: attributes['LAST-MSN'], lastPart: attributes['LAST-PART'] })); } else if (name === 'EXT-X-SKIP') { if (!attributes['SKIPPED-SEGMENTS']) { utils.INVALIDPLAYLIST('EXT-X-SKIP: SKIPPED-SEGMENTS attribute is mandatory'); } if (params.compatibleVersion < 9) { params.compatibleVersion = 9; } playlist.skip = attributes['SKIPPED-SEGMENTS']; mediaSequence += playlist.skip; } else if (name === 'EXT-X-PREFETCH') { const segment = parsePrefetchSegment(lines, value, segmentStart === -1 ? index : segmentStart, index - 1, mediaSequence++, discontinuitySequence, params); if (segment) { if (segment.discontinuity) { segment.discontinuitySequence++; discontinuitySequence = segment.discontinuitySequence; } if (segment.key) { currentKey = segment.key; } else { segment.key = currentKey; } playlist.prefetchSegments.push(segment); } prefetchFound = true; segmentStart = -1; } else if (name === 'EXT-X-DEFINE') { playlist.defines ||= []; playlist.defines.push(attributes); } else if (name === 'EXT-X-DATERANGE') { const dateRange = parseDateRange(attributes); playlist.dateRanges.push(dateRange); } else if (typeof line === 'string') { // uri if (segmentStart === -1) { utils.INVALIDPLAYLIST('A URI line is not preceded by any segment tags'); } if (!playlist.targetDuration) { utils.INVALIDPLAYLIST('The EXT-X-TARGETDURATION tag is REQUIRED'); } if (prefetchFound) { utils.INVALIDPLAYLIST('These segments must appear after all complete segments.'); } const segment = parseSegment(lines, line, segmentStart, index - 1, mediaSequence++, discontinuitySequence, params); if (segment) { [discontinuitySequence, currentKey, currentMap] = addSegment(playlist, segment, discontinuitySequence, currentKey!, currentMap!); if (!containsParts && segment.parts.length > 0) { containsParts = true; } } segmentStart = -1; } } if (segmentStart !== -1) { const segment = parseSegment(lines, '', segmentStart, lines.length - 1, mediaSequence++, discontinuitySequence, params); if (segment) { const {parts} = segment; if (parts.length > 0 && !playlist.endlist && !parts.at(-1)?.hint) { utils.INVALIDPLAYLIST('If the Playlist contains EXT-X-PART tags and does not contain an EXT-X-ENDLIST tag, the Playlist must contain an EXT-X-PRELOAD-HINT tag with a TYPE=PART attribute'); } // @ts-expect-error TODO check if this is not a bug the third argument should be a discontinuitySequence addSegment(playlist, segment, currentKey, currentMap); if (!containsParts && segment.parts.length > 0) { containsParts = true; } } } checkDateRange(playlist.segments); if (playlist.lowLatencyCompatibility) { checkLowLatencyCompatibility(playlist, containsParts); } return playlist; } function addSegment(playlist: MediaPlaylist, segment: Segment, discontinuitySequence: number, currentKey?: Key, currentMap?: MediaInitializationSection): [number, Key, MediaInitializationSection] { const {discontinuity, key, map, byterange, uri} = segment; if (discontinuity) { segment.discontinuitySequence = discontinuitySequence + 1; } if (!key) { segment.key = currentKey; } if (!map) { segment.map = currentMap!; } if (byterange && byterange.offset === -1) { const {segments} = playlist; if (segments.length > 0) { const prevSegment = segments.at(-1)!; if (prevSegment.byterange && prevSegment.uri === uri) { byterange.offset = prevSegment.byterange.offset + prevSegment.byterange.length; } else { utils.INVALIDPLAYLIST('If offset of EXT-X-BYTERANGE is not present, a previous Media Segment MUST be a sub-range of the same media resource'); } } else { utils.INVALIDPLAYLIST('If offset of EXT-X-BYTERANGE is not present, a previous Media Segment MUST appear in the Playlist file'); } } playlist.segments.push(segment); return [segment.discontinuitySequence, segment.key!, segment.map]; } function checkDateRange(segments: Segment[]) { const earliestDates = new Map(); const rangeList = new Map(); let hasDateRange = false; let hasProgramDateTime = false; for (let i = segments.length - 1; i >= 0; i--) { const {programDateTime, dateRange} = segments[i]; if (programDateTime) { hasProgramDateTime = true; } if (dateRange && dateRange.start) { hasDateRange = true; if (dateRange.endOnNext && (dateRange.end || dateRange.duration)) { utils.INVALIDPLAYLIST('An EXT-X-DATERANGE tag with an END-ON-NEXT=YES attribute MUST NOT contain DURATION or END-DATE attributes.'); } const start = dateRange.start.getTime(); const duration = dateRange.duration || 0; if (dateRange.end && dateRange.duration) { if ((start + duration * 1000) !== dateRange.end.getTime()) { utils.INVALIDPLAYLIST('END-DATE MUST be equal to the value of the START-DATE attribute plus the value of the DURATION'); } } if (dateRange.endOnNext) { dateRange.end = earliestDates.get(dateRange.classId); } earliestDates.set(dateRange.classId, dateRange.start); const end = dateRange.end ? dateRange.end.getTime() : dateRange.start.getTime() + (dateRange.duration || 0) * 1000; const range = rangeList.get(dateRange.classId); if (range) { for (const entry of range) { if ((entry.start <= start && entry.end > start) || (entry.start >= start && entry.start < end)) { utils.INVALIDPLAYLIST('DATERANGE tags with the same CLASS should not overlap'); } } range.push({start, end}); } else if (dateRange.classId) { rangeList.set(dateRange.classId, [{start, end}]); } } } if (hasDateRange && !hasProgramDateTime) { utils.INVALIDPLAYLIST('If a Playlist contains an EXT-X-DATERANGE tag, it MUST also contain at least one EXT-X-PROGRAM-DATE-TIME tag.'); } } function checkLowLatencyCompatibility({lowLatencyCompatibility, targetDuration, partTargetDuration, segments, renditionReports}: any, containsParts) { const {canSkipUntil, holdBack, partHoldBack} = lowLatencyCompatibility; if (canSkipUntil < targetDuration * 6) { utils.INVALIDPLAYLIST('The Skip Boundary must be at least six times the EXT-X-TARGETDURATION.'); } // Its value is a floating-point number of seconds and . if (holdBack < targetDuration * 3) { utils.INVALIDPLAYLIST('HOLD-BACK must be at least three times the EXT-X-TARGETDURATION.'); } if (containsParts) { if (partTargetDuration === undefined) { utils.INVALIDPLAYLIST('EXT-X-PART-INF is required if a Playlist contains one or more EXT-X-PART tags'); } if (partHoldBack === undefined) { utils.INVALIDPLAYLIST('EXT-X-PART: PART-HOLD-BACK attribute is mandatory'); } if (partHoldBack < partTargetDuration) { utils.INVALIDPLAYLIST('PART-HOLD-BACK must be at least PART-TARGET'); } for (const [segmentIndex, {parts}] of segments.entries()) { if (parts.length > 0 && segmentIndex < segments.length - 3) { utils.INVALIDPLAYLIST('Remove EXT-X-PART tags from the Playlist after they are greater than three target durations from the end of the Playlist.'); } for (const [partIndex, {duration}] of parts.entries()) { if (duration === undefined) { continue; } if (duration > partTargetDuration) { utils.INVALIDPLAYLIST('PART-TARGET is the maximum duration of any Partial Segment'); } if (partIndex < parts.length - 1 && duration < partTargetDuration * 0.85) { utils.INVALIDPLAYLIST('All Partial Segments except the last part of a segment must have a duration of at least 85% of PART-TARGET'); } } } } for (const report of renditionReports) { const lastSegment = segments.at(-1); report.lastMSN ??= lastSegment.mediaSequenceNumber; if ((report.lastPart === null || report.lastPart === undefined) && lastSegment.parts.length > 0) { report.lastPart = lastSegment.parts.length - 1; } } } function CHECKTAGCATEGORY(category: TagCategory, params: Record) { if (category === 'Segment' || category === 'MediaPlaylist') { if (params.isMasterPlaylist === undefined) { params.isMasterPlaylist = false; return; } if (params.isMasterPlaylist) { MIXEDTAGS(); } return; } if (category === 'MasterPlaylist') { if (params.isMasterPlaylist === undefined) { params.isMasterPlaylist = true; return; } if (params.isMasterPlaylist === false) { MIXEDTAGS(); } } // category === 'Basic' or 'MediaorMasterPlaylist' or 'Unknown' } type Tag = { name: string; category: TagCategory; value: any; attributes: any; }; function parseTag(line: string, params: Record): Tag | null { const [name, param] = splitTag(line); const category = getTagCategory(name); CHECKTAGCATEGORY(category, params); if (category === 'Unknown') { return null; } if (category === 'MediaPlaylist' && name !== 'EXT-X-RENDITION-REPORT' && name !== 'EXT-X-PREFETCH') { if (params.hash[name]) { utils.INVALIDPLAYLIST('There MUST NOT be more than one Media Playlist tag of each type in any Media Playlist'); } params.hash[name] = true; } const [value, attributes] = parseTagParam(name, param); return {name, category, value, attributes}; } type Line = string | Tag; function lexicalParse(text: string, params: Record): Line[] { const lines: Line[] = []; for (const l of text.split('\n')) { // V8 has garbage collection issues when cleaning up substrings split from strings greater // than 13 characters so before we continue we need to safely copy over each line so that it // doesn't hold any reference to the containing string. const line = l.trim(); if (!line) { // empty line continue; } if (line.startsWith('#')) { if (line.startsWith('#EXT')) { // tag const tag = parseTag(line, params); if (tag) { lines.push(tag); } } // comment continue; } // uri lines.push(line); } if (lines.length === 0 || (lines[0] as Tag).name !== 'EXTM3U') { utils.INVALIDPLAYLIST('The EXTM3U tag MUST be the first line.'); } return lines; } function semanticParse(lines: Line[], params: Record): MasterPlaylist | MediaPlaylist { let playlist; if (params.isMasterPlaylist) { playlist = parseMasterPlaylist(lines, params); } else { playlist = parseMediaPlaylist(lines, params); if (!playlist.isIFrame && params.hasMap && params.compatibleVersion < 6) { params.compatibleVersion = 6; } } if (params.compatibleVersion > 1) { if (!playlist.version || playlist.version < params.compatibleVersion) { utils.INVALIDPLAYLIST(`EXT-X-VERSION needs to be ${params.compatibleVersion} or higher.`); } } return playlist; } function parse(text: string): MasterPlaylist | MediaPlaylist { const params: Record = { version: undefined, isMasterPlaylist: undefined, hasMap: false, targetDuration: 0, compatibleVersion: 1, isClosedCaptionsNone: false, hash: {} }; const lines = lexicalParse(text, params); const playlist = semanticParse(lines, params); playlist.source = text; return playlist; } function mapTo(value: T | string): Partial { return typeof value === 'string' ? {} : value; } export default parse; ================================================ FILE: stringify.ts ================================================ import * as utils from './utils'; import { Byterange, DateRange, Key, MasterPlaylist, MediaInitializationSection, MediaPlaylist, PartialSegment, Rendition, Segment, SessionData, SpliceInfo, Variant, PostProcess, ContentSteering, } from './types'; const ALLOW_REDUNDANCY = [ '#EXTINF', '#EXT-X-BYTERANGE', '#EXT-X-DISCONTINUITY', '#EXT-X-STREAM-INF', '#EXT-X-CUE-OUT', '#EXT-X-CUE-IN', '#EXT-X-KEY', '#EXT-X-MAP' ]; const SKIP_IF_REDUNDANT = [ '#EXT-X-MEDIA' ]; class LineArray extends Array { baseUri?: string; constructor(baseUri?: string) { super(); this.baseUri = baseUri; } override push(...elems: string[]) { // redundancy check for (const elem of elems) { if (!elem.startsWith('#')) { super.push(elem); continue; } if (ALLOW_REDUNDANCY.some(item => elem.startsWith(item))) { super.push(elem); continue; } if (this.includes(elem)) { if (SKIP_IF_REDUNDANT.some(item => elem.startsWith(item))) { continue; } utils.INVALIDPLAYLIST(`Redundant item (${elem})`); } super.push(elem); } return this.length; } override join(separator: string | undefined = ','): string { for (let i = this.length - 1; i >= 0; i--) { if (!this[i]) { this.splice(i, 1); } } return super.join(separator); } } function buildDecimalFloatingNumber(num: number, fixed?: number) { let roundFactor = 1000; if (fixed) { roundFactor = 10 ** fixed; } const rounded = Math.round(num * roundFactor) / roundFactor; return fixed ? rounded.toFixed(fixed) : rounded; } function getNumberOfDecimalPlaces(num: number) { const str = num.toString(10); const index = str.indexOf('.'); if (index === -1) { return 0; } return str.length - index - 1; } function buildMasterPlaylist(lines: LineArray, playlist: MasterPlaylist, postProcess: PostProcess | undefined) { if (playlist.contentSteering) { lines.push(buildContentSteeringServer(playlist.contentSteering)); } for (const sessionData of playlist.sessionDataList) { lines.push(buildSessionData(sessionData)); } for (const sessionKey of playlist.sessionKeyList) { lines.push(buildKey(sessionKey, true)); } for (const [i, variant] of playlist.variants.entries()) { const base = lines.length; buildVariant(lines, variant); if (postProcess?.variantProcessor) { postProcess.variantProcessor(lines, base, lines.length - 1, variant, i); } } } function buildContentSteeringServer(contentSteering: ContentSteering) { const attrs = [ `SERVER-URI="${contentSteering.serverUri}"`, `PATHWAY-ID="${contentSteering.pathwayId}"` ]; return `#EXT-X-CONTENT-STEERING:${attrs.join(',')}`; } function buildSessionData(sessionData: SessionData) { const attrs = [`DATA-ID="${sessionData.id}"`]; if (sessionData.language) { attrs.push(`LANGUAGE="${sessionData.language}"`); } if (sessionData.value) { attrs.push(`VALUE="${sessionData.value}"`); } else if (sessionData.uri) { attrs.push(`URI="${sessionData.uri}"`); } return `#EXT-X-SESSION-DATA:${attrs.join(',')}`; } function buildKey(key: Key, isSessionKey?: boolean) { const name = isSessionKey ? '#EXT-X-SESSION-KEY' : '#EXT-X-KEY'; const attrs = [`METHOD=${key.method}`]; if (key.uri) { attrs.push(`URI="${key.uri}"`); } if (key.iv) { if (key.iv.byteLength !== 16) { utils.INVALIDPLAYLIST('IV must be a 128-bit unsigned integer'); } attrs.push(`IV=${utils.byteSequenceToHex(key.iv)}`); } if (key.format) { attrs.push(`KEYFORMAT="${key.format}"`); } if (key.formatVersion) { attrs.push(`KEYFORMATVERSIONS="${key.formatVersion}"`); } return `${name}:${attrs.join(',')}`; } function buildVariant(lines: LineArray, variant: Variant) { const name = variant.isIFrameOnly ? '#EXT-X-I-FRAME-STREAM-INF' : '#EXT-X-STREAM-INF'; const attrs = [`BANDWIDTH=${variant.bandwidth}`]; if (variant.averageBandwidth) { attrs.push(`AVERAGE-BANDWIDTH=${variant.averageBandwidth}`); } if (variant.isIFrameOnly) { attrs.push(`URI="${variant.uri}"`); } if (variant.codecs) { attrs.push(`CODECS="${variant.codecs}"`); } if (variant.resolution) { attrs.push(`RESOLUTION=${variant.resolution.width}x${variant.resolution.height}`); } if (variant.frameRate) { attrs.push(`FRAME-RATE=${buildDecimalFloatingNumber(variant.frameRate, 3)}`); } if (variant.hdcpLevel) { attrs.push(`HDCP-LEVEL=${variant.hdcpLevel}`); } if (variant.audio.length > 0) { attrs.push(`AUDIO="${variant.audio[0].groupId}"`); for (const rendition of variant.audio) { lines.push(buildRendition(rendition)); } } if (variant.video.length > 0) { attrs.push(`VIDEO="${variant.video[0].groupId}"`); for (const rendition of variant.video) { lines.push(buildRendition(rendition)); } } if (variant.subtitles.length > 0) { attrs.push(`SUBTITLES="${variant.subtitles[0].groupId}"`); for (const rendition of variant.subtitles) { lines.push(buildRendition(rendition)); } } if (utils.getOptions().allowClosedCaptionsNone && variant.closedCaptions.length === 0) { attrs.push(`CLOSED-CAPTIONS=NONE`); } else if (variant.closedCaptions.length > 0) { attrs.push(`CLOSED-CAPTIONS="${variant.closedCaptions[0].groupId}"`); for (const rendition of variant.closedCaptions) { lines.push((buildRendition(rendition))); } } if (variant.score) { attrs.push(`SCORE=${variant.score}`); } if (variant.allowedCpc) { const list: string[] = []; for (const {format, cpcList} of variant.allowedCpc) { list.push(`${format}:${cpcList.join('/')}`); } attrs.push(`ALLOWED-CPC="${list.join(',')}"`); } if (variant.videoRange) { attrs.push(`VIDEO-RANGE=${variant.videoRange}`); } if (variant.stableVariantId) { attrs.push(`STABLE-VARIANT-ID="${variant.stableVariantId}"`); } if (variant.pathwayId) { attrs.push(`PATHWAY-ID="${variant.pathwayId}"`); } if (variant.programId) { attrs.push(`PROGRAM-ID=${variant.programId}`); } lines.push(`${name}:${attrs.join(',')}`); if (!variant.isIFrameOnly) { lines.push(`${variant.uri}`); } } function buildRendition(rendition: Rendition) { const attrs = [ `TYPE=${rendition.type}`, `GROUP-ID="${rendition.groupId}"`, `NAME="${rendition.name}"` ]; if (rendition.isDefault !== undefined) { attrs.push(`DEFAULT=${rendition.isDefault ? 'YES' : 'NO'}`); } if (rendition.autoselect !== undefined) { attrs.push(`AUTOSELECT=${rendition.autoselect ? 'YES' : 'NO'}`); } if (rendition.forced !== undefined) { attrs.push(`FORCED=${rendition.forced ? 'YES' : 'NO'}`); } if (rendition.language) { attrs.push(`LANGUAGE="${rendition.language}"`); } if (rendition.assocLanguage) { attrs.push(`ASSOC-LANGUAGE="${rendition.assocLanguage}"`); } if (rendition.instreamId) { attrs.push(`INSTREAM-ID="${rendition.instreamId}"`); } if (rendition.characteristics) { attrs.push(`CHARACTERISTICS="${rendition.characteristics}"`); } if (rendition.channels) { attrs.push(`CHANNELS="${rendition.channels}"`); } if (rendition.uri) { attrs.push(`URI="${rendition.uri}"`); } return `#EXT-X-MEDIA:${attrs.join(',')}`; } function buildMediaPlaylist(lines: LineArray, playlist: MediaPlaylist, postProcess: PostProcess | undefined) { let lastKey = ''; let lastMap = ''; let unclosedCueIn = false; if (playlist.targetDuration) { lines.push(`#EXT-X-TARGETDURATION:${playlist.targetDuration}`); } if (playlist.lowLatencyCompatibility) { const {canBlockReload, canSkipUntil, holdBack, partHoldBack} = playlist.lowLatencyCompatibility; const params: string[] = []; params.push(`CAN-BLOCK-RELOAD=${canBlockReload ? 'YES' : 'NO'}`); if (canSkipUntil !== undefined) { params.push(`CAN-SKIP-UNTIL=${canSkipUntil}`); } if (holdBack !== undefined) { params.push(`HOLD-BACK=${holdBack}`); } if (partHoldBack !== undefined) { params.push(`PART-HOLD-BACK=${partHoldBack}`); } lines.push(`#EXT-X-SERVER-CONTROL:${params.join(',')}`); } if (playlist.partTargetDuration) { lines.push(`#EXT-X-PART-INF:PART-TARGET=${playlist.partTargetDuration}`); } if (playlist.mediaSequenceBase) { lines.push(`#EXT-X-MEDIA-SEQUENCE:${playlist.mediaSequenceBase}`); } if (playlist.discontinuitySequenceBase) { lines.push(`#EXT-X-DISCONTINUITY-SEQUENCE:${playlist.discontinuitySequenceBase}`); } if (playlist.playlistType) { lines.push(`#EXT-X-PLAYLIST-TYPE:${playlist.playlistType}`); } if (playlist.isIFrame) { lines.push(`#EXT-X-I-FRAMES-ONLY`); } if (playlist.skip > 0) { lines.push(`#EXT-X-SKIP:SKIPPED-SEGMENTS=${playlist.skip}`); } for (const dateRange of playlist.dateRanges) { lines.push(buildDateRange(dateRange)); } for (const [i, segment] of playlist.segments.entries()) { const base = lines.length; let markerType = ''; [lastKey, lastMap, markerType] = buildSegment(lines, segment, lastKey, lastMap, playlist.version); if (markerType === 'OUT') { unclosedCueIn = true; } else if (markerType === 'IN' && unclosedCueIn) { unclosedCueIn = false; } if (postProcess?.segmentProcessor) { postProcess.segmentProcessor(lines, base, lines.length - 1, segment, i); } } if (playlist.playlistType === 'VOD' && unclosedCueIn) { lines.push('#EXT-X-CUE-IN'); } if (playlist.prefetchSegments.length > 2) { utils.INVALIDPLAYLIST('The server must deliver no more than two prefetch segments'); } for (const segment of playlist.prefetchSegments) { if (segment.discontinuity) { lines.push(`#EXT-X-PREFETCH-DISCONTINUITY`); } lines.push(`#EXT-X-PREFETCH:${segment.uri}`); } if (playlist.endlist) { lines.push(`#EXT-X-ENDLIST`); } for (const report of playlist.renditionReports) { const params: string[] = []; params.push(`URI="${report.uri}"`, `LAST-MSN=${report.lastMSN}`); if (report.lastPart !== undefined) { params.push(`LAST-PART=${report.lastPart}`); } lines.push(`#EXT-X-RENDITION-REPORT:${params.join(',')}`); } } function buildSegment(lines: LineArray, segment: Segment, lastKey: string, lastMap: string, version = 1) { let hint = false; let markerType = ''; if (segment.discontinuity) { lines.push(`#EXT-X-DISCONTINUITY`); } if (segment.gap) { lines.push(`#EXT-X-GAP`); } if (segment.key) { const line = buildKey(segment.key); if (line !== lastKey) { lines.push(line); lastKey = line; } } if (segment.map) { const line = buildMap(segment.map); if (line !== lastMap) { lines.push(line); lastMap = line; } } if (segment.programDateTime) { lines.push(`#EXT-X-PROGRAM-DATE-TIME:${utils.formatDate(segment.programDateTime)}`); } if (segment.dateRange) { lines.push(buildDateRange(segment.dateRange)); } if (segment.markers.length > 0) { markerType = buildMarkers(lines, segment.markers); } if (segment.parts.length > 0) { hint = buildParts(lines, segment.parts); } if (hint) { return [lastKey, lastMap]; } if (typeof segment.duration === 'number' && !Number.isNaN(segment.duration)) { const duration = version < 3 ? Math.round(segment.duration) : buildDecimalFloatingNumber(segment.duration, getNumberOfDecimalPlaces(segment.duration)); lines.push(`#EXTINF:${duration},${unescape(encodeURIComponent(segment.title || ''))}`); } if (segment.byterange) { lines.push(`#EXT-X-BYTERANGE:${buildByteRange(segment.byterange)}`); } Array.prototype.push.call(lines, `${segment.uri}`); // URIs could be redundant when EXT-X-BYTERANGE is used return [lastKey, lastMap, markerType]; } function buildMap(map: MediaInitializationSection) { const attrs = [`URI="${map.uri}"`]; if (map.byterange) { attrs.push(`BYTERANGE="${buildByteRange(map.byterange)}"`); } return `#EXT-X-MAP:${attrs.join(',')}`; } function buildByteRange({offset, length}: Byterange) { return `${length}@${offset}`; } function buildDateRange(dateRange: DateRange) { const attrs = [ `ID="${dateRange.id}"` ]; if (dateRange.start) { attrs.push(`START-DATE="${utils.formatDate(dateRange.start)}"`); } if (dateRange.cue) { attrs.push(`CUE="${dateRange.cue}"`); } if (dateRange.end) { attrs.push(`END-DATE="${utils.formatDate(dateRange.end)}"`); } if (dateRange.duration) { attrs.push(`DURATION=${dateRange.duration}`); } if (dateRange.plannedDuration) { attrs.push(`PLANNED-DURATION=${dateRange.plannedDuration}`); } if (dateRange.classId) { attrs.push(`CLASS="${dateRange.classId}"`); } if (dateRange.endOnNext) { attrs.push(`END-ON-NEXT=YES`); } for (const key of Object.keys(dateRange.attributes)) { if (key.startsWith('X-')) { if (typeof dateRange.attributes[key] === 'number') { attrs.push(`${key}=${dateRange.attributes[key]}`); } else { attrs.push(`${key}="${dateRange.attributes[key]}"`); } } else if (key.startsWith('SCTE35-')) { attrs.push(`${key}=${utils.byteSequenceToHex(dateRange.attributes[key])}`); } } return `#EXT-X-DATERANGE:${attrs.join(',')}`; } function buildMarkers(lines: LineArray, markers: SpliceInfo[]) { let type = ''; for (const marker of markers) { if (marker.type === 'OUT') { type = 'OUT'; lines.push(`#EXT-X-CUE-OUT:DURATION=${marker.duration}`); } else if (marker.type === 'IN') { type = 'IN'; lines.push('#EXT-X-CUE-IN'); } else if (marker.type === 'RAW') { const value = marker.value ? `:${marker.value}` : ''; lines.push(`#${marker.tagName}${value}`); } } return type; } function buildParts(lines: LineArray, parts: PartialSegment[]) { let hint = false; for (const part of parts) { if (part.hint) { const params: string[] = []; params.push('TYPE=PART', `URI="${part.uri}"`); if (part.byterange) { const {offset, length} = part.byterange; params.push(`BYTERANGE-START=${offset}`); if (length) { params.push(`BYTERANGE-LENGTH=${length}`); } } lines.push(`#EXT-X-PRELOAD-HINT:${params.join(',')}`); hint = true; } else { const params: string[] = []; params.push(`DURATION=${part.duration}`, `URI="${part.uri}"`); if (part.byterange) { params.push(`BYTERANGE=${buildByteRange(part.byterange)}`); } if (part.independent) { params.push('INDEPENDENT=YES'); } if (part.gap) { params.push('GAP=YES'); } lines.push(`#EXT-X-PART:${params.join(',')}`); } } return hint; } function buildDefines(define: Record) { const attrs: string[] = []; for (const attr in define) { attrs.push(`${attr}="${define[attr]}"`); } return `#EXT-X-DEFINE:${attrs.join(',')}`; } function stringify(playlist: MasterPlaylist | MediaPlaylist, postProcess?: PostProcess): string { utils.PARAMCHECK(playlist); utils.ASSERT('Not a playlist', playlist.type === 'playlist'); const lines = new LineArray(playlist.uri); lines.push('#EXTM3U'); if (playlist.version) { lines.push(`#EXT-X-VERSION:${playlist.version}`); } if (playlist.independentSegments) { lines.push('#EXT-X-INDEPENDENT-SEGMENTS'); } if (playlist.start) { lines.push(`#EXT-X-START:TIME-OFFSET=${buildDecimalFloatingNumber(playlist.start.offset)}${playlist.start.precise ? ',PRECISE=YES' : ''}`); } if (playlist.defines) { for (const session of playlist.defines) { lines.push(buildDefines(session)); } } if (playlist.isMasterPlaylist) { buildMasterPlaylist(lines, playlist, postProcess); } else { buildMediaPlaylist(lines, playlist, postProcess); } // console.log('<<<'); // console.log(lines.join('\n')); // console.log('>>>'); return lines.join('\n'); } export default stringify; ================================================ FILE: test/fixtures/m3u8/8.1-Simple-Media-Playlist.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:9.009, http://media.example.com/first.ts #EXTINF:9.009, http://media.example.com/second.ts #EXTINF:3.003, http://media.example.com/third.ts #EXT-X-ENDLIST ================================================ FILE: test/fixtures/m3u8/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.m3u8 ================================================ # This example shows two EXT-X-DATERANGE tags that describe a single # Date Range, with a SCTE-35 "out" splice_insert() command that is # subsequently updated with an SCTE-35 "in" splice_insert() command. #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:30 #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z #EXTINF:30, http://media.example.com/01.ts #EXTINF:30, http://media.example.com/02.ts #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000 #EXTINF:30, http://ads.example.com/ad-01.ts #EXTINF:30, http://ads.example.com/ad-02.ts #EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000 #EXTINF:30, http://media.example.com/03.ts #EXTINF:3.003, http://media.example.com/04.ts ================================================ FILE: test/fixtures/m3u8/8.11-EXT-X-CUE-OUT-Media-Playlist.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:9.009, http://media.example.com/first.ts #EXT-X-CUE-OUT:DURATION=15 #EXTINF:9.009, http://media.example.com/second.ts #EXTINF:3.003, http://media.example.com/third.ts #EXT-X-ENDLIST ================================================ FILE: test/fixtures/m3u8/8.2-Live-Media-Playlist_using-HTTPS.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:8 #EXT-X-MEDIA-SEQUENCE:2680 #EXTINF:7.975, https://priv.example.com/fileSequence2680.ts #EXTINF:7.941, https://priv.example.com/fileSequence2681.ts #EXTINF:7.975, https://priv.example.com/fileSequence2682.ts ================================================ FILE: test/fixtures/m3u8/8.3-Playlist-with-encrypted-Media-Segments.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:15 #EXT-X-MEDIA-SEQUENCE:7794 #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" #EXTINF:2.833, http://media.example.com/fileSequence52-A.ts #EXTINF:15, http://media.example.com/fileSequence52-B.ts #EXTINF:13.333, http://media.example.com/fileSequence52-C.ts #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53" #EXTINF:15, http://media.example.com/fileSequence53-A.ts ================================================ FILE: test/fixtures/m3u8/8.4-Master-Playlist.m3u8 ================================================ #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000,CODECS="avc1.640029,mp4a.40.2" http://example.com/low.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000,CODECS="avc1.640029,mp4a.40.2" http://example.com/mid.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000,CODECS="avc1.640029,mp4a.40.2" http://example.com/hi.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" http://example.com/audio-only.m3u8 ================================================ FILE: test/fixtures/m3u8/8.5-Master-Playlist-with-I-Frames.m3u8 ================================================ #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="avc1.640029,mp4a.40.2" low/audio-video.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8",CODECS="avc1.640029" #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="avc1.640029,mp4a.40.2" mid/audio-video.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8",CODECS="avc1.640029" #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="avc1.640029,mp4a.40.2" hi/audio-video.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8",CODECS="avc1.640029" #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" audio-only.m3u8 ================================================ FILE: test/fixtures/m3u8/8.6-Master-Playlist-with-Alternative-audio.m3u8 ================================================ # In this example, the CODECS attributes have been condensed for space. # A '\' is used to indicate that the tag continues on the following # line with whitespace removed: #EXTM3U #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="main/english-audio.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de",URI="main/german-audio.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en",URI="commentary/audio-only.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="mp4a.40.2",AUDIO="aac" low/video-only.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="mp4a.40.2",AUDIO="aac" mid/video-only.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="mp4a.40.2",AUDIO="aac" hi/video-only.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac" main/english-audio.m3u8 ================================================ FILE: test/fixtures/m3u8/8.7-Master-Playlist-with-Alternative-video.m3u8 ================================================ # This example shows 3 different video Renditions (Main, Centerfield # and Dugout), and 3 different Variant Streams (low, mid and high). In # this example, clients that did not support the EXT-X-MEDIA tag and # the VIDEO attribute of the EXT-X-STREAM-INF tag would only be able to # play the video Rendition "Main". # Since the EXT-X-STREAM-INF tag has no AUDIO attribute, all video # Renditions would be required to contain the audio. # # In this example, the CODECS attributes have been condensed for space. # A '\' is used to indicate that the tag continues on the following # line with whitespace removed: #EXTM3U #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="avc1.640029,mp4a.40.2",VIDEO="low" low/main/audio-video.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=NO,URI="mid/main/audio-video.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=YES,URI="mid/centerfield/audio-video.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="avc1.640029,mp4a.40.2",VIDEO="mid" mid/main/audio-video.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="avc1.640029,mp4a.40.2",VIDEO="hi" hi/main/audio-video.m3u8 ================================================ FILE: test/fixtures/m3u8/8.8-Session-Data-in-a-Master-Playlist.m3u8 ================================================ # In this example, only the EXT-X-SESSION-DATA is shown: #EXTM3U #EXT-X-SESSION-DATA:DATA-ID="com.example.lyrics",URI="lyrics.json" #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="es",VALUE="Este es un ejemplo" ================================================ FILE: test/fixtures/m3u8/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.m3u8 ================================================ # In this example, the CODECS attributes have been condensed for space. # A '\' is used to indicate that the tag continues on the following # line with whitespace removed: #EXTM3U #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.easy-to-read",URI="main/english-audio.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.easy-to-read",URI="main/german-audio.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.easy-to-read",URI="commentary/audio-only.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="mp4a.40.2",AUDIO="aac" low/video-only.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="mp4a.40.2",AUDIO="aac" mid/video-only.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="mp4a.40.2",AUDIO="aac" hi/video-only.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac" main/english-audio.m3u8 ================================================ FILE: test/fixtures/m3u8/Low-Latency_Example-01_Low-Latency_HLS_Playlist.m3u8 ================================================ #EXTM3U # This Playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_msn=273&_HLS_part=2 #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:4 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1 #EXT-X-PART-INF:PART-TARGET=0.33334 #EXT-X-MEDIA-SEQUENCE:266 #EXT-X-MAP:URI="init.mp4" #EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z #EXTINF:4.00008, fileSequence266.mp4 #EXTINF:4.00008, fileSequence267.mp4 #EXTINF:4.00008, fileSequence268.mp4 #EXTINF:4.00008, fileSequence269.mp4 #EXTINF:4.00008, fileSequence270.mp4 #EXT-X-PART:DURATION=0.33334,URI="filePart271.0.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.1.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.2.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.3.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.4.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart271.5.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.6.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.7.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4" #EXTINF:4.00008, fileSequence271.mp4 #EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z #EXT-X-PART:DURATION=0.33334,URI="filePart272.a.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.b.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.c.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.d.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.e.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.f.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart272.g.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.h.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.i.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.j.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.k.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.l.mp4" #EXTINF:4.00008, fileSequence272.mp4 #EXT-X-PART:DURATION=0.33334,URI="filePart273.0.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4" #EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.3.mp4" #EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=2 #EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=1 ================================================ FILE: test/fixtures/m3u8/Low-Latency_Example-02_Playlist_Delta_Update.m3u8 ================================================ #EXTM3U # Following the example above, this Playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_msn=273&_HLS_part=3 &_HLS_skip=YES #EXT-X-VERSION:9 #EXT-X-TARGETDURATION:4 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1 #EXT-X-PART-INF:PART-TARGET=0.33334 #EXT-X-MEDIA-SEQUENCE:266 #EXT-X-SKIP:SKIPPED-SEGMENTS=3 #EXTINF:4.00008, fileSequence269.mp4 #EXTINF:4.00008, fileSequence270.mp4 #EXT-X-PART:DURATION=0.33334,URI="filePart271.0.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.1.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.2.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.3.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.4.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart271.5.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.6.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.7.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4" #EXTINF:4.00008, fileSequence271.mp4 #EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z #EXT-X-PART:DURATION=0.33334,URI="filePart272.a.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.b.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.c.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.d.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.e.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.f.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart272.g.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.h.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.i.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.j.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.k.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart272.l.mp4" #EXTINF:4.00008, fileSequence272.mp4 #EXT-X-PART:DURATION=0.33334,URI="filePart273.0.mp4",INDEPENDENT=YES #EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4" #EXT-X-PART:DURATION=0.33334,URI="filePart273.3.mp4" #EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.4.mp4" #EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=3 #EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=3 ================================================ FILE: test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-01.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:9 #EXT-X-TARGETDURATION:4 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1.02 #EXT-X-PART-INF:PART-TARGET=1.02 #EXT-X-MEDIA-SEQUENCE:266 #EXT-X-SKIP:SKIPPED-SEGMENTS=3 #EXTINF:4.00008, fileSequence269.mp4 #EXTINF:4.00008, fileSequence270.mp4 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=18000@43000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence271.mp4",BYTERANGE-START=61000 ================================================ FILE: test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-02.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:9 #EXT-X-TARGETDURATION:4 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1.02 #EXT-X-PART-INF:PART-TARGET=1.02 #EXT-X-MEDIA-SEQUENCE:266 #EXT-X-SKIP:SKIPPED-SEGMENTS=3 #EXTINF:4.00008, fileSequence269.mp4 #EXTINF:4.00008, fileSequence270.mp4 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=18000@43000 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=19000@61000 #EXTINF:4.00008, fileSequence271.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence272.mp4",BYTERANGE-START=0 ================================================ FILE: test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-03.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:9 #EXT-X-TARGETDURATION:4 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1.02 #EXT-X-PART-INF:PART-TARGET=1.02 #EXT-X-MEDIA-SEQUENCE:266 #EXT-X-SKIP:SKIPPED-SEGMENTS=3 #EXTINF:4.00008, fileSequence269.mp4 #EXTINF:4.00008, fileSequence270.mp4 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=18000@43000 #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=19000@61000 #EXTINF:4.00008, fileSequence271.mp4 #EXT-X-PART:DURATION=1.02,URI="fileSequence272.mp4",BYTERANGE=21000@0 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence272.mp4",BYTERANGE-START=21000 ================================================ FILE: test/fixtures/m3u8/Multiple-rendition-groups.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:4 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="English",DEFAULT=YES,URI="aac_high_eng.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="Japanese",DEFAULT=NO,URI="aac_high_jp.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO="aac_high" 1080p.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="English",DEFAULT=YES,URI="aac_mid_eng.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="Japanese",DEFAULT=NO,URI="aac_mid_jp.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO="aac_mid" 720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO="aac_mid" 540p.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="English",DEFAULT=YES,URI="aac_low_eng.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="Japanese",DEFAULT=NO,URI="aac_low_jp.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO="aac_low" 360p.m3u8 ================================================ FILE: test/fixtures/m3u8/RedundantSegments.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXTINF:9.009, http://media.example.com/first.ts #EXTINF:9.009, http://media.example.com/second.ts #EXTINF:9.009, #EXT-X-BYTERANGE:128@256 http://media.example.com/second.ts #EXTINF:3.003, http://media.example.com/third.ts #EXT-X-ENDLIST ================================================ FILE: test/fixtures/m3u8/SCTE-35_01.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:8 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:8.008, 1.ts #EXT-X-CUE-OUT:DURATION=15 #EXTINF:8, 2.ts #EXTINF:7, 3.ts #EXT-X-CUE-IN #EXTINF:8.008, 4.ts #EXTINF:8.008, 5.ts #EXT-X-ENDLIST ================================================ FILE: test/fixtures/m3u8/SCTE-35_02.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:8 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:8.008, 1.ts #EXT-X-CUE-OUT:DURATION=23 #EXTINF:8, 2.ts #EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23 #EXTINF:8, 3.ts #EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23 #EXTINF:7, 4.ts #EXT-X-CUE-IN #EXTINF:8.008, 5.ts #EXT-X-ENDLIST ================================================ FILE: test/fixtures/m3u8/SCTE-35_03.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:8 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:8.008, 1.ts #EXT-X-CUE:DURATION="15.0",ID="0",TYPE="SpliceOut",TIME="414.171" #EXTINF:8, 2.ts #EXTINF:7, 3.ts #EXTINF:8.008, 4.ts #EXTINF:8.008, 5.ts #EXT-X-ENDLIST ================================================ FILE: test/fixtures/m3u8/SCTE-35_04.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:8 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:8.008, 1.ts #EXT-OATCLS-SCTE35:/DA0AAAAAAAAAAAABQb+ADAQ6QAeAhxDVUVJQAAAO3/PAAEUrEoICAAAAAAg+2UBNAAANvrtoQ== #EXT-X-ASSET:CAID=0x0000000020FB6501 #EXT-X-CUE-OUT:DURATION=15 #EXTINF:8, 2.ts #EXT-X-CUE-OUT-CONT:ElapsedTime=5.939,Duration=25.0,SCTE35=/DA0AAAA+…AAg+2UBNAAANvrtoQ== #EXTINF:7, 3.ts #EXT-X-CUE-IN #EXTINF:8.008, 4.ts #EXTINF:8.008, 5.ts #EXT-X-ENDLIST ================================================ FILE: test/fixtures/m3u8/SCTE-35_05.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:8 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:8.008, 1.ts #EXT-X-SCTE35:TYPE=0x34,DURATION=15.0,CUE-OUT=YES,UPID="0x08:0x9425BC",CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”pIViS5” #EXTINF:8, 2.ts #EXTINF:7, 3.ts #EXT-X-SCTE35:TYPE=0x35,CUE-IN=YES,CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”f6UrRd” #EXTINF:8.008, 4.ts #EXTINF:8.008, 5.ts #EXT-X-ENDLIST ================================================ FILE: test/fixtures/m3u8/SCTE-35_06.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:8 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:8.008, 1.ts #EXT-X-CUE-OUT:DURATION=23 #EXTINF:8, 2.ts #EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23 #EXTINF:8, 3.ts #EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23 #EXTINF:7, 4.ts #EXT-X-CUE-IN #EXTINF:8.008, 5.ts #EXT-X-CUE-OUT:DURATION=23 #EXTINF:8, 6.ts #EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23 #EXTINF:8, 7.ts #EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23 #EXTINF:7, 8.ts #EXT-X-CUE-IN #EXTINF:8.008, 9.ts #EXT-X-ENDLIST ================================================ FILE: test/fixtures/m3u8/SCTE-35_07.m3u8 ================================================ # This example shows two EXT-X-DATERANGE tags that describe a single # Date Range, with a SCTE-35 "out" splice_insert() command that is # subsequently updated with an SCTE-35 "in" splice_insert() command. #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:30 #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z #EXTINF:30, http://media.example.com/01.ts #EXTINF:30, http://media.example.com/02.ts #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2023-10-09T06:16:00.820Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC30250001D1F7E25300FFF0140565239AA07FEFFE015C3F90FE00526362000101010000A7C1792D #EXTINF:30, http://ads.example.com/ad-01.ts #EXTINF:30, http://ads.example.com/ad-02.ts #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2023-10-09T06:16:00.820Z",END-DATE="2023-10-09T06:17:01.514Z",DURATION=60.694 #EXTINF:30, http://media.example.com/03.ts #EXTINF:3.003, http://media.example.com/04.ts ================================================ FILE: test/fixtures/m3u8/Streaming-Examples_bipbop_16x9_variant.m3u8 ================================================ #EXTM3U #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",NAME="BipBop Audio 1",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="eng" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",NAME="BipBop Audio 2",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="eng",URI="alternate_audio_aac/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/eng/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="en",URI="subtitles/eng_forced/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/fra/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="fr",URI="subtitles/fra_forced/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/spa/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="es",URI="subtitles/spa_forced/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/jpn/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語 (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="ja",URI="subtitles/jpn_forced/prog_index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS="mp4a.40.2, avc1.4d400d",RESOLUTION=416x234,AUDIO="bipbop_audio",SUBTITLES="subs" gear1/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=28451,URI="gear1/iframe_index.m3u8",CODECS="avc1.4d400d" #EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs" gear2/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=181534,URI="gear2/iframe_index.m3u8",CODECS="avc1.4d401e" #EXT-X-STREAM-INF:BANDWIDTH=915905,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=960x540,AUDIO="bipbop_audio",SUBTITLES="subs" gear3/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=297056,URI="gear3/iframe_index.m3u8",CODECS="avc1.4d401f" #EXT-X-STREAM-INF:BANDWIDTH=1030138,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1280x720,AUDIO="bipbop_audio",SUBTITLES="subs" gear4/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=339492,URI="gear4/iframe_index.m3u8",CODECS="avc1.4d401f" #EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs" gear5/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=669554,URI="gear5/iframe_index.m3u8",CODECS="avc1.4d401f" #EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS="mp4a.40.2",AUDIO="bipbop_audio",SUBTITLES="subs" gear0/prog_index.m3u8 ================================================ FILE: test/fixtures/m3u8/Streaming-Examples_img_bipbop_adv_example_ts_master.m3u8 ================================================ #EXTM3U #EXT-X-VERSION:6 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="2",URI="a1/prog_index.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",URI="s1/en/prog_index.m3u8" #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc1",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",INSTREAM-ID="CC1" #EXT-X-STREAM-INF:BANDWIDTH=2227464,AVERAGE-BANDWIDTH=2218327,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v5/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=8178040,AVERAGE-BANDWIDTH=8144656,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v9/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=6453202,AVERAGE-BANDWIDTH=6307144,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v8/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=5054232,AVERAGE-BANDWIDTH=4775338,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v7/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=3289288,AVERAGE-BANDWIDTH=3240596,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v6/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1296989,AVERAGE-BANDWIDTH=1292926,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=768x432,FRAME-RATE=30.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v4/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=922242,AVERAGE-BANDWIDTH=914722,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=30.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v3/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=553010,AVERAGE-BANDWIDTH=541239,CODECS="avc1.640015,mp4a.40.2",RESOLUTION=480x270,FRAME-RATE=30.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v2/prog_index.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="6",URI="a2/prog_index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=2448841,AVERAGE-BANDWIDTH=2439704,CODECS="avc1.640020,ac-3",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v5/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=8399417,AVERAGE-BANDWIDTH=8366033,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v9/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=6674579,AVERAGE-BANDWIDTH=6528521,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v8/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=5275609,AVERAGE-BANDWIDTH=4996715,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v7/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=3510665,AVERAGE-BANDWIDTH=3461973,CODECS="avc1.640020,ac-3",RESOLUTION=1280x720,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v6/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1518366,AVERAGE-BANDWIDTH=1514303,CODECS="avc1.64001e,ac-3",RESOLUTION=768x432,FRAME-RATE=30.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v4/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1143619,AVERAGE-BANDWIDTH=1136099,CODECS="avc1.64001e,ac-3",RESOLUTION=640x360,FRAME-RATE=30.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v3/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=774387,AVERAGE-BANDWIDTH=762616,CODECS="avc1.640015,ac-3",RESOLUTION=480x270,FRAME-RATE=30.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v2/prog_index.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud3",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="6",URI="a3/prog_index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=2256841,AVERAGE-BANDWIDTH=2247704,CODECS="avc1.640020,ec-3",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v5/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=8207417,AVERAGE-BANDWIDTH=8174033,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v9/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=6482579,AVERAGE-BANDWIDTH=6336521,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v8/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=5083609,AVERAGE-BANDWIDTH=4804715,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v7/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=3318665,AVERAGE-BANDWIDTH=3269973,CODECS="avc1.640020,ec-3",RESOLUTION=1280x720,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v6/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1326366,AVERAGE-BANDWIDTH=1322303,CODECS="avc1.64001e,ec-3",RESOLUTION=768x432,FRAME-RATE=30.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v4/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=951619,AVERAGE-BANDWIDTH=944099,CODECS="avc1.64001e,ec-3",RESOLUTION=640x360,FRAME-RATE=30.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v3/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=582387,AVERAGE-BANDWIDTH=570616,CODECS="avc1.640015,ec-3",RESOLUTION=480x270,FRAME-RATE=30.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" v2/prog_index.m3u8 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=186522,AVERAGE-BANDWIDTH=182077,URI="v7/iframe_index.m3u8",CODECS="avc1.64002a",RESOLUTION=1920x1080 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=133856,AVERAGE-BANDWIDTH=129936,URI="v6/iframe_index.m3u8",CODECS="avc1.640020",RESOLUTION=1280x720 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=98136,AVERAGE-BANDWIDTH=94286,URI="v5/iframe_index.m3u8",CODECS="avc1.640020",RESOLUTION=960x540 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=76704,AVERAGE-BANDWIDTH=74767,URI="v4/iframe_index.m3u8",CODECS="avc1.64001e",RESOLUTION=768x432 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=64078,AVERAGE-BANDWIDTH=62251,URI="v3/iframe_index.m3u8",CODECS="avc1.64001e",RESOLUTION=640x360 #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=38728,AVERAGE-BANDWIDTH=37866,URI="v2/iframe_index.m3u8",CODECS="avc1.640015",RESOLUTION=480x270 ================================================ FILE: test/fixtures/objects/8.1-Simple-Media-Playlist.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const playlist = new MediaPlaylist({ version: 3, targetDuration: 10, segments: createSegments(), endlist: true }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'http://media.example.com/first.ts', duration: 9.009, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://media.example.com/second.ts', duration: 9.009, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://media.example.com/third.ts', duration: 3.003, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js ================================================ const {MediaPlaylist, Segment, DateRange} = require('../../../types'); const utils = require('../../../utils'); const playlist = new MediaPlaylist({ version: 3, targetDuration: 30, segments: createSegments() }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'http://media.example.com/01.ts', duration: 30, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0, programDateTime: new Date('2014-03-05T11:14:00Z') })); segments.push(new Segment({ uri: 'http://media.example.com/02.ts', duration: 30, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://ads.example.com/ad-01.ts', duration: 30, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0, dateRange: new DateRange({ id: 'splice-6FFFFFF0', start: new Date('2014-03-05T11:15:00Z'), plannedDuration: 59.993, attributes: { 'SCTE35-OUT': utils.hexToByteSequence('0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000') } }) })); segments.push(new Segment({ uri: 'http://ads.example.com/ad-02.ts', duration: 30, title: '', mediaSequenceNumber: 3, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://media.example.com/03.ts', duration: 30, title: '', mediaSequenceNumber: 4, discontinuitySequence: 0, dateRange: new DateRange({ id: 'splice-6FFFFFF0', duration: 59.993, attributes: { 'SCTE35-IN': utils.hexToByteSequence('0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000') } }) })); segments.push(new Segment({ uri: 'http://media.example.com/04.ts', duration: 3.003, title: '', mediaSequenceNumber: 5, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.11-EXT-X-CUE-OUT-Media-Playlist.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const playlist = new MediaPlaylist({ version: 3, targetDuration: 10, segments: createSegments(), endlist: true }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'http://media.example.com/first.ts', duration: 9.009, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://media.example.com/second.ts', duration: 9.009, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0, markers: [{ type: 'OUT', duration: 15 }] })); segments.push(new Segment({ uri: 'http://media.example.com/third.ts', duration: 3.003, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.2-Live-Media-Playlist_using-HTTPS.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const mediaSequenceBase = 2680; const playlist = new MediaPlaylist({ version: 3, targetDuration: 8, mediaSequenceBase, segments: createSegments() }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'https://priv.example.com/fileSequence2680.ts', duration: 7.975, title: '', mediaSequenceNumber: mediaSequenceBase + 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'https://priv.example.com/fileSequence2681.ts', duration: 7.941, title: '', mediaSequenceNumber: mediaSequenceBase + 1, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'https://priv.example.com/fileSequence2682.ts', duration: 7.975, title: '', mediaSequenceNumber: mediaSequenceBase + 2, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.3-Playlist-with-encrypted-Media-Segments.js ================================================ const {MediaPlaylist, Segment, Key} = require('../../../types'); const mediaSequenceBase = 7794; const key1 = new Key({method: 'AES-128', uri: 'https://priv.example.com/key.php?r=52'}); const key2 = new Key({method: 'AES-128', uri: 'https://priv.example.com/key.php?r=53'}); const playlist = new MediaPlaylist({ version: 3, targetDuration: 15, mediaSequenceBase, segments: createSegments() }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'http://media.example.com/fileSequence52-A.ts', duration: 2.833, title: '', mediaSequenceNumber: mediaSequenceBase + 0, discontinuitySequence: 0, key: key1 })); segments.push(new Segment({ uri: 'http://media.example.com/fileSequence52-B.ts', duration: 15.0, title: '', mediaSequenceNumber: mediaSequenceBase + 1, discontinuitySequence: 0, key: key1 })); segments.push(new Segment({ uri: 'http://media.example.com/fileSequence52-C.ts', duration: 13.333, title: '', mediaSequenceNumber: mediaSequenceBase + 2, discontinuitySequence: 0, key: key1 })); segments.push(new Segment({ uri: 'http://media.example.com/fileSequence53-A.ts', duration: 15.0, title: '', mediaSequenceNumber: mediaSequenceBase + 3, discontinuitySequence: 0, key: key2 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.4-Master-Playlist.js ================================================ const {MasterPlaylist, Variant} = require('../../../types'); const playlist = new MasterPlaylist({ variants: createVariants() }); function createVariants() { const variants = []; variants.push(new Variant({ uri: 'http://example.com/low.m3u8', bandwidth: 1280000, averageBandwidth: 1000000, codecs: 'avc1.640029,mp4a.40.2' })); variants.push(new Variant({ uri: 'http://example.com/mid.m3u8', bandwidth: 2560000, averageBandwidth: 2000000, codecs: 'avc1.640029,mp4a.40.2' })); variants.push(new Variant({ uri: 'http://example.com/hi.m3u8', bandwidth: 7680000, averageBandwidth: 6000000, codecs: 'avc1.640029,mp4a.40.2' })); variants.push(new Variant({ uri: 'http://example.com/audio-only.m3u8', bandwidth: 65000, codecs: 'mp4a.40.5' })); return variants; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.5-Master-Playlist-with-I-Frames.js ================================================ const {MasterPlaylist, Variant} = require('../../../types'); const playlist = new MasterPlaylist({ variants: createVariants() }); function createVariants() { const variants = []; variants.push(new Variant({ uri: 'low/audio-video.m3u8', bandwidth: 1280000, codecs: 'avc1.640029,mp4a.40.2' })); variants.push(new Variant({ uri: 'low/iframe.m3u8', isIFrameOnly: true, bandwidth: 86000, codecs: 'avc1.640029' })); variants.push(new Variant({ uri: 'mid/audio-video.m3u8', bandwidth: 2560000, codecs: 'avc1.640029,mp4a.40.2' })); variants.push(new Variant({ uri: 'mid/iframe.m3u8', isIFrameOnly: true, bandwidth: 150000, codecs: 'avc1.640029' })); variants.push(new Variant({ uri: 'hi/audio-video.m3u8', bandwidth: 7680000, codecs: 'avc1.640029,mp4a.40.2' })); variants.push(new Variant({ uri: 'hi/iframe.m3u8', isIFrameOnly: true, bandwidth: 550000, codecs: 'avc1.640029' })); variants.push(new Variant({ uri: 'audio-only.m3u8', bandwidth: 65000, codecs: 'mp4a.40.5' })); return variants; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.6-Master-Playlist-with-Alternative-audio.js ================================================ const {MasterPlaylist, Variant, Rendition} = require('../../../types'); const renditions = createRendition(); function createRendition() { const renditions = []; renditions.push(new Rendition({ type: 'AUDIO', uri: 'main/english-audio.m3u8', groupId: 'aac', language: 'en', name: 'English', isDefault: true, autoselect: true })); renditions.push(new Rendition({ type: 'AUDIO', uri: 'main/german-audio.m3u8', groupId: 'aac', language: 'de', name: 'Deutsch', isDefault: false, autoselect: true })); renditions.push(new Rendition({ type: 'AUDIO', uri: 'commentary/audio-only.m3u8', groupId: 'aac', language: 'en', name: 'Commentary', isDefault: false, autoselect: false })); return renditions; } const playlist = new MasterPlaylist({ variants: createVariants() }); function createVariants() { const variants = []; variants.push(new Variant({ uri: 'low/video-only.m3u8', bandwidth: 1280000, codecs: 'mp4a.40.2', audio: renditions, currentRenditions: {audio: 0} })); variants.push(new Variant({ uri: 'mid/video-only.m3u8', bandwidth: 2560000, codecs: 'mp4a.40.2', audio: renditions, currentRenditions: {audio: 0} })); variants.push(new Variant({ uri: 'hi/video-only.m3u8', bandwidth: 7680000, codecs: 'mp4a.40.2', audio: renditions, currentRenditions: {audio: 0} })); variants.push(new Variant({ uri: 'main/english-audio.m3u8', bandwidth: 65000, codecs: 'mp4a.40.5', audio: renditions, currentRenditions: {audio: 0} })); return variants; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.7-Master-Playlist-with-Alternative-video.js ================================================ const {MasterPlaylist, Variant, Rendition} = require('../../../types'); function createRendition(groupId) { const renditions = []; renditions.push(new Rendition({ type: 'VIDEO', uri: `${groupId}/main/audio-video.m3u8`, groupId, name: 'Main', isDefault: !(groupId === 'mid') })); renditions.push(new Rendition({ type: 'VIDEO', uri: `${groupId}/centerfield/audio-video.m3u8`, groupId, name: 'Centerfield', isDefault: groupId === 'mid' })); renditions.push(new Rendition({ type: 'VIDEO', uri: `${groupId}/dugout/audio-video.m3u8`, groupId, name: 'Dugout', isDefault: false })); return renditions; } const playlist = new MasterPlaylist({ variants: createVariants() }); function createVariants() { const variants = []; variants.push(new Variant({ uri: 'low/main/audio-video.m3u8', bandwidth: 1280000, codecs: 'avc1.640029,mp4a.40.2', video: createRendition('low'), currentRenditions: {video: 0} })); variants.push(new Variant({ uri: 'mid/main/audio-video.m3u8', bandwidth: 2560000, codecs: 'avc1.640029,mp4a.40.2', video: createRendition('mid'), currentRenditions: {video: 1} })); variants.push(new Variant({ uri: 'hi/main/audio-video.m3u8', bandwidth: 7680000, codecs: 'avc1.640029,mp4a.40.2', video: createRendition('hi') })); return variants; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.8-Session-Data-in-a-Master-Playlist.js ================================================ const {MasterPlaylist, SessionData} = require('../../../types'); const playlist = new MasterPlaylist({ sessionDataList: createSetssionDataList() }); function createSetssionDataList() { const setssionDataList = []; setssionDataList.push(new SessionData({ id: 'com.example.lyrics', uri: 'lyrics.json' })); setssionDataList.push(new SessionData({ id: 'com.example.title', language: 'en', value: 'This is an example' })); setssionDataList.push(new SessionData({ id: 'com.example.title', language: 'es', value: 'Este es un ejemplo' })); return setssionDataList; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js ================================================ const {MasterPlaylist, Variant, Rendition} = require('../../../types'); const renditions = createRendition(); function createRendition() { const renditions = []; renditions.push(new Rendition({ type: 'AUDIO', uri: 'main/english-audio.m3u8', groupId: 'aac', language: 'en', name: 'English', isDefault: true, autoselect: true, characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read' })); renditions.push(new Rendition({ type: 'AUDIO', uri: 'main/german-audio.m3u8', groupId: 'aac', language: 'de', name: 'Deutsch', isDefault: false, autoselect: true, characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read' })); renditions.push(new Rendition({ type: 'AUDIO', uri: 'commentary/audio-only.m3u8', groupId: 'aac', language: 'en', name: 'Commentary', isDefault: false, autoselect: false, characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read' })); return renditions; } const playlist = new MasterPlaylist({ variants: createVariants() }); function createVariants() { const variants = []; variants.push(new Variant({ uri: 'low/video-only.m3u8', bandwidth: 1280000, codecs: 'mp4a.40.2', audio: renditions, currentRenditions: {audio: 0} })); variants.push(new Variant({ uri: 'mid/video-only.m3u8', bandwidth: 2560000, codecs: 'mp4a.40.2', audio: renditions, currentRenditions: {audio: 0} })); variants.push(new Variant({ uri: 'hi/video-only.m3u8', bandwidth: 7680000, codecs: 'mp4a.40.2', audio: renditions, currentRenditions: {audio: 0} })); variants.push(new Variant({ uri: 'main/english-audio.m3u8', bandwidth: 65000, codecs: 'mp4a.40.5', audio: renditions, currentRenditions: {audio: 0} })); return variants; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/Low-Latency_Example-01_Low-Latency_HLS_Playlist.js ================================================ const {MediaPlaylist, Segment, PartialSegment, MediaInitializationSection, RenditionReport} = require('../../../types'); const playlist = new MediaPlaylist({ version: 6, targetDuration: 4, mediaSequenceBase: 266, lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24, partHoldBack: 1}, partTargetDuration: 0.33334, segments: createSegments(), renditionReports: createRenditionReports() }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'fileSequence266.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 266, discontinuitySequence: 0, programDateTime: new Date('2019-02-14T02:13:36.106Z'), map: new MediaInitializationSection({uri: 'init.mp4'}) })); segments.push(new Segment({ uri: 'fileSequence267.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 267, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence268.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 268, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence269.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 269, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence270.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 270, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence271.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 271, discontinuitySequence: 0, parts: createParts1() })); segments.push(new Segment({ uri: 'fileSequence272.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 272, discontinuitySequence: 0, programDateTime: new Date('2019-02-14T02:14:00.106Z'), parts: createParts2() })); segments.push(new Segment({ mediaSequenceNumber: 273, parts: createParts3() })); return segments; } function createRenditionReports() { const reports = []; reports.push(new RenditionReport({ uri: '../1M/waitForMSN.php', lastMSN: 273, lastPart: 2 })); reports.push(new RenditionReport({ uri: '../4M/waitForMSN.php', lastMSN: 273, lastPart: 1 })); return reports; } function createParts1() { const parts = []; for (let i = 0; i < 12; i++) { parts.push(new PartialSegment({ uri: `filePart271.${i}.mp4`, duration: 0.33334, independent: (i === 4 || i === 8) })); } return parts; } function createParts2() { const parts = []; const aCode = 'a'.charCodeAt(0); for (let i = 0; i < 12; i++) { parts.push(new PartialSegment({ uri: `filePart272.${String.fromCharCode(aCode + i)}.mp4`, duration: 0.33334, independent: (i === 5) })); } return parts; } function createParts3() { const parts = []; for (let i = 0; i < 4; i++) { parts.push(new PartialSegment({ uri: `filePart273.${i}.mp4`, duration: 0.33334, independent: (i === 0), hint: (i === 3) })); } return parts; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/Low-Latency_Example-02_Playlist_Delta_Update.js ================================================ const {MediaPlaylist, Segment, PartialSegment, RenditionReport} = require('../../../types'); const playlist = new MediaPlaylist({ version: 9, targetDuration: 4, mediaSequenceBase: 266, lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.0}, partTargetDuration: 0.33334, skip: 3, segments: createSegments(), renditionReports: createRenditionReports() }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'fileSequence269.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 269, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence270.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 270, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence271.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 271, discontinuitySequence: 0, parts: createParts1() })); segments.push(new Segment({ uri: 'fileSequence272.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 272, discontinuitySequence: 0, programDateTime: new Date('2019-02-14T02:14:00.106Z'), parts: createParts2() })); segments.push(new Segment({ mediaSequenceNumber: 273, parts: createParts3() })); return segments; } function createRenditionReports() { const reports = []; reports.push(new RenditionReport({ uri: '../1M/waitForMSN.php', lastMSN: 273, lastPart: 3 })); reports.push(new RenditionReport({ uri: '../4M/waitForMSN.php', lastMSN: 273, lastPart: 3 })); return reports; } function createParts1() { const parts = []; for (let i = 0; i < 12; i++) { parts.push(new PartialSegment({ uri: `filePart271.${i}.mp4`, duration: 0.33334, independent: (i === 4 || i === 8) })); } return parts; } function createParts2() { const parts = []; const aCode = 'a'.charCodeAt(0); for (let i = 0; i < 12; i++) { parts.push(new PartialSegment({ uri: `filePart272.${String.fromCharCode(aCode + i)}.mp4`, duration: 0.33334, independent: (i === 5) })); } return parts; } function createParts3() { const parts = []; for (let i = 0; i < 5; i++) { parts.push(new PartialSegment({ uri: `filePart273.${i}.mp4`, duration: 0.33334, independent: (i === 0), hint: (i === 4) })); } return parts; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-01.js ================================================ const {MediaPlaylist, Segment, PartialSegment} = require('../../../types'); const playlist = new MediaPlaylist({ version: 9, targetDuration: 4, mediaSequenceBase: 266, lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02}, partTargetDuration: 1.02, skip: 3, segments: createSegments() }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'fileSequence269.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 269, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence270.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 270, discontinuitySequence: 0 })); segments.push(new Segment({ mediaSequenceNumber: 271, parts: createParts() })); return segments; } function createParts() { const parts = []; parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 0, length: 20000} })); parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 20000, length: 23000} })); parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 43000, length: 18000} })); parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', byterange: {offset: 61000}, hint: true })); return parts; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-02.js ================================================ const {MediaPlaylist, Segment, PartialSegment} = require('../../../types'); const playlist = new MediaPlaylist({ version: 9, targetDuration: 4, mediaSequenceBase: 266, lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02}, partTargetDuration: 1.02, skip: 3, segments: createSegments() }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'fileSequence269.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 269, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence270.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 270, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence271.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 271, discontinuitySequence: 0, parts: createParts() })); segments.push(new Segment({ mediaSequenceNumber: 272, parts: [new PartialSegment({ uri: 'fileSequence272.mp4', byterange: {offset: 0}, hint: true })] })); return segments; } function createParts() { const parts = []; parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 0, length: 20000} })); parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 20000, length: 23000} })); parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 43000, length: 18000} })); parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 61000, length: 19000} })); return parts; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-03.js ================================================ const {MediaPlaylist, Segment, PartialSegment} = require('../../../types'); const playlist = new MediaPlaylist({ version: 9, targetDuration: 4, mediaSequenceBase: 266, lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02}, partTargetDuration: 1.02, skip: 3, segments: createSegments() }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'fileSequence269.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 269, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence270.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 270, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'fileSequence271.mp4', duration: 4.00008, title: '', mediaSequenceNumber: 271, discontinuitySequence: 0, parts: createParts() })); segments.push(new Segment({ mediaSequenceNumber: 272, parts: [ new PartialSegment({ uri: 'fileSequence272.mp4', duration: 1.02, byterange: {offset: 0, length: 21000} }), new PartialSegment({ uri: 'fileSequence272.mp4', byterange: {offset: 21000}, hint: true }) ] })); return segments; } function createParts() { const parts = []; parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 0, length: 20000} })); parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 20000, length: 23000} })); parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 43000, length: 18000} })); parts.push(new PartialSegment({ uri: 'fileSequence271.mp4', duration: 1.02, byterange: {offset: 61000, length: 19000} })); return parts; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/Multiple-rendition-groups.js ================================================ const {MasterPlaylist, Variant, Rendition} = require('../../../types'); const renditions = [ new Rendition({type: 'AUDIO', groupId: 'aac_high', name: 'English', isDefault: true, uri: 'aac_high_eng.m3u8'}), new Rendition({type: 'AUDIO', groupId: 'aac_high', name: 'Japanese', isDefault: false, uri: 'aac_high_jp.m3u8'}), new Rendition({type: 'AUDIO', groupId: 'aac_mid', name: 'English', isDefault: true, uri: 'aac_mid_eng.m3u8'}), new Rendition({type: 'AUDIO', groupId: 'aac_mid', name: 'Japanese', isDefault: false, uri: 'aac_mid_jp.m3u8'}), new Rendition({type: 'AUDIO', groupId: 'aac_low', name: 'English', isDefault: true, uri: 'aac_low_eng.m3u8'}), new Rendition({type: 'AUDIO', groupId: 'aac_low', name: 'Japanese', isDefault: false, uri: 'aac_low_jp.m3u8'}), ]; const variants = [ {uri: '1080p.m3u8', bandwidth: 6000000, audioId: 'aac_high'}, {uri: '720p.m3u8', bandwidth: 3000000, audioId: 'aac_mid'}, {uri: '540p.m3u8', bandwidth: 1500000, audioId: 'aac_mid'}, {uri: '360p.m3u8', bandwidth: 1000000, audioId: 'aac_low'}, ].map( ({uri, bandwidth, audioId}) => new Variant({ uri, bandwidth, audio: renditions.filter(({groupId}) => groupId === audioId) }) ); const playlist = new MasterPlaylist({ version: 4, independentSegments: true, variants, }); module.exports = playlist; ================================================ FILE: test/fixtures/objects/RedundantSegments.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const playlist = new MediaPlaylist({ version: 4, targetDuration: 10, segments: createSegments(), endlist: true }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'http://media.example.com/first.ts', duration: 9.009, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://media.example.com/second.ts', duration: 9.009, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://media.example.com/second.ts', byterange: {offset: 256, length: 128}, duration: 9.009, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://media.example.com/third.ts', duration: 3.003, title: '', mediaSequenceNumber: 3, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/SCTE-35_01.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const playlist = new MediaPlaylist({ playlistType: 'VOD', version: 3, targetDuration: 8, segments: createSegments(), endlist: true }); function createSegments() { const segments = []; segments.push(new Segment({ uri: '1.ts', duration: 8.008, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '2.ts', duration: 8, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0, markers: [{ type: 'OUT', duration: 15.0 }] })); segments.push(new Segment({ uri: '3.ts', duration: 7, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '4.ts', duration: 8.008, title: '', mediaSequenceNumber: 3, discontinuitySequence: 0, markers: [{ type: 'IN' }] })); segments.push(new Segment({ uri: '5.ts', duration: 8.008, title: '', mediaSequenceNumber: 4, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/SCTE-35_02.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const playlist = new MediaPlaylist({ playlistType: 'VOD', version: 3, targetDuration: 8, segments: createSegments(), endlist: true }); function createSegments() { const segments = []; segments.push(new Segment({ uri: '1.ts', duration: 8.008, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '2.ts', duration: 8, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0, markers: [{ type: 'OUT', duration: 23.0 }] })); segments.push(new Segment({ uri: '3.ts', duration: 8, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-CUE-OUT-CONT', value: 'ElapsedTime=8,Duration=23' }] })); segments.push(new Segment({ uri: '4.ts', duration: 7, title: '', mediaSequenceNumber: 3, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-CUE-OUT-CONT', value: 'ElapsedTime=16,Duration=23' }] })); segments.push(new Segment({ uri: '5.ts', duration: 8.008, title: '', mediaSequenceNumber: 4, discontinuitySequence: 0, markers: [{ type: 'IN' }] })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/SCTE-35_03.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const playlist = new MediaPlaylist({ playlistType: 'VOD', version: 3, targetDuration: 8, segments: createSegments(), endlist: true }); function createSegments() { const segments = []; segments.push(new Segment({ uri: '1.ts', duration: 8.008, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '2.ts', duration: 8, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-CUE', value: 'DURATION="15.0",ID="0",TYPE="SpliceOut",TIME="414.171"' }] })); segments.push(new Segment({ uri: '3.ts', duration: 7, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '4.ts', duration: 8.008, title: '', mediaSequenceNumber: 3, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '5.ts', duration: 8.008, title: '', mediaSequenceNumber: 4, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/SCTE-35_04.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const playlist = new MediaPlaylist({ playlistType: 'VOD', version: 3, targetDuration: 8, segments: createSegments(), endlist: true }); function createSegments() { const segments = []; segments.push(new Segment({ uri: '1.ts', duration: 8.008, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '2.ts', duration: 8, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0, markers: [ { type: 'RAW', tagName: 'EXT-OATCLS-SCTE35', value: '/DA0AAAAAAAAAAAABQb+ADAQ6QAeAhxDVUVJQAAAO3/PAAEUrEoICAAAAAAg+2UBNAAANvrtoQ==' }, { type: 'RAW', tagName: 'EXT-X-ASSET', value: 'CAID=0x0000000020FB6501' }, { type: 'OUT', duration: 15.0 } ] })); segments.push(new Segment({ uri: '3.ts', duration: 7, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-CUE-OUT-CONT', value: 'ElapsedTime=5.939,Duration=25.0,SCTE35=/DA0AAAA+…AAg+2UBNAAANvrtoQ==' }] })); segments.push(new Segment({ uri: '4.ts', duration: 8.008, title: '', mediaSequenceNumber: 3, discontinuitySequence: 0, markers: [{ type: 'IN' }] })); segments.push(new Segment({ uri: '5.ts', duration: 8.008, title: '', mediaSequenceNumber: 4, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/SCTE-35_05.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const playlist = new MediaPlaylist({ playlistType: 'VOD', version: 3, targetDuration: 8, segments: createSegments(), endlist: true }); function createSegments() { const segments = []; segments.push(new Segment({ uri: '1.ts', duration: 8.008, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '2.ts', duration: 8, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-SCTE35', value: 'TYPE=0x34,DURATION=15.0,CUE-OUT=YES,UPID="0x08:0x9425BC",CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”pIViS5”' }] })); segments.push(new Segment({ uri: '3.ts', duration: 7, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '4.ts', duration: 8.008, title: '', mediaSequenceNumber: 3, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-SCTE35', value: 'TYPE=0x35,CUE-IN=YES,CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”f6UrRd”' }] })); segments.push(new Segment({ uri: '5.ts', duration: 8.008, title: '', mediaSequenceNumber: 4, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/SCTE-35_06.js ================================================ const {MediaPlaylist, Segment} = require('../../../types'); const playlist = new MediaPlaylist({ playlistType: 'VOD', version: 3, targetDuration: 8, segments: createSegments(), endlist: true }); function createSegments() { const segments = []; segments.push(new Segment({ uri: '1.ts', duration: 8.008, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0 })); segments.push(new Segment({ uri: '2.ts', duration: 8, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0, markers: [{ type: 'OUT', duration: 23.0 }] })); segments.push(new Segment({ uri: '3.ts', duration: 8, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-CUE-OUT-CONT', value: 'ElapsedTime=8,Duration=23' }] })); segments.push(new Segment({ uri: '4.ts', duration: 7, title: '', mediaSequenceNumber: 3, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-CUE-OUT-CONT', value: 'ElapsedTime=16,Duration=23' }] })); segments.push(new Segment({ uri: '5.ts', duration: 8.008, title: '', mediaSequenceNumber: 4, discontinuitySequence: 0, markers: [{ type: 'IN' }] })); segments.push(new Segment({ uri: '6.ts', duration: 8, title: '', mediaSequenceNumber: 5, discontinuitySequence: 0, markers: [{ type: 'OUT', duration: 23.0 }] })); segments.push(new Segment({ uri: '7.ts', duration: 8, title: '', mediaSequenceNumber: 6, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-CUE-OUT-CONT', value: 'ElapsedTime=8,Duration=23' }] })); segments.push(new Segment({ uri: '8.ts', duration: 7, title: '', mediaSequenceNumber: 7, discontinuitySequence: 0, markers: [{ type: 'RAW', tagName: 'EXT-X-CUE-OUT-CONT', value: 'ElapsedTime=16,Duration=23' }] })); segments.push(new Segment({ uri: '9.ts', duration: 8.008, title: '', mediaSequenceNumber: 8, discontinuitySequence: 0, markers: [{ type: 'IN' }] })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/SCTE-35_07.js ================================================ const {MediaPlaylist, Segment, DateRange} = require('../../../types'); const utils = require('../../../utils'); const playlist = new MediaPlaylist({ version: 3, targetDuration: 30, segments: createSegments() }); function createSegments() { const segments = []; segments.push(new Segment({ uri: 'http://media.example.com/01.ts', duration: 30, title: '', mediaSequenceNumber: 0, discontinuitySequence: 0, programDateTime: new Date('2014-03-05T11:14:00Z') })); segments.push(new Segment({ uri: 'http://media.example.com/02.ts', duration: 30, title: '', mediaSequenceNumber: 1, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://ads.example.com/ad-01.ts', duration: 30, title: '', mediaSequenceNumber: 2, discontinuitySequence: 0, dateRange: new DateRange({ id: 'splice-6FFFFFF0', start: new Date('2023-10-09T06:16:00.820Z'), plannedDuration: 59.993, attributes: { 'SCTE35-OUT': utils.hexToByteSequence('0xFC30250001D1F7E25300FFF0140565239AA07FEFFE015C3F90FE00526362000101010000A7C1792D') } }) })); segments.push(new Segment({ uri: 'http://ads.example.com/ad-02.ts', duration: 30, title: '', mediaSequenceNumber: 3, discontinuitySequence: 0 })); segments.push(new Segment({ uri: 'http://media.example.com/03.ts', duration: 30, title: '', mediaSequenceNumber: 4, discontinuitySequence: 0, dateRange: new DateRange({ id: 'splice-6FFFFFF0', start: new Date('2023-10-09T06:16:00.820Z'), end: new Date('2023-10-09T06:17:01.514Z'), duration: 60.694 }) })); segments.push(new Segment({ uri: 'http://media.example.com/04.ts', duration: 3.003, title: '', mediaSequenceNumber: 5, discontinuitySequence: 0 })); return segments; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/Streaming-Examples_bipbop_16x9_variant.js ================================================ const {MasterPlaylist, Variant, Rendition} = require('../../../types'); const renditions = { bipbop_audio: [ new Rendition({ type: 'AUDIO', groupId: 'bipbop_audio', language: 'eng', name: 'BipBop Audio 1', autoselect: true, isDefault: true }), new Rendition({ type: 'AUDIO', uri: 'alternate_audio_aac/prog_index.m3u8', groupId: 'bipbop_audio', language: 'eng', name: 'BipBop Audio 2', autoselect: false, isDefault: false }) ], subs: [ new Rendition({ type: 'SUBTITLES', uri: 'subtitles/eng/prog_index.m3u8', groupId: 'subs', language: 'en', name: 'English', autoselect: true, isDefault: true, forced: false, characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound' }), new Rendition({ type: 'SUBTITLES', uri: 'subtitles/eng_forced/prog_index.m3u8', groupId: 'subs', language: 'en', name: 'English (Forced)', autoselect: false, isDefault: false, forced: true }), new Rendition({ type: 'SUBTITLES', uri: 'subtitles/fra/prog_index.m3u8', groupId: 'subs', language: 'fr', name: 'Français', autoselect: true, isDefault: false, forced: false, characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound' }), new Rendition({ type: 'SUBTITLES', uri: 'subtitles/fra_forced/prog_index.m3u8', groupId: 'subs', language: 'fr', name: 'Français (Forced)', autoselect: false, isDefault: false, forced: true }), new Rendition({ type: 'SUBTITLES', uri: 'subtitles/spa/prog_index.m3u8', groupId: 'subs', language: 'es', name: 'Español', autoselect: true, isDefault: false, forced: false, characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound' }), new Rendition({ type: 'SUBTITLES', uri: 'subtitles/spa_forced/prog_index.m3u8', groupId: 'subs', language: 'es', name: 'Español (Forced)', autoselect: false, isDefault: false, forced: true }), new Rendition({ type: 'SUBTITLES', uri: 'subtitles/jpn/prog_index.m3u8', groupId: 'subs', language: 'ja', name: '日本語', autoselect: true, isDefault: false, forced: false, characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound' }), new Rendition({ type: 'SUBTITLES', uri: 'subtitles/jpn_forced/prog_index.m3u8', groupId: 'subs', language: 'ja', name: '日本語 (Forced)', autoselect: false, isDefault: false, forced: true }) ] }; const playlist = new MasterPlaylist({ variants: createVariants() }); function createVariants() { const variants = []; variants.push(new Variant({ uri: 'gear1/prog_index.m3u8', bandwidth: 263851, codecs: 'mp4a.40.2, avc1.4d400d', resolution: {width: 416, height: 234}, audio: renditions.bipbop_audio, subtitles: renditions.subs })); variants.push(new Variant({ uri: 'gear1/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 28451, codecs: 'avc1.4d400d' })); variants.push(new Variant({ uri: 'gear2/prog_index.m3u8', bandwidth: 577610, codecs: 'mp4a.40.2, avc1.4d401e', resolution: {width: 640, height: 360}, audio: renditions.bipbop_audio, subtitles: renditions.subs })); variants.push(new Variant({ uri: 'gear2/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 181534, codecs: 'avc1.4d401e' })); variants.push(new Variant({ uri: 'gear3/prog_index.m3u8', bandwidth: 915905, codecs: 'mp4a.40.2, avc1.4d401f', resolution: {width: 960, height: 540}, audio: renditions.bipbop_audio, subtitles: renditions.subs })); variants.push(new Variant({ uri: 'gear3/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 297056, codecs: 'avc1.4d401f' })); variants.push(new Variant({ uri: 'gear4/prog_index.m3u8', bandwidth: 1030138, codecs: 'mp4a.40.2, avc1.4d401f', resolution: {width: 1280, height: 720}, audio: renditions.bipbop_audio, subtitles: renditions.subs })); variants.push(new Variant({ uri: 'gear4/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 339492, codecs: 'avc1.4d401f' })); variants.push(new Variant({ uri: 'gear5/prog_index.m3u8', bandwidth: 1924009, codecs: 'mp4a.40.2, avc1.4d401f', resolution: {width: 1920, height: 1080}, audio: renditions.bipbop_audio, subtitles: renditions.subs })); variants.push(new Variant({ uri: 'gear5/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 669554, codecs: 'avc1.4d401f' })); variants.push(new Variant({ uri: 'gear0/prog_index.m3u8', bandwidth: 41457, codecs: 'mp4a.40.2', audio: renditions.bipbop_audio, subtitles: renditions.subs })); return variants; } module.exports = playlist; ================================================ FILE: test/fixtures/objects/Streaming-Examples_img_bipbop_adv_example_ts_master.js ================================================ const {MasterPlaylist, Variant, Rendition} = require('../../../types'); const renditions = { aud1: [ new Rendition({ type: 'AUDIO', uri: 'a1/prog_index.m3u8', groupId: 'aud1', language: 'en', name: 'English', autoselect: true, isDefault: true, channels: '2' }) ], aud2: [ new Rendition({ type: 'AUDIO', uri: 'a2/prog_index.m3u8', groupId: 'aud2', language: 'en', name: 'English', autoselect: true, isDefault: true, channels: '6' }) ], aud3: [ new Rendition({ type: 'AUDIO', uri: 'a3/prog_index.m3u8', groupId: 'aud3', language: 'en', name: 'English', autoselect: true, isDefault: true, channels: '6' }) ], cc1: [ new Rendition({ type: 'CLOSED-CAPTIONS', groupId: 'cc1', language: 'en', name: 'English', autoselect: true, isDefault: true, instreamId: 'CC1' }) ], sub1: [ new Rendition({ type: 'SUBTITLES', uri: 's1/en/prog_index.m3u8', groupId: 'sub1', language: 'en', name: 'English', autoselect: true, isDefault: true, forced: false }) ] }; const playlist = new MasterPlaylist({ version: 6, independentSegments: true, variants: createVariants() }); function createVariants() { const variants = []; variants.push(new Variant({ uri: 'v5/prog_index.m3u8', bandwidth: 2227464, averageBandwidth: 2218327, codecs: 'avc1.640020,mp4a.40.2', resolution: {width: 960, height: 540}, frameRate: 60.0, audio: renditions.aud1, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v9/prog_index.m3u8', bandwidth: 8178040, averageBandwidth: 8144656, codecs: 'avc1.64002a,mp4a.40.2', resolution: {width: 1920, height: 1080}, frameRate: 60.0, audio: renditions.aud1, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v8/prog_index.m3u8', bandwidth: 6453202, averageBandwidth: 6307144, codecs: 'avc1.64002a,mp4a.40.2', resolution: {width: 1920, height: 1080}, frameRate: 60.0, audio: renditions.aud1, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v7/prog_index.m3u8', bandwidth: 5054232, averageBandwidth: 4775338, codecs: 'avc1.64002a,mp4a.40.2', resolution: {width: 1920, height: 1080}, frameRate: 60.0, audio: renditions.aud1, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v6/prog_index.m3u8', bandwidth: 3289288, averageBandwidth: 3240596, codecs: 'avc1.640020,mp4a.40.2', resolution: {width: 1280, height: 720}, frameRate: 60.0, audio: renditions.aud1, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v4/prog_index.m3u8', bandwidth: 1296989, averageBandwidth: 1292926, codecs: 'avc1.64001e,mp4a.40.2', resolution: {width: 768, height: 432}, frameRate: 30.0, audio: renditions.aud1, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v3/prog_index.m3u8', bandwidth: 922242, averageBandwidth: 914722, codecs: 'avc1.64001e,mp4a.40.2', resolution: {width: 640, height: 360}, frameRate: 30.0, audio: renditions.aud1, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v2/prog_index.m3u8', bandwidth: 553010, averageBandwidth: 541239, codecs: 'avc1.640015,mp4a.40.2', resolution: {width: 480, height: 270}, frameRate: 30.0, audio: renditions.aud1, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v5/prog_index.m3u8', bandwidth: 2448841, averageBandwidth: 2439704, codecs: 'avc1.640020,ac-3', resolution: {width: 960, height: 540}, frameRate: 60.0, audio: renditions.aud2, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v9/prog_index.m3u8', bandwidth: 8399417, averageBandwidth: 8366033, codecs: 'avc1.64002a,ac-3', resolution: {width: 1920, height: 1080}, frameRate: 60.0, audio: renditions.aud2, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v8/prog_index.m3u8', bandwidth: 6674579, averageBandwidth: 6528521, codecs: 'avc1.64002a,ac-3', resolution: {width: 1920, height: 1080}, frameRate: 60.0, audio: renditions.aud2, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v7/prog_index.m3u8', bandwidth: 5275609, averageBandwidth: 4996715, codecs: 'avc1.64002a,ac-3', resolution: {width: 1920, height: 1080}, frameRate: 60.0, audio: renditions.aud2, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v6/prog_index.m3u8', bandwidth: 3510665, averageBandwidth: 3461973, codecs: 'avc1.640020,ac-3', resolution: {width: 1280, height: 720}, frameRate: 60.0, audio: renditions.aud2, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v4/prog_index.m3u8', bandwidth: 1518366, averageBandwidth: 1514303, codecs: 'avc1.64001e,ac-3', resolution: {width: 768, height: 432}, frameRate: 30.0, audio: renditions.aud2, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v3/prog_index.m3u8', bandwidth: 1143619, averageBandwidth: 1136099, codecs: 'avc1.64001e,ac-3', resolution: {width: 640, height: 360}, frameRate: 30.0, audio: renditions.aud2, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v2/prog_index.m3u8', bandwidth: 774387, averageBandwidth: 762616, codecs: 'avc1.640015,ac-3', resolution: {width: 480, height: 270}, frameRate: 30.0, audio: renditions.aud2, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v5/prog_index.m3u8', bandwidth: 2256841, averageBandwidth: 2247704, codecs: 'avc1.640020,ec-3', resolution: {width: 960, height: 540}, frameRate: 60.0, audio: renditions.aud3, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v9/prog_index.m3u8', bandwidth: 8207417, averageBandwidth: 8174033, codecs: 'avc1.64002a,ec-3', resolution: {width: 1920, height: 1080}, frameRate: 60.0, audio: renditions.aud3, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v8/prog_index.m3u8', bandwidth: 6482579, averageBandwidth: 6336521, codecs: 'avc1.64002a,ec-3', resolution: {width: 1920, height: 1080}, frameRate: 60.0, audio: renditions.aud3, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v7/prog_index.m3u8', bandwidth: 5083609, averageBandwidth: 4804715, codecs: 'avc1.64002a,ec-3', resolution: {width: 1920, height: 1080}, frameRate: 60.0, audio: renditions.aud3, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v6/prog_index.m3u8', bandwidth: 3318665, averageBandwidth: 3269973, codecs: 'avc1.640020,ec-3', resolution: {width: 1280, height: 720}, frameRate: 60.0, audio: renditions.aud3, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v4/prog_index.m3u8', bandwidth: 1326366, averageBandwidth: 1322303, codecs: 'avc1.64001e,ec-3', resolution: {width: 768, height: 432}, frameRate: 30.0, audio: renditions.aud3, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v3/prog_index.m3u8', bandwidth: 951619, averageBandwidth: 944099, codecs: 'avc1.64001e,ec-3', resolution: {width: 640, height: 360}, frameRate: 30.0, audio: renditions.aud3, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v2/prog_index.m3u8', bandwidth: 582387, averageBandwidth: 570616, codecs: 'avc1.640015,ec-3', resolution: {width: 480, height: 270}, frameRate: 30.0, audio: renditions.aud3, subtitles: renditions.sub1, closedCaptions: renditions.cc1 })); variants.push(new Variant({ uri: 'v7/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 186522, averageBandwidth: 182077, codecs: 'avc1.64002a', resolution: {width: 1920, height: 1080} })); variants.push(new Variant({ uri: 'v6/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 133856, averageBandwidth: 129936, codecs: 'avc1.640020', resolution: {width: 1280, height: 720} })); variants.push(new Variant({ uri: 'v5/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 98136, averageBandwidth: 94286, codecs: 'avc1.640020', resolution: {width: 960, height: 540} })); variants.push(new Variant({ uri: 'v4/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 76704, averageBandwidth: 74767, codecs: 'avc1.64001e', resolution: {width: 768, height: 432} })); variants.push(new Variant({ uri: 'v3/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 64078, averageBandwidth: 62251, codecs: 'avc1.64001e', resolution: {width: 640, height: 360} })); variants.push(new Variant({ uri: 'v2/iframe_index.m3u8', isIFrameOnly: true, bandwidth: 38728, averageBandwidth: 37866, codecs: 'avc1.640015', resolution: {width: 480, height: 270} })); return variants; } module.exports = playlist; ================================================ FILE: test/helpers/fixtures.js ================================================ const path = require('node:path'); const fs = require('node:fs'); const fixtures = []; const baseDir = path.join(__dirname, '../fixtures/m3u8'); const filenames = fs.readdirSync(baseDir); for (const filename of filenames) { if (filename.endsWith('.m3u8')) { const name = path.basename(filename, '.m3u8'); const filepath = path.join(baseDir, filename); const m3u8 = fs.readFileSync(filepath, 'utf8'); const object = require(`../fixtures/objects/${name}.js`); fixtures.push({name, m3u8, object}); } } module.exports = fixtures; ================================================ FILE: test/helpers/matchers.js ================================================ function removeSpaceFromLine(line) { let inside = false; let str = ''; for (const ch of line) { if (ch === '"') { inside = !inside; } else if (!inside && ch === ' ') { continue; } str += ch; } return str; } function strip(playlist) { playlist = playlist.trim(); const filtered = []; for (let line of playlist.split('\n')) { line = removeSpaceFromLine(line); if (line.startsWith('#')) { if (line.startsWith('#EXT')) { filtered.push(line); } } else { filtered.push(line); } } return filtered.join('\n'); } function equalPlaylist(t, expected, actual) { expected &&= strip(expected); actual &&= strip(actual); if (expected === actual) { return t.pass(); } t.fail(`expected="${expected}", actual="${actual}"`); } function notEqualPlaylist(t, expected, actual) { expected &&= strip(expected); actual &&= strip(actual); if (expected === actual) { t.fail(`expected="${expected}", actual="${actual}"`); return t.fail(); } t.pass(); } module.exports = { equalPlaylist, notEqualPlaylist }; ================================================ FILE: test/helpers/utils.js ================================================ const HLS = require('../..'); HLS.setOptions({strictMode: true}); function parsePass(t, text) { let obj; try { obj = HLS.parse(text); } catch (err) { t.fail(err.stack); } t.truthy(obj); return obj; } function stringifyPass(t, obj) { let text; try { text = HLS.stringify(obj); } catch (err) { t.fail(err.stack); } t.truthy(text); return text; } function bothPass(t, text) { const obj = parsePass(t, text); return stringifyPass(t, obj); } function parseFail(t, text) { try { HLS.parse(text); } catch (err) { return t.truthy(err); } t.fail('HLS.parse() did not fail'); } function stringifyFail(t, obj) { try { HLS.stringify(obj); } catch (err) { return t.truthy(err); } t.fail('HLS.stringify() did not fail'); } function stripSpaces(text) { const chars = []; let insideDoubleQuotes = false; for (const ch of text) { if (ch === '"') { insideDoubleQuotes = !insideDoubleQuotes; } else if (ch === ' ') { if (!insideDoubleQuotes) { continue; } } chars.push(ch); } return chars.join(''); } function stripCommentsAndEmptyLines(text) { const lines = []; for (const l of text.split('\n')) { const line = l.trim(); if (!line) { // empty line continue; } if (line.startsWith('#')) { if (line.startsWith('#EXT')) { // tag lines.push(stripSpaces(line)); } // comment continue; } // uri lines.push(line); } return lines.join('\n'); } module.exports = { parsePass, stringifyPass, bothPass, parseFail, stringifyFail, stripCommentsAndEmptyLines }; ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.1_EXTM3U.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // It MUST be the first line of every Media Playlist and // every Master Playlist. test('#EXTM3U-01', t => { // Media Playlist utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.parseFail(t, ` #EXT-X-TARGETDURATION:10 #EXTM3U #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); // Master Playlist utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 http://example.com/low.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000 http://example.com/mid.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000 http://example.com/hi.m3u8 `); utils.parseFail(t, ` #EXT-X-STREAM-INF:BANDWIDTH=1280000 http://example.com/low.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000 http://example.com/mid.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000 http://example.com/hi.m3u8 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.2_EXT-X-VERSION.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // A Playlist file MUST NOT contain more than one EXT-X-VERSION tag. test('#EXT-X-VERSION_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:9.9, http://example.com/1 #EXTINF:10.0, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:9.9, http://example.com/1 #EXTINF:10.0, http://example.com/2 #EXT-X-VERSION:4 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.1_EXTINF.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // This tag is REQUIRED for each Media Segment test('#EXTINF_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 http://example.com/2 `); }); // If the compatibility version number is less than 3, // durations MUST be integers. test('#EXTINF_02', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:9.9, http://example.com/1 `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:2 #EXT-X-TARGETDURATION:10 #EXTINF:9.9, http://example.com/1 `); }); // The remainder of the line following the comma is an optional human- // readable informative title of the Media Segment expressed as raw // UTF-8 text. test('#EXTINF_03', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10,abc http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10,${unescape(encodeURIComponent('\u3042'))} http://example.com/1 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.2_EXT-X-BYTERANGE.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // It applies only to the next URI line that follows it in the Playlist. test('#EXT-X-BYTERANGE_01', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-BYTERANGE:100@200 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); t.is(playlist.segments[0].byterange.offset, 200); t.is(playlist.segments[0].byterange.length, 100); t.falsy(playlist.segments[1].byterange); }); // If o is not present, the sub-range begins at the next byte following // the sub-range of the previous Media Segment. test('#EXT-X-BYTERANGE_02', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-BYTERANGE:100@200 #EXTINF:9.9, http://example.com/1 #EXT-X-BYTERANGE:100 #EXTINF:9.9, http://example.com/1 #EXT-X-BYTERANGE:100 #EXTINF:9.9, http://example.com/1 `); t.is(playlist.segments[0].byterange.offset, 200); t.is(playlist.segments[1].byterange.offset, 300); t.is(playlist.segments[2].byterange.offset, 400); }); // If o is not present, a previous Media Segment MUST appear in the // Playlist file and MUST be a sub-range of the same media resource, or // the Media Segment is undefined and the Playlist MUST be rejected. test('#EXT-X-BYTERANGE_03', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-BYTERANGE:100 #EXTINF:9.9, http://example.com/1 `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-BYTERANGE:100@200 #EXTINF:9.9, http://example.com/1 #EXT-X-BYTERANGE:100 #EXTINF:9.9, http://example.com/1 #EXT-X-BYTERANGE:100 #EXTINF:9.9, http://example.com/2 `); utils.parsePass(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-BYTERANGE:100@200 #EXTINF:9.9, http://example.com/1 #EXT-X-BYTERANGE:100 #EXTINF:9.9, http://example.com/1 #EXT-X-BYTERANGE:100@200 #EXTINF:9.9, http://example.com/2 `); }); // Use of the EXT-X-BYTERANGE tag REQUIRES a compatibility version // number of 4 or greater. test('#EXT-X-BYTERANGE_04', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-BYTERANGE:100@200 #EXTINF:9.9, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-BYTERANGE:100@200 #EXTINF:9.9, http://example.com/1 `); }); // EXT-X-BYTERANGE should come at end of segment. test('#EXT-X-BYTERANGE_05', t => { t.is( utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXTINF:9.9,comment #EXT-X-BYTERANGE:100@200 http://example.com/1 #EXT-X-DISCONTINUITY #EXTINF:9.9,comment #EXT-X-BYTERANGE:100@200 http://example.com/2 `), utils.stripCommentsAndEmptyLines(` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXTINF:9.9,comment #EXT-X-BYTERANGE:100@200 http://example.com/1 #EXT-X-DISCONTINUITY #EXTINF:9.9,comment #EXT-X-BYTERANGE:100@200 http://example.com/2 `) ); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.3_EXT-X-DISCONTINUITY.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); // The EXT-X-DISCONTINUITY tag indicates a discontinuity between the // Media Segment that follows it and the one that preceded it. test('#EXT-X-DISCONTINUITY_01', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXT-X-DISCONTINUITY #EXTINF:10, http://example.com/2 #EXTINF:10, http://example.com/3 `); t.falsy(playlist.segments[0].discontinuity); t.true(playlist.segments[1].discontinuity); t.falsy(playlist.segments[2].discontinuity); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.4_EXT-X-KEY.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // It applies to every Media Segment that appears between // it and the next EXT-X-KEY tag in the Playlist file with the same // KEYFORMAT attribute (or the end of the Playlist file). test('#EXT-X-KEY_01', t => { let playlist; // Until the end of the Playlist file playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" #EXTINF:10, http://example.com/2 #EXTINF:10, http://example.com/3 `); t.falsy(playlist.segments[0].key); t.truthy(playlist.segments[1].key); t.truthy(playlist.segments[2].key); // Until the next EXT-X-KEY tag in the Playlist file with the same // KEYFORMAT attribute playlist = HLS.parse(` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity" #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity" #EXTINF:10, http://example.com/3 `); t.is(playlist.segments[0].key.uri, 'http://example.com/key-1'); t.is(playlist.segments[1].key.uri, 'http://example.com/key-1'); t.is(playlist.segments[2].key.uri, 'http://example.com/key-2'); }); // METHOD: This attribute is REQUIRED. test('#EXT-X-KEY_02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-KEY:URI="http://example.com" #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" #EXTINF:10, http://example.com/2 `); }); // If the encryption method is NONE, other attributes // MUST NOT be present. test('#EXT-X-KEY_03', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=NONE,URI="http://example.com" #EXTINF:10, http://example.com/2 `); }); // URI: This attribute is REQUIRED unless the METHOD is NONE. test('#EXT-X-KEY_04', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128 #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=NONE #EXTINF:10, http://example.com/2 `); }); // Use of the IV attribute REQUIRES a compatibility version number of // 2 or greater. test('#EXT-X-KEY_05', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:1 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 #EXTINF:10, http://example.com/2 `); const playlist = utils.parsePass(t, ` #EXTM3U #EXT-X-VERSION:2 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 #EXTINF:10, http://example.com/2 `); t.is(playlist.segments[0].key.iv.length, 16); }); // The tag place should be preserved test('#EXT-X-KEY_06', t => { const sourceText = ` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity" #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity" #EXTINF:10, http://example.com/3 #EXTINF:10, http://example.com/4 `; const obj = HLS.parse(sourceText); const text = HLS.stringify(obj); t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); }); // The same tag can appear multiple times test('#EXT-X-KEY_07', t => { const sourceText = ` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity" #EXTINF:10, http://example.com/1 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity" #EXTINF:10, http://example.com/2 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity" #EXTINF:10, http://example.com/3 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity" #EXTINF:10, http://example.com/4 `; const obj = HLS.parse(sourceText); const text = HLS.stringify(obj); t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.5_EXT-X-MAP.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // It applies to every Media Segment that appears after it in the // Playlist until the next EXT-X-MAP tag or until the end of the // playlist. test('#EXT-X-MAP_01', t => { let playlist; // Until the end of the Playlist playlist = HLS.parse(` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXT-X-MAP:URI="http://example.com/map-1" #EXTINF:10, http://example.com/2 #EXTINF:10, http://example.com/3 `); t.falsy(playlist.segments[0].map); t.truthy(playlist.segments[1].map); t.truthy(playlist.segments[2].map); // Until the next EXT-X-MAP tag playlist = HLS.parse(` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:10 #EXT-X-MAP:URI="http://example.com/map-1" #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 #EXT-X-MAP:URI="http://example.com/map-2" #EXTINF:10, http://example.com/3 `); t.is(playlist.segments[0].map.uri, 'http://example.com/map-1'); t.is(playlist.segments[1].map.uri, 'http://example.com/map-1'); t.is(playlist.segments[2].map.uri, 'http://example.com/map-2'); HLS.stringify(playlist); }); // URI: This attribute is REQUIRED. test('#EXT-X-MAP_02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:10 #EXT-X-MAP:BYTERANGE="256@128" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:10 #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" #EXTINF:10, http://example.com/1 `); }); // Use of the EXT-X-MAP tag in a Media Playlist that contains the // EXT-X-I-FRAMES-ONLY tag REQUIRES a compatibility version number of 5 // or greater. // URI: This attribute is REQUIRED. test('#EXT-X-MAP_03', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-I-FRAMES-ONLY #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-I-FRAMES-ONLY #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" #EXTINF:10, http://example.com/1 `); }); // Use of the EXT-X-MAP tag in a Media Playlist that DOES // NOT contain the EXT-X-I-FRAMES-ONLY tag REQUIRES a compatibility // version number of 6 or greater. test('#EXT-X-MAP_04', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:10 #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" #EXTINF:10, http://example.com/1 `); }); // The tag place should be preserved test('#EXT-X-MAP_05', t => { const sourceText = ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:10 #EXT-X-MAP:URI="http://example.com/map-1" #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 #EXT-X-MAP:URI="http://example.com/map-2" #EXTINF:10, http://example.com/3 #EXTINF:10, http://example.com/4 `; const obj = HLS.parse(sourceText); const text = HLS.stringify(obj); t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); }); // The same tag can appear multiple times test('#EXT-X-MAP_06', t => { const sourceText = ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:10 #EXT-X-MAP:URI="http://example.com/map-1" #EXTINF:10, http://example.com/1 #EXT-X-MAP:URI="http://example.com/map-2" #EXTINF:10, http://example.com/2 #EXT-X-MAP:URI="http://example.com/map-1" #EXTINF:10, http://example.com/3 #EXT-X-MAP:URI="http://example.com/map-2" #EXTINF:10, http://example.com/4 `; const obj = HLS.parse(sourceText); const text = HLS.stringify(obj); t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.6_EXT-X-PROGRAM-DATE-TIME.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); // It applies only to the next Media Segment. test('#EXT-X-PROGRAM-DATE-TIME_01', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); t.truthy(playlist.segments[0].programDateTime); t.falsy(playlist.segments[1].programDateTime); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.7_EXT-X-DATERANGE.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // ID attribute is REQUIRED. test('#EXT-X-DATERANGE_01', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",ID="ads" #EXTINF:10, http://example.com/1 `); }); // START-DATE attribute is REQUIRED. // * removed because START-DATE is omitted in case of 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags /* test('#EXT-X-DATERANGE_02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:ID="ads" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",ID="ads" #EXTINF:10, http://example.com/1 `); }); */ // START-DATE attribute is not REQUIRED test('#EXT-X-DATERANGE_02', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:ID="ads" #EXTINF:10, http://example.com/1 `); }); // END-DATE MUST be equal to or later than the value of the // START-DATE attribute. test('#EXT-X-DATERANGE_03', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",END-DATE="2010-02-19T14:54:22Z",ID="ads" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",END-DATE="2010-02-19T14:54:23Z",ID="ads" #EXTINF:10, http://example.com/1 `); }); // DURATION MUST NOT be negative. test('#EXT-X-DATERANGE_04', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",DURATION=-180.0,ID="ads" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",DURATION=180.0,ID="ads" #EXTINF:10, http://example.com/1 `); }); // PLANNED-DURATION MUST NOT be negative. test('#EXT-X-DATERANGE_05', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",PLANNED-DURATION=-180.0,ID="ads" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",PLANNED-DURATION=180.0,ID="ads" #EXTINF:10, http://example.com/1 `); }); // X- // The attribute value MUST be a quoted-string, // a hexadecimal-sequence, or a decimal-floating-point. test('#EXT-X-DATERANGE_06', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",ID="ads",X-STR="abc" #EXTINF:10, http://example.com/1 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",ID="ads",X-BYTE=0xFFFEFDFC #EXTINF:10, http://example.com/2 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:23Z",ID="ads",X-FLOAT=0.999 #EXTINF:10, http://example.com/3 `); t.is(playlist.segments[0].dateRange.attributes['X-STR'], 'abc'); t.deepEqual(playlist.segments[1].dateRange.attributes['X-BYTE'], new Uint8Array([255, 254, 253, 252])); t.is(playlist.segments[2].dateRange.attributes['X-FLOAT'], 0.999); }); // END-ON-NEXT attribute indicates that the end of the range containing it // is equal to the START-DATE of its Following Range. The Following Range is // the Date Range of the same CLASS that has the earliest START-DATE // after the START-DATE of the range in question. test('#EXT-X-DATERANGE_07', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",ID="ads",CLASS="A",END-ON-NEXT=YES #EXTINF:10, http://example.com/1 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:55:00Z",ID="ads",CLASS="B",END-ON-NEXT=YES #EXTINF:10, http://example.com/2 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:56:00Z",ID="ads",CLASS="A",END-ON-NEXT=YES #EXTINF:10, http://example.com/3 `); t.true(playlist.segments[0].dateRange.endOnNext); t.not(playlist.segments[0].dateRange.end.getTime(), playlist.segments[1].dateRange.start.getTime()); t.is(playlist.segments[0].dateRange.end.getTime(), playlist.segments[2].dateRange.start.getTime()); }); // An EXT-X-DATERANGE tag with an END-ON-NEXT=YES attribute MUST have a // CLASS attribute. test('#EXT-X-DATERANGE_08', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",ID="ads",END-ON-NEXT=YES #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",ID="ads",CLASS="A",END-ON-NEXT=YES #EXTINF:10, http://example.com/1 `); }); // Other EXT-X-DATERANGE tags with the same CLASS // attribute MUST NOT specify Date Ranges that overlap. test('#EXT-X-DATERANGE_09', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",DURATION=60.0,ID="ads",CLASS="A" #EXTINF:10, http://example.com/1 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:55:00Z",DURATION=61.0,ID="ads",CLASS="A" #EXTINF:10, http://example.com/2 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:56:00Z",DURATION=60.0,ID="ads",CLASS="A" #EXTINF:10, http://example.com/3 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",DURATION=60.0,ID="ads",CLASS="A" #EXTINF:10, http://example.com/1 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:55:00Z",DURATION=60.0,ID="ads",CLASS="A" #EXTINF:10, http://example.com/2 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:56:00Z",DURATION=60.0,ID="ads",CLASS="A" #EXTINF:10, http://example.com/3 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",DURATION=60.0,ID="ads",CLASS="A" #EXTINF:10, http://example.com/1 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:55:00Z",DURATION=61.0,ID="ads",CLASS="B" #EXTINF:10, http://example.com/2 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:56:00Z",DURATION=60.0,ID="ads",CLASS="A" #EXTINF:10, http://example.com/3 `); }); // An EXT-X-DATERANGE tag with an END-ON-NEXT=YES attribute MUST NOT // contain DURATION or END-DATE attributes. test('#EXT-X-DATERANGE_11', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #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 #EXTINF:10, http://example.com/1 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",DURATION=120.0,ID="ads",CLASS="A",END-ON-NEXT=YES #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",ID="ads",CLASS="A",END-ON-NEXT=YES #EXTINF:10, http://example.com/1 `); }); // If a Playlist contains an EXT-X-DATERANGE tag, it MUST also contain // at least one EXT-X-PROGRAM-DATE-TIME tag. test('#EXT-X-DATERANGE_12', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",ID="ads" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",ID="ads" #EXTINF:10, http://example.com/1 `); }); // If a Date Range contains both a DURATION attribute and an END-DATE // attribute, the value of the END-DATE attribute MUST be equal to the // value of the START-DATE attribute plus the value of the DURATION // attribute. test('#EXT-X-DATERANGE_13', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",ID="ads",DURATION=60.0,END-DATE="2010-02-19T14:56:00Z" #EXTINF:10, http://example.com/1 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031Z #EXT-X-DATERANGE:START-DATE="2010-02-19T14:54:00Z",ID="ads",DURATION=60.0,END-DATE="2010-02-19T14:55:00Z" #EXTINF:10, http://example.com/1 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2_Media-Segment-Tags.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // A Media Segment tag MUST NOT appear in a Master Playlist. Clients // MUST reject Playlists that contain both Media Segment Tags and Master // Playlist tags. test('Media-Segment-Tags', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 http://example.com/low.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000 http://example.com/mid.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000 http://example.com/hi.m3u8 `); utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 http://example.com/low.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000 http://example.com/mid.m3u8 #EXT-X-DISCONTINUITY #EXT-X-STREAM-INF:BANDWIDTH=7680000 http://example.com/hi.m3u8 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.4.4.7_EXT-X-GAP.spec.js ================================================ const test = require("ava"); const HLS = require('../../../../..'); const utils = require("../../../../helpers/utils"); const {equalPlaylist} = require("../../../../helpers/matchers"); // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.7 test('#EXT-X-TAG_01', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:8 #EXT-X-GAP 1.ts `); utils.parsePass(t, ` #EXTM3U #EXT-X-VERSION:8 #EXT-X-TARGETDURATION:5 #EXT-X-GAP #EXTINF:4, 1.ts `); }); test('#EXT-X-TAG_02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:8 #EXT-X-TARGETDURATION:5 #EXT-X-GAP #EXT-X-PART:DURATION=2,URI="1.ts" #EXT-X-ENDLIST `); utils.parsePass(t, ` #EXTM3U #EXT-X-VERSION:8 #EXT-X-TARGETDURATION:5 #EXT-X-GAP #EXT-X-PART:DURATION=2,URI="1.ts",GAP=YES #EXT-X-ENDLIST `); }); test('#EXT-X-TAG_03', t => { const txt = ` #EXTM3U #EXT-X-VERSION:8 #EXT-X-TARGETDURATION:5 #EXT-X-GAP #EXTINF:4, 1.ts `; const playlist = HLS.parse(txt); t.truthy(playlist.segments[0].gap); equalPlaylist(t, txt, HLS.stringify(playlist)); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.1_EXT-X-TARGETDURATION.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // The EXTINF duration of each Media Segment in the Playlist // file, when rounded to the nearest integer, MUST be less than or equal // to the target duration test('#EXT-X-TARGETDURATION_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:9.9, http://example.com/1 #EXTINF:10.4, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:9.9, http://example.com/1 #EXTINF:10.5, http://example.com/2 `); }); // The EXT-X-TARGETDURATION tag is REQUIRED. test('#EXT-X-TARGETDURATION_02', t => { utils.parseFail(t, ` #EXTM3U #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.2_EXT-X-MEDIA-SEQUENCE.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // If the Media Playlist file does not contain an EXT-X-MEDIA-SEQUENCE // tag then the Media Sequence Number of the first Media Segment in the // Media Playlist SHALL be considered to be 0. test('#EXT-X-MEDIA-SEQUENCE_01', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); t.is(playlist.mediaSequenceBase, 0); }); // The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media // Segment in the Playlist. test('#EXT-X-MEDIA-SEQUENCE_02', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-MEDIA-SEQUENCE:20 #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:9, http://example.com/1 #EXT-X-MEDIA-SEQUENCE:20 #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:9, #EXT-X-MEDIA-SEQUENCE:20 http://example.com/1 #EXTINF:10, http://example.com/2 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.3_EXT-X-DISCONTINUITY-SEQUENCE.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // If the Media Playlist does not contain an EXT-X-DISCONTINUITY- // SEQUENCE tag, then the Discontinuity Sequence Number of the first // Media Segment in the Playlist SHALL be considered to be 0. test('#EXT-X-DISCONTINUITY-SEQUENCE_01', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXT-X-DISCONTINUITY #EXTINF:10, http://example.com/2 `); t.is(playlist.discontinuitySequenceBase, 0); }); // The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before the first // Media Segment in the Playlist. test('#EXT-X-DISCONTINUITY-SEQUENCE_02', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-DISCONTINUITY-SEQUENCE:20 #EXTINF:9, http://example.com/1 #EXT-X-DISCONTINUITY #EXTINF:10, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:9, http://example.com/1 #EXT-X-DISCONTINUITY-SEQUENCE:20 #EXT-X-DISCONTINUITY #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:9, #EXT-X-DISCONTINUITY-SEQUENCE:20 http://example.com/1 #EXT-X-DISCONTINUITY #EXTINF:10, http://example.com/2 `); }); // The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before any // EXT-X-DISCONTINUITY tag. test('#EXT-X-DISCONTINUITY-SEQUENCE_03', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-DISCONTINUITY #EXT-X-DISCONTINUITY-SEQUENCE:20 #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-DISCONTINUITY-SEQUENCE:20 #EXT-X-DISCONTINUITY #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.4_EXT-X-ENDLIST.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // It MAY occur anywhere in the Media Playlist file. test('#EXT-X-ENDLIST_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-ENDLIST #EXT-X-TARGETDURATION:10 #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-ENDLIST #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:9, #EXT-X-ENDLIST http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:9, http://example.com/1 #EXT-X-ENDLIST #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:9, http://example.com/1 #EXTINF:10, #EXT-X-ENDLIST http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 #EXT-X-ENDLIST `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.5_EXT-X-PLAYLIST-TYPE.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); // #EXT-X-PLAYLIST-TYPE: test('#EXT-X-PLAYLIST-TYPE_01', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PLAYLIST-TYPE:EVENT #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); t.is(playlist.playlistType, 'EVENT'); }); // #EXT-X-PLAYLIST-TYPE: test('#EXT-X-PLAYLIST-TYPE_02', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PLAYLIST-TYPE:VOD #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); t.is(playlist.playlistType, 'VOD'); }); // #EXT-X-PLAYLIST-TYPE: test('#EXT-X-PLAYLIST-TYPE_03', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); t.is(playlist.playlistType, undefined); }); ================================================ FILE: 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 ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // Use of the EXT-X-I-FRAMES-ONLY REQUIRES a compatibility version // number of 4 or greater. test('#EXT-X-I-FRAMES-ONLY_01', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-VERSION:3 #EXT-X-I-FRAMES-ONLY #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-VERSION:4 #EXT-X-I-FRAMES-ONLY #EXTINF:9, http://example.com/1 #EXTINF:10, http://example.com/2 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.7_EXT-X-CUE-OUT.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); test('#EXT-X-CUE-OUT_01', t => { let obj = utils.parsePass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-VERSION:3 #EXTINF:9, http://example.com/1 #EXT-X-CUE-OUT:30 #EXTINF:10, http://example.com/2 `); t.is(obj.segments[1].markers[0].duration, 30); obj = utils.parsePass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-VERSION:3 #EXTINF:9, http://example.com/1 #EXT-X-CUE-OUT:DURATION=30 #EXTINF:10, http://example.com/2 `); t.is(obj.segments[1].markers[0].duration, 30); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3_Media-Playlist-Tags.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // There MUST NOT be more than one Media Playlist tag of each type in // any Media Playlist. test('Media-Playlist-Tags', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/2 `); }); // A Media Playlist Tag MUST NOT appear in a Master Playlist test('Media-Segment-Tags', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 http://example.com/low.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000 http://example.com/mid.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000 http://example.com/hi.m3u8 `); utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 http://example.com/low.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000 http://example.com/mid.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000 http://example.com/hi.m3u8 #EXT-X-ENDLIST `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.1_EXT-X-MEDIA.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // TYPE attribute is REQUIRED. test('#EXT-X-MEDIA_01', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="audio" /video/main.m3u8 #EXT-X-MEDIA:GROUP-ID="audio",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="audio" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); }); // TYPE attribute: valid strings are AUDIO, VIDEO, // SUBTITLES and CLOSED-CAPTIONS. test('#EXT-X-MEDIA_02', t => { let playlist = HLS.parse(` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); t.is(playlist.variants[0].audio[0].type, 'AUDIO'); playlist = HLS.parse(` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/video/en.m3u8" `); t.is(playlist.variants[0].video[0].type, 'VIDEO'); playlist = HLS.parse(` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/subtitles/en.m3u8" `); t.is(playlist.variants[0].subtitles[0].type, 'SUBTITLES'); playlist = HLS.parse(` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" `); t.is(playlist.variants[0].closedCaptions[0].type, 'CLOSED-CAPTIONS'); }); // If the TYPE is CLOSED-CAPTIONS, the URI // attribute MUST NOT be present. test('#EXT-X-MEDIA_03', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1",URI="/audio/en.m3u8" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" `); }); // GROUP-ID attribute is REQUIRED. test('#EXT-X-MEDIA_04', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,NAME="en",DEFAULT=YES" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES" `); }); // NAME attribute is REQUIRED. test('#EXT-X-MEDIA_05', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",DEFAULT=YES" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES" `); }); // The FORCED attribute MUST NOT be present unless the // TYPE is SUBTITLES. test('#EXT-X-MEDIA_06', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,FORCED=YES `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/subtitles/en.m3u8",FORCED=YES `); }); // INSTREAM-ID attribute is REQUIRED if the TYPE attribute is CLOSED-CAPTIONS test('#EXT-X-MEDIA_07', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" `); }); // All EXT-X-MEDIA tags in the same Group MUST have different NAME attributes. test('#EXT-X-MEDIA_08', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="fr",URI="/audio/fr.m3u8" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test2",NAME="ja",URI="/audio/ja.m3u8" `); }); // A Group MUST NOT have more than one member with a DEFAULT attribute of YES. test('#EXT-X-MEDIA_09', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="fr",DEFAULT=YES,URI="/audio/fr.m3u8" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="fr",URI="/audio/fr.m3u8" `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.2_EXT-X-STREAM-INF.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // The URI line is REQUIRED test('#EXT-X-STREAM-INF_01', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 `); }); // Every EXT-X-STREAM-INF tag MUST include the BANDWIDTH attribute. test('#EXT-X-STREAM-INF_02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=1280000 /video/main.m3u8 `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 `); }); // RESOLUTION The value is a decimal-resolution: // two decimal-integers separated by the "x" // character. The first integer is a horizontal pixel dimension // (width); the second is a vertical pixel dimension (height). test('#EXT-X-STREAM-INF_03', t => { const playlist = HLS.parse(` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,RESOLUTION=123x456 /video/main.m3u8 `); t.deepEqual(playlist.variants[0].resolution, {width: 123, height: 456}); }); // AUDIO attribute MUST match the value of the // GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Master // Playlist whose TYPE attribute is AUDIO. test('#EXT-X-STREAM-INF_04', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test1",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); }); // VIDEO MUST match the value of the // GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Master // Playlist whose TYPE attribute is VIDEO. test('#EXT-X-STREAM-INF_05', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="test1",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); }); // SUBTITLES MUST match the value of the // GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Master // Playlist whose TYPE attribute is SUBTITLES. test('#EXT-X-STREAM-INF_06', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="test1",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); }); // CLOSED-CAPTIONS: it MUST match the value of the // GROUP-ID attribute of an EXT-X-MEDIA tag elsewhere in the Playlist // whose TYPE attribute is CLOSED-CAPTIONS test('#EXT-X-STREAM-INF_07', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test1",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" `); }); // CLOSED-CAPTIONS: The value can be either a quoted-string or an enumerated-string with the value NONE. test('#EXT-X-STREAM-INF_07-01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE /video/main.m3u8 `); const playlist = HLS.parse(` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE /video/main.m3u8 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" `); t.is(playlist.variants[0].closedCaptions.length, 0); }); // CLOSED-CAPTIONS: If the value is the enumerated-string value NONE, // all EXT-X-STREAM-INF tags MUST have this attribute with a value of NONE, // indicating that there are no closed captions in any Variant Stream in the Master Playlist. test('#EXT-X-STREAM-INF_07-02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2040000,CLOSED-CAPTIONS="test" /video/high.m3u8 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" `); }); /* test('#EXT-X-STREAM-INF_07-03', t => { const sourceText = ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2040000,CLOSED-CAPTIONS=NONE /video/high.m3u8 `; HLS.setOptions({allowClosedCaptionsNone: true}); const obj = HLS.parse(sourceText); const text = HLS.stringify(obj); t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); }); */ // The URI attribute of the EXT-X-MEDIA tag is REQUIRED if the media // type is SUBTITLES test('#EXT-X-STREAM-INF_08', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="test",NAME="en",DEFAULT=YES `); utils.bothPass(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES="test" /video/main.m3u8 #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" `); }); // SCORE: The value is a positive decimal-floating-point number. test('#EXT-X-STREAM-INF_09', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,SCORE=-0.5 /video/main.m3u8 `); const expected = ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,SCORE=0.5 /video/main.m3u8 `; const actual = utils.bothPass(t, expected); t.is(actual, utils.stripCommentsAndEmptyLines(expected)); }); // The SCORE attribute is OPTIONAL, but if any Variant Stream // contains the SCORE attribute, then all Variant Streams in the // Master Playlist SHOULD have a SCORE attribute. test('#EXT-X-STREAM-INF_10', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="avc1.640029,mp4a.40.2",SCORE=0.5 low/main/audio-video.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="avc1.640029,mp4a.40.2" mid/main/audio-video.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="avc1.640029,mp4a.40.2" hi/main/audio-video.m3u8 `); const expected = ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="avc1.640029,mp4a.40.2",SCORE=0.5 low/main/audio-video.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="avc1.640029,mp4a.40.2",SCORE=0.3 mid/main/audio-video.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="avc1.640029,mp4a.40.2",SCORE=0.1 hi/main/audio-video.m3u8 `; const actual = utils.bothPass(t, expected); t.is(actual, utils.stripCommentsAndEmptyLines(expected)); }); // ALLOWED-CPC: Its value is a quoted-string containing // a comma-separated list of entries. Each entry consists // of a KEYFORMAT attribute value followed by a colon character (:) // followed by a sequence of Content Protection Configuration (CPC) // Labels separated by slash (/) characters. Each CPC Label is a // string containing characters from the set [A..Z], [0..9], and '-'. test('#EXT-X-STREAM-INF_11', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,ALLOWED-CPC="abc" /video/main.m3u8 `); const expected = ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,ALLOWED-CPC="com.example.drm1:SMART-TV/PC,com.example.drm2:HW" /video/main.m3u8 `; const actual = utils.bothPass(t, expected); t.is(actual, utils.stripCommentsAndEmptyLines(expected)); }); // VIDEO-RANGE: The value is an enumerated-string; valid strings are SDR, HLG and PQ. test('#EXT-X-STREAM-INF_12', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO-RANGE=abc /video/main.m3u8 `); const expected = ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO-RANGE=HLG /video/main.m3u8 `; const actual = utils.bothPass(t, expected); t.is(actual, utils.stripCommentsAndEmptyLines(expected)); }); // STABLE-VARIANT-ID: The value is a quoted-string which is // a stable identifier for the URI within the Master Playlist. test('#EXT-X-STREAM-INF_13', t => { const expected = ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,STABLE-VARIANT-ID="abc" /video/main.m3u8 `; const actual = utils.bothPass(t, expected); t.is(actual, utils.stripCommentsAndEmptyLines(expected)); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.2_EXT-X-STREAM-INF_2.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); test('#EXT-X-STREAM-INF_07-03', t => { const sourceText = ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2040000,CLOSED-CAPTIONS=NONE /video/high.m3u8 `; HLS.setOptions({allowClosedCaptionsNone: true}); const obj = HLS.parse(sourceText); const text = HLS.stringify(obj); t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.3_EXT-X-I-FRAME-STREAM-INF.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // Every EXT-X-I-FRAME-STREAM-INF tag MUST include a BANDWIDTH attribute // and a URI attribute. test('#EXT-X-I-FRAME-STREAM-INF_01', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1280000 `); utils.bothPass(t, ` #EXTM3U #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1280000,URI=/video/main.m3u8 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.4_EXT-X-SESSION-DATA.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // DATA-ID attribute is REQUIRED. test('#EXT-X-SESSION-DATA_01', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-SESSION-DATA:LANGUAGE="en",VALUE="This is an example" `); utils.bothPass(t, ` #EXTM3U #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" `); }); // Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI // attribute, but not both. test('#EXT-X-SESSION-DATA_02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en" `); utils.bothPass(t, ` #EXTM3U #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" `); utils.bothPass(t, ` #EXTM3U #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",URI="/data/title.json" `); utils.parseFail(t, ` #EXTM3U #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example",URI="/data/title.json" `); }); // A Playlist MUST NOT contain more than one EXT-X-SESSION-DATA tag // with the same DATA-ID attribute and the same LANGUAGE attribute. test('#EXT-X-SESSION-DATA_03', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" `); utils.bothPass(t, ` #EXTM3U #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="ja",VALUE="This is an example" `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.5_EXT-X-SESSION-KEY.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // The value of the METHOD attribute MUST NOT be NONE test('#EXT-X-SESSION-KEY_01', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-SESSION-KEY:METHOD=NONE `); utils.bothPass(t, ` #EXTM3U #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com" `); }); // 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. test('#EXT-X-SESSION-KEY_02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:2 #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:2 #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221101 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4_Master-Playlist-Tags.spec.js ================================================ const test = require('ava'); const utils = require('../../../../helpers/utils'); // Master Playlist Tags MUST NOT appear in a Media Playlist test('Master-Playlist-Tags', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.5_Media-or-Master-Playlist-Tags/4.3.5.1_EXT-X-INDEPENDENT-SEGMENTS.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // The tags in this section can appear in either Master Playlists or // Media Playlists. test('#EXT-X-INDEPENDENT-SEGMENTS_01', t => { const mediaPlaylist = HLS.parse(` #EXTM3U #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); t.true(mediaPlaylist.independentSegments); const masterPlaylist = HLS.parse(` #EXTM3U #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=640000 /video/low.m3u8 `); t.true(masterPlaylist.independentSegments); }); // These tags MUST NOT appear more than once in a Playlist. If a tag // appears more than once, clients MUST reject the playlist. test('#EXT-X-INDEPENDENT-SEGMENTS_02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 #EXT-X-INDEPENDENT-SEGMENTS `); utils.bothPass(t, ` #EXTM3U #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=640000 /video/low.m3u8 #EXT-X-INDEPENDENT-SEGMENTS `); utils.bothPass(t, ` #EXTM3U #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=640000 /video/low.m3u8 `); }); ================================================ FILE: test/spec/4_Playlists/4.3_Playlist-Tags/4.3.5_Media-or-Master-Playlist-Tags/4.3.5.2_EXT-X-START.spec.js ================================================ const test = require('ava'); const HLS = require('../../../../..'); const utils = require('../../../../helpers/utils'); // The tags in this section can appear in either Master Playlists or // Media Playlists. test('#EXT-X-START_01', t => { const mediaPlaylist = HLS.parse(` #EXTM3U #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); t.is(mediaPlaylist.start.offset, -10); t.true(mediaPlaylist.start.precise); const masterPlaylist = HLS.parse(` #EXTM3U #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=640000 /video/low.m3u8 `); t.is(masterPlaylist.start.offset, -10); t.true(masterPlaylist.start.precise); }); // These tags MUST NOT appear more than once in a Playlist. If a tag // appears more than once, clients MUST reject the playlist. test('#EXT-X-START_02', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-START:TIME-OFFSET=-10 #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 #EXT-X-START:TIME-OFFSET=-20 `); utils.bothPass(t, ` #EXTM3U #EXT-X-START:TIME-OFFSET=-10 #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-START:TIME-OFFSET=-10 #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=640000 /video/low.m3u8 #EXT-X-START:TIME-OFFSET=-20 `); utils.bothPass(t, ` #EXTM3U #EXT-X-START:TIME-OFFSET=-10 #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=640000 /video/low.m3u8 `); }); // TIME-OFFSET attribute is REQUIRED. test('#EXT-X-START_03', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-START:PRECISE=YES #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.bothPass(t, ` #EXTM3U #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com/1 #EXTINF:10, http://example.com/2 `); utils.parseFail(t, ` #EXTM3U #EXT-X-START:PRECISE=YES #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=640000 /video/low.m3u8 `); utils.bothPass(t, ` #EXTM3U #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES #EXT-X-STREAM-INF:BANDWIDTH=1280000 /video/main.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=640000 /video/low.m3u8 `); }); ================================================ FILE: test/spec/7_Protocol-version-compatibility/7_EXT-X-VERSION.spec.js ================================================ const test = require('ava'); const utils = require('../../helpers/utils'); // A Playlist that contains tags or attributes that are not compatible // with protocol version 1 MUST include an EXT-X-VERSION tag. test('#EXT-X-VERSION_02', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com" `); utils.parseFail(t, ` #EXTM3U #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:2 #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 `); }); // A Media Playlist MUST indicate a EXT-X-VERSION of 2 or higher if it // contains: // - The IV attribute of the EXT-X-KEY tag. test('#EXT-X-VERSION_03', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:1 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" #EXTINF:10, http://example.com `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:1 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 #EXTINF:10, http://example.com `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:2 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 #EXTINF:10, http://example.com `); }); // A Media Playlist MUST indicate a EXT-X-VERSION of 3 or higher if it // contains: // - Floating-point EXTINF duration values. test('#EXT-X-VERSION_04', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:2 #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:2 #EXT-X-TARGETDURATION:10 #EXTINF:9.9, http://example.com `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:9.9, http://example.com `); }); // A Media Playlist MUST indicate a EXT-X-VERSION of 4 or higher if it // contains: // - The EXT-X-BYTERANGE tag. test('#EXT-X-VERSION_05', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXTINF:10, http://example.com `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-BYTERANGE:256@100 #EXTINF:10, http://example.com `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-BYTERANGE:256@100 #EXTINF:10, http://example.com `); }); // A Media Playlist MUST indicate a EXT-X-VERSION of 4 or higher if it // contains: // - The EXT-X-I-FRAMES-ONLY tag. test('#EXT-X-VERSION_06', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:10 #EXT-X-I-FRAMES-ONLY `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-I-FRAMES-ONLY `); }); // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it // contains: // - The KEYFORMAT attributes of the EXT-X-KEY tag. test('#EXT-X-VERSION_07', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" #EXTINF:10, http://example.com `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",KEYFORMAT="identity" #EXTINF:10, http://example.com `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",KEYFORMAT="identity" #EXTINF:10, http://example.com `); }); // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it // contains: // - The KEYFORMATVERSIONS attributes of the EXT-X-KEY tag. test('#EXT-X-VERSION_08', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" #EXTINF:10, http://example.com `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",KEYFORMATVERSIONS="1" #EXTINF:10, http://example.com `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",KEYFORMATVERSIONS="1" #EXTINF:10, http://example.com `); }); // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it // contains: // - The EXT-X-MAP tag. test('#EXT-X-VERSION_09', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-I-FRAMES-ONLY `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-TARGETDURATION:10 #EXT-X-I-FRAMES-ONLY #EXT-X-MAP:URI="http://example.com" #EXTINF:10, http://example.com `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-I-FRAMES-ONLY #EXT-X-MAP:URI="http://example.com" #EXTINF:10, http://example.com `); }); // A Media Playlist MUST indicate a EXT-X-VERSION of 6 or higher if it // contains: // - The EXT-X-MAP tag in a Media Playlist that does not contain EXT-X-I-FRAMES-ONLY. test('#EXT-X-VERSION_10', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-I-FRAMES-ONLY #EXT-X-MAP:URI="http://example.com" #EXTINF:10, http://example.com `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:5 #EXT-X-TARGETDURATION:10 #EXT-X-MAP:URI="http://example.com" #EXTINF:10, http://example.com `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:10 #EXT-X-MAP:URI="http://example.com" #EXTINF:10, http://example.com `); }); // A Master Playlist MUST indicate a EXT-X-VERSION of 7 or higher if it // contains: // - "SERVICE" values for the INSTREAM-ID attribute of the EXT-X-MEDIA tag. test('#EXT-X-VERSION_11', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="JP",INSTREAM-ID="CC1" #EXT-X-STREAM-INF:BANDWIDTH=500000,CLOSED-CAPTIONS="cc" http://example.com `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="JP",INSTREAM-ID="SERVICE1" #EXT-X-STREAM-INF:BANDWIDTH=500000,CLOSED-CAPTIONS="cc" http://example.com `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:7 #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="JP",INSTREAM-ID="SERVICE1" #EXT-X-STREAM-INF:BANDWIDTH=500000,CLOSED-CAPTIONS="cc" http://example.com `); }); // A Media Playlist MUST indicate a EXT-X-VERSION of 8 or higher if it // contains: // - the "EXT-X-GAP" tag. test('#EXT-X-VERSION_12', t => { utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:1 #EXTINF:5 #EXT-X-GAP http://example.com `); }); ================================================ FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/01_EXT-X-SERVER-CONTROL.spec.js ================================================ const test = require('ava'); const utils = require('../../../helpers/utils'); const HLS = require('../../../..'); // CAN-BLOCK-RELOAD=YES: ... // It is mandatory for Low-Latency HLS. test('#EXT-X-SERVER-CONTROL_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=12.0 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); }); // CAN-SKIP-UNTIL=: (optional) test('#EXT-X-SERVER-CONTROL_02', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); }); // CAN-SKIP-UNTIL=: ... // The Skip Boundary must be at least six times the EXT-X-TARGETDURATION. test('#EXT-X-SERVER-CONTROL_03', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=11.9 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); }); // HOLD-BACK=: (optional) test('#EXT-X-SERVER-CONTROL_04', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=6.0 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); }); // HOLD-BACK=: ... // Its value is a floating-point number of seconds and must be at least // three times the EXT-X-TARGETDURATION. test('#EXT-X-SERVER-CONTROL_05', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=6.0 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=5.9 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 `); }); // PART-HOLD-BACK=: ... // It is mandatory if the Playlist contains EXT-X-PART tags. test('#EXT-X-SERVER-CONTROL_06', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); }); // PART-HOLD-BACK=: ... // This attribute's value is a floating-point number of seconds and must be // at least PART-TARGET. test('#EXT-X-SERVER-CONTROL_07', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.19 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); }); test('#EXT-X-SERVER-CONTROL_08', t => { const {lowLatencyCompatibility} = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); t.truthy(lowLatencyCompatibility); t.is(lowLatencyCompatibility.canBlockReload, true); t.is(lowLatencyCompatibility.canSkipUntil, 12); t.is(lowLatencyCompatibility.holdBack, 6); t.is(lowLatencyCompatibility.partHoldBack, 0.2); }); ================================================ FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/02_EXT-X-PART-INF.spec.js ================================================ const test = require('ava'); const utils = require('../../../helpers/utils'); const HLS = require('../../../..'); // EXT-X-PART-INF provides information about HLS Partial Segments in the Playlist. It is // required if a Playlist contains one or more EXT-X-PART tags. test('#EXT-X-PART-INF_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); }); // PART-TARGET=: (mandatory) test('#EXT-X-PART-INF_02', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); }); // PART-TARGET=: (mandatory) Indicates the part target duration in floating-point seconds // and is the maximum duration of any Partial Segment. test('#EXT-X-PART-INF_03', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@20000 #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@40000 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.17 #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@20000 #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@40000 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); }); // All Partial Segments except the last part of a segment // must have a duration of at least 85% of PART-TARGET. test('#EXT-X-PART-INF_04', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=18000@43000 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@61000 #EXT-X-PART:DURATION=0.05,URI="fs241.mp4",BYTERANGE=10000@81000 #EXTINF:2, fs241.mp4 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=18000@43000 #EXT-X-PART:DURATION=0.16,URI="fs241.mp4",BYTERANGE=20000@61000 #EXT-X-PART:DURATION=0.05,URI="fs241.mp4",BYTERANGE=10000@81000 #EXTINF:2, fs241.mp4 `); }); test('#EXT-X-PART-INF_05', t => { const {partTargetDuration} = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); t.is(partTargetDuration, 0.2); }); ================================================ FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/03_EXT-X-PART.spec.js ================================================ const test = require('ava'); const utils = require('../../../helpers/utils'); const HLS = require('../../../..'); // All Media Segment tags except for EXT-X-DATERANGE, EXT-X-BYTERANGE, // and EXT-X-GAP that are applied to a Parent Segment must appear before // the first EXT-X-PART tag of the Parent Segment. These tags include // EXT-X-MAP, EXT-X-DISCONTINUITY, and EXT-X-KEY. test('#EXT-X-PART_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-MAP:URI="http://example.com/map-1" #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 #EXT-X-MAP:URI="http://example.com/map-1" `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-DISCONTINUITY #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 #EXT-X-DISCONTINUITY `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" `); }); // Remove EXT-X-PART tags from the Playlist after they are greater than // three target durations from the end of the Playlist. Partial Segments // are useful for navigating close to the live edge, after which their // presence does not justify the increase in the Playlist size and the // responsibility of retaining the parallel Partial Segment stream on the server. test('#EXT-X-PART_02', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:1 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.9 #EXT-X-PART-INF:PART-TARGET=0.4 #EXTINF:1, fs240.mp4 #EXT-X-PART:DURATION=0.34,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.34,URI="fs241.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=0.4,URI="fs241.mp4",BYTERANGE=18000@43000 #EXTINF:1, fs241.mp4 #EXT-X-PART:DURATION=0.34,URI="fs242.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.34,URI="fs242.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=0.4,URI="fs242.mp4",BYTERANGE=18000@43000 #EXTINF:1, fs242.mp4 #EXT-X-PART:DURATION=0.34,URI="fs243.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.34,URI="fs243.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=0.4,URI="fs243.mp4",BYTERANGE=18000@43000 #EXTINF:1, fs243.mp4 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:1 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.9 #EXT-X-PART-INF:PART-TARGET=0.4 #EXTINF:1, fs240.mp4 #EXT-X-PART:DURATION=0.34,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.34,URI="fs241.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=0.4,URI="fs241.mp4",BYTERANGE=18000@43000 #EXTINF:1, fs241.mp4 #EXT-X-PART:DURATION=0.34,URI="fs242.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.34,URI="fs242.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=0.4,URI="fs242.mp4",BYTERANGE=18000@43000 #EXTINF:1, fs242.mp4 #EXT-X-PART:DURATION=0.34,URI="fs243.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.34,URI="fs243.mp4",BYTERANGE=23000@20000 #EXT-X-PART:DURATION=0.4,URI="fs243.mp4",BYTERANGE=18000@43000 #EXTINF:1, fs243.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs244.mp4",BYTERANGE-START=0,BYTERANGE-LENGTH=20000 `); }); // DURATION=: (mandatory) Indicates the duration of the Partial Segment // in floating-point seconds. test('#EXT-X-PART_03', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000 `); }); // URI=: (mandatory) Indicates the URI for the Partial Segment. test('#EXT-X-PART_04', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.17,BYTERANGE=20000@0 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000 `); }); test('#EXT-X-PART_05', t => { const {segments} = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0,INDEPENDENT=YES,GAP=YES #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000,INDEPENDENT=YES,GAP=YES #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); t.is(segments.length, 2); const {parts} = segments[1]; t.is(parts.length, 3); let offset = 0; const length = 20000; for (const [index, part] of parts.entries()) { t.is(part.uri, 'fs241.mp4'); t.deepEqual(part.byterange, {offset, length}); offset += length; if (index < 2) { t.is(part.duration, 0.2); t.is(part.independent, true); t.is(part.gap, true); } } }); // EXTINF can be ommitted test('#EXT-X-PART_06', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-ENDLIST `); }); ================================================ FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/04_EXT-X-PRELOAD-HINT.spec.js ================================================ const test = require('ava'); const utils = require('../../../helpers/utils'); const HLS = require('../../../..'); // TYPE=: (mandatory) test('#EXT-X-PRELOAD-HINT_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:URI="fs241.mp4",BYTERANGE-LENGTH=20000 `); }); // If hint-type is PART, the resource is an upcoming Partial Segment. test('#EXT-X-PRELOAD-HINT_02', t => { const {segments} = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); t.is(segments.length, 2); const {parts} = segments[1]; t.is(parts.length, 3); let offset = 0; const length = 20000; for (const [index, part] of parts.entries()) { t.is(part.uri, 'fs241.mp4'); t.deepEqual(part.byterange, {offset, length}); offset += length; if (index === 2) { t.true(part.hint); } else { t.false(part.hint); } } }); // If hint-type is MAP, the resource is an upcoming Media Initialization Section. test('#EXT-X-PRELOAD-HINT_03', t => { const {segments} = HLS.parse(` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXT-X-MAP:URI="map-0" #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 #EXT-X-PRELOAD-HINT:TYPE=MAP,URI="map-1" `); t.is(segments.length, 2); for (const [index, {map}] of segments.entries()) { t.is(map.uri, `map-${index}`); } }); // URI=: (mandatory) test('#EXT-X-PRELOAD-HINT_04', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4" `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART `); }); // BYTERANGE-START=: ... Its absence implies a value of 0. test('#EXT-X-PRELOAD-HINT_05', t => { const {segments} = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 `); const {parts} = segments[1]; t.is(parts[0].byterange.offset, 0); }); // 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 // to hint the URI of the next EXT-X-PART tag that is expected to be added to the // Playlist (and its byte range, if applicable). test('#EXT-X-PRELOAD-HINT_06', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 `); utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-ENDLIST `); }); // Servers should not add more than one EXT-X-PRELOAD-HINT // tag with the same TYPE attribute to a Playlist. test('#EXT-X-PRELOAD-HINT_07', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000 `); utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXT-X-MAP:URI="map-0" #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 #EXT-X-PRELOAD-HINT:TYPE=MAP,URI="map-1" `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXT-X-MAP:URI="map-0" #EXTINF:2, fs240.mp4 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 #EXT-X-PRELOAD-HINT:TYPE=MAP,URI="map-1" #EXT-X-PRELOAD-HINT:TYPE=MAP,URI="map-2" `); }); ================================================ FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/05_EXT-X-RENDITION-REPORT.spec.js ================================================ const test = require('ava'); const utils = require('../../../helpers/utils'); const HLS = require('../../../..'); // URI=: (mandatory) test('#EXT-X-RENDITION-REPORT_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXTINF:2, fs240.mp4 #EXT-X-RENDITION-REPORT:URI="mid.m3u8",LAST-MSN=1999 #EXT-X-RENDITION-REPORT:URI="high.m3u8",LAST-MSN=1999 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXTINF:2, fs240.mp4 #EXT-X-RENDITION-REPORT:LAST-MSN=1999 #EXT-X-RENDITION-REPORT:LAST-MSN=1999 `); }); // URI=: (mandatory) ... It must be relative to the URI of the Media Playlist // containing the EXT-X-RENDITION-REPORT tag. test('#EXT-X-RENDITION-REPORT_02', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXTINF:2, fs240.mp4 #EXT-X-RENDITION-REPORT:URI="mid.m3u8",LAST-MSN=1999 #EXT-X-RENDITION-REPORT:URI="high.m3u8",LAST-MSN=1999 `); utils.parseFail(t, ` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXTINF:2, fs240.mp4 #EXT-X-RENDITION-REPORT:URI="https://example.com/mid.m3u8",LAST-MSN=1999 #EXT-X-RENDITION-REPORT:URI="https://example.com/high.m3u8",LAST-MSN=1999 `); }); // A server may omit adding an attribute to an EXT-X-RENDITION-REPORT // tag — even a mandatory attribute — if its value is the same as that // of the Rendition Report of the Media Playlist to which the EXT-X-RENDITION-REPORT // tag is being added. This step reduces the size of the Rendition Report. test('#EXT-X-RENDITION-REPORT_03', t => { const {renditionReports} = HLS.parse(` #EXTM3U #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE:1990 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXTINF:2, fs240.mp4 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 #EXT-X-RENDITION-REPORT:URI="main-0.m3u8" #EXT-X-RENDITION-REPORT:URI="main-1.m3u8" `); t.is(renditionReports.length, 2); for (const [index, report] of renditionReports.entries()) { t.is(report.uri, `main-${index}.m3u8`); t.is(report.lastMSN, 1991); t.is(report.lastPart, 2); } }); // Handle 0-indexed segment parts in rendition reports test('#EXT-X-RENDITION-REPORT_04', t => { const {renditionReports} = HLS.parse(` #EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:3 #EXT-X-SERVER-CONTROL:PART-HOLD-BACK=3.150000,CAN-BLOCK-RELOAD=YES #EXT-X-PART-INF:PART-TARGET=1 #EXT-X-PROGRAM-DATE-TIME:2022-08-12T15:53:22Z media_b128000_cmaf_a_6.mp4 #EXT-X-PROGRAM-DATE-TIME:2022-08-12T15:53:31Z #EXT-X-PART:DURATION=1,INDEPENDENT=YES,URI="media_b128000_cmaf_a_7_p0.mp4" #EXT-X-PRELOAD-HINT:TYPE=PART,URI="media_b128000_cmaf_a_7_p1.mp4" #EXT-X-RENDITION-REPORT:URI="chunklist_b56000_cmaf_a.m3u8?max_segments=10",LAST-MSN=7,LAST-PART=0 #EXT-X-RENDITION-REPORT:URI="chunklist_b256000_cmaf_a.m3u8?max_segments=10",LAST-MSN=7,LAST-PART=0 `); t.is(renditionReports.length, 2); for (const [index, report] of renditionReports.entries()) { console.log(index, report); t.is(report.lastMSN, 7); t.is(report.lastPart, 0); } }); ================================================ FILE: test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/06_EXT-X-SKIP.spec.js ================================================ const test = require('ava'); const utils = require('../../../helpers/utils'); const HLS = require('../../../..'); // SKIPPED-SEGMENTS=: (mandatory) test('#EXT-X-SKIP_01', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:9 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXT-X-SKIP:SKIPPED-SEGMENTS=20 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 #EXTINF:2, fs242.mp4 #EXTINF:2, fs243.mp4 #EXTINF:2, fs244.mp4 #EXTINF:2, fs245.mp4 `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:9 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXT-X-SKIP:NUM=20 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 #EXTINF:2, fs242.mp4 #EXTINF:2, fs243.mp4 #EXTINF:2, fs244.mp4 #EXTINF:2, fs245.mp4 `); }); // SKIPPED-SEGMENTS=: (mandatory) Indicates how many // Media Segments were replaced by the EXT-X-SKIP tag, // along with their associated tags. test('#EXT-X-SKIP_02', t => { const {skip, segments} = HLS.parse(` #EXTM3U #EXT-X-VERSION:9 #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE:9000 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXT-X-SKIP:SKIPPED-SEGMENTS=20 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 #EXTINF:2, fs242.mp4 #EXTINF:2, fs243.mp4 #EXTINF:2, fs244.mp4 #EXTINF:2, fs245.mp4 `); t.is(skip, 20); t.is(segments[0].mediaSequenceNumber, 9020); }); // A Playlist containing an EXT-X-SKIP tag must have // an EXT-X-VERSION tag with a value of nine or higher. test('#EXT-X-SKIP_03', t => { utils.bothPass(t, ` #EXTM3U #EXT-X-VERSION:9 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXT-X-SKIP:SKIPPED-SEGMENTS=20 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 #EXTINF:2, fs242.mp4 #EXTINF:2, fs243.mp4 #EXTINF:2, fs244.mp4 #EXTINF:2, fs245.mp4 `); utils.parseFail(t, ` #EXTM3U #EXT-X-VERSION:8 #EXT-X-TARGETDURATION:2 #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 #EXT-X-PART-INF:PART-TARGET=0.2 #EXT-X-SKIP:SKIPPED-SEGMENTS=20 #EXTINF:2, fs240.mp4 #EXTINF:2, fs241.mp4 #EXTINF:2, fs242.mp4 #EXTINF:2, fs243.mp4 #EXTINF:2, fs244.mp4 #EXTINF:2, fs245.mp4 `); }); ================================================ FILE: test/spec/Apple_HLS_Overview/02_Using_HLS.spec.js ================================================ const test = require('ava'); const HLS = require('../../..'); const utils = require('../../helpers/utils'); // 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), // the client attempts to switch to an alternate stream. test('Redundant_Streams_01', t => { const sourceText = ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=200000, RESOLUTION=720x480 http://ALPHA.mycompany.com/lo/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=200000, RESOLUTION=720x480 http://BETA.mycompany.com/lo/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=500000, RESOLUTION=1920x1080 http://ALPHA.mycompany.com/md/prog_index.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=500000, RESOLUTION=1920x1080 http://BETA.mycompany.com/md/prog_index.m3u8 `; const obj = HLS.parse(sourceText); const text = HLS.stringify(obj); t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); }); ================================================ FILE: test/spec/HLSJS-LHLS/01_EXT-X-PREFETCH.spec.js ================================================ const test = require("ava"); const utils = require("../../helpers/utils"); const HLS = require("../../.."); test("#EXT-X-PREFETCH_01", t => { utils.bothPass( t, ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE: 0 #EXT-X-DISCONTINUITY-SEQUENCE: 0 #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z #EXTINF:2.000 https://foo.com/bar/0.ts #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z #EXTINF:2.000 https://foo.com/bar/1.ts #EXT-X-PREFETCH:https://foo.com/bar/2.ts #EXT-X-PREFETCH:https://foo.com/bar/3.ts ` ); }); test("#EXT-X-PREFETCH_02", t => { const parsed = HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE: 0 #EXT-X-DISCONTINUITY-SEQUENCE: 0 #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z #EXTINF:2.000 https://foo.com/bar/0.ts #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z #EXTINF:2.000 https://foo.com/bar/1.ts #EXT-X-PREFETCH:https://foo.com/bar/2.ts #EXT-X-PREFETCH:https://foo.com/bar/3.ts `); const {prefetchSegments} = parsed; t.is(prefetchSegments.length, 2); t.is(prefetchSegments[0].uri, "https://foo.com/bar/2.ts"); t.is(prefetchSegments[1].uri, "https://foo.com/bar/3.ts"); const stringified = HLS.stringify(parsed); t.true(stringified.includes('#EXT-X-PREFETCH:https://foo.com/bar/2.ts')); t.true(stringified.includes('#EXT-X-PREFETCH:https://foo.com/bar/3.ts')); }); // If delivering a low-latency stream, the server must deliver at least one // prefetch segment, but no more than two. test("#EXT-X-PREFETCH_03", t => { const parsed = HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE: 0 #EXT-X-DISCONTINUITY-SEQUENCE: 0 #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z #EXTINF:2.000 https://foo.com/bar/0.ts #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z #EXTINF:2.000 https://foo.com/bar/1.ts #EXT-X-PREFETCH:https://foo.com/bar/2.ts #EXT-X-PREFETCH:https://foo.com/bar/3.ts #EXT-X-PREFETCH:https://foo.com/bar/4.ts `); const {prefetchSegments} = parsed; t.is(prefetchSegments.length, 3); try { HLS.stringify(parsed); t.fail('The server must deliver no more than two prefetch segments'); } catch { t.pass(); } }); // These segments must appear after all complete segments. test("#EXT-X-PREFETCH_04", t => { try { HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE: 0 #EXT-X-DISCONTINUITY-SEQUENCE: 0 #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z #EXTINF:2.000 https://foo.com/bar/0.ts #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z #EXTINF:2.000 https://foo.com/bar/1.ts #EXT-X-PREFETCH:https://foo.com/bar/2.ts #EXT-X-PREFETCH:https://foo.com/bar/3.ts #EXTINF:2.000 https://foo.com/bar/4.ts `); t.fail('Prefetch segments must appear after all complete segments'); } catch { t.pass(); } }); // A prefetch segment's Discontinuity Sequence Number is the value of the // EXT-X-DISCONTINUITY-SEQUENCE tag (or zero if none) plus the number of // EXT-X-DISCONTINUITY and EXT-X-PREFETCH-DISCONTINUITY tags in the Playlist // preceding the URI line of the segment. test("#EXT-X-PREFETCH_05", t => { const parsed = HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE: 0 #EXT-X-DISCONTINUITY-SEQUENCE: 100 #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z #EXTINF:2.000 https://foo.com/bar/0.ts #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z #EXTINF:2.000 https://foo.com/bar/1.ts #EXT-X-DISCONTINUITY #EXTINF:2.000 https://foo.com/bar/1.ts #EXT-X-PREFETCH-DISCONTINUITY #EXT-X-PREFETCH:https://foo.com/bar/5.ts #EXT-X-PREFETCH:https://foo.com/bar/6.ts `); const {prefetchSegments} = parsed; t.is(prefetchSegments[1].discontinuitySequence, 102); }); // If a prefetch segment is the first segment in a manifest, its Media Sequence // Number is either 0, or declared in the Playlist. // The Media Sequence Number of every other prefetch segment is equal to the // Media Sequence Number of the complete segment or prefetch segment that // precedes it plus one. test("#EXT-X-PREFETCH_06", t => { let parsed; parsed = HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-PREFETCH:https://foo.com/bar/5.ts #EXT-X-PREFETCH:https://foo.com/bar/6.ts `); t.is(parsed.prefetchSegments[0].mediaSequenceNumber, 0); t.is(parsed.prefetchSegments[1].mediaSequenceNumber, 1); parsed = HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE: 100 #EXT-X-PREFETCH:https://foo.com/bar/5.ts #EXT-X-PREFETCH:https://foo.com/bar/6.ts `); t.is(parsed.prefetchSegments[0].mediaSequenceNumber, 100); t.is(parsed.prefetchSegments[1].mediaSequenceNumber, 101); }); // A prefetch segment must not be advertised with an EXTINF tag. The duration of // a prefetch segment must be equal to or less than what is specified by the // EXT-X-TARGETDURATION tag. test("#EXT-X-PREFETCH_07", t => { try { HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXTINF:2.000 #EXT-X-PREFETCH:https://foo.com/bar/5.ts #EXTINF:2.000 #EXT-X-PREFETCH:https://foo.com/bar/6.ts `); t.fail('A prefetch segment must not be advertised with an EXTINF tag'); } catch { t.pass(); } }); // A prefetch segment must not be advertised with an EXT-X-DISCONTINUITY tag. // To insert a discontinuity just for prefetch segments, the server must insert // the EXT-X-PREFETCH-DISCONTINUITY tag before the newest EXT-X-PREFETCH tag of // the new discontinuous range. test("#EXT-X-PREFETCH_08", t => { try { HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-DISCONTINUITY #EXT-X-PREFETCH:https://foo.com/bar/5.ts #EXT-X-PREFETCH:https://foo.com/bar/6.ts `); t.fail('A prefetch segment must not be advertised with an EXT-X-DISCONTINUITY tag'); } catch { t.pass(); } }); // Prefetch segments must not be advertised with an EXT-X-MAP tag. test("#EXT-X-PREFETCH_09", t => { try { HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-MAP:URI="http://example.com/map-1" #EXT-X-PREFETCH:https://foo.com/bar/5.ts #EXT-X-PREFETCH:https://foo.com/bar/6.ts `); t.fail('Prefetch segments must not be advertised with an EXT-X-MAP tag'); } catch { t.pass(); } }); // Prefetch segments may be advertised with an EXT-X-KEY tag. The key itself // must be complete; the server must not expect the client to progressively stream keys. test("#EXT-X-PREFETCH_10", t => { const parsed = HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" #EXT-X-PREFETCH:https://foo.com/bar/5.ts #EXT-X-PREFETCH:https://foo.com/bar/6.ts `); const {prefetchSegments} = parsed; t.truthy(prefetchSegments[0].key); t.is(prefetchSegments[0].key.uri, 'http://example.com'); t.truthy(prefetchSegments[1].key); t.is(prefetchSegments[1].key.uri, 'http://example.com'); }); ================================================ FILE: test/spec/HLSJS-LHLS/02_EXT-X-PREFETCH-DISCONTINUITY.spec.js ================================================ const test = require("ava"); const utils = require("../../helpers/utils"); const HLS = require("../../.."); test("#EXT-X-PREFETCH-DISCONTINUITY_01", t => { const text = ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE: 0 #EXT-X-DISCONTINUITY-SEQUENCE: 0 #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z #EXTINF:2.000 https://foo.com/bar/0.ts #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z #EXTINF:2.000 https://foo.com/bar/1.ts #EXT-X-PREFETCH-DISCONTINUITY #EXT-X-PREFETCH:https://foo.com/bar/5.ts #EXT-X-PREFETCH:https://foo.com/bar/6.ts `; utils.bothPass(t, text); const {prefetchSegments} = HLS.parse(text); t.is(prefetchSegments.length, 2); t.true(prefetchSegments[0].discontinuity); t.falsy(prefetchSegments[1].discontinuity); }); test("#EXT-X-PREFETCH-DISCONTINUITY_02", t => { const text = ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:2 #EXT-X-MEDIA-SEQUENCE: 1 #EXT-X-DISCONTINUITY-SEQUENCE: 0 #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z #EXTINF:2.000 https://foo.com/bar/1.ts #EXT-X-DISCONTINUITY #EXT-X-PROGRAM-DATE-TIME:2018-09-05T21:59:10.531Z #EXTINF:2.000 https://foo.com/bar/5.ts #EXT-X-PREFETCH:https://foo.com/bar/6.ts #EXT-X-PREFETCH-DISCONTINUITY #EXT-X-PREFETCH:https://foo.com/bar/9.ts `; utils.bothPass(t, text); const {prefetchSegments} = HLS.parse(text); t.is(prefetchSegments.length, 2); t.falsy(prefetchSegments[0].discontinuity); t.true(prefetchSegments[1].discontinuity); }); ================================================ FILE: test/spec/misc/multiple-rendition-groups.js ================================================ const test = require("ava"); const utils = require("../../helpers/utils"); const HLS = require("../../.."); test("Multiple-Rendition-Groups_01", t => { const shouldRead = ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="English",DEFAULT=YES,URI="aac_high_eng.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="Japanese",DEFAULT=NO,URI="aac_high_jp.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="English",DEFAULT=YES,URI="aac_mid_eng.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="Japanese",DEFAULT=NO,URI="aac_mid_jp.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="English",DEFAULT=YES,URI="aac_low_eng.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="Japanese",DEFAULT=NO,URI="aac_low_jp.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO="aac_high" 1080p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO="aac_mid" 720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO="aac_mid" 540p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO="aac_low" 360p.m3u8 `; const playlist = HLS.parse(shouldRead); const shouldWrite = ` #EXTM3U #EXT-X-VERSION:4 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="English",DEFAULT=YES,URI="aac_high_eng.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="Japanese",DEFAULT=NO,URI="aac_high_jp.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO="aac_high" 1080p.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="English",DEFAULT=YES,URI="aac_mid_eng.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="Japanese",DEFAULT=NO,URI="aac_mid_jp.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO="aac_mid" 720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO="aac_mid" 540p.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="English",DEFAULT=YES,URI="aac_low_eng.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="Japanese",DEFAULT=NO,URI="aac_low_jp.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO="aac_low" 360p.m3u8 `; t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(shouldWrite)); }); ================================================ FILE: test/spec/misc/scte-35.spec.js ================================================ const test = require("ava"); const utils = require("../../helpers/utils"); const HLS = require("../../.."); test("#EXT-X-CUE-IN_01", t => { const {MediaPlaylist, Segment} = HLS.types; const segments = [...Array.from({length: 3})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10})); segments[0].discontinuity = true; segments[0].markers.push({type: 'OUT', duration: 30}); const playlist = new MediaPlaylist({ targetDuration: 10, segments }); // For live media playlist, unclosed CUE-OUT is allowed. const expected = ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-DISCONTINUITY #EXT-X-CUE-OUT:DURATION=30 #EXTINF:10, https://example.com/0.ts #EXTINF:10, https://example.com/1.ts #EXTINF:10, https://example.com/2.ts `; t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected)); }); test("#EXT-X-CUE-IN_02", t => { const {MediaPlaylist, Segment} = HLS.types; const segments = [...Array.from({length: 3})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10})); segments[0].discontinuity = true; segments[0].markers.push({type: 'OUT', duration: 30}); const playlist = new MediaPlaylist({ playlistType: 'VOD', targetDuration: 10, segments }); // For VOD media playlist, unclosed CUE-OUT is not allowed. // CUE-IN will be added. const expected = ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-DISCONTINUITY #EXT-X-CUE-OUT:DURATION=30 #EXTINF:10, https://example.com/0.ts #EXTINF:10, https://example.com/1.ts #EXTINF:10, https://example.com/2.ts #EXT-X-CUE-IN `; t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected)); }); test("#EXT-X-CUE-IN_03", t => { const {MediaPlaylist, Segment} = HLS.types; const segments = [...Array.from({length: 6})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10})); segments[0].markers.push({type: 'OUT', duration: 20}); segments[2].markers.push({type: 'IN'}); segments[4].markers.push({type: 'OUT', duration: 20}); const playlist = new MediaPlaylist({ playlistType: 'EVENT', targetDuration: 10, segments }); // For live media playlist, unclosed CUE-OUT is allowed. const expected = ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PLAYLIST-TYPE:EVENT #EXT-X-CUE-OUT:DURATION=20 #EXTINF:10, https://example.com/0.ts #EXTINF:10, https://example.com/1.ts #EXT-X-CUE-IN #EXTINF:10, https://example.com/2.ts #EXTINF:10, https://example.com/3.ts #EXT-X-CUE-OUT:DURATION=20 #EXTINF:10, https://example.com/4.ts #EXTINF:10, https://example.com/5.ts `; t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected)); }); test("#EXT-X-CUE-IN_04", t => { const {MediaPlaylist, Segment} = HLS.types; const segments = [...Array.from({length: 6})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10})); segments[0].markers.push({type: 'OUT', duration: 20}); segments[2].markers.push({type: 'IN'}); segments[4].markers.push({type: 'OUT', duration: 20}); const playlist = new MediaPlaylist({ playlistType: 'VOD', targetDuration: 10, segments }); // For VOD media playlist, unclosed CUE-OUT is not allowed. // CUE-IN will be added. const expected = ` #EXTM3U #EXT-X-TARGETDURATION:10 #EXT-X-PLAYLIST-TYPE:VOD #EXT-X-CUE-OUT:DURATION=20 #EXTINF:10, https://example.com/0.ts #EXTINF:10, https://example.com/1.ts #EXT-X-CUE-IN #EXTINF:10, https://example.com/2.ts #EXTINF:10, https://example.com/3.ts #EXT-X-CUE-OUT:DURATION=20 #EXTINF:10, https://example.com/4.ts #EXTINF:10, https://example.com/5.ts #EXT-X-CUE-IN `; t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected)); }); ================================================ FILE: test/spec/parser.spec.js ================================================ const test = require('ava'); const fixtures = require('../helpers/fixtures'); const HLS = require('../..'); HLS.setOptions({strictMode: true}); const {Playlist} = HLS.types; for (const {name, m3u8, object} of fixtures) { test(name, t => { const result = HLS.parse(m3u8); if (result.source === m3u8 && deepEqual(t, result, object)) { t.pass(); } }); } function buildMessage(propName, actual, expected) { if (actual && typeof actual === 'object') { actual = JSON.parse(actual); } if (expected && typeof expected === 'object') { expected = JSON.parse(expected); } return ` ${propName} does not match. expected: ${expected} actual: ${actual} `; } function deepEqual(t, actual, expected) { if (!expected) { return; } let errorMessage; if (actual instanceof Playlist === false) { return t.fail('The result is not an instance of Playlist'); } if (actual.isMasterPlaylist !== expected.isMasterPlaylist) { return t.fail(buildMessage('Playlist.isMasterPlaylist', actual.isMasterPlaylist, expected.isMasterPlaylist)); } if (expected.uri) { if (!actual.uri || actual.uri.href !== expected.uri.href) { return t.fail(buildMessage('Playlist.uri', actual.uri, expected.uri)); } } if (actual.version !== expected.version) { return t.fail(buildMessage('Playlist.version', actual.version, expected.version)); } if (actual.independentSegments !== expected.independentSegments) { return t.fail(buildMessage('Playlist.independentSegments', actual.independentSegments, expected.independentSegments)); } if (actual.offset !== expected.offset) { return t.fail(buildMessage('Playlist.offset', actual.offset, expected.offset)); } if (expected.isMasterPlaylist) { // MasterPlaylist if (expected.variants) { if (!actual.variants || actual.variants.length !== expected.variants.length) { return t.fail(buildMessage('Playlist.variants', actual.variants, expected.variants)); } for (const [index, actualVariant] of actual.variants.entries()) { if (errorMessage = deepEqualVariant(t, actualVariant, expected.variants[index])) { return t.fail(errorMessage); } } } if (actual.currentVariant !== expected.currentVariant) { return t.fail(buildMessage('MasterPlaylist.currentVariant', actual.currentVariant, expected.currentVariant)); } if (expected.sessionDataList) { if (!actual.sessionDataList || actual.sessionDataList.length !== expected.sessionDataList.length) { return t.fail(buildMessage('MasterPlaylist.sessionDataList', actual.sessionDataList, expected.sessionDataList)); } for (const [index, actualSessionData] of actual.sessionDataList.entries()) { if (errorMessage = deepEqualSessionData(t, actualSessionData, expected.sessionDataList[index])) { return t.fail(errorMessage); } } } if (expected.sessionKeyList) { if (!actual.sessionKeyList || actual.sessionKeyList.length !== expected.sessionKeyList.length) { return t.fail(buildMessage('MasterPlaylist.sessionKeyList', actual.sessionKeyList, expected.sessionKeyList)); } for (const [index, actualSessionKey] of actual.sessionKeyList.entries()) { if (errorMessage = deepEqualKey(t, actualSessionKey, expected.sessionKeyList[index])) { return t.fail(errorMessage); } } } } else { // MediaPlaylist if (actual.targetDuration !== expected.targetDuration) { return t.fail(buildMessage('MediaPlaylist.targetDuration', actual.targetDuration, expected.targetDuration)); } if (actual.mediaSequenceBase !== expected.mediaSequenceBase) { return t.fail(buildMessage('MediaPlaylist.mediaSequenceBase', actual.mediaSequenceBase, expected.mediaSequenceBase)); } if (actual.discontinuitySequenceBase !== expected.discontinuitySequenceBase) { return t.fail(buildMessage('MediaPlaylist.discontinuitySequenceBase', actual.discontinuitySequenceBase, expected.discontinuitySequenceBase)); } if (actual.endlist !== expected.endlist) { return t.fail(buildMessage('MediaPlaylist.endlist', actual.endlist, expected.endlist)); } if (actual.playlistType !== expected.playlistType) { return t.fail(buildMessage('MediaPlaylist.playlistType', actual.playlistType, expected.playlistType)); } if (actual.isIFrame !== expected.isIFrame) { return t.fail(buildMessage('MediaPlaylist.isIFrame', actual.isIFrame, expected.isIFrame)); } if (expected.segments) { if (!actual.segments || actual.segments.length !== expected.segments.length) { return t.fail(buildMessage('MediaPlaylist.segments', actual.segments, expected.segments)); } for (const [index, actualSegment] of actual.segments.entries()) { if (errorMessage = deepEqualSegment(t, actualSegment, expected.segments[index])) { return t.fail(errorMessage); } } } if (actual.hash !== expected.hash) { return t.fail(buildMessage('MediaPlaylist.hash', actual.hash, expected.hash)); } } return true; } function deepEqualVariant(t, actual, expected) { if (!expected) { return; } let errorMessage; if (expected.uri) { if (!actual.uri || actual.uri.href !== expected.uri.href) { return t.fail(buildMessage('Variant.uri', actual.uri, expected.uri)); } } if (actual.isIFrameOnly !== expected.isIFrameOnly) { return t.fail(buildMessage('Variant.isIFrameOnly', actual.isIFrameOnly, expected.isIFrameOnly)); } if (actual.bandwidth !== expected.bandwidth) { return t.fail(buildMessage('Variant.bandwidth', actual.bandwidth, expected.bandwidth)); } if (actual.averageBandwidth !== expected.averageBandwidth) { return t.fail(buildMessage('Variant.averageBandwidth', actual.averageBandwidth, expected.averageBandwidth)); } if (actual.codecs !== expected.codecs) { return t.fail(buildMessage('Variant.codecs', actual.codecs, expected.codecs)); } if (expected.resolution) { if (!actual.resolution || actual.resolution.width !== expected.resolution.width || actual.resolution.height !== expected.resolution.height) { return t.fail(buildMessage('Variant.resolution', actual.resolution, expected.resolution)); } } if (actual.frameRate !== expected.frameRate) { return t.fail(buildMessage('Variant.frameRate', actual.frameRate, expected.frameRate)); } if (actual.hdcpLevel !== expected.hdcpLevel) { return t.fail(buildMessage('Variant.hdcpLevel', actual.hdcpLevel, expected.hdcpLevel)); } if (expected.audio) { if (!actual.audio || actual.audio.length !== expected.audio.length) { return t.fail(buildMessage('Variant.audio', actual.audio, expected.audio)); } for (const [index, actualRendition] of actual.audio.entries()) { if (errorMessage = deepEqualRendition(t, actualRendition, expected.audio[index])) { return t.fail(errorMessage); } } } if (expected.video) { if (!actual.video || actual.video.length !== expected.video.length) { return t.fail(buildMessage('Variant.video', actual.video, expected.video)); } for (const [index, actualRendition] of actual.video.entries()) { if (errorMessage = deepEqualRendition(t, actualRendition, expected.video[index])) { return t.fail(errorMessage); } } } if (expected.subtitles) { if (!actual.subtitles || actual.subtitles.length !== expected.subtitles.length) { return t.fail(buildMessage('Variant.subtitles', actual.subtitles, expected.subtitles)); } for (const [index, actualRendition] of actual.subtitles.entries()) { if (errorMessage = deepEqualRendition(t, actualRendition, expected.subtitles[index])) { return t.fail(errorMessage); } } } if (expected.closedCaptions) { if (!actual.closedCaptions || actual.closedCaptions.length !== expected.closedCaptions.length) { return t.fail(buildMessage('Variant.closedCaptions', actual.closedCaptions, expected.closedCaptions)); } for (const [index, actualRendition] of actual.closedCaptions.entries()) { if (errorMessage = deepEqualRendition(t, actualRendition, expected.closedCaptions[index])) { return t.fail(errorMessage); } } } if (expected.currentRenditions) { const expectedCurrentRenditions = expected.currentRenditions; const actualCurrentRenditions = actual.currentRenditions; for (const key of Object.keys(expectedCurrentRenditions)) { if (actualCurrentRenditions[key] !== expectedCurrentRenditions[key]) { return t.fail(buildMessage('Variant.currentRenditions', actualCurrentRenditions[key], expectedCurrentRenditions[key])); } } } } function deepEqualSessionData(t, actual, expected) { if (!expected) { return; } if (actual.id !== expected.id) { return t.fail(buildMessage('SessionData.id', actual.id, expected.id)); } if (actual.value !== expected.value) { return t.fail(buildMessage('SessionData.value', actual.value, expected.value)); } if (expected.uri) { if (!actual.uri || actual.uri.href !== expected.uri.href) { return t.fail(buildMessage('SessionData.uri', actual.uri, expected.uri)); } } if (actual.language !== expected.language) { return t.fail(buildMessage('SessionData.language', actual.language, expected.language)); } } function deepEqualKey(t, actual, expected) { if (!expected) { return; } if (actual.method !== expected.method) { return t.fail(buildMessage('Key.method', actual.method, expected.method)); } if (expected.uri) { if (!actual.uri || actual.uri.href !== expected.uri.href) { return t.fail(buildMessage('Key.uri', actual.uri, expected.uri)); } } if (expected.iv) { if (!actual.iv || actual.iv.length !== expected.iv.length) { return t.fail(buildMessage('Key.iv', actual.iv, expected.iv)); } for (let i = 0; i < actual.iv.length; i++) { if (actual.iv[i] !== expected.iv[i]) { return t.fail(buildMessage('Key.iv', actual.iv, expected.iv)); } } } if (actual.format !== expected.format) { return t.fail(buildMessage('Key.format', actual.format, expected.format)); } if (actual.formatVersion !== expected.formatVersion) { return t.fail(buildMessage('Key.formatVersion', actual.formatVersion, expected.formatVersion)); } } function deepEqualSegment(t, actual, expected) { if (!expected) { return; } let errorMessage; if (expected.uri) { if (!actual.uri || actual.uri.href !== expected.uri.href) { return t.fail(buildMessage('Segment.uri', actual.uri, expected.uri)); } } if (expected.data) { if (!actual.data || actual.data.length !== expected.data.length) { return t.fail(buildMessage('Segment.data', actual.data, expected.data)); } for (let i = 0; i < actual.data.length; i++) { if (actual.data[i] !== expected.data[i]) { return t.fail(buildMessage('Segment.data', actual.data, expected.data)); } } } if (actual.duration !== expected.duration) { return t.fail(buildMessage('Segment.duration', actual.duration, expected.duration)); } if (actual.title !== expected.title) { return t.fail(buildMessage('Segment.title', actual.title, expected.title)); } if (expected.byterange) { if (!actual.byterange || actual.byterange.length !== expected.byterange.length || actual.byterange.offset !== expected.byterange.offset) { return t.fail(buildMessage('Segment.byterange', actual.byterange, expected.byterange)); } } if (actual.discontinuity !== expected.discontinuity) { return t.fail(buildMessage('Segment.discontinuity', actual.discontinuity, expected.discontinuity)); } if (actual.mediaSequenceNumber !== expected.mediaSequenceNumber) { return t.fail(buildMessage('Segment.mediaSequenceNumber', actual.mediaSequenceNumber, expected.mediaSequenceNumber)); } if (actual.discontinuitySequence !== expected.discontinuitySequence) { return t.fail(buildMessage('Segment.discontinuitySequence', actual.discontinuitySequence, expected.discontinuitySequence)); } if (errorMessage = deepEqualKey(t, actual.key, expected.key)) { return t.fail(errorMessage); } if (errorMessage = deepEqualMap(t, actual.map, expected.map)) { return t.fail(errorMessage); } if (expected.programDateTime) { if (!actual.programDateTime || actual.programDateTime.getTime() !== expected.programDateTime.getTime()) { return t.fail(buildMessage('Segment.programDateTime', actual.programDateTime, expected.programDateTime)); } } if (errorMessage = deepEqualDateRange(t, actual.dateRange, expected.dateRange)) { return t.fail(errorMessage); } } function deepEqualRendition(t, actual, expected) { if (!expected) { return; } if (actual.type !== expected.type) { return t.fail(buildMessage('Rendition.type', actual.type, expected.type)); } if (expected.uri) { if (!actual.uri || actual.uri.href !== expected.uri.href) { return t.fail(buildMessage('Rendition.uri', actual.uri, expected.uri)); } } if (actual.groupId !== expected.groupId) { return t.fail(buildMessage('Rendition.groupId', actual.groupId, expected.groupId)); } if (actual.language !== expected.language) { return t.fail(buildMessage('Rendition.language', actual.language, expected.language)); } if (actual.assocLanguage !== expected.assocLanguage) { return t.fail(buildMessage('Rendition.assocLanguage', actual.assocLanguage, expected.assocLanguage)); } if (actual.name !== expected.name) { return t.fail(buildMessage('Rendition.name', actual.name, expected.name)); } if (actual.isDefault !== expected.isDefault) { return t.fail(buildMessage('Rendition.isDefault', actual.isDefault, expected.isDefault)); } if (actual.autoselect !== expected.autoselect) { return t.fail(buildMessage('Rendition.autoselect', actual.autoselect, expected.autoselect)); } if (actual.forced !== expected.forced) { return t.fail(buildMessage('Rendition.forced', actual.forced, expected.forced)); } if (actual.instreamId !== expected.instreamId) { return t.fail(buildMessage('Rendition.instreamId', actual.instreamId, expected.instreamId)); } if (actual.characteristics !== expected.characteristics) { return t.fail(buildMessage('Rendition.characteristics', actual.characteristics, expected.characteristics)); } if (actual.channels !== expected.channels) { return t.fail(buildMessage('Rendition.channels', actual.channels, expected.channels)); } } function deepEqualMap(t, actual, expected) { if (!expected) { return; } if (expected.uri) { if (!actual.uri || actual.uri.href !== expected.uri.href) { return t.fail(buildMessage('MediaInitializationSection.uri', actual.uri, expected.uri)); } } if (expected.byterange) { if (!actual.byterange || actual.byterange.length !== expected.byterange.length || actual.byterange.offset !== expected.byterange.offset) { return t.fail(buildMessage('MediaInitializationSection.byterange', actual.byterange, expected.byterange)); } } } function deepEqualDateRange(t, actual, expected) { if (!expected) { return; } if (actual.id !== expected.id) { return t.fail(buildMessage('DateRange.id', actual.id, expected.id)); } if (actual.classId !== expected.classId) { return t.fail(buildMessage('DateRange.classId', actual.classId, expected.classId)); } if (expected.start) { if (!actual.start || actual.start.getTime() !== expected.start.getTime()) { return t.fail(buildMessage('DateRange.start', actual.start, expected.start)); } } if (expected.end) { if (!actual.end || actual.end.getTime() !== expected.end.getTime()) { return t.fail(buildMessage('DateRange.end', actual.end, expected.end)); } } if (actual.duration !== expected.duration) { return t.fail(buildMessage('DateRange.duration', actual.duration, expected.duration)); } if (actual.plannedDuration !== expected.plannedDuration) { return t.fail(buildMessage('DateRange.plannedDuration', actual.plannedDuration, expected.plannedDuration)); } if (actual.endOnNext !== expected.endOnNext) { return t.fail(buildMessage('DateRange.endOnNext', actual.endOnNext, expected.endOnNext)); } } ================================================ FILE: test/spec/stringify.spec.js ================================================ const test = require('ava'); const fixtures = require('../helpers/fixtures'); const utils = require('../helpers/utils'); const HLS = require('../..'); HLS.setOptions({strictMode: true}); for (const {name, m3u8, object} of fixtures) { test(name, t => { const result = HLS.stringify(object); t.is(result, utils.stripCommentsAndEmptyLines(m3u8)); }); } test('stringify.postProcess.segment.add', t => { const obj = HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXTINF:6.006, http://media.example.com/01.ts #EXTINF:6.006, http://media.example.com/02.ts #EXTINF:6.006, http://ads.example.com/ad-01.ts #EXTINF:6.006, http://ads.example.com/ad-02.ts #EXTINF:6.006, http://media.example.com/03.ts #EXTINF:3.003, http://media.example.com/04.ts `); const expected = ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z #EXTINF:6.006, http://media.example.com/01.ts #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z #EXTINF:6.006, http://media.example.com/02.ts #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z #EXTINF:6.006, http://ads.example.com/ad-01.ts #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z #EXTINF:6.006, http://ads.example.com/ad-02.ts #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z #EXTINF:6.006, http://media.example.com/03.ts #EXT-X-MY-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z #EXTINF:3.003, http://media.example.com/04.ts `; let time = new Date('2014-03-05T11:14:00.000Z').getTime(); const segmentProcessor = (lines, start, end, segment) => { let hasPdt = false; for (let i = start; i <= end; i++) { if (lines[i].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { hasPdt = true; break; } } if (!hasPdt) { lines.splice(start, 0, `#EXT-X-MY-PROGRAM-DATE-TIME:${new Date(Math.round(time)).toISOString()}`); } time += segment.duration * 1000; }; const result = HLS.stringify(obj, {segmentProcessor}); t.is(result, utils.stripCommentsAndEmptyLines(expected)); }); test('stringify.postProcess.segment.delete', t => { const obj = HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z #EXTINF:6.006, http://media.example.com/01.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z #EXTINF:6.006, http://media.example.com/02.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000 #EXTINF:6.006, http://ads.example.com/ad-01.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z #EXTINF:6.006, http://ads.example.com/ad-02.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z #EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000 #EXTINF:6.006, http://media.example.com/03.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z #EXTINF:3.003, http://media.example.com/04.ts `); const expected = ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z #EXTINF:6.006, http://media.example.com/01.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z #EXTINF:6.006, http://media.example.com/02.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z #EXTINF:6.006, http://ads.example.com/ad-01.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z #EXTINF:6.006, http://ads.example.com/ad-02.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z #EXTINF:6.006, http://media.example.com/03.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z #EXTINF:3.003, http://media.example.com/04.ts `; const segmentProcessor = (lines, start, end) => { for (let i = start; i <= end; i++) { const line = lines[i]; if (line.startsWith('#EXT-X-DATERANGE')) { lines[i] = ''; } } }; const result = HLS.stringify(obj, {segmentProcessor}); t.is(result, utils.stripCommentsAndEmptyLines(expected)); }); test('stringify.postProcess.segment.update', t => { const obj = HLS.parse(` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z #EXTINF:6.006, http://media.example.com/01.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z #EXTINF:6.006, http://media.example.com/02.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000 #EXTINF:6.006, http://ads.example.com/ad-01.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z #EXTINF:6.006, http://ads.example.com/ad-02.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z #EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000 #EXTINF:6.006, http://media.example.com/03.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z #EXTINF:3.003, http://media.example.com/04.ts `); const expected = ` #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:6 #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z #EXTINF:6.006, http://media.example.com/01.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:06.006Z #EXTINF:6.006, http://media.example.com/02.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:12.012Z #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000 #EXTINF:6.006, http://ads.example.com/ad-01.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:18.018Z #EXTINF:6.006, http://ads.example.com/ad-02.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:24.024Z #EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000 #EXTINF:6.006, http://media.example.com/03.ts #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:30.030Z #EXTINF:3.003, http://media.example.com/04.ts `; const segmentProcessor = (lines, start, end) => { for (let i = start; i <= end; i++) { const line = lines[i]; if (line.startsWith('#EXT-X-DATERANGE')) { if (line.includes('PLANNED-DURATION')) { lines[start] = `${lines[start]}`; } else if (start > 0) { lines[start - 1] = `${lines[start - 1]}`; } } } }; const result = HLS.stringify(obj, {segmentProcessor}); t.is(result, utils.stripCommentsAndEmptyLines(expected)); }); test('stringify.postProcess.variant.update', t => { const obj = HLS.parse(` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000 http://example.com/low.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000 http://example.com/mid.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000 http://example.com/hi.m3u8 `); const expected = ` #EXTM3U #EXT-X-STREAM-INF:BANDWIDTH=1280000,MY-RESOLUTION=1280x720 http://example.com/low.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=2560000,MY-RESOLUTION=1920x1080 http://example.com/mid.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=7680000,MY-RESOLUTION=3840x2160 http://example.com/hi.m3u8 `; const variantProcessor = (lines, start, end, {bandwidth}) => { for (let i = start; i <= end; i++) { const line = lines[i]; if (line.startsWith('#EXT-X-STREAM-INF')) { let resolution = '640x360'; if (bandwidth >= 1000000 && bandwidth < 2000000) { resolution = '1280x720'; } else if (bandwidth >= 2000000 && bandwidth < 3000000) { resolution = '1920x1080'; } else if (bandwidth >= 3000000) { resolution = '3840x2160'; } lines[i] = `${line},MY-RESOLUTION=${resolution}`; } } }; const result = HLS.stringify(obj, {variantProcessor}); t.is(result, utils.stripCommentsAndEmptyLines(expected)); }); ================================================ FILE: test/spec/utils.spec.js ================================================ const test = require('ava'); const rewire = require('rewire'); const utils = require('../../utils'); utils.setOptions({strictMode: true}); test('utils.THROW', t => { try { utils.THROW(new Error('abc')); } catch (err) { t.truthy(err); t.is(err.message, 'abc'); } }); test('utils.ASSERT', t => { utils.ASSERT('No error occurred', 1, 2, 3); try { utils.ASSERT('Error occurred', 1, 2, false); } catch (err) { t.truthy(err); t.is(err.message, 'Error occurred : Failed at [2]'); } }); test('utils.CONDITIONALASSERT', t => { utils.CONDITIONALASSERT([true, 1], [true, 2], [true, 3]); utils.CONDITIONALASSERT([false, 0], [false, 1], [false, 2]); try { utils.CONDITIONALASSERT([false, 0], [true, 1], [true, 0]); } catch (err) { t.truthy(err); t.is(err.message, 'Conditional Assert : Failed at [2]'); } }); test('utils.PARAMCHECK', t => { utils.PARAMCHECK(1, 2, 3); try { utils.PARAMCHECK(1, 2, undefined); } catch (err) { t.truthy(err); t.is(err.message, 'Param Check : Failed at [2]'); } }); test('utils.CONDITIONALPARAMCHECK', t => { utils.CONDITIONALPARAMCHECK([true, 1], [true, 2], [true, 3]); utils.CONDITIONALPARAMCHECK([false, undefined], [false, 1], [false, 2]); try { utils.CONDITIONALPARAMCHECK([false, undefined], [true, 1], [true, undefined]); } catch (err) { t.truthy(err); t.is(err.message, 'Conditional Param Check : Failed at [2]'); } }); test('utils.toNumber', t => { t.is(utils.toNumber('123'), 123); t.is(utils.toNumber(123), 123); t.is(utils.toNumber('abc'), 0); t.is(utils.toNumber('8bc'), 8); }); test('utils.hexToByteSequence', t => { t.deepEqual(utils.hexToByteSequence('0x000000'), new Uint8Array([0, 0, 0])); t.deepEqual(utils.hexToByteSequence('0xFFFFFF'), new Uint8Array([255, 255, 255])); t.deepEqual(utils.hexToByteSequence('FFFFFF'), new Uint8Array([255, 255, 255])); }); test('utils.byteSequenceToHex', t => { t.is(utils.byteSequenceToHex(new Uint8Array([0, 0, 0])), '0x000000'); t.is(utils.byteSequenceToHex(new Uint8Array([255, 255, 255])), '0xFFFFFF'); t.is(utils.byteSequenceToHex(new Uint8Array([255, 255, 256])), '0xFFFF00'); }); test('utils.tryCatch', t => { let result = utils.tryCatch( () => { return 1; }, () => { return 0; } ); t.is(result, 1); result = utils.tryCatch( () => { return JSON.parse('{{'); }, () => { return 0; } ); t.is(result, 0); t.throws(() => { utils.tryCatch( () => { return JSON.parse('{{'); }, () => { return JSON.parse('}}'); } ); }); }); test('utils.splitAt', t => { t.deepEqual(utils.splitAt('a=1', '='), ['a', '1']); t.deepEqual(utils.splitAt('a=1=2', '='), ['a', '1=2']); t.deepEqual(utils.splitAt('a=1=2=3', '='), ['a', '1=2=3']); t.deepEqual(utils.splitAt('a=1=2=3', '=', 0), ['a', '1=2=3']); t.deepEqual(utils.splitAt('a=1=2=3', '=', 1), ['a=1', '2=3']); t.deepEqual(utils.splitAt('a=1=2=3', '=', 2), ['a=1=2', '3']); t.deepEqual(utils.splitAt('a=1=2=3', '=', -1), ['a=1=2', '3']); }); test('utils.trim', t => { t.is(utils.trim(' abc '), 'abc'); t.is(utils.trim(' abc ', ' '), 'abc'); t.is(utils.trim('"abc"', '"'), 'abc'); t.is(utils.trim('abc:', ':'), 'abc'); t.is(utils.trim('abc'), 'abc'); t.is(utils.trim(' "abc" ', '"'), 'abc'); }); test('utils.splitWithPreservingQuotes', t => { 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"']); }); test('utls.camelify', t => { const props = ['caption', 'Caption', 'captioN', 'CAPTION', 'closed-captions', 'closed_captions', 'CLOSED-CAPTIONS']; const results = ['caption', 'caption', 'caption', 'caption', 'closedCaptions', 'closedCaptions', 'closedCaptions']; t.deepEqual(props.map(p => utils.camelify(p)), results); }); test('utils.formatDate', t => { const DATE = '2014-03-05T11:15:00.000Z'; t.is(utils.formatDate(new Date(DATE)), DATE); const LOCALDATE = '2000-01-01T08:59:59.999+09:00'; const UTC = '1999-12-31T23:59:59.999Z'; t.is(utils.formatDate(new Date(LOCALDATE)), UTC); }); test('utils.setOptions/getOptions', t => { const params = {a: 1, b: 'b', c: [1, 2, 3], strictMode: true}; utils.setOptions(params); t.deepEqual(params, utils.getOptions()); params.strictMode = false; t.notDeepEqual(params, utils.getOptions()); t.is(utils.getOptions().strictMode, true); }); test('utils.THROW.strictMode', t => { const message = 'Error Message'; utils.setOptions({strictMode: false}); try { utils.THROW({message}); } catch { t.fail(); } utils.setOptions({strictMode: true}); try { utils.THROW({message}); t.fail(); } catch (e) { t.is(e.message, message); } t.pass(); }); test('utils.THROW.silent', t => { let silent = false; const utils = rewire('../../utils'); const errorHandler = msg => { if (silent) { t.is(msg, 'end'); } else { t.is(msg, message); } }; utils.__set__({ console: { error: errorHandler, log: console.log } }); const message = 'Error Message'; utils.setOptions({strictMode: false}); utils.THROW({message}); silent = true; utils.setOptions({silent}); utils.THROW({message}); console.error('end'); utils.setOptions({strictMode: true}); }); ================================================ FILE: tsconfig.json ================================================ { "extends": "@tsconfig/node18/tsconfig.json", "module": "node", "compilerOptions": { "allowJs": true, "checkJs": true, "declaration": true, "noImplicitAny": false }, "exclude": [ "webpack.config.js", "dist", "test" ] } ================================================ FILE: types.ts ================================================ import * as utils from './utils'; type RenditionType = 'AUDIO' | 'VIDEO' | 'SUBTITLES' | 'CLOSED-CAPTIONS'; class Rendition { type: RenditionType; uri?: string; groupId: string; language?: string; assocLanguage?: string; name: string; isDefault: boolean; autoselect: boolean; forced: boolean; instreamId?: string; characteristics?: string; channels?: string; pathwayId?: string; constructor({ type, // required uri, // required if type='SUBTITLES' groupId, // required language, assocLanguage, name, // required isDefault, autoselect, forced, instreamId, // required if type=CLOSED-CAPTIONS characteristics, channels, pathwayId }: Rendition) { utils.PARAMCHECK(type, groupId, name); utils.CONDITIONALASSERT([type === 'SUBTITLES', uri], [type === 'CLOSED-CAPTIONS', instreamId], [type === 'CLOSED-CAPTIONS', !uri], [forced, type === 'SUBTITLES']); this.type = type; this.uri = uri; this.groupId = groupId; this.language = language; this.assocLanguage = assocLanguage; this.name = name; this.isDefault = isDefault; this.autoselect = autoselect; this.forced = forced; this.instreamId = instreamId; this.characteristics = characteristics; this.channels = channels; this.pathwayId = pathwayId; } } class Variant { uri: string; isIFrameOnly?: boolean; bandwidth: number; averageBandwidth?: number; score: number; codecs?: string; resolution?: Resolution; frameRate?: number; hdcpLevel?: string; allowedCpc: AllowedCpc[]; videoRange: 'SDR' | 'HLG' | 'PQ'; stableVariantId: string; pathwayId: string; programId: any; audio: (Rendition & {type: 'AUDIO'})[]; video: (Rendition & {type: 'VIDEO'})[]; subtitles: (Rendition & {type: 'SUBTITLES'})[]; closedCaptions: (Rendition & {type: 'CLOSED-CAPTIONS'})[]; currentRenditions: { audio: number; video: number; subtitles: number; closedCaptions: number; }; constructor({ uri, // required isIFrameOnly = false, bandwidth, // required averageBandwidth, score, codecs, // required? resolution, frameRate, hdcpLevel, allowedCpc, videoRange, stableVariantId, pathwayId, programId, audio = [], video = [], subtitles = [], closedCaptions = [], currentRenditions = {audio: 0, video: 0, subtitles: 0, closedCaptions: 0} }: any) { // utils.PARAMCHECK(uri, bandwidth, codecs); utils.PARAMCHECK(uri, bandwidth); // the spec states that CODECS is required but not true in the real world this.uri = uri; this.isIFrameOnly = isIFrameOnly; this.bandwidth = bandwidth; this.averageBandwidth = averageBandwidth; this.score = score; this.codecs = codecs; this.resolution = resolution; this.frameRate = frameRate; this.hdcpLevel = hdcpLevel; this.allowedCpc = allowedCpc; this.videoRange = videoRange; this.stableVariantId = stableVariantId; this.pathwayId = pathwayId; this.programId = programId; this.audio = audio; this.video = video; this.subtitles = subtitles; this.closedCaptions = closedCaptions; this.currentRenditions = currentRenditions; } } class SessionData { id: string; value?: string; uri?: string; language?: string; constructor({ id, // required value, uri, language }: SessionData) { utils.PARAMCHECK(id, value || uri); utils.ASSERT('SessionData cannot have both value and uri, shoud be either.', !(value && uri)); this.id = id; this.value = value; this.uri = uri; this.language = language; } } class Key { method: string; uri?: string; iv?: ArrayBuffer; format?: string; formatVersion?: string; constructor({ method, // required uri, // required unless method=NONE iv, format, formatVersion }: Key) { utils.PARAMCHECK(method); utils.CONDITIONALPARAMCHECK([method !== 'NONE', uri]); utils.CONDITIONALASSERT([method === 'NONE', !(uri || iv || format || formatVersion)]); this.method = method; this.uri = uri; this.iv = iv; this.format = format; this.formatVersion = formatVersion; } } class ContentSteering { serverUri: string; pathwayId: string; constructor({ serverUri, pathwayId }: ContentSteering) { this.serverUri = serverUri; this.pathwayId = pathwayId; } } export type Byterange = { length: number; offset: number; }; class MediaInitializationSection { hint: boolean; uri: string; mimeType?: string; byterange?: Byterange; constructor({ hint = false, uri, // required mimeType, byterange }: Partial & {uri: string}) { utils.PARAMCHECK(uri); this.hint = hint; this.uri = uri; this.mimeType = mimeType; this.byterange = byterange; } } class DateRange { id: string; classId?: string; start?: Date; cue?: string; end?: Date; duration?: number; plannedDuration?: number; endOnNext?: boolean; attributes: Record; constructor({ id, // required classId, // required if endOnNext is true start, cue, end, duration, plannedDuration, endOnNext, attributes = {} }: DateRange) { utils.PARAMCHECK(id); utils.CONDITIONALPARAMCHECK([endOnNext === true, classId]); utils.CONDITIONALASSERT([end, start], [end, start! <= end!], [duration, duration! >= 0], [plannedDuration, plannedDuration! >= 0]); this.id = id; this.classId = classId; this.start = start; this.cue = cue; this.end = end; this.duration = duration; this.plannedDuration = plannedDuration; this.endOnNext = endOnNext; this.attributes = attributes; } } class SpliceInfo { type: string; duration?: number; tagName?: string; value?: string; constructor({ type, // required duration, // required if the type is 'OUT' tagName, // required if the type is 'RAW' value }: SpliceInfo) { utils.PARAMCHECK(type); utils.CONDITIONALPARAMCHECK([type === 'OUT', duration]); utils.CONDITIONALPARAMCHECK([type === 'RAW', tagName]); this.type = type; this.duration = duration; this.tagName = tagName; this.value = value; } } type DataType = 'part' | 'playlist' | 'prefetch' | 'segment'; class Data { type: DataType; constructor(type: DataType) { utils.PARAMCHECK(type); this.type = type; } } class Playlist extends Data { isMasterPlaylist: boolean; uri?: string; version?: number; independentSegments: boolean; start?: { offset: number; precise: boolean }; source?: string; defines?: Record[]; constructor({ isMasterPlaylist, // required uri, version, independentSegments = false, start, source, defines, }: Partial & { isMasterPlaylist: boolean }) { super('playlist'); utils.PARAMCHECK(isMasterPlaylist); this.isMasterPlaylist = isMasterPlaylist; this.uri = uri; this.version = version; this.independentSegments = independentSegments; this.start = start; this.source = source; this.defines = defines; } } class MasterPlaylist extends Playlist { declare isMasterPlaylist: true; variants: Variant[]; currentVariant?: number; sessionDataList: SessionData[]; sessionKeyList: Key[]; contentSteering?: ContentSteering; constructor(params: Partial = {}) { super({...params, isMasterPlaylist: true}); const { variants = [], currentVariant, sessionDataList = [], sessionKeyList = [], contentSteering = undefined } = params; this.variants = variants; this.currentVariant = currentVariant; this.sessionDataList = sessionDataList; this.sessionKeyList = sessionKeyList; this.contentSteering = contentSteering; } } type LowLatencyCompatibility = { canBlockReload: boolean, canSkipUntil: number, holdBack: number, partHoldBack: number, }; class MediaPlaylist extends Playlist { declare isMasterPlaylist: false; targetDuration: number; mediaSequenceBase?: number; discontinuitySequenceBase?: number; endlist: boolean; playlistType?: 'EVENT' | 'VOD'; isIFrame?: boolean; dateRanges: DateRange[]; segments: Segment[]; prefetchSegments: PrefetchSegment[]; lowLatencyCompatibility?: LowLatencyCompatibility; partTargetDuration?: number; renditionReports: RenditionReport[]; skip: number; hash?: Record; constructor(params: Partial = {}) { super({...params, isMasterPlaylist: false}); const { targetDuration, mediaSequenceBase = 0, discontinuitySequenceBase = 0, endlist = false, playlistType, isIFrame, dateRanges = [], segments = [], prefetchSegments = [], lowLatencyCompatibility, partTargetDuration, renditionReports = [], skip = 0, hash } = params; this.targetDuration = targetDuration!; this.mediaSequenceBase = mediaSequenceBase; this.discontinuitySequenceBase = discontinuitySequenceBase; this.endlist = endlist; this.playlistType = playlistType; this.isIFrame = isIFrame; this.dateRanges = dateRanges; this.segments = segments; this.prefetchSegments = prefetchSegments; this.lowLatencyCompatibility = lowLatencyCompatibility; this.partTargetDuration = partTargetDuration; this.renditionReports = renditionReports; this.skip = skip; this.hash = hash; } } class Segment extends Data { uri: string; mimeType: string; data: any; duration: number; title?: string; byterange: Byterange; discontinuity?: boolean; mediaSequenceNumber: number; discontinuitySequence: number; key?: Key; map: MediaInitializationSection; programDateTime?: Date; dateRange?: DateRange; markers: SpliceInfo[]; parts: PartialSegment[]; gap?: boolean; constructor({ uri, mimeType, data, duration, title, byterange, discontinuity, mediaSequenceNumber = 0, discontinuitySequence = 0, key, map, programDateTime, dateRange, markers = [], parts = [], gap }: any) { super('segment'); // utils.PARAMCHECK(uri, mediaSequenceNumber, discontinuitySequence); this.uri = uri; this.mimeType = mimeType; this.data = data; this.duration = duration; this.title = title; this.byterange = byterange; this.discontinuity = discontinuity; this.mediaSequenceNumber = mediaSequenceNumber; this.discontinuitySequence = discontinuitySequence; this.key = key; this.map = map; this.programDateTime = programDateTime; this.dateRange = dateRange; this.markers = markers; this.parts = parts; this.gap = gap; } } class PartialSegment extends Data { hint: boolean; uri: string; duration?: number; independent?: boolean; byterange?: Byterange; gap?: boolean; constructor({ hint = false, uri, // required duration, independent, byterange, gap }: Omit) { super('part'); utils.PARAMCHECK(uri); this.hint = hint; this.uri = uri; this.duration = duration; this.independent = independent; this.duration = duration; this.byterange = byterange; this.gap = gap; } } class PrefetchSegment extends Data { uri: string; discontinuity?: boolean; mediaSequenceNumber: number; discontinuitySequence: number; key?: Key | null; constructor({ uri, // required discontinuity, mediaSequenceNumber = 0, discontinuitySequence = 0, key }: Omit) { super('prefetch'); utils.PARAMCHECK(uri); this.uri = uri; this.discontinuity = discontinuity; this.mediaSequenceNumber = mediaSequenceNumber; this.discontinuitySequence = discontinuitySequence; this.key = key; } } class RenditionReport { uri: string; lastMSN?: number; lastPart: number; constructor({ uri, // required lastMSN, lastPart }: RenditionReport) { utils.PARAMCHECK(uri); this.uri = uri; this.lastMSN = lastMSN; this.lastPart = lastPart; } } export { Rendition, Variant, SessionData, Key, MediaInitializationSection, DateRange, SpliceInfo, Playlist, MasterPlaylist, MediaPlaylist, Segment, PartialSegment, PrefetchSegment, RenditionReport, ContentSteering }; export type AllowedCpc = { format: string; cpcList: string[]; }; export type ExtInfo = { duration: number; title: string; }; export type Resolution = { width: number; height: number; }; export type TagParam = | [ null, null ] | [ number, null ] | [ null, Record ] | [ ExtInfo, null ] | [ Byterange, null ] | [ Date, null ]; export type UserAttribute = number | string | Uint8Array; export type PostProcess = { segmentProcessor?: ((lines: string[], start: number, end: number, segment: Segment, i: number) => void); variantProcessor?: ((lines: string[], start: number, end: number, variant: Variant, i: number) => void); }; ================================================ FILE: utils.ts ================================================ type Options = { strictMode?: boolean, allowClosedCaptionsNone?: boolean, silent?: boolean }; let options: Options = {}; function THROW(err: Error) { if (!options.strictMode) { if (!options.silent) { console.error(err.message); } return; } throw err; } function ASSERT(msg: string, ...options: boolean[]) { for (const [index, param] of options.entries()) { if (!param) { THROW(new Error(`${msg} : Failed at [${index}]`)); } } } function CONDITIONALASSERT(...options) { for (const [index, [cond, param]] of options.entries()) { if (!cond) { continue; } if (!param) { THROW(new Error(`Conditional Assert : Failed at [${index}]`)); } } } function PARAMCHECK(...options) { for (const [index, param] of options.entries()) { if (param === undefined) { THROW(new Error(`Param Check : Failed at [${index}]`)); } } } function CONDITIONALPARAMCHECK(...options) { for (const [index, [cond, param]] of options.entries()) { if (!cond) { continue; } if (param === undefined) { THROW(new Error(`Conditional Param Check : Failed at [${index}]`)); } } } function INVALIDPLAYLIST(msg: string) { THROW(new Error(`Invalid Playlist : ${msg}`)); } function toNumber(str: string, radix = 10) { if (typeof str === 'number') { return str; } const num = radix === 10 ? Number.parseFloat(str) : Number.parseInt(str, radix); if (Number.isNaN(num)) { return 0; } return num; } function hexToByteSequence(str: string): Uint8Array { if (str.startsWith('0x') || str.startsWith('0X')) { str = str.slice(2); } const numArray = new Uint8Array(str.length / 2); for (let i = 0; i < str.length; i += 2) { numArray[i / 2] = Number.parseInt(str.slice(i, i + 2), 16); } return numArray; } function byteSequenceToHex(sequence: ArrayBuffer, start = 0, end = sequence.byteLength) { if (end <= start) { THROW(new Error(`end must be larger than start : start=${start}, end=${end}`)); } const array: string[] = []; for (let i = start; i < end; i++) { array.push(`0${(sequence[i] & 0xFF).toString(16).toUpperCase()}`.slice(-2)); } return `0x${array.join('')}`; } function tryCatch(body: () => T, errorHandler: (err: unknown) => T): T { try { return body(); } catch (err) { return errorHandler(err); } } function splitAt(str: string, delimiter: string, index = 0): [string] | [string, string] { let lastDelimiterPos = -1; for (let i = 0, j = 0; i < str.length; i++) { if (str[i] === delimiter) { if (j++ === index) { return [str.slice(0, i), str.slice(i + 1)]; } lastDelimiterPos = i; } } if (lastDelimiterPos !== -1) { return [str.slice(0, lastDelimiterPos), str.slice(lastDelimiterPos + 1)]; } return [str]; } function trim(str: string | undefined, char = ' ') { if (!str) { return str; } str = str.trim(); if (char === ' ') { return str; } if (str.startsWith(char)) { str = str.slice(1); } if (str.endsWith(char)) { str = str.slice(0, -1); } return str; } function splitByCommaWithPreservingQuotes(str: string) { const list: string[] = []; let doParse = true; let start = 0; const prevQuotes: string[] = []; for (let i = 0; i < str.length; i++) { const curr = str[i]; if (doParse && curr === ',') { list.push(str.slice(start, i).trim()); start = i + 1; continue; } if (curr === '"' || curr === '\'') { if (doParse) { prevQuotes.push(curr); doParse = false; } else if (curr === prevQuotes.at(-1)) { prevQuotes.pop(); doParse = true; } else { prevQuotes.push(curr); } } } list.push(str.slice(start).trim()); return list; } function camelify(str: string) { const array: string[] = []; let nextUpper = false; for (const ch of str) { if (ch === '-' || ch === '_') { nextUpper = true; continue; } if (nextUpper) { array.push(ch.toUpperCase()); nextUpper = false; continue; } array.push(ch.toLowerCase()); } return array.join(''); } function formatDate(date: Date) { const YYYY = date.getUTCFullYear(); const MM = ('0' + (date.getUTCMonth() + 1)).slice(-2); const DD = ('0' + date.getUTCDate()).slice(-2); const hh = ('0' + date.getUTCHours()).slice(-2); const mm = ('0' + date.getUTCMinutes()).slice(-2); const ss = ('0' + date.getUTCSeconds()).slice(-2); const msc = ('00' + date.getUTCMilliseconds()).slice(-3); return `${YYYY}-${MM}-${DD}T${hh}:${mm}:${ss}.${msc}Z`; } function hasOwnProp(obj: object, propName: string): boolean { return Object.hasOwn(obj, propName); } function setOptions(newOptions: Partial = {}): void { options = Object.assign(options, newOptions); } function getOptions(): Options { return Object.assign({}, options); } export { THROW, ASSERT, CONDITIONALASSERT, PARAMCHECK, CONDITIONALPARAMCHECK, INVALIDPLAYLIST, toNumber, hexToByteSequence, byteSequenceToHex, tryCatch, splitAt, trim, splitByCommaWithPreservingQuotes, camelify, formatDate, hasOwnProp, setOptions, getOptions };