hls-parser
Advanced tools
Comparing version 0.5.1 to 0.6.0
{ | ||
"name": "hls-parser", | ||
"version": "0.5.1", | ||
"version": "0.6.0", | ||
"description": "A simple library to read/write HLS playlists", | ||
@@ -56,2 +56,3 @@ "main": "index.js", | ||
"rules": { | ||
"ava/no-ignored-test-files": 0, | ||
"camelcase": 0, | ||
@@ -58,0 +59,0 @@ "capitalized-comments": 0, |
232
parse.js
@@ -12,3 +12,5 @@ const utils = require('./utils'); | ||
MediaPlaylist, | ||
Segment | ||
Segment, | ||
PartialSegment, | ||
RenditionReport | ||
} = require('./types'); | ||
@@ -39,2 +41,4 @@ | ||
case 'EXT-X-SCTE35': | ||
case 'EXT-X-PART': | ||
case 'EXT-X-PRELOAD-HINT': | ||
return 'Segment'; | ||
@@ -47,2 +51,6 @@ case 'EXT-X-TARGETDURATION': | ||
case 'EXT-X-I-FRAMES-ONLY': | ||
case 'EXT-X-SERVER-CONTROL': | ||
case 'EXT-X-PART-INF': | ||
case 'EXT-X-RENDITION-REPORT': | ||
case 'EXT-X-SKIP': | ||
return 'MediaPlaylist'; | ||
@@ -133,2 +141,5 @@ case 'EXT-X-MEDIA': | ||
case 'PRECISE': | ||
case 'CAN-BLOCK-RELOAD': | ||
case 'INDEPENDENT': | ||
case 'GAP': | ||
attributes[key] = val === 'YES'; | ||
@@ -142,2 +153,11 @@ break; | ||
case 'TIME-OFFSET': | ||
case 'CAN-SKIP-UNTIL': | ||
case 'HOLD-BACK': | ||
case 'PART-HOLD-BACK': | ||
case 'PART-TARGET': | ||
case 'BYTERANGE-START': | ||
case 'BYTERANGE-LENGTH': | ||
case 'LAST-MSN': | ||
case 'LAST-PART': | ||
case 'SKIPPED-SEGMENTS': | ||
attributes[key] = utils.toNumber(val); | ||
@@ -182,2 +202,8 @@ break; | ||
case 'EXT-X-START': | ||
case 'EXT-X-SERVER-CONTROL': | ||
case 'EXT-X-PART-INF': | ||
case 'EXT-X-PART': | ||
case 'EXT-X-PRELOAD-HINT': | ||
case 'EXT-X-RENDITION-REPORT': | ||
case 'EXT-X-SKIP': | ||
return [null, parseAttributeList(param)]; | ||
@@ -406,2 +432,4 @@ case 'EXTINF': | ||
const segment = new Segment({uri, mediaSequenceNumber, discontinuitySequence}); | ||
let mapHint = false; | ||
let partHint = false; | ||
for (let i = start; i <= end; i++) { | ||
@@ -424,4 +452,10 @@ const {name, value, attributes} = lines[i]; | ||
} else if (name === 'EXT-X-DISCONTINUITY') { | ||
if (segment.parts.length > 0) { | ||
utils.INVALIDPLAYLIST('EXT-X-DISCONTINUITY must appear before the first EXT-X-PART tag of the Parent Segment.'); | ||
} | ||
segment.discontinuity = true; | ||
} else if (name === 'EXT-X-KEY') { | ||
if (segment.parts.length > 0) { | ||
utils.INVALIDPLAYLIST('EXT-X-KEY must appear before the first EXT-X-PART tag of the Parent Segment.'); | ||
} | ||
setCompatibleVersionOfKey(params, attributes); | ||
@@ -436,2 +470,5 @@ segment.key = new Key({ | ||
} else if (name === 'EXT-X-MAP') { | ||
if (segment.parts.length > 0) { | ||
utils.INVALIDPLAYLIST('EXT-X-MAP must appear before the first EXT-X-PART tag of the Parent Segment.'); | ||
} | ||
if (params.compatibleVersion < 5) { | ||
@@ -485,2 +522,35 @@ params.compatibleVersion = 5; | ||
})); | ||
} else if (name === 'EXT-X-PRELOAD-HINT' && !attributes['TYPE']) { | ||
utils.INVALIDPLAYLIST('EXT-X-PRELOAD-HINT: TYPE attribute is mandatory'); | ||
} else if (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'PART' && partHint) { | ||
utils.INVALIDPLAYLIST('Servers should not add more than one EXT-X-PRELOAD-HINT tag with the same TYPE attribute to a Playlist.'); | ||
} else if ((name === 'EXT-X-PART' || name === 'EXT-X-PRELOAD-HINT') && !attributes['URI']) { | ||
utils.INVALIDPLAYLIST('EXT-X-PART / EXT-X-PRELOAD-HINT: URI attribute is mandatory'); | ||
} else if (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'MAP') { | ||
if (mapHint) { | ||
utils.INVALIDPLAYLIST('Servers should not add more than one EXT-X-PRELOAD-HINT tag with the same TYPE attribute to a Playlist.'); | ||
} | ||
mapHint = true; | ||
params.hasMap = true; | ||
segment.map = new MediaInitializationSection({ | ||
hint: true, | ||
uri: attributes['URI'], | ||
byterange: {length: attributes['BYTERANGE-LENGTH'], offset: attributes['BYTERANGE-START'] || 0} | ||
}); | ||
} else if (name === 'EXT-X-PART' || (name === 'EXT-X-PRELOAD-HINT' && attributes['TYPE'] === 'PART')) { | ||
if (name === 'EXT-X-PART' && !attributes['DURATION']) { | ||
utils.INVALIDPLAYLIST('EXT-X-PART: DURATION attribute is mandatory'); | ||
} | ||
if (name === 'EXT-X-PRELOAD-HINT') { | ||
partHint = true; | ||
} | ||
const partialSegment = new PartialSegment({ | ||
hint: (name === 'EXT-X-PRELOAD-HINT'), | ||
uri: attributes['URI'], | ||
byterange: (name === 'EXT-X-PART' ? attributes['BYTERANGE'] : {length: attributes['BYTERANGE-LENGTH'], offset: attributes['BYTERANGE-START'] || 0}), | ||
duration: attributes['DURATION'], | ||
independent: attributes['INDEPENDENT'], | ||
gap: attributes['GAP'] | ||
}); | ||
segment.parts.push(partialSegment); | ||
} | ||
@@ -499,2 +569,3 @@ } | ||
let currentMap = null; | ||
let containsParts = false; | ||
for (const [index, line] of lines.entries()) { | ||
@@ -554,2 +625,38 @@ const {name, value, attributes, category} = line; | ||
playlist.start = {offset: attributes['TIME-OFFSET'], precise: attributes['PRECISE'] || false}; | ||
} else if (name === 'EXT-X-SERVER-CONTROL') { | ||
if (!attributes['CAN-BLOCK-RELOAD']) { | ||
utils.INVALIDPLAYLIST('EXT-X-SERVER-CONTROL: CAN-BLOCK-RELOAD=YES is mandatory for Low-Latency HLS'); | ||
} | ||
playlist.lowLatencyCompatibility = { | ||
canBlockReload: attributes['CAN-BLOCK-RELOAD'], | ||
canSkipUntil: attributes['CAN-SKIP-UNTIL'], | ||
holdBack: attributes['HOLD-BACK'], | ||
partHoldBack: attributes['PART-HOLD-BACK'] | ||
}; | ||
} else if (name === 'EXT-X-PART-INF') { | ||
if (!attributes['PART-TARGET']) { | ||
utils.INVALIDPLAYLIST('EXT-X-PART-INF: PART-TARGET attribute is mandatory'); | ||
} | ||
playlist.partTargetDuration = attributes['PART-TARGET']; | ||
} else if (name === 'EXT-X-RENDITION-REPORT') { | ||
if (!attributes['URI']) { | ||
utils.INVALIDPLAYLIST('EXT-X-RENDITION-REPORT: URI attribute is mandatory'); | ||
} | ||
if (attributes['URI'].search(/^[a-z]+:/) === 0) { | ||
utils.INVALIDPLAYLIST('EXT-X-RENDITION-REPORT: URI must be relative to the playlist uri'); | ||
} | ||
playlist.renditionReports.push(new RenditionReport({ | ||
uri: attributes['URI'], | ||
lastMSN: attributes['LAST-MSN'], | ||
lastPart: attributes['LAST-PART'] | ||
})); | ||
} else if (name === 'EXT-X-SKIP') { | ||
if (!attributes['SKIPPED-SEGMENTS']) { | ||
utils.INVALIDPLAYLIST('EXT-X-SKIP: SKIPPED-SEGMENTS attribute is mandatory'); | ||
} | ||
if (params.compatibleVersion < 9) { | ||
params.compatibleVersion = 9; | ||
} | ||
playlist.skip = attributes['SKIPPED-SEGMENTS']; | ||
mediaSequence += playlist.skip; | ||
} else if (typeof line === 'string') { | ||
@@ -565,30 +672,6 @@ // uri | ||
if (segment) { | ||
const {discontinuity, key, map, byterange, uri} = segment; | ||
if (discontinuity) { | ||
segment.discontinuitySequence = ++discontinuitySequence; | ||
[discontinuitySequence, currentKey, currentMap] = addSegment(playlist, segment, discontinuitySequence, currentKey, currentMap); | ||
if (!containsParts && segment.parts.length > 0) { | ||
containsParts = true; | ||
} | ||
if (key) { | ||
currentKey = key; | ||
} else if (currentKey) { | ||
segment.key = currentKey; | ||
} | ||
if (map) { | ||
currentMap = map; | ||
} else if (currentMap) { | ||
segment.map = currentMap; | ||
} | ||
if (byterange && byterange.offset === -1) { | ||
const {segments} = playlist; | ||
if (segments.length > 0) { | ||
const prevSegment = segments[segments.length - 1]; | ||
if (prevSegment.byterange && prevSegment.uri === uri) { | ||
byterange.offset = prevSegment.byterange.offset + prevSegment.byterange.length; | ||
} else { | ||
utils.INVALIDPLAYLIST('If offset of EXT-X-BYTERANGE is not present, a previous Media Segment MUST be a sub-range of the same media resource'); | ||
} | ||
} else { | ||
utils.INVALIDPLAYLIST('If offset of EXT-X-BYTERANGE is not present, a previous Media Segment MUST appear in the Playlist file'); | ||
} | ||
} | ||
playlist.segments.push(segment); | ||
} | ||
@@ -598,6 +681,50 @@ segmentStart = -1; | ||
} | ||
if (segmentStart !== -1) { | ||
const segment = parseSegment(lines, '', segmentStart, lines.length - 1, mediaSequence++, discontinuitySequence, params); | ||
if (segment) { | ||
const {parts} = segment; | ||
if (parts.length > 0 && !playlist.endlist && !parts[parts.length - 1].hint) { | ||
utils.INVALIDPLAYLIST('If the Playlist contains EXT-X-PART tags and does not contain an EXT-X-ENDLIST tag, the Playlist must contain an EXT-X-PRELOAD-HINT tag with a TYPE=PART attribute'); | ||
} | ||
addSegment(playlist, segment, currentKey, currentMap); | ||
if (!containsParts && segment.parts.length > 0) { | ||
containsParts = true; | ||
} | ||
} | ||
} | ||
checkDateRange(playlist.segments); | ||
if (playlist.lowLatencyCompatibility) { | ||
checkLowLatencyCompatibility(playlist, containsParts); | ||
} | ||
return playlist; | ||
} | ||
function addSegment(playlist, segment, discontinuitySequence, currentKey, currentMap) { | ||
const {discontinuity, key, map, byterange, uri} = segment; | ||
if (discontinuity) { | ||
segment.discontinuitySequence = discontinuitySequence + 1; | ||
} | ||
if (!key) { | ||
segment.key = currentKey; | ||
} | ||
if (!map) { | ||
segment.map = currentMap; | ||
} | ||
if (byterange && byterange.offset === -1) { | ||
const {segments} = playlist; | ||
if (segments.length > 0) { | ||
const prevSegment = segments[segments.length - 1]; | ||
if (prevSegment.byterange && prevSegment.uri === uri) { | ||
byterange.offset = prevSegment.byterange.offset + prevSegment.byterange.length; | ||
} else { | ||
utils.INVALIDPLAYLIST('If offset of EXT-X-BYTERANGE is not present, a previous Media Segment MUST be a sub-range of the same media resource'); | ||
} | ||
} else { | ||
utils.INVALIDPLAYLIST('If offset of EXT-X-BYTERANGE is not present, a previous Media Segment MUST appear in the Playlist file'); | ||
} | ||
} | ||
playlist.segments.push(segment); | ||
return [segment.discontinuitySequence, segment.key, segment.map]; | ||
} | ||
function checkDateRange(segments) { | ||
@@ -648,2 +775,49 @@ const earliestDates = new Map(); | ||
function checkLowLatencyCompatibility({lowLatencyCompatibility, targetDuration, partTargetDuration, segments, renditionReports}, containsParts) { | ||
const {canSkipUntil, holdBack, partHoldBack} = lowLatencyCompatibility; | ||
if (canSkipUntil < targetDuration * 6) { | ||
utils.INVALIDPLAYLIST('The Skip Boundary must be at least six times the EXT-X-TARGETDURATION.'); | ||
} | ||
// Its value is a floating-point number of seconds and . | ||
if (holdBack < targetDuration * 3) { | ||
utils.INVALIDPLAYLIST('HOLD-BACK must be at least three times the EXT-X-TARGETDURATION.'); | ||
} | ||
if (containsParts) { | ||
if (partTargetDuration === undefined) { | ||
utils.INVALIDPLAYLIST('EXT-X-PART-INF is required if a Playlist contains one or more EXT-X-PART tags'); | ||
} | ||
if (partHoldBack === undefined) { | ||
utils.INVALIDPLAYLIST('EXT-X-PART: PART-HOLD-BACK attribute is mandatory'); | ||
} | ||
if (partHoldBack < partTargetDuration) { | ||
utils.INVALIDPLAYLIST('PART-HOLD-BACK must be at least PART-TARGET'); | ||
} | ||
for (const [segmentIndex, {parts}] of segments.entries()) { | ||
if (parts.length > 0 && segmentIndex < segments.length - 3) { | ||
utils.INVALIDPLAYLIST('Remove EXT-X-PART tags from the Playlist after they are greater than three target durations from the end of the Playlist.'); | ||
} | ||
for (const [partIndex, {duration}] of parts.entries()) { | ||
if (duration === undefined) { | ||
continue; | ||
} | ||
if (duration > partTargetDuration) { | ||
utils.INVALIDPLAYLIST('PART-TARGET is the maximum duration of any Partial Segment'); | ||
} | ||
if (partIndex < parts.length - 1 && duration < partTargetDuration * 0.85) { | ||
utils.INVALIDPLAYLIST('All Partial Segments except the last part of a segment must have a duration of at least 85% of PART-TARGET'); | ||
} | ||
} | ||
} | ||
} | ||
for (const report of renditionReports) { | ||
const lastSegment = segments[segments.length - 1]; | ||
if (!report.lastMSN) { | ||
report.lastMSN = lastSegment.mediaSequenceNumber; | ||
} | ||
if (!report.lastPart && lastSegment.parts.length > 0) { | ||
report.lastPart = lastSegment.parts.length - 1; | ||
} | ||
} | ||
} | ||
function CHECKTAGCATEGORY(category, params) { | ||
@@ -679,3 +853,3 @@ if (category === 'Segment' || category === 'MediaPlaylist') { | ||
} | ||
if (category === 'MediaPlaylist') { | ||
if (category === 'MediaPlaylist' && name !== 'EXT-X-RENDITION-REPORT') { | ||
if (params.hash[name]) { | ||
@@ -682,0 +856,0 @@ utils.INVALIDPLAYLIST('There MUST NOT be more than one Media Playlist tag of each type in any Media Playlist'); |
@@ -12,3 +12,3 @@ [![Build Status](https://travis-ci.org/kuu/hls-parser.svg?branch=master)](https://travis-ci.org/kuu/hls-parser) | ||
Provides synchronous functions to read/write HLS playlists (conforms to [the HLS spec rev.23](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23)) | ||
Provides synchronous functions to read/write HLS playlists (conforms to [the HLS spec rev.23](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23) and [the Apple Low-Latency Spec rev. 2020/02/05](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification)) | ||
@@ -39,16 +39,23 @@ ## Install | ||
new Segment({ | ||
uri: 'low/1.m3u8' | ||
duration: 9, | ||
mediaSequenceNumber: 0, | ||
discontinuitySequence: 0 | ||
uri: 'low/1.m3u8', | ||
duration: 9 | ||
}) | ||
] | ||
})); | ||
}); | ||
// Convert the object into a text | ||
const text = HLS.stringify(obj); | ||
HLS.stringify(obj); | ||
/* | ||
#EXTM3U | ||
#EXT-X-TARGETDURATION:9 | ||
#EXT-X-PLAYLIST-TYPE:VOD | ||
#EXTINF:9, | ||
low/1.m3u8 | ||
*/ | ||
``` | ||
## API | ||
### `HLS.parse(str)` | ||
Converts a text playlist into a structured JS object | ||
#### params | ||
@@ -58,2 +65,3 @@ | Name | Type | Required | Default | Description | | ||
| str | string | Yes | N/A | A text data that conforms to [the HLS playlist spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.1) | | ||
#### return value | ||
@@ -64,2 +72,3 @@ An instance of either `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.) | ||
Converts a JS object into a plain text playlist | ||
#### params | ||
@@ -69,2 +78,3 @@ | Name | Type | Required | Default | Description | | ||
| obj | `MasterPlaylist` or `MediaPlaylist` (See **Data format** below.) | Yes | N/A | An object returned by `HLS.parse()` or a manually created object | | ||
#### return value | ||
@@ -75,2 +85,3 @@ A text data that conforms to [the HLS playlist spec](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.1) | ||
Updates the option values | ||
#### params | ||
@@ -80,2 +91,3 @@ | Name | Type | Required | Default | Description | | ||
| obj | Object | Yes | {} | An object holding option values which will be used to overwrite the internal option values. | | ||
##### supported options | ||
@@ -89,2 +101,3 @@ | Name | Type | Default | Description | | ||
Retrieves the current option values | ||
#### return value | ||
@@ -105,3 +118,3 @@ A cloned object containing the current option values | ||
| ---------------- | ------------- | -------- | ------- | ------------- | | ||
| `type` | string | Yes | N/A | Either `playlist` or `segment`} | | ||
| `type` | string | Yes | N/A | Either `playlist` or `segment` or `part`} | | ||
@@ -177,2 +190,6 @@ ### `Playlist` (extends `Data`) | ||
| `segments` | [`Segment`] | No | [] | A list of available segments | | ||
| `lowLatencyCompatibility` | object ({canBlockReload: boolean, canSkipUntil: number, holdBack: number, partHoldBack: number}) | No | undefined | See `CAN-BLOCK-RELOAD`, `CAN-SKIP-UNTIL`, `HOLD-BACK`, and `PART-HOLD-BACK` attributes in [EXT-X-SERVER-CONTROL](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3281374) | | ||
| `partTargetDuration` | number | No* | undefined | *Required if the playlist contains one or more `EXT-X-PART` tags. See [EXT-X-PART-INF](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282434) | | ||
| `renditionReports` | [`RenditionReport`] | No | [] | Update status of the associated renditions | | ||
| `skip` | number | No | 0 | See [EXT-X-SKIP](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282433) | | ||
@@ -182,9 +199,9 @@ ### `Segment` (extends `Data`) | ||
| ----------------- | -------- | -------- | --------- | ------------- | | ||
| `uri` | string | Yes | N/A | URI of the media segment | | ||
| `duration` | number | Yes | N/A | See [EXTINF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.1) | | ||
| `uri` | string | Yes* | N/A | URI of the media segment. *Not required if the segment contains `EXT-X-PRELOAD-HINT` tag | | ||
| `duration` | number | Yes* | N/A | See [EXTINF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.1) *Not required if the segment contains `EXT-X-PRELOAD-HINT` tag | | ||
| `title` | string | No | undefined | See [EXTINF](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.1) | | ||
| `byterange` | object ({length: number, offset: number}) | No | undefined | See [EXT-X-BYTERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2) | | ||
| `discontinuity` | boolean | No | undefined | See [EXT-X-DISCONTINUITY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.3) | | ||
| `mediaSequenceNumber` | number | Yes | N/A | See the description about 'Media Sequence Number' in [3. Media Segments](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#page-5) | | ||
| `discontinuitySequence` | number | Yes | N/A | See the description about 'Discontinuity Sequence Number' in [6.2.1. General Server Responsibilities](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.1) | | ||
| `mediaSequenceNumber` | number | No | 0 | See the description about 'Media Sequence Number' in [3. Media Segments](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#page-5) | | ||
| `discontinuitySequence` | number | No | 0 | See the description about 'Discontinuity Sequence Number' in [6.2.1. General Server Responsibilities](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.1) | | ||
| `key` | `Key` | No | undefined | See [EXT-X-KEY](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.4) | | ||
@@ -194,4 +211,16 @@ | `map` | `MediaInitializationSection` | No | undefined | See [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) | | ||
| `dateRange` | `DateRange` | No | undefined | See [EXT-X-DATERANGE](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.7) | | ||
| `markers` | [`SpliceInfo`] | No | undefined | SCTE-35 messages associated with this segment| | ||
| `markers` | [`SpliceInfo`] | No | [] | SCTE-35 messages associated with this segment| | ||
| `parts` | [`PartialSegment`] | No | [] | Partial Segments that constitute this segment | | ||
### `PartialSegment` (extends `Data`) | ||
| Property | Type | Required | Default | Description | | ||
| ----------------- | -------- | -------- | --------- | ------------- | | ||
| `hint` | boolean | No | false | `true` indicates a hinted resource (`TYPE=PART`) See [EXT-X-PRELOAD-HINT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3526694) | | ||
| `uri` | string | Yes | N/A | See `URI` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) | | ||
| `duration` | number | Yes* | N/A | See `DURATION` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) *Not required if `hint` is `true`| | ||
| `independent` | boolean | No | undefined | See `INDEPENDENT` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) | | ||
| `byterange` | object ({length: number, offset: number}) | No | undefined | See `BYTERANGE` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) | | ||
| `gap` | boolean | No | undefined | See `GAP` attribute in [EXT-X-PART](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282436) | | ||
### `Key` | ||
@@ -209,2 +238,3 @@ | Property | Type | Required | Default | Description | | ||
| ----------------- | -------- | -------- | --------- | ------------- | | ||
| `hint` | boolean | No | false | `true` indicates a hinted resource (`TYPE=MAP`) See [EXT-X-PRELOAD-HINT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3526694) | | ||
| `uri` | string | Yes | N/A | See URI attribute in [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) | | ||
@@ -234,1 +264,8 @@ | `byterange` | object ({length: number, offset: number}) | No | undefined | See BYTERANGE attribute in [EXT-X-MAP](https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5) | | ||
| `value` | string | No | undefined | Holds a raw (string) value for the unsupported tag. | | ||
### `RenditionReport` | ||
| Property | Type | Required | Default | Description | | ||
| ----------------- | -------- | -------- | --------- | ------------- | | ||
| `uri` | string | Yes | N/A | See `URI` attribute in [EXT-X-RENDITION-REPORT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282435) | | ||
| `lastMSN` | number | No | undefined | See `LAST-MSN` attribute in [EXT-X-RENDITION-REPORT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282435) | | ||
| `lastPart` | number | No | undefined | See `LAST-PART` attribute in [EXT-X-RENDITION-REPORT](https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification#3282435) | |
@@ -48,6 +48,19 @@ const utils = require('./utils'); | ||
function buildDecimalFloatingNumber(num, fixed) { | ||
const rounded = Math.round(num * 1000) / 1000; | ||
let roundFactor = 1000; | ||
if (fixed) { | ||
roundFactor = 10 ** fixed; | ||
} | ||
const rounded = Math.round(num * roundFactor) / roundFactor; | ||
return fixed ? rounded.toFixed(fixed) : rounded; | ||
} | ||
function getNumberOfDecimalPlaces(num) { | ||
const str = num.toString(10); | ||
const index = str.indexOf('.'); | ||
if (index === -1) { | ||
return 0; | ||
} | ||
return str.length - index - 1; | ||
} | ||
function buildMasterPlaylist(lines, playlist) { | ||
@@ -192,2 +205,20 @@ for (const sessionData of playlist.sessionDataList) { | ||
} | ||
if (playlist.lowLatencyCompatibility) { | ||
const {canBlockReload, canSkipUntil, holdBack, partHoldBack} = playlist.lowLatencyCompatibility; | ||
const params = []; | ||
params.push(`CAN-BLOCK-RELOAD=${canBlockReload ? 'YES' : 'NO'}`); | ||
if (canSkipUntil !== undefined) { | ||
params.push(`CAN-SKIP-UNTIL=${canSkipUntil}`); | ||
} | ||
if (holdBack !== undefined) { | ||
params.push(`HOLD-BACK=${holdBack}`); | ||
} | ||
if (partHoldBack !== undefined) { | ||
params.push(`PART-HOLD-BACK=${partHoldBack}`); | ||
} | ||
lines.push(`#EXT-X-SERVER-CONTROL:${params.join(',')}`); | ||
} | ||
if (playlist.partTargetDuration) { | ||
lines.push(`#EXT-X-PART-INF:PART-TARGET=${playlist.partTargetDuration}`); | ||
} | ||
if (playlist.mediaSequenceBase) { | ||
@@ -205,2 +236,5 @@ lines.push(`#EXT-X-MEDIA-SEQUENCE:${playlist.mediaSequenceBase}`); | ||
} | ||
if (playlist.skip > 0) { | ||
lines.push(`#EXT-X-SKIP:SKIPPED-SEGMENTS=${playlist.skip}`); | ||
} | ||
for (const segment of playlist.segments) { | ||
@@ -212,5 +246,15 @@ buildSegment(lines, segment, playlist.version); | ||
} | ||
for (const report of playlist.renditionReports) { | ||
const params = []; | ||
params.push(`URI="${report.uri}"`); | ||
params.push(`LAST-MSN=${report.lastMSN}`); | ||
if (report.lastPart !== undefined) { | ||
params.push(`LAST-PART=${report.lastPart}`); | ||
} | ||
lines.push(`#EXT-X-RENDITION-REPORT:${params.join(',')}`); | ||
} | ||
} | ||
function buildSegment(lines, segment, version = 1) { | ||
let hint = false; | ||
if (segment.byterange) { | ||
@@ -237,3 +281,9 @@ lines.push(`#EXT-X-BYTERANGE:${buildByteRange(segment.byterange)}`); | ||
} | ||
const duration = version < 3 ? Math.round(segment.duration) : buildDecimalFloatingNumber(segment.duration); | ||
if (segment.parts.length > 0) { | ||
hint = buildParts(lines, segment.parts); | ||
} | ||
if (hint) { | ||
return; | ||
} | ||
const duration = version < 3 ? Math.round(segment.duration) : buildDecimalFloatingNumber(segment.duration, getNumberOfDecimalPlaces(segment.duration)); | ||
lines.push(`#EXTINF:${duration},${unescape(encodeURIComponent(segment.title || ''))}`); | ||
@@ -251,4 +301,4 @@ Array.prototype.push.call(lines, `${segment.uri}`); // URIs could be redundant when EXT-X-BYTERANGE is used | ||
function buildByteRange(byterange) { | ||
return `${byterange.length}@${byterange.offset}`; | ||
function buildByteRange({offset, length}) { | ||
return `${length}@${offset}`; | ||
} | ||
@@ -303,2 +353,37 @@ | ||
function buildParts(lines, parts) { | ||
let hint = false; | ||
for (const part of parts) { | ||
if (part.hint) { | ||
const params = []; | ||
params.push('TYPE=PART'); | ||
params.push(`URI="${part.uri}"`); | ||
if (part.byterange) { | ||
const {offset, length} = part.byterange; | ||
params.push(`BYTERANGE-START=${offset}`); | ||
if (length) { | ||
params.push(`BYTERANGE-LENGTH=${length}`); | ||
} | ||
} | ||
lines.push(`#EXT-X-PRELOAD-HINT:${params.join(',')}`); | ||
hint = true; | ||
} else { | ||
const params = []; | ||
params.push(`DURATION=${part.duration}`); | ||
params.push(`URI="${part.uri}"`); | ||
if (part.byterange) { | ||
params.push(`BYTERANGE=${buildByteRange(part.byterange)}`); | ||
} | ||
if (part.independent) { | ||
params.push('INDEPENDENT=YES'); | ||
} | ||
if (part.gap) { | ||
params.push('GAP=YES'); | ||
} | ||
lines.push(`#EXT-X-PART:${params.join(',')}`); | ||
} | ||
} | ||
return hint; | ||
} | ||
function stringify(playlist) { | ||
@@ -305,0 +390,0 @@ utils.PARAMCHECK(playlist); |
62
types.js
@@ -19,3 +19,3 @@ const utils = require('./utils'); | ||
utils.PARAMCHECK(type, groupId, name); | ||
utils.CONDITIONALASSERT([type === 'SUBTITLES', uri], [type === 'CLOSED-CAPTIONS', instreamId], [type === 'CLOSED-CAPTIONS', !uri], [forced, type === 'CLOSED-CAPTIONS']); | ||
utils.CONDITIONALASSERT([type === 'SUBTITLES', uri], [type === 'CLOSED-CAPTIONS', instreamId], [type === 'CLOSED-CAPTIONS', !uri], [forced, type === 'SUBTITLES']); | ||
this.type = type; | ||
@@ -107,2 +107,3 @@ this.uri = uri; | ||
constructor({ | ||
hint = false, | ||
uri, // required | ||
@@ -113,2 +114,3 @@ mimeType, | ||
utils.PARAMCHECK(uri); | ||
this.hint = hint; | ||
this.uri = uri; | ||
@@ -218,2 +220,6 @@ this.mimeType = mimeType; | ||
segments = [], | ||
lowLatencyCompatibility, | ||
partTargetDuration, | ||
renditionReports = [], | ||
skip = 0, | ||
hash | ||
@@ -228,2 +234,6 @@ } = params; | ||
this.segments = segments; | ||
this.lowLatencyCompatibility = lowLatencyCompatibility; | ||
this.partTargetDuration = partTargetDuration; | ||
this.renditionReports = renditionReports; | ||
this.skip = skip; | ||
this.hash = hash; | ||
@@ -235,3 +245,3 @@ } | ||
constructor({ | ||
uri, // required | ||
uri, | ||
mimeType, | ||
@@ -243,4 +253,4 @@ data, | ||
discontinuity, | ||
mediaSequenceNumber, | ||
discontinuitySequence, | ||
mediaSequenceNumber = 0, | ||
discontinuitySequence = 0, | ||
key, | ||
@@ -250,6 +260,7 @@ map, | ||
dateRange, | ||
markers = [] | ||
markers = [], | ||
parts = [] | ||
}) { | ||
super('segment'); | ||
utils.PARAMCHECK(uri, mediaSequenceNumber, discontinuitySequence); | ||
// utils.PARAMCHECK(uri, mediaSequenceNumber, discontinuitySequence); | ||
this.uri = uri; | ||
@@ -269,5 +280,40 @@ this.mimeType = mimeType; | ||
this.markers = markers; | ||
this.parts = parts; | ||
} | ||
} | ||
class PartialSegment extends Data { | ||
constructor({ | ||
hint = false, | ||
uri, // required | ||
duration, | ||
independent, | ||
byterange, | ||
gap | ||
}) { | ||
super('part'); | ||
utils.PARAMCHECK(uri); | ||
this.hint = hint; | ||
this.uri = uri; | ||
this.duration = duration; | ||
this.independent = independent; | ||
this.duration = duration; | ||
this.byterange = byterange; | ||
this.gap = gap; | ||
} | ||
} | ||
class RenditionReport { | ||
constructor({ | ||
uri, // required | ||
lastMSN, | ||
lastPart | ||
}) { | ||
utils.PARAMCHECK(uri); | ||
this.uri = uri; | ||
this.lastMSN = lastMSN; | ||
this.lastPart = lastPart; | ||
} | ||
} | ||
module.exports = { | ||
@@ -284,3 +330,5 @@ Rendition, | ||
MediaPlaylist, | ||
Segment | ||
Segment, | ||
PartialSegment, | ||
RenditionReport | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
689582
6221
258