Compare commits

..

7 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
72 changed files with 1144 additions and 11973 deletions

2
.nvmrc
View file

@ -1 +1 @@
16
10

View file

@ -1,3 +1,17 @@
<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)

View file

@ -13,12 +13,13 @@ m3u8 parser
- [Installation](#installation)
- [Usage](#usage)
- [Constructor Options](#constructor-options)
- [Parsed Output](#parsed-output)
- [Supported Tags](#supported-tags)
- [Basic Playlist Tags](#basic-playlist-tags)
- [Media Segment Tags](#media-segment-tags)
- [Media Playlist Tags](#media-playlist-tags)
- [Master Playlist Tags](#master-playlist-tags)
- [Main Playlist Tags](#main-playlist-tags)
- [Experimental Tags](#experimental-tags)
- [EXT-X-CUE-OUT](#ext-x-cue-out)
- [EXT-X-CUE-OUT-CONT](#ext-x-cue-out-cont)
@ -70,6 +71,21 @@ parser.end();
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
@ -163,6 +179,7 @@ Manifest {
* [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-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
@ -173,11 +190,15 @@ Manifest {
* [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-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-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
@ -242,8 +263,6 @@ Example media playlist using `EXT-X-CUE-` tags.
### Not Yet Supported
* [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-KEY](http://tools.ietf.org/html/draft-pantos-http-live-streaming#section-4.3.4.5)

11879
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
{
"name": "m3u8-parser",
"version": "7.1.0",
"version": "7.2.0",
"description": "m3u8 parser",
"main": "dist/m3u8-parser.cjs.js",
"module": "dist/m3u8-parser.es.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git@github.com:videojs/m3u8-parser.git"
@ -28,7 +27,6 @@
"build-prod": "cross-env-shell NO_TEST_BUNDLE=1 'npm run build'",
"build": "npm-run-all -s clean -p build:*",
"build:js": "rollup -c scripts/rollup.config.js",
"build:types": "tsc",
"clean": "shx rm -rf ./dist ./test/dist && shx mkdir -p ./dist ./test/dist",
"lint": "vjsstandard",
"prepublishOnly": "npm-run-all build-prod && vjsverify --verbose --skip-es-check",
@ -67,7 +65,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.5",
"@videojs/vhs-utils": "^4.1.1",
"global": "^4.4.0"
},
"devDependencies": {
@ -77,7 +75,6 @@
"rollup": "^2.38.0",
"rollup-plugin-data-files": "^0.1.0",
"sinon": "^9.2.3",
"typescript": "^5.2.2",
"videojs-generate-karma-config": "^8.0.1",
"videojs-generate-rollup-config": "~7.0.0",
"videojs-generator-verify": "~3.0.1",

View file

@ -71,6 +71,29 @@ const parseAttributes = function(attributes) {
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
* line at a time and performs a context-free parse of its contents. A stream
@ -296,16 +319,7 @@ export default class ParseStream extends Stream {
event.attributes = parseAttributes(match[1]);
if (event.attributes.RESOLUTION) {
const split = event.attributes.RESOLUTION.split('x');
const resolution = {};
if (split[0]) {
resolution.width = parseInt(split[0], 10);
}
if (split[1]) {
resolution.height = parseInt(split[1], 10);
}
event.attributes.RESOLUTION = resolution;
event.attributes.RESOLUTION = parseResolution(event.attributes.RESOLUTION);
}
if (event.attributes.BANDWIDTH) {
event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
@ -429,7 +443,7 @@ export default class ParseStream extends Stream {
this.trigger('data', event);
return;
}
match = (/^#EXT-X-CUE-IN:(.*)?$/).exec(newLine);
match = (/^#EXT-X-CUE-IN:?(.*)?$/).exec(newLine);
if (match) {
event = {
type: 'tag',
@ -624,6 +638,16 @@ export default class ParseStream extends Stream {
});
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 = {
@ -632,6 +656,51 @@ export default class ParseStream extends Stream {
};
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;
}

View file

@ -6,92 +6,6 @@ import decodeB64ToUint8Array from '@videojs/vhs-utils/es/decode-b64-to-uint8-arr
import LineStream from './line-stream';
import ParseStream from './parse-stream';
/**
* @typedef {Object} DateRange
* @property {string} [id] - A quoted-string that uniquely identifies a Date Range in the Playlist.
* @property {string} [class] - A client-defined quoted-string that specifies some set of attributes and their associated value semantics.
* @property {Date} [startDate] - date/time at which the Date Range begins.
* @property {Date} [endDate] - date/time at which the Date Range ends.
* @property {number} [duration] - The duration of the Date Range.
* @property {number} [plannedDuration] - The expected duration of the Date Range.
* @property {boolean} [endOnNext] - This attribute indicates that the end of the range containing it is equal to the START-DATE of its Following Range.
* @property {string} [scte35Cmd] - SCTE-35 splice_info_section()
* @property {string} [scte35Out] - SCTE-35 splice-in
* @property {string} [scte35In] - SCTE-35 splice-out
*/
/**
* @typedef {Object} Playlist
*/
/**
* @typedef {Object} ByteRange
* @property {number} [length] - byte range length
* @property {number} [offset] - byte range offset
*/
/**
* @typdef {Object} Segment
* @property {number} [programDateTime] - associated program date time.
* @property {ByteRange} [byterange] - associated byte range
*/
/**
*
*
*
*
* @typedef {{
* allowCache: boolean;
* discontinuityStarts: Array<number>;
* dateRanges: Array<DateRange>;
* segments: Array<Segment>;
* playlists?: Array<Playlist>;
* mediaGroups?: {
* AUDIO?: Record<string, unknown>;
* VIDEO?: Record<string, unknown>;
* 'CLOSED-CAPTIONS'?: Record<string, unknown>;
* SUBTITLES?: Record<string, unknown>;
* };
* preloadSegment?: string;
* version?: string;
* endList?: boolean;
* mediaSequence?: number;
* discontinuitySequence?: number;
* contentProtection?: {
* 'com.apple.fps.1_0'?: {
* attributes: Record<string, unknown>
* },
* 'com.microsoft.playready'?: {
* uri: string
* },
* 'com.widevine.alpha'?: {
* attributes: {
* schemeIdUri: string;
* keyId: string;
* },
* pssh: Uint8Array
* }
* },
* playlistType?: 'VOD' | 'EVENT';
* dateTimeString?: string;
* dateTimeObject?: Date;
* targetDuration?: number;
* start?: {
* timeOffset: number;
* precise: boolean;
* };
* skip: Record<string, unknown>;
* renditionReports?: Array<RenditionReport>;
* serverControl?: Record<string, unknown>;
* partInf?: Record<string, unknown>;
* partTargetDuration?: number;
* independentSegments?: boolean;
* contentSteering?: Record<string, unknown>;
* custom?: Record<string, unknown>;
* }} Manifest
*/
const camelCase = (str) => str
.toLowerCase()
.replace(/-(\w)/g, (a) => a[1].toUpperCase());
@ -174,15 +88,19 @@ const setHoldBack = function(manifest) {
* requires some property of the manifest object to be defaulted.
*
* @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
*/
export default class Parser extends Stream {
constructor() {
constructor(opts = {}) {
super();
this.lineStream = new LineStream();
this.parseStream = new 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 */
@ -213,6 +131,7 @@ export default class Parser extends Stream {
allowCache: true,
discontinuityStarts: [],
dateRanges: [],
iFramePlaylists: [],
segments: []
};
// keep track of the last seen segment's byte range end, as segments are not required
@ -249,6 +168,22 @@ export default class Parser extends Stream {
let mediaGroup;
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() {
// switch based on the tag type
@ -809,6 +744,11 @@ export default class Parser extends Stream {
'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_(
@ -816,7 +756,115 @@ export default class Parser extends Stream {
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);
},
uri() {
@ -870,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) {
const missing = [];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ module.exports = {
discontinuitySequence: 0,
discontinuityStarts: [],
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 7794,
segments: [
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,5 +2,6 @@ module.exports = {
allowCache: true,
dateRanges: [],
discontinuityStarts: [],
iFramePlaylists: [],
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,6 +1,7 @@
module.exports = {
allowCache: true,
dateRanges: [],
iFramePlaylists: [],
mediaSequence: 0,
playlistType: 'VOD',
segments: [

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ module.exports = {
dateRanges: [],
discontinuitySequence: 0,
discontinuityStarts: [],
iFramePlaylists: [],
mediaSequence: 0,
playlistType: 'VOD',
preloadSegment: {

View file

@ -3,6 +3,7 @@ module.exports = {
dateRanges: [],
discontinuitySequence: 0,
discontinuityStarts: [],
iFramePlaylists: [],
mediaSequence: 0,
playlistType: 'VOD',
preloadSegment: {

View file

@ -5,6 +5,7 @@ module.exports = {
dateRanges: [],
discontinuitySequence: 0,
discontinuityStarts: [],
iFramePlaylists: [],
mediaSequence: 266,
preloadSegment: {
map: {uri: 'init.mp4'},

View file

@ -5,6 +5,7 @@ module.exports = {
dateRanges: [],
discontinuitySequence: 0,
discontinuityStarts: [],
iFramePlaylists: [],
mediaSequence: 266,
preloadSegment: {
timeline: 0,

View file

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

View file

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

View file

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

View file

@ -2,6 +2,92 @@ module.exports = {
allowCache: true,
dateRanges: [],
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: {
'AUDIO': {
aud1: {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -997,3 +997,40 @@ QUnit.test('ignores empty lines', function(assert) {
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

@ -1158,6 +1158,126 @@ QUnit.module('m3u8s', function(hooks) {
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',
@ -1187,6 +1307,155 @@ QUnit.module('m3u8s', function(hooks) {
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');
for (const key in testDataExpected) {

View file

@ -1,22 +0,0 @@
{
// Change this to match your project
"include": ["src/**/*"],
"compilerOptions": {
// Tells TypeScript to read JS files, as
// normally they are ignored as source files
"allowJs": true,
// Generate d.ts files
"declaration": true,
// This compiler run should
// only output d.ts files
"emitDeclarationOnly": true,
// Types should go into this directory.
// Removing this would place the .d.ts files
// next to the .js files
"outDir": "dist/types",
// go to js file when using IDE functions like
// "Go to Definition" in VSCode
"declarationMap": true,
"skipLibCheck":true
}
}