Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add llhls preloadSegment, add parts to segments, and fix byterange for parts/preloadHints #137

Merged
merged 9 commits into from
Mar 4, 2021
Merged
59 changes: 37 additions & 22 deletions src/parse-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ import Stream from '@videojs/vhs-utils/es/stream.js';

const TAB = String.fromCharCode(0x09);

const parseByterange = function(byterangeString) {
Copy link
Contributor Author

@brandonocasey brandonocasey Feb 5, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved all byterange parsing into here as we do this for map, byterange, and part

// optinally match and capture 0+ digits before `@`
// optinally match and capture 0+ digits after `@`
brandonocasey marked this conversation as resolved.
Show resolved Hide resolved
const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || '');
brandonocasey marked this conversation as resolved.
Show resolved Hide resolved
const result = {};

if (match[1]) {
result.length = parseInt(match[1], 10);
}

if (match[2]) {
result.offset = parseInt(match[2], 10);
}

return result;
};

/**
* "forgiving" attribute list psuedo-grammar:
* attributes -> keyvalue (',' keyvalue)*
Expand Down Expand Up @@ -222,18 +239,17 @@ export default class ParseStream extends Stream {
this.trigger('data', event);
return;
}
match = (/^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/).exec(newLine);
match = (/^#EXT-X-BYTERANGE:?(.*)?$/).exec(newLine);
if (match) {
event = {
type: 'tag',
tagType: 'byterange'
};
if (match[1]) {
event.length = parseInt(match[1], 10);
}
if (match[2]) {
event.offset = parseInt(match[2], 10);
}
const result = parseByterange(match[1]);

Object.keys(result).forEach(function(k) {
event[k] = result[k];
});
brandonocasey marked this conversation as resolved.
Show resolved Hide resolved
this.trigger('data', event);
return;
}
Expand Down Expand Up @@ -263,15 +279,12 @@ export default class ParseStream extends Stream {
event.uri = attributes.URI;
}
if (attributes.BYTERANGE) {
const [length, offset] = attributes.BYTERANGE.split('@');
const result = parseByterange(attributes.BYTERANGE);

event.byterange = {};
if (length) {
event.byterange.length = parseInt(length, 10);
}
if (offset) {
event.byterange.offset = parseInt(offset, 10);
}
Object.keys(result).forEach(function(k) {
event.byterange[k] = result[k];
});
brandonocasey marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -472,15 +485,12 @@ export default class ParseStream extends Stream {
});

if (event.attributes.hasOwnProperty('BYTERANGE')) {
const [length, offset] = event.attributes.BYTERANGE.split('@');
const result = parseByterange(event.attributes.BYTERANGE);

event.byterange = {};
if (length) {
event.byterange.length = parseInt(length, 10);
}
if (offset) {
event.byterange.offset = parseInt(offset, 10);
}
event.attributes.byterange = {};
Object.keys(result).forEach(function(k) {
event.attributes.byterange[k] = result[k];
});
brandonocasey marked this conversation as resolved.
Show resolved Hide resolved
}

this.trigger('data', event);
Expand Down Expand Up @@ -535,6 +545,11 @@ export default class ParseStream extends Stream {
['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function(key) {
if (event.attributes.hasOwnProperty(key)) {
event.attributes[key] = parseInt(event.attributes[key], 10);

const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is a bit different as we have attributes for both offest and length


event.attributes.byterange = event.attributes.byterange || {};
event.attributes.byterange[subkey] = event.attributes[key];
}
});

Expand Down
92 changes: 83 additions & 9 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export default class Parser extends Stream {
let currentMap;
// if specified, the active decryption key
let key;
let hasParts = false;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this to keep renditionReport warnings firing if we get a rendition report before any parts.

const noop = function() {};
const defaultMediaGroups = {
'AUDIO': {},
Expand All @@ -115,6 +116,29 @@ export default class Parser extends Stream {
// to provide the offset, in which case it defaults to the next byte after the
// previous segment
let lastByterangeEnd = 0;
// keep track of the last seen part's byte range end.
let lastPartByterangeEnd = 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep track of the current part byterange


this.on('end', () => {
// only add preloadSegment if we don't yet have a uri for it.
// and we actually have parts/preloadHints
if (currentUri.uri || (!currentUri.parts && !currentUri.preloadHints)) {
return;
}
if (!currentUri.map && currentMap) {
currentUri.map = currentMap;
}

if (!currentUri.key && key) {
currentUri.key = key;
}

if (!currentUri.timeline && typeof currentTimeline === 'number') {
currentUri.timeline = currentTimeline;
}

this.manifest.preloadSegment = currentUri;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build preload segment if we get end without getting an actual segment uri.

});

// update the manifest with the m3u8 entry from the parse stream
this.parseStream.on('data', function(entry) {
Expand Down Expand Up @@ -441,8 +465,22 @@ export default class Parser extends Stream {
}
},
'part'() {
this.manifest.parts = this.manifest.parts || [];
this.manifest.parts.push(entry.attributes);
hasParts = true;
// parts are always specifed before a segment
const segmentIndex = this.manifest.segments.length;

currentUri.parts = currentUri.parts || [];
currentUri.parts.push(entry.attributes);

if (entry.attributes.byterange) {
const byterange = entry.attributes.byterange;

if (!byterange.hasOwnProperty('offset')) {
byterange.offset = lastPartByterangeEnd;
}
lastPartByterangeEnd = byterange.offset + byterange.length;
}

const missingAttributes = [];

['URI', 'DURATION'].forEach(function(k) {
Expand All @@ -452,10 +490,10 @@ export default class Parser extends Stream {
});

if (missingAttributes.length) {
const index = this.manifest.parts.length - 1;
const partIndex = currentUri.parts.length - 1;

this.trigger('warn', {
message: `#EXT-X-PART #${index} lacks required attribute(s): ${missingAttributes.join(', ')}`
message: `#EXT-X-PART #${partIndex} for segment #${segmentIndex} lacks required attribute(s): ${missingAttributes.join(', ')}`
});
}

Expand Down Expand Up @@ -489,8 +527,25 @@ export default class Parser extends Stream {
}
},
'preload-hint'() {
this.manifest.preloadHints = this.manifest.preloadHints || [];
this.manifest.preloadHints.push(entry.attributes);
// parts are always specifed before a segment
const segmentIndex = this.manifest.segments.length;

currentUri.preloadHints = currentUri.preloadHints || [];
currentUri.preloadHints.push(entry.attributes);

if (entry.attributes.byterange) {
const byterange = entry.attributes.byterange;

if (!byterange.hasOwnProperty('offset')) {
byterange.offset = 0;
if (entry.attributes.TYPE && entry.attributes.TYPE === 'PART') {
// use last segment byterange end if we are the first part.
// otherwise use lastPartByterangeEnd
byterange.offset = lastPartByterangeEnd;
lastPartByterangeEnd = byterange.offset + byterange.length;
}
}
}

const missingAttributes = [];

Expand All @@ -499,14 +554,28 @@ export default class Parser extends Stream {
missingAttributes.push(k);
}
});
const index = currentUri.preloadHints.length - 1;

if (missingAttributes.length) {
const index = this.manifest.preloadHints.length - 1;

this.trigger('warn', {
message: `#EXT-X-PRELOAD-HINT #${index} lacks required attribute(s): ${missingAttributes.join(', ')}`
message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} lacks required attribute(s): ${missingAttributes.join(', ')}`
brandonocasey marked this conversation as resolved.
Show resolved Hide resolved
});
}

if (entry.attributes.TYPE) {
// search through all preload hints except for the current one for
// a duplicate type.
for (let i = 0; i < currentUri.preloadHints.length - 1; i++) {
const hint = currentUri.preloadHints[i];

if (hint.TYPE && hint.TYPE === entry.attributes.TYPE) {
this.trigger('warn', {
message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${entry.attributes.TYPE} as preload hint #${i}`
});
}
}
}
},
'rendition-report'() {
this.manifest.renditionReports = this.manifest.renditionReports || [];
Expand All @@ -521,7 +590,7 @@ export default class Parser extends Stream {
}
});

if (this.manifest.parts && !entry.attributes['LAST-PART']) {
if (hasParts && !entry.attributes['LAST-PART']) {
missingAttributes.push('LAST-PART');
}

Expand Down Expand Up @@ -565,6 +634,9 @@ export default class Parser extends Stream {
currentUri.map = currentMap;
}

// reset the last byterange end as it needs to be 0 between parts
lastPartByterangeEnd = 0;

// prepare for the next URI
currentUri = {};
},
Expand Down Expand Up @@ -603,6 +675,8 @@ export default class Parser extends Stream {
end() {
// flush any buffered input
this.lineStream.push('\n');

this.trigger('end');
}
/**
* Add an additional parser for non-standard tags
Expand Down
Loading