Compare commits

...

31 commits

Author SHA1 Message Date
mister-ben
26d880375e 7.2.0 2024-08-21 22:06:55 +02:00
André M.
c060bc7338
chore: update vhs-utils dependency (#182)
* chore: update vhs-utils dependency to 4.1.1

---------

Co-authored-by: mister-ben <1676039+mister-ben@users.noreply.github.com>
2024-08-21 22:06:15 +02:00
mister-ben
ba6e7cbafe
feat: add support for #EXT-X-DEFINE (#185)
* feat: add support for EXT-X-DEFINE

* tests

* readme

* cleanup

* work with relative URLs

* missing return

* fix typo

Co-authored-by: Dzianis Dashkevich <98566601+dzianis-dashkevich@users.noreply.github.com>

* lint

* lint

---------

Co-authored-by: Dzianis Dashkevich <98566601+dzianis-dashkevich@users.noreply.github.com>
2024-08-21 18:10:14 +02:00
André M.
e5dbdb6288
feat: add support for #EXT-X-I-FRAMES-ONLY (#173)
* feat: add support for #EXT-X-I-FRAMES-ONLY

Handles I-frames-only `segments`, providing a basis for the creation of trick-play functionality.

**parse-stream.js**

- add match statement for parsing the `EXT-X-I-FRAMES-ONLY` tag
- add test case

**parser.js**

- add a property `iFramesOnly` to the `manifest`
- add a function to validate the minimum version required
- trigger a `warn` event if the minimum version required is not supported or undefined, as required by the specification
- add test case

https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.6

- update `README.md` documentation

* Update src/parse-stream.js

* Update test for #171 changes

---------

Co-authored-by: mister-ben <1676039+mister-ben@users.noreply.github.com>
2024-08-16 17:49:27 +02:00
Logan Song
3f49bb4331
EXT-X-CUE-IN ":" is not necessary (#181) 2024-07-06 07:43:15 +01:00
André M
990c6ced71
feat: add support for #EXT-X-I-FRAME-STREAM-INF (#171)
* feat: add support for #EXT-X-I-FRAME-STREAM-INF

Exposes I-frame playlists through the `iFramePlaylists` property, providing a basis for the creation of trick-play functionality.

**parse-stream.js**

- add match statement for parsing the `EXT-X-I-FRAME-STREAM-INF` tag
  - apply type conversions as indicated in the specification for attributes `BANDWIDTH`, `AVERAGE-BANDWIDTH`, `FRAME-RATE`
  - overwrite the `RESOLUTION` attribute with an object representing the resolution
- extract a function to parse the `RESOLUTION`
- add test case

https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.4.2

**parser.js**

- add an array property `iFramePlaylists` to the `manifest`
- add each `i-frame playlist` to `iFramePlaylists`
- trigger a `warn` event if the `BANDWIDTH` or `URI` attributes are missing, as required by the specification
- add test case

https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.4.3

- update `master-fmp4.js` to add `iFramePlaylists`
- update `README.md` documentation

* test: update fixtures to take iFramePlaylists property into account

* refactor(stream-inf): uses the parseResolution function to extract a resolution object

---------

Co-authored-by: mister-ben <1676039+mister-ben@users.noreply.github.com>
2024-07-06 07:41:01 +01:00
Adam Waldron
f8c9817a95
chore: add content-steering tag to readme (#177) 2023-08-15 15:39:09 -07:00
hswaminathan
b2d44f2042 7.1.0 2023-08-07 12:26:05 -04:00
Adam Waldron
42472c5979
feat: parse content steering tags and attributes (#176) 2023-08-07 09:19:35 -07:00
Harisha Rajam Swaminathan
73d934ce58
fix: merge dateRange tags with same IDs and no conflicting attributes (#175) 2023-08-07 11:58:23 -04:00
Harisha Rajam Swaminathan
6944bb1b2f
fix: add dateTimeObject and dateTimeString for backward compatibility (#174) 2023-08-07 11:57:27 -04:00
Harisha Rajam Swaminathan
72da994469
chore: update v7.0.0 documentation (#172) 2023-07-24 18:09:50 -04:00
hswaminathan
a673efcae1 7.0.0 2023-07-10 11:03:32 -04:00
Piotr Błażejewicz (Peter Blazejewicz)
4d3e6ce140
docs: correct customType option name (#147)
`Parser.addParser` is a pass through to `ParseStream.addParser`, which uses and
documents `customType`. The same is in public docs (README)

Thanks!
2023-07-07 20:01:09 -04:00
Harisha Rajam Swaminathan
e7c683f5f6
feat: Add PDT to each segment (#168) 2023-07-07 15:10:40 -04:00
Genteure
4adaa2c600
feat: output segment title from EXTINF (#158) 2023-07-06 19:03:35 -07:00
Harisha Rajam Swaminathan
516ab67d17
fix: rename daterange to dateRanges (#166) 2023-06-28 14:14:06 -04:00
Adam Waldron
ad1f11f17d 6.2.0 2023-05-25 11:32:40 -07:00
Adam Waldron
8c47d81a6c
feat: add independent-segments support (#165) 2023-05-22 21:34:13 -07:00
hswaminathan
055d7b760e 6.1.0 2023-05-12 12:09:53 -04:00
Harisha Rajam Swaminathan
cf744dbcf4
Merge pull request #163 from videojs/feat/daterange
Add DATERANGE support
2023-05-01 18:03:49 -04:00
hswaminathan
fc746e7d7f parse end-on-next 2023-05-01 17:23:03 -04:00
hswaminathan
d35b7acaf1 Add tests for DURATION and PLANNED-DURATION attributes 2023-04-24 19:24:18 -04:00
hswaminathan
33b24c4cb5 add support for multiple daterange tags and add tests 2023-04-22 01:07:58 -04:00
hswaminathan
1124e08af5 update docs, add warning and tests 2023-04-20 21:58:49 -04:00
hswaminathan
9e4c3ad0ba add test 2023-04-18 16:01:23 -04:00
hswaminathan
5a76ec0716 remove comment 2023-04-18 15:35:19 -04:00
hswaminathan
8059d61a73 Add support for Daterange 2023-04-18 00:47:17 -04:00
Sarah Rimron-Soutter
f38d60de6c 6.0.0 2022-09-27 17:08:00 +01:00
Essk
8d56f30c4d
fix: non standard tag match (#156)
Prevent non-standard tags from being erroneously matched as standard tags by enforcing the colon tag delimiter

BREAKING CHANGE: Missing colon (:) tag delimiters are no longer supported
Closes #22
2022-09-27 17:05:11 +01:00
Essk
6fe98eeea4
Merge pull request #157 from videojs/remove-version-tests
chore: don't run tests on version
2022-09-27 16:58:38 +01:00
70 changed files with 2010 additions and 133 deletions

View file

@ -1,3 +1,76 @@
<a name="7.2.0"></a>
# [7.2.0](https://github.com/videojs/m3u8-parser/compare/v7.1.0...v7.2.0) (2024-08-21)
### Features
* add support for #EXT-X-DEFINE ([#185](https://github.com/videojs/m3u8-parser/issues/185)) ([ba6e7cb](https://github.com/videojs/m3u8-parser/commit/ba6e7cb))
* add support for #EXT-X-I-FRAME-STREAM-INF ([#171](https://github.com/videojs/m3u8-parser/issues/171)) ([990c6ce](https://github.com/videojs/m3u8-parser/commit/990c6ce)), closes [/datatracker.ietf.org/doc/html/rfc8216#section-4](https://github.com//datatracker.ietf.org/doc/html/rfc8216/issues/section-4) [/datatracker.ietf.org/doc/html/rfc8216#section-4](https://github.com//datatracker.ietf.org/doc/html/rfc8216/issues/section-4)
* add support for #EXT-X-I-FRAMES-ONLY ([#173](https://github.com/videojs/m3u8-parser/issues/173)) ([e5dbdb6](https://github.com/videojs/m3u8-parser/commit/e5dbdb6)), closes [/datatracker.ietf.org/doc/html/rfc8216#section-4](https://github.com//datatracker.ietf.org/doc/html/rfc8216/issues/section-4) [#171](https://github.com/videojs/m3u8-parser/issues/171)
### Chores
* add content-steering tag to readme ([#177](https://github.com/videojs/m3u8-parser/issues/177)) ([f8c9817](https://github.com/videojs/m3u8-parser/commit/f8c9817))
* update vhs-utils dependency ([#182](https://github.com/videojs/m3u8-parser/issues/182)) ([c060bc7](https://github.com/videojs/m3u8-parser/commit/c060bc7))
<a name="7.1.0"></a>
# [7.1.0](https://github.com/videojs/m3u8-parser/compare/v7.0.0...v7.1.0) (2023-08-07)
### Features
* parse content steering tags and attributes ([#176](https://github.com/videojs/m3u8-parser/issues/176)) ([42472c5](https://github.com/videojs/m3u8-parser/commit/42472c5))
### Bug Fixes
* add dateTimeObject and dateTimeString for backward compatibility ([#174](https://github.com/videojs/m3u8-parser/issues/174)) ([6944bb1](https://github.com/videojs/m3u8-parser/commit/6944bb1))
* merge dateRange tags with same IDs and no conflicting attributes ([#175](https://github.com/videojs/m3u8-parser/issues/175)) ([73d934c](https://github.com/videojs/m3u8-parser/commit/73d934c))
### Chores
* update v7.0.0 documentation ([#172](https://github.com/videojs/m3u8-parser/issues/172)) ([72da994](https://github.com/videojs/m3u8-parser/commit/72da994))
<a name="7.0.0"></a>
# [7.0.0](https://github.com/videojs/m3u8-parser/compare/v6.2.0...v7.0.0) (2023-07-10)
### Features
* Add PDT to each segment ([#168](https://github.com/videojs/m3u8-parser/issues/168)) ([e7c683f](https://github.com/videojs/m3u8-parser/commit/e7c683f))
* output segment title from EXTINF ([#158](https://github.com/videojs/m3u8-parser/issues/158)) ([4adaa2c](https://github.com/videojs/m3u8-parser/commit/4adaa2c))
### Documentation
* correct `customType` option name ([#147](https://github.com/videojs/m3u8-parser/issues/147)) ([4d3e6ce](https://github.com/videojs/m3u8-parser/commit/4d3e6ce))
### BREAKING CHANGES
* rename `daterange` to `dateRanges`
* remove `dateTimeObject` and `dateTimeString` from parsed segment and replaces it with `programDateTime` which represents the timestamp in milliseconds
<a name="6.2.0"></a>
# [6.2.0](https://github.com/videojs/m3u8-parser/compare/v6.1.0...v6.2.0) (2023-05-25)
### Features
* add independent-segments support ([#165](https://github.com/videojs/m3u8-parser/issues/165)) ([8c47d81](https://github.com/videojs/m3u8-parser/commit/8c47d81))
<a name="6.1.0"></a>
# [6.1.0](https://github.com/videojs/m3u8-parser/compare/v6.0.0...v6.1.0) (2023-05-12)
<a name="6.0.0"></a>
# [6.0.0](https://github.com/videojs/m3u8-parser/compare/v5.0.0...v6.0.0) (2022-09-27)
### Bug Fixes
* non standard tag match ([#156](https://github.com/videojs/m3u8-parser/issues/156)) ([8d56f30](https://github.com/videojs/m3u8-parser/commit/8d56f30)), closes [#22](https://github.com/videojs/m3u8-parser/issues/22)
### Chores
* don't run tests on version ([b84575f](https://github.com/videojs/m3u8-parser/commit/b84575f))
### BREAKING CHANGES
* Missing colon (:) tag delimiters are no longer supported
<a name="5.0.0"></a> <a name="5.0.0"></a>
# [5.0.0](https://github.com/videojs/m3u8-parser/compare/v4.7.1...v5.0.0) (2022-08-19) # [5.0.0](https://github.com/videojs/m3u8-parser/compare/v4.7.1...v5.0.0) (2022-08-19)

View file

@ -13,12 +13,13 @@ m3u8 parser
- [Installation](#installation) - [Installation](#installation)
- [Usage](#usage) - [Usage](#usage)
- [Constructor Options](#constructor-options)
- [Parsed Output](#parsed-output) - [Parsed Output](#parsed-output)
- [Supported Tags](#supported-tags) - [Supported Tags](#supported-tags)
- [Basic Playlist Tags](#basic-playlist-tags) - [Basic Playlist Tags](#basic-playlist-tags)
- [Media Segment Tags](#media-segment-tags) - [Media Segment Tags](#media-segment-tags)
- [Media Playlist Tags](#media-playlist-tags) - [Media Playlist Tags](#media-playlist-tags)
- [Master Playlist Tags](#master-playlist-tags) - [Main Playlist Tags](#main-playlist-tags)
- [Experimental Tags](#experimental-tags) - [Experimental Tags](#experimental-tags)
- [EXT-X-CUE-OUT](#ext-x-cue-out) - [EXT-X-CUE-OUT](#ext-x-cue-out)
- [EXT-X-CUE-OUT-CONT](#ext-x-cue-out-cont) - [EXT-X-CUE-OUT-CONT](#ext-x-cue-out-cont)
@ -57,6 +58,7 @@ var manifest = [
'0.ts', '0.ts',
'#EXTINF:6,', '#EXTINF:6,',
'1.ts', '1.ts',
'#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z'
'#EXTINF:6,', '#EXTINF:6,',
'2.ts', '2.ts',
'#EXT-X-ENDLIST' '#EXT-X-ENDLIST'
@ -69,6 +71,21 @@ parser.end();
var parsedManifest = parser.manifest; var parsedManifest = parser.manifest;
``` ```
### Constructor Options
The constructor optinally takes an options object with two properties. These are needed when using `#EXT-X-DEFINE` for variable replacement.
```js
var parser = new m3u8Parser.Parser({
url: 'https://exmaple.com/video.m3u8?param_a=34&param_b=abc',
mainDefinitions: {
param_c: 'def'
}
});
```
* `options.url` _string_ The URL from which the playlist was fetched. If the request was redirected this should be the final URL. This is required if using `QUERYSTRING` rules with `#EXT-X-DEFINE`.
* `options.mainDefinitions` _object_ An object of definitions from the main playlist. This is required if using `IMPORT` rules with `#EXT-X-DEFINE`.
### Parsed Output ### Parsed Output
@ -79,6 +96,7 @@ Manifest {
allowCache: boolean, allowCache: boolean,
endList: boolean, endList: boolean,
mediaSequence: number, mediaSequence: number,
dateRanges: [],
discontinuitySequence: number, discontinuitySequence: number,
playlistType: string, playlistType: string,
custom: {}, custom: {},
@ -113,11 +131,13 @@ Manifest {
discontinuityStarts: [number], discontinuityStarts: [number],
segments: [ segments: [
{ {
title: string,
byterange: { byterange: {
length: number, length: number,
offset: number offset: number
}, },
duration: number, duration: number,
programDateTime: number,
attributes: {}, attributes: {},
discontinuity: number, discontinuity: number,
uri: string, uri: string,
@ -158,6 +178,8 @@ Manifest {
* [EXT-X-KEY](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.2.4) * [EXT-X-KEY](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.2.4)
* [EXT-X-MAP](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.2.5) * [EXT-X-MAP](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.2.5)
* [EXT-X-PROGRAM-DATE-TIME](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.2.6) * [EXT-X-PROGRAM-DATE-TIME](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.2.6)
* [EXT-X-DATERANGE](https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-23#section-4.3.2.7)
* [EXT-X-I-FRAMES-ONLY](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.3.6)
### Media Playlist Tags ### Media Playlist Tags
@ -167,11 +189,16 @@ Manifest {
* [EXT-X-ENDLIST](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.3.4) * [EXT-X-ENDLIST](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.3.4)
* [EXT-X-PLAYLIST-TYPE](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.3.5) * [EXT-X-PLAYLIST-TYPE](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.3.5)
* [EXT-X-START](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.5.2) * [EXT-X-START](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.5.2)
* [EXT-X-INDEPENDENT-SEGMENTS](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.5.1)
* [EXT-X-DEFINE](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.2.3)
### Master Playlist Tags ### Main Playlist Tags
* [EXT-X-MEDIA](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.1) * [EXT-X-MEDIA](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.1)
* [EXT-X-STREAM-INF](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.2) * [EXT-X-STREAM-INF](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.2)
* [EXT-X-I-FRAME-STREAM-INF](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.3)
* [EXT-X-CONTENT-STEERING](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.6.6)
* [EXT-X-DEFINE](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.2.3)
### Experimental Tags ### Experimental Tags
@ -236,12 +263,8 @@ Example media playlist using `EXT-X-CUE-` tags.
### Not Yet Supported ### Not Yet Supported
* [EXT-X-DATERANGE](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.2.7)
* [EXT-X-I-FRAMES-ONLY](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.3.6)
* [EXT-X-I-FRAME-STREAM-INF](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.3)
* [EXT-X-SESSION-DATA](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.4) * [EXT-X-SESSION-DATA](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.4)
* [EXT-X-SESSION-KEY](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.5) * [EXT-X-SESSION-KEY](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.5)
* [EXT-X-INDEPENDENT-SEGMENTS](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.5.1)
### Custom Parsers ### Custom Parsers

16
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "m3u8-parser", "name": "m3u8-parser",
"version": "5.0.0", "version": "7.2.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1579,13 +1579,12 @@
} }
}, },
"@videojs/vhs-utils": { "@videojs/vhs-utils": {
"version": "3.0.5", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
"integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==",
"requires": { "requires": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"global": "^4.4.0", "global": "^4.4.0"
"url-toolkit": "^2.2.1"
} }
}, },
"JSONStream": { "JSONStream": {
@ -8813,11 +8812,6 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"url-toolkit": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz",
"integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg=="
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "m3u8-parser", "name": "m3u8-parser",
"version": "5.0.0", "version": "7.2.0",
"description": "m3u8 parser", "description": "m3u8 parser",
"main": "dist/m3u8-parser.cjs.js", "main": "dist/m3u8-parser.cjs.js",
"module": "dist/m3u8-parser.es.js", "module": "dist/m3u8-parser.es.js",
@ -65,7 +65,7 @@
], ],
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.5", "@videojs/vhs-utils": "^4.1.1",
"global": "^4.4.0" "global": "^4.4.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -43,9 +43,14 @@ const attributeSeparator = function() {
* @param {string} attributes the attribute line to parse * @param {string} attributes the attribute line to parse
*/ */
const parseAttributes = function(attributes) { const parseAttributes = function(attributes) {
const result = {};
if (!attributes) {
return result;
}
// split the string using attributes as the separator // split the string using attributes as the separator
const attrs = attributes.split(attributeSeparator()); const attrs = attributes.split(attributeSeparator());
const result = {};
let i = attrs.length; let i = attrs.length;
let attr; let attr;
@ -66,6 +71,29 @@ const parseAttributes = function(attributes) {
return result; return result;
}; };
/**
* Converts a string into a resolution object
*
* @param {string} resolution a string such as 3840x2160
*
* @return {Object} An object representing the resolution
*
*/
const parseResolution = (resolution) => {
const split = resolution.split('x');
const result = {};
if (split[0]) {
result.width = parseInt(split[0], 10);
}
if (split[1]) {
result.height = parseInt(split[1], 10);
}
return result;
};
/** /**
* A line-level M3U8 parser event stream. It expects to receive input one * A line-level M3U8 parser event stream. It expects to receive input one
* line at a time and performs a context-free parse of its contents. A stream * line at a time and performs a context-free parse of its contents. A stream
@ -164,7 +192,7 @@ export default class ParseStream extends Stream {
}); });
return; return;
} }
match = (/^#EXTINF:?([0-9\.]*)?,?(.*)?$/).exec(newLine); match = (/^#EXTINF:([0-9\.]*)?,?(.*)?$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -179,7 +207,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-TARGETDURATION:?([0-9.]*)?/).exec(newLine); match = (/^#EXT-X-TARGETDURATION:([0-9.]*)?/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -191,7 +219,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-VERSION:?([0-9.]*)?/).exec(newLine); match = (/^#EXT-X-VERSION:([0-9.]*)?/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -203,7 +231,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/).exec(newLine); match = (/^#EXT-X-MEDIA-SEQUENCE:(\-?[0-9.]*)?/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -215,7 +243,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/).exec(newLine); match = (/^#EXT-X-DISCONTINUITY-SEQUENCE:(\-?[0-9.]*)?/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -227,7 +255,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(newLine); match = (/^#EXT-X-PLAYLIST-TYPE:(.*)?$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -239,7 +267,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-BYTERANGE:?(.*)?$/).exec(newLine); match = (/^#EXT-X-BYTERANGE:(.*)?$/).exec(newLine);
if (match) { if (match) {
event = Object.assign(parseByterange(match[1]), { event = Object.assign(parseByterange(match[1]), {
type: 'tag', type: 'tag',
@ -248,7 +276,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-ALLOW-CACHE:?(YES|NO)?/).exec(newLine); match = (/^#EXT-X-ALLOW-CACHE:(YES|NO)?/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -260,7 +288,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-MAP:?(.*)$/).exec(newLine); match = (/^#EXT-X-MAP:(.*)$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -281,7 +309,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-STREAM-INF:?(.*)$/).exec(newLine); match = (/^#EXT-X-STREAM-INF:(.*)$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -291,16 +319,7 @@ export default class ParseStream extends Stream {
event.attributes = parseAttributes(match[1]); event.attributes = parseAttributes(match[1]);
if (event.attributes.RESOLUTION) { if (event.attributes.RESOLUTION) {
const split = event.attributes.RESOLUTION.split('x'); event.attributes.RESOLUTION = parseResolution(event.attributes.RESOLUTION);
const resolution = {};
if (split[0]) {
resolution.width = parseInt(split[0], 10);
}
if (split[1]) {
resolution.height = parseInt(split[1], 10);
}
event.attributes.RESOLUTION = resolution;
} }
if (event.attributes.BANDWIDTH) { if (event.attributes.BANDWIDTH) {
event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
@ -315,7 +334,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-MEDIA:?(.*)$/).exec(newLine); match = (/^#EXT-X-MEDIA:(.*)$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -343,7 +362,7 @@ export default class ParseStream extends Stream {
}); });
return; return;
} }
match = (/^#EXT-X-PROGRAM-DATE-TIME:?(.*)$/).exec(newLine); match = (/^#EXT-X-PROGRAM-DATE-TIME:(.*)$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -356,7 +375,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-KEY:?(.*)$/).exec(newLine); match = (/^#EXT-X-KEY:(.*)$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -381,7 +400,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-START:?(.*)$/).exec(newLine); match = (/^#EXT-X-START:(.*)$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -396,7 +415,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-CUE-OUT-CONT:?(.*)?$/).exec(newLine); match = (/^#EXT-X-CUE-OUT-CONT:(.*)?$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -410,7 +429,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-CUE-OUT:?(.*)?$/).exec(newLine); match = (/^#EXT-X-CUE-OUT:(.*)?$/).exec(newLine);
if (match) { if (match) {
event = { event = {
type: 'tag', type: 'tag',
@ -562,6 +581,128 @@ export default class ParseStream extends Stream {
this.trigger('data', event); this.trigger('data', event);
return; return;
} }
match = (/^#EXT-X-DATERANGE:(.*)$/).exec(newLine);
if (match && match[1]) {
event = {
type: 'tag',
tagType: 'daterange'
};
event.attributes = parseAttributes(match[1]);
['ID', 'CLASS'].forEach(function(key) {
if (event.attributes.hasOwnProperty(key)) {
event.attributes[key] = String(event.attributes[key]);
}
});
['START-DATE', 'END-DATE'].forEach(function(key) {
if (event.attributes.hasOwnProperty(key)) {
event.attributes[key] = new Date(event.attributes[key]);
}
});
['DURATION', 'PLANNED-DURATION'].forEach(function(key) {
if (event.attributes.hasOwnProperty(key)) {
event.attributes[key] = parseFloat(event.attributes[key]);
}
});
['END-ON-NEXT'].forEach(function(key) {
if (event.attributes.hasOwnProperty(key)) {
event.attributes[key] = (/YES/i).test(event.attributes[key]);
}
});
['SCTE35-CMD', ' SCTE35-OUT', 'SCTE35-IN'].forEach(function(key) {
if (event.attributes.hasOwnProperty(key)) {
event.attributes[key] = event.attributes[key].toString(16);
}
});
const clientAttributePattern = /^X-([A-Z]+-)+[A-Z]+$/;
for (const key in event.attributes) {
if (!clientAttributePattern.test(key)) {
continue;
}
const isHexaDecimal = (/[0-9A-Fa-f]{6}/g).test(event.attributes[key]);
const isDecimalFloating = (/^\d+(\.\d+)?$/).test(event.attributes[key]);
event.attributes[key] = isHexaDecimal ? event.attributes[key].toString(16) : isDecimalFloating ? parseFloat(event.attributes[key]) : String(event.attributes[key]);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-INDEPENDENT-SEGMENTS/).exec(newLine);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'independent-segments'
});
return;
}
match = (/^#EXT-X-I-FRAMES-ONLY/).exec(newLine);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'i-frames-only'
});
return;
}
match = (/^#EXT-X-CONTENT-STEERING:(.*)$/).exec(newLine);
if (match) {
event = {
type: 'tag',
tagType: 'content-steering'
};
event.attributes = parseAttributes(match[1]);
this.trigger('data', event);
return;
}
match = (/^#EXT-X-I-FRAME-STREAM-INF:(.*)$/).exec(newLine);
if (match) {
event = {
type: 'tag',
tagType: 'i-frame-playlist'
};
event.attributes = parseAttributes(match[1]);
if (event.attributes.URI) {
event.uri = event.attributes.URI;
}
if (event.attributes.BANDWIDTH) {
event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
}
if (event.attributes.RESOLUTION) {
event.attributes.RESOLUTION = parseResolution(event.attributes.RESOLUTION);
}
if (event.attributes['AVERAGE-BANDWIDTH']) {
event.attributes['AVERAGE-BANDWIDTH'] = parseInt(event.attributes['AVERAGE-BANDWIDTH'], 10);
}
if (event.attributes['FRAME-RATE']) {
event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-DEFINE:(.*)$/).exec(newLine);
if (match) {
event = {
type: 'tag',
tagType: 'define'
};
event.attributes = parseAttributes(match[1]);
this.trigger('data', event);
return;
}
// unknown tag type // unknown tag type
this.trigger('data', { this.trigger('data', {

View file

@ -88,14 +88,20 @@ const setHoldBack = function(manifest) {
* requires some property of the manifest object to be defaulted. * requires some property of the manifest object to be defaulted.
* *
* @class Parser * @class Parser
* @param {Object} [opts] Options for the constructor, needed for substitutions
* @param {string} [opts.uri] URL to check for query params
* @param {Object} [opts.mainDefinitions] Definitions on main playlist that can be imported
* @extends Stream * @extends Stream
*/ */
export default class Parser extends Stream { export default class Parser extends Stream {
constructor() { constructor(opts = {}) {
super(); super();
this.lineStream = new LineStream(); this.lineStream = new LineStream();
this.parseStream = new ParseStream(); this.parseStream = new ParseStream();
this.lineStream.pipe(this.parseStream); this.lineStream.pipe(this.parseStream);
this.mainDefinitions = opts.mainDefinitions || {};
this.params = new URL(opts.uri, 'https://a.com').searchParams;
this.lastProgramDateTime = null;
/* eslint-disable consistent-this */ /* eslint-disable consistent-this */
const self = this; const self = this;
@ -124,6 +130,8 @@ export default class Parser extends Stream {
this.manifest = { this.manifest = {
allowCache: true, allowCache: true,
discontinuityStarts: [], discontinuityStarts: [],
dateRanges: [],
iFramePlaylists: [],
segments: [] segments: []
}; };
// keep track of the last seen segment's byte range end, as segments are not required // keep track of the last seen segment's byte range end, as segments are not required
@ -132,6 +140,7 @@ export default class Parser extends Stream {
let lastByterangeEnd = 0; let lastByterangeEnd = 0;
// keep track of the last seen part's byte range end. // keep track of the last seen part's byte range end.
let lastPartByterangeEnd = 0; let lastPartByterangeEnd = 0;
const dateRangeTags = {};
this.on('end', () => { this.on('end', () => {
// only add preloadSegment if we don't yet have a uri for it. // only add preloadSegment if we don't yet have a uri for it.
@ -159,6 +168,22 @@ export default class Parser extends Stream {
let mediaGroup; let mediaGroup;
let rendition; let rendition;
// Replace variables in uris and attributes as defined in #EXT-X-DEFINE tags
if (self.manifest.definitions) {
for (const def in self.manifest.definitions) {
if (entry.uri) {
entry.uri = entry.uri.replace(`{$${def}}`, self.manifest.definitions[def]);
}
if (entry.attributes) {
for (const attr in entry.attributes) {
if (typeof entry.attributes[attr] === 'string') {
entry.attributes[attr] = entry.attributes[attr].replace(`{$${def}}`, self.manifest.definitions[def]);
}
}
}
}
}
({ ({
tag() { tag() {
// switch based on the tag type // switch based on the tag type
@ -220,6 +245,11 @@ export default class Parser extends Stream {
message: 'defaulting discontinuity sequence to zero' message: 'defaulting discontinuity sequence to zero'
}); });
} }
if (entry.title) {
currentUri.title = entry.title;
}
if (entry.duration > 0) { if (entry.duration > 0) {
currentUri.duration = entry.duration; currentUri.duration = entry.duration;
} }
@ -458,9 +488,24 @@ export default class Parser extends Stream {
this.manifest.dateTimeString = entry.dateTimeString; this.manifest.dateTimeString = entry.dateTimeString;
this.manifest.dateTimeObject = entry.dateTimeObject; this.manifest.dateTimeObject = entry.dateTimeObject;
} }
currentUri.dateTimeString = entry.dateTimeString; currentUri.dateTimeString = entry.dateTimeString;
currentUri.dateTimeObject = entry.dateTimeObject; currentUri.dateTimeObject = entry.dateTimeObject;
const { lastProgramDateTime } = this;
this.lastProgramDateTime = new Date(entry.dateTimeString).getTime();
// We should extrapolate Program Date Time backward only during first program date time occurrence.
// Once we have at least one program date time point, we can always extrapolate it forward using lastProgramDateTime reference.
if (lastProgramDateTime === null) {
// Extrapolate Program Date Time backward
// Since it is first program date time occurrence we're assuming that
// all this.manifest.segments have no program date time info
this.manifest.segments.reduceRight((programDateTime, segment) => {
segment.programDateTime = programDateTime - (segment.duration * 1000);
return segment.programDateTime;
}, this.lastProgramDateTime);
}
}, },
targetduration() { targetduration() {
if (!isFinite(entry.duration) || entry.duration < 0) { if (!isFinite(entry.duration) || entry.duration < 0) {
@ -631,7 +676,195 @@ export default class Parser extends Stream {
} }
setHoldBack.call(this, this.manifest); setHoldBack.call(this, this.manifest);
},
'daterange'() {
this.manifest.dateRanges.push(camelCaseKeys(entry.attributes));
const index = this.manifest.dateRanges.length - 1;
this.warnOnMissingAttributes_(
`#EXT-X-DATERANGE #${index}`,
entry.attributes,
['ID', 'START-DATE']
);
const dateRange = this.manifest.dateRanges[index];
if (dateRange.endDate && dateRange.startDate && new Date(dateRange.endDate) < new Date(dateRange.startDate)) {
this.trigger('warn', {
message: 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE'
});
}
if (dateRange.duration && dateRange.duration < 0) {
this.trigger('warn', {
message: 'EXT-X-DATERANGE DURATION must not be negative'
});
}
if (dateRange.plannedDuration && dateRange.plannedDuration < 0) {
this.trigger('warn', {
message: 'EXT-X-DATERANGE PLANNED-DURATION must not be negative'
});
}
const endOnNextYes = !!dateRange.endOnNext;
if (endOnNextYes && !dateRange.class) {
this.trigger('warn', {
message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute'
});
}
if (endOnNextYes && (dateRange.duration || dateRange.endDate)) {
this.trigger('warn', {
message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes'
});
}
if (dateRange.duration && dateRange.endDate) {
const startDate = dateRange.startDate;
const newDateInSeconds = startDate.getTime() + (dateRange.duration * 1000);
this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds);
}
if (!dateRangeTags[dateRange.id]) {
dateRangeTags[dateRange.id] = dateRange;
} else {
for (const attribute in dateRangeTags[dateRange.id]) {
if (!!dateRange[attribute] && JSON.stringify(dateRangeTags[dateRange.id][attribute]) !== JSON.stringify(dateRange[attribute])) {
this.trigger('warn', {
message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values'
});
break;
}
}
// if tags with the same ID do not have conflicting attributes, merge them
const dateRangeWithSameId = this.manifest.dateRanges.findIndex((dateRangeToFind) => dateRangeToFind.id === dateRange.id);
this.manifest.dateRanges[dateRangeWithSameId] = Object.assign(this.manifest.dateRanges[dateRangeWithSameId], dateRange);
dateRangeTags[dateRange.id] = Object.assign(dateRangeTags[dateRange.id], dateRange);
// after merging, delete the duplicate dateRange that was added last
this.manifest.dateRanges.pop();
}
},
'independent-segments'() {
this.manifest.independentSegments = true;
},
'i-frames-only'() {
this.manifest.iFramesOnly = true;
this.requiredCompatibilityversion(this.manifest.version, 4);
},
'content-steering'() {
this.manifest.contentSteering = camelCaseKeys(entry.attributes);
this.warnOnMissingAttributes_(
'#EXT-X-CONTENT-STEERING',
entry.attributes,
['SERVER-URI']
);
},
/** @this {Parser} */
define() {
this.manifest.definitions = this.manifest.definitions || { };
const addDef = (n, v) => {
if (n in this.manifest.definitions) {
// An EXT-X-DEFINE tag MUST NOT specify the same Variable Name as any other
// EXT-X-DEFINE tag in the same Playlist. Parsers that encounter duplicate
// Variable Name declarations MUST fail to parse the Playlist.
this.trigger('error', {
message: `EXT-X-DEFINE: Duplicate name ${n}`
});
return;
}
this.manifest.definitions[n] = v;
};
if ('QUERYPARAM' in entry.attributes) {
if ('NAME' in entry.attributes || 'IMPORT' in entry.attributes) {
// An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a
// QUERYPARAM attribute, but only one of the three. Otherwise, the
// client MUST fail to parse the Playlist.
this.trigger('error', {
message: 'EXT-X-DEFINE: Invalid attributes'
});
return;
}
const val = this.params.get(entry.attributes.QUERYPARAM);
if (!val) {
// If the QUERYPARAM attribute value does not match any query parameter in
// the URI or the matching parameter has no associated value, the parser
// MUST fail to parse the Playlist. If more than one parameter matches,
// any of the associated values MAY be used.
this.trigger('error', {
message: `EXT-X-DEFINE: No query param ${entry.attributes.QUERYPARAM}`
});
return;
}
addDef(entry.attributes.QUERYPARAM, decodeURIComponent(val));
return;
}
if ('NAME' in entry.attributes) {
if ('IMPORT' in entry.attributes) {
// An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a
// QUERYPARAM attribute, but only one of the three. Otherwise, the
// client MUST fail to parse the Playlist.
this.trigger('error', {
message: 'EXT-X-DEFINE: Invalid attributes'
});
return;
}
if (!('VALUE' in entry.attributes) || typeof entry.attributes.VALUE !== 'string') {
// This attribute is REQUIRED if the EXT-X-DEFINE tag has a NAME attribute.
// The quoted-string MAY be empty.
this.trigger('error', {
message: `EXT-X-DEFINE: No value for ${entry.attributes.NAME}`
});
return;
}
addDef(entry.attributes.NAME, entry.attributes.VALUE);
return;
}
if ('IMPORT' in entry.attributes) {
if (!this.mainDefinitions[entry.attributes.IMPORT]) {
// Covers two conditions, as mainDefinitions will always be empty on main
//
// EXT-X-DEFINE tags containing the IMPORT attribute MUST NOT occur in
// Multivariant Playlists; they are only allowed in Media Playlists.
//
// If the IMPORT attribute value does not match any Variable Name in the
// Multivariant Playlist, or if the Media Playlist loaded from a
// Multivariant Playlist, the parser MUST fail the Playlist.
this.trigger('error', {
message: `EXT-X-DEFINE: No value ${entry.attributes.IMPORT} to import, or IMPORT used on main playlist`
});
return;
}
addDef(entry.attributes.IMPORT, this.mainDefinitions[entry.attributes.IMPORT]);
return;
}
// An EXT-X-DEFINE tag MUST contain either a NAME, an IMPORT, or a QUERYPARAM
// attribute, but only one of the three. Otherwise, the client MUST fail to
// parse the Playlist.
this.trigger('error', {
message: 'EXT-X-DEFINE: No attribute'
});
},
'i-frame-playlist'() {
this.manifest.iFramePlaylists.push({
attributes: entry.attributes,
uri: entry.uri,
timeline: currentTimeline
});
this.warnOnMissingAttributes_(
'#EXT-X-I-FRAME-STREAM-INF',
entry.attributes,
['BANDWIDTH', 'URI']
);
} }
})[entry.tagType] || noop).call(self); })[entry.tagType] || noop).call(self);
}, },
uri() { uri() {
@ -658,6 +891,12 @@ export default class Parser extends Stream {
// reset the last byterange end as it needs to be 0 between parts // reset the last byterange end as it needs to be 0 between parts
lastPartByterangeEnd = 0; lastPartByterangeEnd = 0;
// Once we have at least one program date time we can always extrapolate it forward
if (this.lastProgramDateTime !== null) {
currentUri.programDateTime = this.lastProgramDateTime;
this.lastProgramDateTime += currentUri.duration * 1000;
}
// prepare for the next URI // prepare for the next URI
currentUri = {}; currentUri = {};
}, },
@ -679,6 +918,14 @@ export default class Parser extends Stream {
}); });
} }
requiredCompatibilityversion(currentVersion, targetVersion) {
if (currentVersion < targetVersion || !currentVersion) {
this.trigger('warn', {
message: `manifest must be at least version ${targetVersion}`
});
}
}
warnOnMissingAttributes_(identifier, attributes, required) { warnOnMissingAttributes_(identifier, attributes, required) {
const missing = []; const missing = [];
@ -710,7 +957,13 @@ export default class Parser extends Stream {
end() { end() {
// flush any buffered input // flush any buffered input
this.lineStream.push('\n'); this.lineStream.push('\n');
if (this.manifest.dateRanges.length && this.lastProgramDateTime === null) {
this.trigger('warn', {
message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
});
}
this.lastProgramDateTime = null;
this.trigger('end'); this.trigger('end');
} }
/** /**
@ -718,7 +971,7 @@ export default class Parser extends Stream {
* *
* @param {Object} options a map of options for the added parser * @param {Object} options a map of options for the added parser
* @param {RegExp} options.expression a regular expression to match the custom header * @param {RegExp} options.expression a regular expression to match the custom header
* @param {string} options.type the type to register to the output * @param {string} options.customType the custom type to register to the output
* @param {Function} [options.dataParser] function to parse the line into an object * @param {Function} [options.dataParser] function to parse the line into an object
* @param {boolean} [options.segment] should tag data be attached to the segment object * @param {boolean} [options.segment] should tag data be attached to the segment object
*/ */

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,6 +1,8 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
dateRanges: [],
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {

View file

@ -1,6 +1,8 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
dateRanges: [],
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {

View file

@ -1,6 +1,8 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
discontinuityStarts: [], discontinuityStarts: [],
dateRanges: [],
iFramePlaylists: [],
mediaGroups: { mediaGroups: {
// TYPE // TYPE
'AUDIO': { 'AUDIO': {

View file

@ -1,6 +1,8 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
discontinuityStarts: [], discontinuityStarts: [],
dateRanges: [],
iFramePlaylists: [],
mediaGroups: { mediaGroups: {
'AUDIO': { 'AUDIO': {
aac: { aac: {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
iFramePlaylists: [],
dateRanges: [],
playlists: [ playlists: [
{ {
attributes: { attributes: {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,11 +1,14 @@
module.exports = { module.exports = {
allowCache: false, allowCache: false,
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
dateRanges: [],
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
dateTimeString: '2016-06-22T09:20:16.166-04:00', dateTimeString: '2016-06-22T09:20:16.166-04:00',
dateTimeObject: new Date('2016-06-22T09:20:16.166-04:00'), dateTimeObject: new Date('2016-06-22T09:20:16.166-04:00'),
programDateTime: 1466601616166,
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: 'hls_450k_video.ts' uri: 'hls_450k_video.ts'
@ -13,6 +16,7 @@ module.exports = {
{ {
dateTimeString: '2016-06-22T09:20:26.166-04:00', dateTimeString: '2016-06-22T09:20:26.166-04:00',
dateTimeObject: new Date('2016-06-22T09:20:26.166-04:00'), dateTimeObject: new Date('2016-06-22T09:20:26.166-04:00'),
programDateTime: 1466601626166,
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: 'hls_450k_video.ts' uri: 'hls_450k_video.ts'

View file

@ -2,6 +2,8 @@ module.exports = {
allowCache: true, allowCache: true,
discontinuitySequence: 0, discontinuitySequence: 0,
discontinuityStarts: [], discontinuityStarts: [],
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 7794, mediaSequence: 7794,
segments: [ segments: [
{ {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: false, allowCache: false,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,28 +1,34 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
discontinuitySequence: 3, discontinuitySequence: 3,
segments: [ segments: [
{ {
duration: 10, duration: 10,
timeline: 3, timeline: 3,
uri: '001.ts' uri: '001.ts',
title: '0'
}, },
{ {
duration: 19, duration: 19,
timeline: 3, timeline: 3,
uri: '002.ts' uri: '002.ts',
title: '0'
}, },
{ {
discontinuity: true, discontinuity: true,
duration: 10, duration: 10,
timeline: 4, timeline: 4,
uri: '003.ts' uri: '003.ts',
title: '0'
}, },
{ {
duration: 11, duration: 11,
timeline: 4, timeline: 4,
uri: '004.ts' uri: '004.ts',
title: '0'
} }
], ],
targetDuration: 19, targetDuration: 19,

View file

@ -1,55 +1,66 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
discontinuitySequence: 0, discontinuitySequence: 0,
segments: [ segments: [
{ {
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: '001.ts' uri: '001.ts',
title: '0'
}, },
{ {
duration: 19, duration: 19,
timeline: 0, timeline: 0,
uri: '002.ts' uri: '002.ts',
title: '0'
}, },
{ {
discontinuity: true, discontinuity: true,
duration: 10, duration: 10,
timeline: 1, timeline: 1,
uri: '003.ts' uri: '003.ts',
title: '0'
}, },
{ {
duration: 11, duration: 11,
timeline: 1, timeline: 1,
uri: '004.ts' uri: '004.ts',
title: '0'
}, },
{ {
discontinuity: true, discontinuity: true,
duration: 10, duration: 10,
timeline: 2, timeline: 2,
uri: '005.ts' uri: '005.ts',
title: '0'
}, },
{ {
duration: 10, duration: 10,
timeline: 2, timeline: 2,
uri: '006.ts' uri: '006.ts',
title: '0'
}, },
{ {
duration: 10, duration: 10,
timeline: 2, timeline: 2,
uri: '007.ts' uri: '007.ts',
title: '0'
}, },
{ {
discontinuity: true, discontinuity: true,
duration: 10, duration: 10,
timeline: 3, timeline: 3,
uri: '008.ts' uri: '008.ts',
title: '0'
}, },
{ {
duration: 16, duration: 16,
timeline: 3, timeline: 3,
uri: '009.ts' uri: '009.ts',
title: '0'
} }
], ],
targetDuration: 19, targetDuration: 19,

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [],
segments: [] segments: []
}; };

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,27 +1,33 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.08, duration: 6.08,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts' uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.6, duration: 6.6,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts' uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts',
title: '{}'
}, },
{ {
duration: 5, duration: 5,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts' uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts',
title: '{}'
} }
], ],
targetDuration: 8, targetDuration: 8,

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
segments: [ segments: [
{ {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
playlists: [ playlists: [
{ {
attributes: { attributes: {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 7794, mediaSequence: 7794,
discontinuitySequence: 0, discontinuitySequence: 0,
discontinuityStarts: [], discontinuityStarts: [],

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'EVENT', playlistType: 'EVENT',
segments: [ segments: [

View file

@ -1,11 +1,14 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 1, mediaSequence: 1,
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
} }
], ],
targetDuration: 8, targetDuration: 8,

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
@ -19,7 +21,8 @@ module.exports = {
}, },
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: 'hls_450k_video.ts' uri: 'hls_450k_video.ts',
title: ';asljasdfii11)))00,'
}, },
{ {
byterange: { byterange: {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 1, mediaSequence: 1,
playlistType: 'VOD', playlistType: 'VOD',
targetDuration: 6, targetDuration: 6,
@ -40,5 +42,6 @@ module.exports = {
} }
], ],
endList: true, endList: true,
version: 7 version: 7,
independentSegments: true
}; };

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [],
segments: [] segments: []
}; };

View file

@ -0,0 +1,288 @@
module.exports = {
allowCache: true,
dateRanges: [],
discontinuityStarts: [],
iFramePlaylists: [
{
attributes: {
'AVERAGE-BANDWIDTH': 248586,
'BANDWIDTH': 593626,
'CODECS': 'hvc1.2.4.L123.B0',
'HDCP-LEVEL': 'NONE',
'RESOLUTION': { width: 1280, height: 720 },
'URI': 'sdr_720/iframe_index.m3u8',
'VIDEO-RANGE': 'SDR'
},
timeline: 0,
uri: 'sdr_720/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 399790,
'BANDWIDTH': 956552,
'CODECS': 'hvc1.2.4.L123.B0',
'HDCP-LEVEL': 'TYPE-0',
'RESOLUTION': { width: 1920, height: 1080 },
'URI': 'sdr_1080/iframe_index.m3u8',
'VIDEO-RANGE': 'SDR'
},
timeline: 0,
uri: 'sdr_1080/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 826971,
'BANDWIDTH': 1941397,
'CODECS': 'hvc1.2.4.L150.B0',
'HDCP-LEVEL': 'TYPE-1',
'RESOLUTION': { width: 3840, height: 2160 },
'URI': 'sdr_2160/iframe_index.m3u8',
'VIDEO-RANGE': 'SDR'
},
timeline: 0,
uri: 'sdr_2160/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 232253,
'BANDWIDTH': 573073,
'CODECS': 'dvh1.05.01',
'HDCP-LEVEL': 'NONE',
'RESOLUTION': { width: 1280, height: 720 },
'URI': 'dolby_720/iframe_index.m3u8',
'VIDEO-RANGE': 'PQ'
},
timeline: 0,
uri: 'dolby_720/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 365337,
'BANDWIDTH': 905037,
'CODECS': 'dvh1.05.03',
'HDCP-LEVEL': 'TYPE-0',
'RESOLUTION': { width: 1920, height: 1080 },
'URI': 'dolby_1080/iframe_index.m3u8',
'VIDEO-RANGE': 'PQ'
},
timeline: 0,
uri: 'dolby_1080/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 739114,
'BANDWIDTH': 1893236,
'CODECS': 'dvh1.05.06',
'HDCP-LEVEL': 'TYPE-1',
'RESOLUTION': { width: 3840, height: 2160 },
'URI': 'dolby_2160/iframe_index.m3u8',
'VIDEO-RANGE': 'PQ'
},
timeline: 0,
uri: 'dolby_2160/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 232511,
'BANDWIDTH': 572673,
'CODECS': 'hvc1.2.4.L123.B0',
'HDCP-LEVEL': 'NONE',
'RESOLUTION': { width: 1280, height: 720 },
'URI': 'hdr10_720/iframe_index.m3u8',
'VIDEO-RANGE': 'PQ'
},
timeline: 0,
uri: 'hdr10_720/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 364552,
'BANDWIDTH': 905053,
'CODECS': 'hvc1.2.4.L123.B0',
'HDCP-LEVEL': 'TYPE-0',
'RESOLUTION': { width: 1920, height: 1080 },
'URI': 'hdr10_1080/iframe_index.m3u8',
'VIDEO-RANGE': 'PQ'
},
timeline: 0,
uri: 'hdr10_1080/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 739757,
'BANDWIDTH': 1895477,
'CODECS': 'hvc1.2.4.L150.B0',
'HDCP-LEVEL': 'TYPE-1',
'RESOLUTION': { width: 3840, height: 2160 },
'URI': 'hdr10_2160/iframe_index.m3u8',
'VIDEO-RANGE': 'PQ'
},
timeline: 0,
uri: 'hdr10_2160/iframe_index.m3u8'
}
],
independentSegments: true,
mediaGroups: {
'AUDIO': {},
'CLOSED-CAPTIONS': {},
'SUBTITLES': {},
'VIDEO': {}
},
playlists: [
{
attributes: {
'HDCP-LEVEL': 'NONE',
'CLOSED-CAPTIONS': 'NONE',
'FRAME-RATE': 23.976,
'RESOLUTION': {
width: 1280,
height: 720
},
'CODECS': 'hvc1.2.4.L123.B0',
'VIDEO-RANGE': 'SDR',
'BANDWIDTH': 3971374,
'AVERAGE-BANDWIDTH': '2778321'
},
uri: 'sdr_720/prog_index.m3u8',
timeline: 0
},
{
attributes: {
'HDCP-LEVEL': 'TYPE-0',
'CLOSED-CAPTIONS': 'NONE',
'FRAME-RATE': 23.976,
'RESOLUTION': {
width: 1920,
height: 1080
},
'CODECS': 'hvc1.2.4.L123.B0',
'VIDEO-RANGE': 'SDR',
'BANDWIDTH': 10022043,
'AVERAGE-BANDWIDTH': '6759875'
},
uri: 'sdr_1080/prog_index.m3u8',
timeline: 0
},
{
attributes: {
'HDCP-LEVEL': 'TYPE-1',
'CLOSED-CAPTIONS': 'NONE',
'FRAME-RATE': 23.976,
'RESOLUTION': {
width: 3840,
height: 2160
},
'CODECS': 'hvc1.2.4.L150.B0',
'VIDEO-RANGE': 'SDR',
'BANDWIDTH': 28058971,
'AVERAGE-BANDWIDTH': '20985770'
},
uri: 'sdr_2160/prog_index.m3u8',
timeline: 0
},
{
attributes: {
'HDCP-LEVEL': 'NONE',
'CLOSED-CAPTIONS': 'NONE',
'FRAME-RATE': 23.976,
'RESOLUTION': {
width: 1280,
height: 720
},
'CODECS': 'dvh1.05.01',
'VIDEO-RANGE': 'PQ',
'BANDWIDTH': 5327059,
'AVERAGE-BANDWIDTH': '3385450'
},
uri: 'dolby_720/prog_index.m3u8',
timeline: 0
},
{
attributes: {
'HDCP-LEVEL': 'TYPE-0',
'CLOSED-CAPTIONS': 'NONE',
'FRAME-RATE': 23.976,
'RESOLUTION': {
width: 1920,
height: 1080
},
'CODECS': 'dvh1.05.03',
'VIDEO-RANGE': 'PQ',
'BANDWIDTH': 12876596,
'AVERAGE-BANDWIDTH': '7999361'
},
uri: 'dolby_1080/prog_index.m3u8',
timeline: 0
},
{
attributes: {
'HDCP-LEVEL': 'TYPE-1',
'CLOSED-CAPTIONS': 'NONE',
'FRAME-RATE': 23.976,
'RESOLUTION': {
width: 3840,
height: 2160
},
'CODECS': 'dvh1.05.06',
'VIDEO-RANGE': 'PQ',
'BANDWIDTH': 30041698,
'AVERAGE-BANDWIDTH': '24975091'
},
uri: 'dolby_2160/prog_index.m3u8',
timeline: 0
},
{
attributes: {
'HDCP-LEVEL': 'NONE',
'CLOSED-CAPTIONS': 'NONE',
'FRAME-RATE': 23.976,
'RESOLUTION': {
width: 1280,
height: 720
},
'CODECS': 'hvc1.2.4.L123.B0',
'VIDEO-RANGE': 'PQ',
'BANDWIDTH': 5280654,
'AVERAGE-BANDWIDTH': '3320040'
},
uri: 'hdr10_720/prog_index.m3u8',
timeline: 0
},
{
attributes: {
'HDCP-LEVEL': 'TYPE-0',
'CLOSED-CAPTIONS': 'NONE',
'FRAME-RATE': 23.976,
'RESOLUTION': {
width: 1920,
height: 1080
},
'CODECS': 'hvc1.2.4.L123.B0',
'VIDEO-RANGE': 'PQ',
'BANDWIDTH': 12886714,
'AVERAGE-BANDWIDTH': '7964551'
},
uri: 'hdr10_1080/prog_index.m3u8',
timeline: 0
},
{
attributes: {
'HDCP-LEVEL': 'TYPE-1',
'CLOSED-CAPTIONS': 'NONE',
'FRAME-RATE': 23.976,
'RESOLUTION': {
width: 3840,
height: 2160
},
'CODECS': 'hvc1.2.4.L150.B0',
'VIDEO-RANGE': 'PQ',
'BANDWIDTH': 29983769,
'AVERAGE-BANDWIDTH': '24833402'
},
uri: 'hdr10_2160/prog_index.m3u8',
timeline: 0
}
],
segments: [],
version: 7
};

View file

@ -0,0 +1,42 @@
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-INDEPENDENT-SEGMENTS
# https://developer.apple.com/documentation/http-live-streaming/hls-authoring-specification-for-apple-devices-appendixes#Example-playlist
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=248586,BANDWIDTH=593626,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,HDCP-LEVEL=NONE,URI="sdr_720/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=399790,BANDWIDTH=956552,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,HDCP-LEVEL=TYPE-0,URI="sdr_1080/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=826971,BANDWIDTH=1941397,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="sdr_2160/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=232253,BANDWIDTH=573073,VIDEO-RANGE=PQ,CODECS="dvh1.05.01",RESOLUTION=1280x720,HDCP-LEVEL=NONE,URI="dolby_720/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=365337,BANDWIDTH=905037,VIDEO-RANGE=PQ,CODECS="dvh1.05.03",RESOLUTION=1920x1080,HDCP-LEVEL=TYPE-0,URI="dolby_1080/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=739114,BANDWIDTH=1893236,VIDEO-RANGE=PQ,CODECS="dvh1.05.06",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="dolby_2160/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=232511,BANDWIDTH=572673,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,HDCP-LEVEL=NONE,URI="hdr10_720/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=364552,BANDWIDTH=905053,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,HDCP-LEVEL=TYPE-0,URI="hdr10_1080/iframe_index.m3u8"
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=739757,BANDWIDTH=1895477,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="hdr10_2160/iframe_index.m3u8"
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2778321,BANDWIDTH=3971374,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=NONE
sdr_720/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=6759875,BANDWIDTH=10022043,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-0
sdr_1080/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=20985770,BANDWIDTH=28058971,VIDEO-RANGE=SDR,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-1
sdr_2160/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3385450,BANDWIDTH=5327059,VIDEO-RANGE=PQ,CODECS="dvh1.05.01",RESOLUTION=1280x720,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=NONE
dolby_720/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=7999361,BANDWIDTH=12876596,VIDEO-RANGE=PQ,CODECS="dvh1.05.03",RESOLUTION=1920x1080,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-0
dolby_1080/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=24975091,BANDWIDTH=30041698,VIDEO-RANGE=PQ,CODECS="dvh1.05.06",RESOLUTION=3840x2160,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-1
dolby_2160/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=3320040,BANDWIDTH=5280654,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1280x720,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=NONE
hdr10_720/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=7964551,BANDWIDTH=12886714,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L123.B0",RESOLUTION=1920x1080,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-0
hdr10_1080/prog_index.m3u8
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=24833402,BANDWIDTH=29983769,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,FRAME-RATE=23.976,CLOSED-CAPTIONS=NONE,HDCP-LEVEL=TYPE-1
hdr10_2160/prog_index.m3u8

View file

@ -0,0 +1,45 @@
module.exports = {
allowCache: true,
dateRanges: [],
iFramePlaylists: [],
iFramesOnly: true,
mediaSequence: 0,
playlistType: 'VOD',
segments: [
{
duration: 2.002,
timeline: 0,
uri: '001.ts'
},
{
duration: 2.002,
timeline: 0,
uri: '002.ts'
},
{
duration: 2.002,
timeline: 0,
uri: '003.ts'
},
{
duration: 2.002,
timeline: 0,
uri: '004.ts'
},
{
duration: 2.002,
timeline: 0,
uri: '005.ts'
},
{
duration: 2.002,
timeline: 0,
uri: '006.ts'
}
],
targetDuration: 3,
endList: true,
discontinuitySequence: 0,
discontinuityStarts: [],
version: 4
};

View file

@ -0,0 +1,19 @@
#EXTM3U
#EXT-X-VERSION:4
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:3
#EXT-X-I-FRAMES-ONLY
#EXTINF:2.002,
001.ts
#EXTINF:2.002,
002.ts
#EXTINF:2.002,
003.ts
#EXTINF:2.002,
004.ts
#EXTINF:2.002,
005.ts
#EXTINF:2.002,
006.ts
#EXT-X-ENDLIST

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,27 +1,33 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.08, duration: 6.08,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts' uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.6, duration: 6.6,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts' uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts',
title: '{}'
}, },
{ {
duration: 5, duration: 5,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts' uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts',
title: '{}'
} }
], ],
targetDuration: 8, targetDuration: 8,

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
segments: [ segments: [
{ {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,12 +1,15 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
}, },
{ {
duration: 8, duration: 8,

View file

@ -1,51 +1,62 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
segments: [ segments: [
{ {
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: '001.ts' uri: '001.ts',
title: '0'
}, },
{ {
duration: 19, duration: 19,
timeline: 0, timeline: 0,
uri: '002.ts' uri: '002.ts',
title: '0'
}, },
{ {
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: '003.ts' uri: '003.ts',
title: '0'
}, },
{ {
duration: 11, duration: 11,
timeline: 0, timeline: 0,
uri: '004.ts' uri: '004.ts',
title: '0'
}, },
{ {
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: '005.ts' uri: '005.ts',
title: '0'
}, },
{ {
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: '006.ts' uri: '006.ts',
title: '0'
}, },
{ {
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: '007.ts' uri: '007.ts',
title: '0'
}, },
{ {
duration: 10, duration: 10,
timeline: 0, timeline: 0,
uri: '008.ts' uri: '008.ts',
title: '0'
}, },
{ {
duration: 16, duration: 16,
timeline: 0, timeline: 0,
uri: '009.ts' uri: '009.ts',
title: '0'
} }
], ],
targetDuration: 10, targetDuration: 10,

View file

@ -1,7 +1,9 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
discontinuitySequence: 0, discontinuitySequence: 0,
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
preloadSegment: { preloadSegment: {

View file

@ -1,7 +1,9 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
discontinuitySequence: 0, discontinuitySequence: 0,
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
preloadSegment: { preloadSegment: {

View file

@ -2,8 +2,10 @@ module.exports = {
allowCache: true, allowCache: true,
dateTimeObject: new Date('2019-02-14T02:13:36.106Z'), dateTimeObject: new Date('2019-02-14T02:13:36.106Z'),
dateTimeString: '2019-02-14T02:13:36.106Z', dateTimeString: '2019-02-14T02:13:36.106Z',
dateRanges: [],
discontinuitySequence: 0, discontinuitySequence: 0,
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [],
mediaSequence: 266, mediaSequence: 266,
preloadSegment: { preloadSegment: {
map: {uri: 'init.mp4'}, map: {uri: 'init.mp4'},
@ -40,6 +42,7 @@ module.exports = {
{ {
dateTimeObject: new Date('2019-02-14T02:13:36.106Z'), dateTimeObject: new Date('2019-02-14T02:13:36.106Z'),
dateTimeString: '2019-02-14T02:13:36.106Z', dateTimeString: '2019-02-14T02:13:36.106Z',
programDateTime: 1550110416106,
duration: 4.00008, duration: 4.00008,
map: { map: {
uri: 'init.mp4' uri: 'init.mp4'
@ -52,6 +55,7 @@ module.exports = {
map: { map: {
uri: 'init.mp4' uri: 'init.mp4'
}, },
programDateTime: 1550110420106.08,
timeline: 0, timeline: 0,
uri: 'fileSequence267.mp4' uri: 'fileSequence267.mp4'
}, },
@ -60,6 +64,7 @@ module.exports = {
map: { map: {
uri: 'init.mp4' uri: 'init.mp4'
}, },
programDateTime: 1550110424106.1602,
timeline: 0, timeline: 0,
uri: 'fileSequence268.mp4' uri: 'fileSequence268.mp4'
}, },
@ -68,6 +73,7 @@ module.exports = {
map: { map: {
uri: 'init.mp4' uri: 'init.mp4'
}, },
programDateTime: 1550110428106.2402,
timeline: 0, timeline: 0,
uri: 'fileSequence269.mp4' uri: 'fileSequence269.mp4'
}, },
@ -76,6 +82,7 @@ module.exports = {
map: { map: {
uri: 'init.mp4' uri: 'init.mp4'
}, },
programDateTime: 1550110432106.3203,
timeline: 0, timeline: 0,
uri: 'fileSequence270.mp4' uri: 'fileSequence270.mp4'
}, },
@ -84,6 +91,7 @@ module.exports = {
map: { map: {
uri: 'init.mp4' uri: 'init.mp4'
}, },
programDateTime: 1550110436106.4004,
timeline: 0, timeline: 0,
uri: 'fileSequence271.mp4', uri: 'fileSequence271.mp4',
parts: [ parts: [
@ -146,6 +154,7 @@ module.exports = {
map: { map: {
uri: 'init.mp4' uri: 'init.mp4'
}, },
programDateTime: 1550110440106,
timeline: 0, timeline: 0,
uri: 'fileSequence272.mp4', uri: 'fileSequence272.mp4',
parts: [ parts: [

View file

@ -2,8 +2,10 @@ module.exports = {
allowCache: true, allowCache: true,
dateTimeObject: new Date('2019-02-14T02:14:00.106Z'), dateTimeObject: new Date('2019-02-14T02:14:00.106Z'),
dateTimeString: '2019-02-14T02:14:00.106Z', dateTimeString: '2019-02-14T02:14:00.106Z',
dateRanges: [],
discontinuitySequence: 0, discontinuitySequence: 0,
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [],
mediaSequence: 266, mediaSequence: 266,
preloadSegment: { preloadSegment: {
timeline: 0, timeline: 0,
@ -42,16 +44,19 @@ module.exports = {
segments: [ segments: [
{ {
duration: 4.00008, duration: 4.00008,
programDateTime: 1550110428105.7598,
timeline: 0, timeline: 0,
uri: 'fileSequence269.mp4' uri: 'fileSequence269.mp4'
}, },
{ {
duration: 4.00008, duration: 4.00008,
programDateTime: 1550110432105.8398,
timeline: 0, timeline: 0,
uri: 'fileSequence270.mp4' uri: 'fileSequence270.mp4'
}, },
{ {
duration: 4.00008, duration: 4.00008,
programDateTime: 1550110436105.92,
timeline: 0, timeline: 0,
uri: 'fileSequence271.mp4', uri: 'fileSequence271.mp4',
parts: [ parts: [
@ -111,6 +116,7 @@ module.exports = {
dateTimeObject: new Date('2019-02-14T02:14:00.106Z'), dateTimeObject: new Date('2019-02-14T02:14:00.106Z'),
dateTimeString: '2019-02-14T02:14:00.106Z', dateTimeString: '2019-02-14T02:14:00.106Z',
duration: 4.00008, duration: 4.00008,
programDateTime: 1550110440106,
timeline: 0, timeline: 0,
uri: 'fileSequence272.mp4', uri: 'fileSequence272.mp4',
parts: [ parts: [

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
segments: [ segments: [
{ {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
segments: [ segments: [
{ {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
segments: [ segments: [
{ {

View file

@ -1,6 +1,93 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [
{
attributes: {
'AVERAGE-BANDWIDTH': 163198,
'BANDWIDTH': 166942,
'CODECS': 'avc1.64002a',
'RESOLUTION': {
height: 1080,
width: 1920
},
'URI': 'v6/iframe_index.m3u8'
},
timeline: 0,
uri: 'v6/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 131314,
'BANDWIDTH': 139041,
'CODECS': 'avc1.640020',
'RESOLUTION': {
height: 720,
width: 1280
},
'URI': 'v5/iframe_index.m3u8'
},
timeline: 0,
uri: 'v5/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 100233,
'BANDWIDTH': 101724,
'CODECS': 'avc1.640020',
'RESOLUTION': {
height: 540,
width: 960
},
'URI': 'v4/iframe_index.m3u8'
},
timeline: 0,
uri: 'v4/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 81002,
'BANDWIDTH': 84112,
'CODECS': 'avc1.64001e',
'RESOLUTION': {
height: 432,
width: 768
},
'URI': 'v3/iframe_index.m3u8'
},
timeline: 0,
uri: 'v3/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 64987,
'BANDWIDTH': 65835,
'CODECS': 'avc1.64001e',
'RESOLUTION': {
height: 360,
width: 640
},
'URI': 'v2/iframe_index.m3u8'
},
timeline: 0,
uri: 'v2/iframe_index.m3u8'
},
{
attributes: {
'AVERAGE-BANDWIDTH': 41547,
'BANDWIDTH': 42106,
'CODECS': 'avc1.640015',
'RESOLUTION': {
height: 270,
width: 480
},
'URI': 'v1/iframe_index.m3u8'
},
timeline: 0,
uri: 'v1/iframe_index.m3u8'
}
],
mediaGroups: { mediaGroups: {
'AUDIO': { 'AUDIO': {
aud1: { aud1: {
@ -461,5 +548,6 @@ module.exports = {
uri: 'v1/prog_index.m3u8' uri: 'v1/prog_index.m3u8'
}], }],
segments: [], segments: [],
version: 6 version: 6,
independentSegments: true
}; };

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
playlists: [ playlists: [
{ {
attributes: { attributes: {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,27 +1,33 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.08, duration: 6.08,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts' uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.6, duration: 6.6,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts' uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts',
title: '{}'
}, },
{ {
duration: 5, duration: 5,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts' uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts',
title: '{}'
} }
], ],
targetDuration: 8, targetDuration: 8,

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
segments: [ segments: [
{ {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,27 +1,33 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.08, duration: 6.08,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts' uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.6, duration: 6.6,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts' uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts',
title: '{}'
}, },
{ {
duration: 5, duration: 5,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts' uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts',
title: '{}'
} }
], ],
targetDuration: 8, targetDuration: 8,

View file

@ -1,12 +1,15 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
}, },
{ {
duration: 8, duration: 8,

View file

@ -1,6 +1,8 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [],
mediaGroups: { mediaGroups: {
'AUDIO': { 'AUDIO': {
'audio-lo': { 'audio-lo': {

View file

@ -1,6 +1,8 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [],
mediaGroups: { mediaGroups: {
'AUDIO': { 'AUDIO': {
'audio-lo': { 'audio-lo': {

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
targetDuration: 10, targetDuration: 10,
segments: [ segments: [

View file

@ -1,6 +1,8 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
discontinuityStarts: [], discontinuityStarts: [],
iFramePlaylists: [],
mediaGroups: { mediaGroups: {
'AUDIO': { 'AUDIO': {
aac: { aac: {

View file

@ -1,27 +1,33 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: -11, mediaSequence: -11,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.08, duration: 6.08,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts' uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.6, duration: 6.6,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts' uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts',
title: '{}'
}, },
{ {
duration: 5, duration: 5,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts' uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts',
title: '{}'
} }
], ],
targetDuration: 8, targetDuration: 8,

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,12 +1,15 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 17, mediaSequence: 17,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
} }
], ],
targetDuration: 8, targetDuration: 8,

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
playlists: [ playlists: [
{ {
attributes: { attributes: {

View file

@ -1,27 +1,33 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 11, mediaSequence: 11,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {
duration: 6.64, duration: 6.64,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts' uri: '/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.08, duration: 6.08,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts' uri: '/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts',
title: '{}'
}, },
{ {
duration: 6.6, duration: 6.6,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts' uri: '/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts',
title: '{}'
}, },
{ {
duration: 5, duration: 5,
timeline: 0, timeline: 0,
uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts' uri: '/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts',
title: '{}'
} }
], ],
targetDuration: 8, targetDuration: 8,

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,5 +1,7 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0, mediaSequence: 0,
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [

View file

@ -1,6 +1,8 @@
module.exports = { module.exports = {
allowCache: true, allowCache: true,
mediaSequence: 0, mediaSequence: 0,
dateRanges: [],
iFramePlaylists: [],
playlistType: 'VOD', playlistType: 'VOD',
segments: [ segments: [
{ {

View file

@ -242,7 +242,7 @@ QUnit.test('parses #EXTM3U tags', function(assert) {
// #EXTINF // #EXTINF
QUnit.test('parses minimal #EXTINF tags', function(assert) { QUnit.test('parses minimal #EXTINF tags', function(assert) {
const manifest = '#EXTINF\n'; const manifest = '#EXTINF:\n';
let element; let element;
this.parseStream.on('data', function(elem) { this.parseStream.on('data', function(elem) {
@ -319,7 +319,7 @@ QUnit.test('parses #EXTINF tags with carriage returns', function(assert) {
// #EXT-X-TARGETDURATION // #EXT-X-TARGETDURATION
QUnit.test('parses minimal #EXT-X-TARGETDURATION tags', function(assert) { QUnit.test('parses minimal #EXT-X-TARGETDURATION tags', function(assert) {
const manifest = '#EXT-X-TARGETDURATION\n'; const manifest = '#EXT-X-TARGETDURATION:\n';
let element; let element;
this.parseStream.on('data', function(elem) { this.parseStream.on('data', function(elem) {
@ -379,7 +379,7 @@ QUnit.test('parses #EXT-X-VERSION with a version', function(assert) {
// #EXT-X-MEDIA-SEQUENCE // #EXT-X-MEDIA-SEQUENCE
QUnit.test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function(assert) { QUnit.test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function(assert) {
const manifest = '#EXT-X-MEDIA-SEQUENCE\n'; const manifest = '#EXT-X-MEDIA-SEQUENCE:\n';
let element; let element;
this.parseStream.on('data', function(elem) { this.parseStream.on('data', function(elem) {
@ -453,7 +453,7 @@ QUnit.test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function(assert)
// #EXT-X-BYTERANGE // #EXT-X-BYTERANGE
QUnit.test('parses minimal #EXT-X-BYTERANGE tags', function(assert) { QUnit.test('parses minimal #EXT-X-BYTERANGE tags', function(assert) {
const manifest = '#EXT-X-BYTERANGE\n'; const manifest = '#EXT-X-BYTERANGE:\n';
let element; let element;
this.parseStream.on('data', function(elem) { this.parseStream.on('data', function(elem) {
@ -589,7 +589,7 @@ QUnit.test('parses #EXT-X-MAP tags with arbitrary attributes', function(assert)
}); });
// #EXT-X-STREAM-INF // #EXT-X-STREAM-INF
QUnit.test('parses minimal #EXT-X-STREAM-INF tags', function(assert) { QUnit.test('parses minimal #EXT-X-STREAM-INF tags', function(assert) {
const manifest = '#EXT-X-STREAM-INF\n'; const manifest = '#EXT-X-STREAM-INF:\n';
let element; let element;
this.parseStream.on('data', function(elem) { this.parseStream.on('data', function(elem) {
@ -604,7 +604,7 @@ QUnit.test('parses minimal #EXT-X-STREAM-INF tags', function(assert) {
}); });
// #EXT-X-PROGRAM-DATE-TIME // #EXT-X-PROGRAM-DATE-TIME
QUnit.test('parses minimal EXT-X-PROGRAM-DATE-TIME tags', function(assert) { QUnit.test('parses minimal EXT-X-PROGRAM-DATE-TIME tags', function(assert) {
const manifest = '#EXT-X-PROGRAM-DATE-TIME\n'; const manifest = '#EXT-X-PROGRAM-DATE-TIME:\n';
let element; let element;
this.parseStream.on('data', function(elem) { this.parseStream.on('data', function(elem) {
@ -698,6 +698,14 @@ QUnit.test('parses #EXT-X-STREAM-INF with common attributes', function(assert) {
'avc1.4d400d, mp4a.40.2', 'avc1.4d400d, mp4a.40.2',
'codecs are parsed' 'codecs are parsed'
); );
manifest = '#EXT-X-STREAM-INF:PATHWAY-ID="CDN-A"\n';
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.type, 'tag', 'the line type is tag');
assert.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
assert.strictEqual(element.attributes['PATHWAY-ID'], 'CDN-A', 'pathway-id is parsed');
}); });
QUnit.test('parses #EXT-X-STREAM-INF with arbitrary attributes', function(assert) { QUnit.test('parses #EXT-X-STREAM-INF with arbitrary attributes', function(assert) {
const manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n'; const manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n';
@ -809,16 +817,6 @@ QUnit.test('parses lightly-broken #EXT-X-KEY tags', function(assert) {
'parsed a single-quoted uri' 'parsed a single-quoted uri'
); );
element = null;
manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n';
this.lineStream.push(manifest);
assert.strictEqual(element.tagType, 'key', 'parsed the tag type');
assert.strictEqual(
element.attributes.URI,
'https://example.com/key',
'inferred a colon after the tag type'
);
element = null; element = null;
manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n'; manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n';
this.lineStream.push(manifest); this.lineStream.push(manifest);
@ -889,6 +887,104 @@ QUnit.test('parses EXT-X-START PRECISE attribute', function(assert) {
assert.strictEqual(element.attributes['TIME-OFFSET'], 1.4, 'parses time offset'); assert.strictEqual(element.attributes['TIME-OFFSET'], 1.4, 'parses time offset');
assert.strictEqual(element.attributes.PRECISE, true, 'parses precise attribute'); assert.strictEqual(element.attributes.PRECISE, true, 'parses precise attribute');
}); });
// #EXT-X-DATERANGE:
QUnit.test('parses minimal EXT-X-DATERANGE tag', function(assert) {
const manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z"\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.type, 'tag', 'the line type is tag');
assert.strictEqual(element.tagType, 'daterange', 'the tag type is daterange');
assert.strictEqual(element.attributes.ID, '12345');
assert.deepEqual(element.attributes['START-DATE'], new Date('2023-04-13T15:15:15.840000Z'));
});
QUnit.test('parses DURATION and PLANNED-DURATION attributes in EXT-X-DATERANGE tag', function(assert) {
const manifest = '#EXT-X-DATERANGE:ID="54545",START-DATE="2023-04-23T18:17:16.54000Z",PLANNED-DURATION="38.4",DURATION="15.5"\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.type, 'tag', 'the line type is tag');
assert.strictEqual(element.tagType, 'daterange', 'the tag type is daterange');
assert.strictEqual(element.attributes.ID, '54545');
assert.deepEqual(element.attributes['START-DATE'], new Date('2023-04-23T18:17:16.540000Z'));
assert.strictEqual(element.attributes.DURATION, 15.5);
assert.strictEqual(element.attributes['PLANNED-DURATION'], 38.4);
});
QUnit.test('parses SCTE35-CMD, SCTE35-OUT, SCTE35-IN EXT-X-DATERANGE tags', function(assert) {
let manifest = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2023-04-13T15:15:15.840000Z",SCTE35-OUT="0xFC002F0000000000FF000014056FFFFFFF0"\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.attributes['SCTE35-OUT'], '0xFC002F0000000000FF000014056FFFFFFF0');
manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",SCTE35-IN="0xFC0000425100FFF0140500000300000000E77FEFFE0011FB9EFE0029004D1932E0000100101002A22"\n';
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.attributes['SCTE35-IN'], '0xFC0000425100FFF0140500000300000000E77FEFFE0011FB9EFE0029004D1932E0000100101002A22');
manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",SCTE35-CMD="0xFC0000425100FFF014"\n';
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.attributes['SCTE35-CMD'], '0xFC0000425100FFF014');
});
QUnit.test('custom attributes in EXT-X-DATERANGE are parsed', function(assert) {
let manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",X-CUSTOM-KEY="value"\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.attributes['X-CUSTOM-KEY'], 'value');
manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",X-CUSTOM-KEY="17.8"\n';
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.attributes['X-CUSTOM-KEY'], 17.8);
manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",X-CUSTOM-KEY="0X12345abcde"\n';
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.attributes['X-CUSTOM-KEY'], '0X12345abcde');
});
QUnit.test('parses EXT-X-INDEPENDENT-SEGMENTS', function(assert) {
const manifest = '#EXT-X-INDEPENDENT-SEGMENTS\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.type, 'tag', 'the line type is tag');
assert.strictEqual(element.tagType, 'independent-segments', 'the tag type is independent-segments');
});
QUnit.test('ignores empty lines', function(assert) { QUnit.test('ignores empty lines', function(assert) {
const manifest = '\n'; const manifest = '\n';
@ -901,3 +997,40 @@ QUnit.test('ignores empty lines', function(assert) {
assert.ok(!event, 'no event is triggered'); assert.ok(!event, 'no event is triggered');
}); });
QUnit.test('parses #EXT-X-I-FRAME-STREAM-INF', function(assert) {
const manifest = '#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=739757,BANDWIDTH=1895477,VIDEO-RANGE=PQ,CODECS="hvc1.2.4.L150.B0",RESOLUTION=3840x2160,HDCP-LEVEL=TYPE-1,URI="hdr10_2160/iframe_index.m3u8"\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.type, 'tag', 'the line type is tag');
assert.strictEqual(element.tagType, 'i-frame-playlist', 'the tag type is i-frame-playlist');
assert.strictEqual(element.uri, 'hdr10_2160/iframe_index.m3u8', 'the uri text is parsed');
assert.strictEqual(element.attributes['AVERAGE-BANDWIDTH'], 739757, 'the average bandwidth is parsed');
assert.strictEqual(element.attributes.BANDWIDTH, 1895477, 'the bandwidth is parsed');
assert.strictEqual(element.attributes['VIDEO-RANGE'], 'PQ', 'the video range is parsed');
assert.strictEqual(element.attributes.CODECS, 'hvc1.2.4.L150.B0', 'the codecs is parsed');
assert.strictEqual(element.attributes.RESOLUTION.width, 3840, 'the resolution width is parsed');
assert.strictEqual(element.attributes.RESOLUTION.height, 2160, 'the resolution height is parsed');
assert.strictEqual(element.attributes['HDCP-LEVEL'], 'TYPE-1', 'the HDCP level is parsed');
assert.strictEqual(element.attributes.URI, 'hdr10_2160/iframe_index.m3u8', 'the uri text is parsed');
});
QUnit.test('parses #EXT-X-I-FRAMES-ONLY', function(assert) {
const manifest = '#EXT-X-I-FRAMES-ONLY\n';
let element;
this.parseStream.on('data', function(elem) {
element = elem;
});
this.lineStream.push(manifest);
assert.ok(element, 'an event was triggered');
assert.strictEqual(element.type, 'tag', 'the line type is tag');
assert.strictEqual(element.tagType, 'i-frames-only', 'the tag type is i-frames-only');
});

View file

@ -108,11 +108,10 @@ QUnit.module('m3u8s', function(hooks) {
'#EXT-X-CUE-OUT:10', '#EXT-X-CUE-OUT:10',
'#EXTINF:5,', '#EXTINF:5,',
'ex2.ts', 'ex2.ts',
'#EXT-X-CUE-OUT15',
'#EXT-UKNOWN-TAG', '#EXT-UKNOWN-TAG',
'#EXTINF:5,', '#EXTINF:5,',
'ex3.ts', 'ex3.ts',
'#EXT-X-CUE-OUT', '#EXT-X-CUE-OUT:',
'#EXTINF:5,', '#EXTINF:5,',
'ex3.ts', 'ex3.ts',
'#EXT-X-ENDLIST' '#EXT-X-ENDLIST'
@ -122,7 +121,6 @@ QUnit.module('m3u8s', function(hooks) {
this.parser.end(); this.parser.end();
assert.equal(this.parser.manifest.segments[1].cueOut, '10', 'parser attached cue out tag'); assert.equal(this.parser.manifest.segments[1].cueOut, '10', 'parser attached cue out tag');
assert.equal(this.parser.manifest.segments[2].cueOut, '15', 'cue out without : seperator');
assert.equal(this.parser.manifest.segments[3].cueOut, '', 'cue out without data'); assert.equal(this.parser.manifest.segments[3].cueOut, '', 'cue out without data');
}); });
@ -135,11 +133,10 @@ QUnit.module('m3u8s', function(hooks) {
'#EXT-X-CUE-OUT-CONT:10/60', '#EXT-X-CUE-OUT-CONT:10/60',
'#EXTINF:5,', '#EXTINF:5,',
'ex2.ts', 'ex2.ts',
'#EXT-X-CUE-OUT-CONT15/30',
'#EXT-UKNOWN-TAG', '#EXT-UKNOWN-TAG',
'#EXTINF:5,', '#EXTINF:5,',
'ex3.ts', 'ex3.ts',
'#EXT-X-CUE-OUT-CONT', '#EXT-X-CUE-OUT-CONT:',
'#EXTINF:5,', '#EXTINF:5,',
'ex3.ts', 'ex3.ts',
'#EXT-X-ENDLIST' '#EXT-X-ENDLIST'
@ -152,10 +149,6 @@ QUnit.module('m3u8s', function(hooks) {
this.parser.manifest.segments[1].cueOutCont, '10/60', this.parser.manifest.segments[1].cueOutCont, '10/60',
'parser attached cue out cont tag' 'parser attached cue out cont tag'
); );
assert.equal(
this.parser.manifest.segments[2].cueOutCont, '15/30',
'cue out cont without : seperator'
);
assert.equal(this.parser.manifest.segments[3].cueOutCont, '', 'cue out cont without data'); assert.equal(this.parser.manifest.segments[3].cueOutCont, '', 'cue out cont without data');
}); });
@ -165,14 +158,13 @@ QUnit.module('m3u8s', function(hooks) {
'#EXTINF:5,', '#EXTINF:5,',
'#COMMENT', '#COMMENT',
'ex1.ts', 'ex1.ts',
'#EXT-X-CUE-IN', '#EXT-X-CUE-IN:',
'#EXTINF:5,', '#EXTINF:5,',
'ex2.ts', 'ex2.ts',
'#EXT-X-CUE-IN:15', '#EXT-X-CUE-IN:15',
'#EXT-UKNOWN-TAG', '#EXT-UKNOWN-TAG',
'#EXTINF:5,', '#EXTINF:5,',
'ex3.ts', 'ex3.ts',
'#EXT-X-CUE-IN=abc',
'#EXTINF:5,', '#EXTINF:5,',
'ex3.ts', 'ex3.ts',
'#EXT-X-ENDLIST' '#EXT-X-ENDLIST'
@ -183,10 +175,6 @@ QUnit.module('m3u8s', function(hooks) {
assert.equal(this.parser.manifest.segments[1].cueIn, '', 'parser attached cue in tag'); assert.equal(this.parser.manifest.segments[1].cueIn, '', 'parser attached cue in tag');
assert.equal(this.parser.manifest.segments[2].cueIn, '15', 'cue in with data'); assert.equal(this.parser.manifest.segments[2].cueIn, '15', 'cue in with data');
assert.equal(
this.parser.manifest.segments[3].cueIn, '=abc',
'cue in without colon seperator'
);
}); });
QUnit.test('parses characteristics attribute', function(assert) { QUnit.test('parses characteristics attribute', function(assert) {
@ -857,6 +845,617 @@ QUnit.module('m3u8s', function(hooks) {
); );
}); });
QUnit.test('PDT value is assigned to segments with explicit #EXT-X-PROGRAM-DATE-TIME tags', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-VERSION:6',
'#EXT-X-TARGETDURATION:8',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXTINF:8.0',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'https://example.com/playlist1.m3u8',
'#EXTINF:8.0,',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T22:14:10.053+00:00',
'https://example.com/playlist2.m3u8',
'#EXT-X-ENDLIST'
].join('\n'));
this.parser.end();
assert.equal(this.parser.manifest.segments[0].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
assert.equal(this.parser.manifest.segments[1].programDateTime, new Date('2017-07-31T22:14:10.053+00:00').getTime());
});
QUnit.test('backfill PDT values when the first EXT-X-PROGRAM-DATE-TIME tag appears after one or more Media Segment URIs', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-VERSION:6',
'#EXT-X-TARGETDURATION:8',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXTINF:8.0',
'https://example.com/playlist1.m3u8',
'#EXTINF:8.0,',
'https://example.com/playlist2.m3u8',
'#EXTINF:8.0',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'https://example.com/playlist3.m3u8',
'#EXT-X-ENDLIST'
].join('\n'));
this.parser.end();
const segments = this.parser.manifest.segments;
assert.equal(segments[2].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
assert.equal(segments[1].programDateTime, segments[2].programDateTime - (segments[1].duration * 1000));
assert.equal(segments[0].programDateTime, segments[1].programDateTime - (segments[0].duration * 1000));
});
QUnit.test('extrapolates forward when subsequent fragments do not have explicit PDT tags', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-VERSION:6',
'#EXT-X-TARGETDURATION:8',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXTINF:8.0',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'https://example.com/playlist1.m3u8',
'#EXTINF:8.0,',
'https://example.com/playlist2.m3u8',
'#EXTINF:8.0',
'https://example.com/playlist3.m3u8',
'#EXT-X-ENDLIST'
].join('\n'));
this.parser.end();
const segments = this.parser.manifest.segments;
assert.equal(segments[0].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
assert.equal(segments[1].programDateTime, segments[0].programDateTime + segments[1].duration * 1000);
assert.equal(segments[2].programDateTime, segments[1].programDateTime + segments[2].duration * 1000);
});
QUnit.test('warns when #EXT-X-DATERANGE missing attribute', function(assert) {
this.parser.push([
'#EXT-X-VERSION:3',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-DISCONTINUITY-SEQUENCE:0',
'#EXTINF:10,',
'media-00001.ts',
'#EXT-X-ENDLIST',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#EXT-X-DATERANGE:ID="12345"'
].join('\n'));
this.parser.end();
const warnings = [
'#EXT-X-DATERANGE #0 lacks required attribute(s): START-DATE'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('warns when #EXT-X-DATERANGE end date attribute is less than start date', function(assert) {
this.parser.push([
'#EXT-X-VERSION:3',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-DISCONTINUITY-SEQUENCE:0',
'#EXTINF:10,',
'media-00001.ts',
'#EXT-X-ENDLIST',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-DATE="2023-04-13T15:15:15.840000Z"'
].join('\n'));
this.parser.end();
const warnings = [
'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('warns when #EXT-X-DATERANGE duration or planned duration attribute is negative', function(assert) {
this.parser.push([
'#EXT-X-VERSION:3',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-DISCONTINUITY-SEQUENCE:0',
'#EXTINF:10,',
'media-00001.ts',
'#EXT-X-ENDLIST',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",PLANNED-DURATION=-38.4,DURATION=-15.5'
].join('\n'));
this.parser.end();
const warnings = [
'EXT-X-DATERANGE DURATION must not be negative',
'EXT-X-DATERANGE PLANNED-DURATION must not be negative'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('warns when #EXT-X-DATERANGE has a END-ON-NEXT=YES attribute and a DURATION or END-DATE attribute', function(assert) {
this.parser.push([
'#EXT-X-VERSION:3',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-DISCONTINUITY-SEQUENCE:0',
'#EXTINF:10,',
'media-00001.ts',
'#EXT-X-ENDLIST',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",END-ON-NEXT=YES, END-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE"'
].join('\n'));
this.parser.end();
const warnings = [
'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('warns when #EXT-X-DATERANGE has a END-ON-NEXT=YES attribute but not a CLASS attribute', function(assert) {
this.parser.push([
'#EXT-X-VERSION:3',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-DISCONTINUITY-SEQUENCE:0',
'#EXTINF:10,',
'media-00001.ts',
'#EXT-X-ENDLIST',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES'
].join('\n'));
this.parser.end();
const warnings = [
'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('warns when playlist has multiple #EXT-X-DATERANGE tag same ID but different attribute values', function(assert) {
this.parser.push([
'#EXT-X-VERSION:3',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-DISCONTINUITY-SEQUENCE:0',
'#EXTINF:10,',
'media-00001.ts',
'#EXT-X-ENDLIST',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES,CLASS="CLASSATTRIBUTE"',
'#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE1"'
].join('\n'));
this.parser.end();
const warnings = [
'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('when #EXT-X-DATERANGE has both DURATION and END-DATE attributes, value of the END-DATE attribute must be START-DATE + DURATION', function(assert) {
this.parser.push([
'#EXT-X-VERSION:3',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-DISCONTINUITY-SEQUENCE:0',
'#EXTINF:10,',
'media-00001.ts',
'#EXT-X-ENDLIST',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:16:15.840000Z",DURATION=14.0,END-DATE="2023-04-13T18:15:15.840000Z"'
].join('\n'));
this.parser.end();
assert.deepEqual(this.parser.manifest.dateRanges[0].endDate, new Date('2023-04-13T15:16:29.840000Z'));
});
QUnit.test('warns when playlist contains #EXT-X-DATERANGE tag but no #EXT-X-PROGRAM-DATE-TIME', function(assert) {
this.parser.push([
'#EXT-X-VERSION:3',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-DISCONTINUITY-SEQUENCE:0',
'#EXTINF:10,',
'media-00001.ts',
'#EXT-X-ENDLIST',
'#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES,CLASS="sampleClassAttrib"'
].join('\n'));
this.parser.end();
const warnings = [
'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('playlist with multiple ext-x-daterange with same ID but no conflicting attributes', function(assert) {
const expectedDateRange = {
id: '12345',
scte35In: '0xFC30200FFF2',
scte35Out: '0xFC30200FFF2',
startDate: new Date('2023-04-13T18:16:15.840000Z'),
class: 'CLASSATTRIBUTE'
};
this.parser.push([
'#EXT-X-VERSION:3',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-DISCONTINUITY-SEQUENCE:0',
'#EXTINF:10,',
'media-00001.ts',
'#EXT-X-ENDLIST',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#EXT-X-DATERANGE:ID="12345",SCTE35-IN=0xFC30200FFF2,START-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE"',
'#EXT-X-DATERANGE:ID="12345",SCTE35-OUT=0xFC30200FFF2,START-DATE="2023-04-13T18:16:15.840000Z"'
].join('\n'));
this.parser.end();
assert.equal(this.parser.manifest.dateRanges.length, 1, 'two dateranges with same ID are merged');
assert.deepEqual(this.parser.manifest.dateRanges[0], expectedDateRange);
});
QUnit.test('playlist with multiple ext-x-daterange ', function(assert) {
this.parser.push([
' #EXTM3U',
'#EXT-X-VERSION:6',
'#EXT-X-TARGETDURATION:8',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
'#EXT-X-DATERANGE:ID="event1",START-DATE="2023-04-20T10:00:00Z",DURATION=30.0,END-DATE="2023-04-20T10:00:30Z",X-CUSTOM-KEY="value"',
'#EXTINF:8.0',
'https://example.com/playlist1.m3u8',
'#EXT-SCTE35-IN:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
'#EXT-X-DATERANGE:ID="event2",START-DATE="2023-04-20T11:00:00Z",DURATION=60.0,END-DATE="2023-04-20T11:01:00Z",X-CUSTOM-KEY="value"',
'#EXTINF:8.0,',
'https://example.com/playlist2.m3u8',
'#EXT-SCTE35-OUT:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
'#EXT-X-DATERANGE:ID="event3",START-DATE="2023-04-20T12:00:00Z",DURATION=120.0,END-DATE="2023-04-20T12:02:00Z",X-CUSTOM-KEY="value"',
'#EXTINF:8.0',
'https://example.com/playlist3.m3u8',
'#EXT-SCTE35-IN:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
'#EXT-SCTE35-OUT:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
'#EXT-X-ENDLIST'
].join('\n'));
this.parser.end();
assert.equal(this.parser.manifest.dateRanges.length, 3);
});
QUnit.test('parses #EXT-X-INDEPENDENT-SEGMENTS', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-VERSION:6',
'#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=3.252,CAN-SKIP-UNTIL=42.0',
'#EXT-X-INDEPENDENT-SEGMENTS'
].join('\n'));
this.parser.end();
assert.equal(this.parser.manifest.independentSegments, true);
});
QUnit.test('parses #EXT-X-I-FRAME-STREAM-INF', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8"',
'#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8"',
'#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8"',
'#EXT-X-STREAM-INF:BANDWIDTH=1280000',
'low/audio-video.m3u8',
'#EXT-X-STREAM-INF:BANDWIDTH=2560000',
'mid/audio-video.m3u8',
'#EXT-X-STREAM-INF:BANDWIDTH=7680000',
'hi/audio-video.m3u8',
'#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"',
'audio-only.m3u8'
].join('\n'));
this.parser.end();
assert.equal(this.parser.manifest.iFramePlaylists.length, 3);
assert.equal(this.parser.manifest.iFramePlaylists[0].uri, 'low/iframe.m3u8');
assert.strictEqual(this.parser.manifest.iFramePlaylists[0].attributes.BANDWIDTH, 86000);
});
QUnit.test('warns when #EXT-X-I-FRAME-STREAM-INF missing BANDWIDTH/URI attributes', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-I-FRAME-STREAM-INF:URI="low/iframe.m3u8"',
'#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8"',
'#EXT-X-I-FRAME-STREAM-INF:',
'#EXT-X-STREAM-INF:BANDWIDTH=1280000',
'low/audio-video.m3u8',
'#EXT-X-STREAM-INF:BANDWIDTH=2560000',
'mid/audio-video.m3u8',
'#EXT-X-STREAM-INF:BANDWIDTH=7680000',
'hi/audio-video.m3u8',
'#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"',
'audio-only.m3u8'
].join('\n'));
this.parser.end();
const warnings = [
'#EXT-X-I-FRAME-STREAM-INF lacks required attribute(s): BANDWIDTH',
'#EXT-X-I-FRAME-STREAM-INF lacks required attribute(s): BANDWIDTH, URI'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('warns when #EXT-X-I-FRAMES-ONLY the minimum version required is not supported', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-VERSION:3',
'#EXT-X-PLAYLIST-TYPE:VOD',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-TARGETDURATION:3',
'#EXT-X-I-FRAMES-ONLY',
'#EXTINF:2.002,',
'001.ts',
'#EXTINF:2.002,',
'002.ts',
'#EXTINF:2.002,',
'003.ts',
'#EXTINF:2.002,',
'004.ts',
'#EXTINF:2.002,',
'005.ts',
'#EXTINF:2.002,',
'006.ts',
'#EXT-X-ENDLIST'
].join('\n'));
this.parser.end();
const warnings = [
'manifest must be at least version 4'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('warns when #EXT-X-I-FRAMES-ONLY does not contain a version number', function(assert) {
this.parser.push([
'#EXTM3U',
'#EXT-X-PLAYLIST-TYPE:VOD',
'#EXT-X-MEDIA-SEQUENCE:0',
'#EXT-X-TARGETDURATION:3',
'#EXT-X-I-FRAMES-ONLY',
'#EXTINF:2.002,',
'001.ts',
'#EXTINF:2.002,',
'002.ts',
'#EXTINF:2.002,',
'003.ts',
'#EXTINF:2.002,',
'004.ts',
'#EXTINF:2.002,',
'005.ts',
'#EXTINF:2.002,',
'006.ts',
'#EXT-X-ENDLIST'
].join('\n'));
this.parser.end();
const warnings = [
'manifest must be at least version 4'
];
assert.deepEqual(
this.warnings,
warnings,
'warnings as expected'
);
});
QUnit.test('parses #EXT-X-CONTENT-STEERING', function(assert) {
const expectedContentSteeringObject = {
serverUri: '/foo?bar=00012',
pathwayId: 'CDN-A'
};
this.parser.push('#EXT-X-CONTENT-STEERING:SERVER-URI="/foo?bar=00012",PATHWAY-ID="CDN-A"');
this.parser.end();
assert.deepEqual(this.parser.manifest.contentSteering, expectedContentSteeringObject);
});
QUnit.test('parses #EXT-X-CONTENT-STEERING without PATHWAY-ID', function(assert) {
const expectedContentSteeringObject = {
serverUri: '/bar?foo=00012'
};
this.parser.push('#EXT-X-CONTENT-STEERING:SERVER-URI="/bar?foo=00012"');
this.parser.end();
assert.deepEqual(this.parser.manifest.contentSteering, expectedContentSteeringObject);
});
QUnit.test('warns on #EXT-X-CONTENT-STEERING missing SERVER-URI', function(assert) {
const warning = ['#EXT-X-CONTENT-STEERING lacks required attribute(s): SERVER-URI'];
this.parser.push('#EXT-X-CONTENT-STEERING:PATHWAY-ID="CDN-A"');
this.parser.end();
assert.deepEqual(this.warnings, warning, 'warnings as expected');
});
QUnit.module('define', {
// https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.2.3
beforeEach() {
this.errors = [];
this.parser.on('error', (err) => this.errors.push(err.message));
}
});
QUnit.test('fails on missing attributes', function(assert) {
const err = ['EXT-X-DEFINE: No attribute'];
this.parser.push('#EXT-X-DEFINE:');
this.parser.end();
assert.deepEqual(this.errors, err, 'errors as expected');
});
QUnit.test('fails on disallowed combinatons', function(assert) {
const permutations = [
'#EXT-X-DEFINE:NAME="a",QUERYPARAM="b"',
'#EXT-X-DEFINE:NAME="a",IMPORT="b"',
'#EXT-X-DEFINE:QUERYPARAM="a",IMPORT="b"',
'#EXT-X-DEFINE:NAME="a",QUERYPARAM="b",IMPORT="c"'
];
assert.expect(permutations.length);
permutations.forEach((p) => {
this.parser = new Parser();
this.parser.on('error', (e) => {
assert.equal(e.message, 'EXT-X-DEFINE: Invalid attributes', `${p} errors as expected`);
});
this.parser.push(p);
this.parser.end();
});
});
QUnit.test('query params substituted', function(assert) {
this.parser = new Parser({
uri: 'https://example.com?aParam=aValue'
});
this.parser.push([
'#EXTM3U',
'#EXT-X-DEFINE:QUERYPARAM="aParam"',
'#EXTINF:10',
'segment.ts?replaced_param={$aParam}'
].join('\n'));
this.parser.end();
assert.equal('aValue', this.parser.manifest.definitions.aParam, 'value of param stored');
assert.equal('segment.ts?replaced_param=aValue', this.parser.manifest.segments[0].uri, 'substituted in url');
});
QUnit.test('query params substituted with relative URL', function(assert) {
this.parser = new Parser({
uri: 'playlist.m3u8?aParam=aValue'
});
this.parser.push([
'#EXTM3U',
'#EXT-X-DEFINE:QUERYPARAM="aParam"',
'#EXTINF:10',
'segment.ts?replaced_param={$aParam}'
].join('\n'));
this.parser.end();
assert.equal('aValue', this.parser.manifest.definitions.aParam, 'value of param stored');
assert.equal('segment.ts?replaced_param=aValue', this.parser.manifest.segments[0].uri, 'substituted in url');
});
QUnit.test('fails with missing query params', function(assert) {
assert.expect(1);
this.parser = new Parser({
uri: 'https://example.com?bParam=bValue'
});
this.parser.on('error', (e) => {
assert.equal(e.message, 'EXT-X-DEFINE: No query param aParam');
});
this.parser.push([
'#EXTM3U',
'#EXT-X-DEFINE:QUERYPARAM="aParam"',
'#EXTINF:10',
'segment.ts?replacedparam={$aParam}'
].join('\n'));
this.parser.end();
});
QUnit.test('fails on redefinition', function(assert) {
const permutations = [
['#EXT-X-DEFINE:NAME="a",VALUE="b"', '#EXT-X-DEFINE:NAME="a",VALUE="c"'],
['#EXT-X-DEFINE:NAME="a",VALUE="b"', '#EXT-X-DEFINE:IMPORT="a"'],
['#EXT-X-DEFINE:NAME="a",VALUE="b"', '#EXT-X-DEFINE:QUERYPARAM="a"'],
['#EXT-X-DEFINE:IMPORT="a"', '#EXT-X-DEFINE:IMPORT="a"'],
['#EXT-X-DEFINE:IMPORT="a"', '#EXT-X-DEFINE:QUERYPARAM="a"'],
['#EXT-X-DEFINE:IMPORT="a"', '#EXT-X-DEFINE:NAME="a",VALUE="c"'],
['#EXT-X-DEFINE:QUERYPARAM="a"', '#EXT-X-DEFINE:IMPORT="a"'],
['#EXT-X-DEFINE:QUERYPARAM="a"', '#EXT-X-DEFINE:QUERYPARAM="a"'],
['#EXT-X-DEFINE:QUERYPARAM="a"', '#EXT-X-DEFINE:NAME="a",VALUE="c"']
];
assert.expect(permutations.length);
permutations.forEach((p) => {
this.parser = new Parser({
uri: 'https:example.com?a=1',
mainDefinitions: {
a: 2
}
});
this.parser.on('error', (e) => {
assert.equal(e.message, 'EXT-X-DEFINE: Duplicate name a', 'errosr on combination');
});
this.parser.push(p.join('\n'));
this.parser.end();
});
});
QUnit.test('fails with IMPORT on main playlist', function(assert) {
this.parser.on('error', function(e) {
assert.equal(e.message, 'EXT-X-DEFINE: No value imported_param to import, or IMPORT used on main playlist', 'fails when missing');
});
this.parser.push('#EXT-X-DEFINE:IMPORT="imported_param"');
this.parser.end();
});
QUnit.test('named and imported substiutions work', function(assert) {
this.parser = new Parser({
mainDefinitions: {
aParam: 'aValue',
engLabel: 'Anglais'
}
});
this.parser.push([
'#EXTM3U',
'#EXT-X-DEFINE:IMPORT="aParam"',
'#EXT-X-DEFINE:NAME="bParam",VALUE="bValue"',
'#EXT-X-DEFINE:IMPORT="engLabel"',
'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="{$engLabel}",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8?bParam={$bParam}"',
'#EXTINF:10',
'segment.ts?aParam={$aParam}&bParam={$bParam}'
].join('\n'));
this.parser.end();
assert.equal('aValue', this.parser.manifest.definitions.aParam, 'value of param from import stored');
assert.equal('bValue', this.parser.manifest.definitions.bParam, 'value of param from name stored');
assert.equal('segment.ts?aParam=aValue&bParam=bValue', this.parser.manifest.segments[0].uri, 'substituted in uri');
assert.ok(this.parser.manifest.mediaGroups.AUDIO.aac.hasOwnProperty('Anglais'), 'replacement in attribute');
assert.equal('eng/prog_index.m3u8?bParam=bValue', this.parser.manifest.mediaGroups.AUDIO.aac.Anglais.uri, 'replacement in uri in attribute');
});
QUnit.module('integration'); QUnit.module('integration');
for (const key in testDataExpected) { for (const key in testDataExpected) {