Skip to content

Commit

Permalink
feat(llhls): preloadSegment, associate parts/preloadHints with segmen…
Browse files Browse the repository at this point in the history
…ts, unify byterange handling (videojs#137)
  • Loading branch information
brandonocasey authored Mar 4, 2021
1 parent 9af651c commit 2ab2db2
Show file tree
Hide file tree
Showing 11 changed files with 934 additions and 294 deletions.
54 changes: 27 additions & 27 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) {
// optionally match and capture 0+ digits before `@`
// optionally match and capture 0+ digits after `@`
const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || '');
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,12 @@ 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 = {
event = Object.assign(parseByterange(match[1]), {
type: 'tag',
tagType: 'byterange'
};
if (match[1]) {
event.length = parseInt(match[1], 10);
}
if (match[2]) {
event.offset = parseInt(match[2], 10);
}
});
this.trigger('data', event);
return;
}
Expand Down Expand Up @@ -263,15 +274,7 @@ export default class ParseStream extends Stream {
event.uri = attributes.URI;
}
if (attributes.BYTERANGE) {
const [length, offset] = attributes.BYTERANGE.split('@');

event.byterange = {};
if (length) {
event.byterange.length = parseInt(length, 10);
}
if (offset) {
event.byterange.offset = parseInt(offset, 10);
}
event.byterange = parseByterange(attributes.BYTERANGE);
}
}

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

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

event.byterange = {};
if (length) {
event.byterange.length = parseInt(length, 10);
}
if (offset) {
event.byterange.offset = parseInt(offset, 10);
}
event.attributes.byterange = parseByterange(event.attributes.BYTERANGE);
}

this.trigger('data', event);
Expand Down Expand Up @@ -535,6 +530,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';

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;
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;

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;
});

// 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(', ')}`
});
}

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

0 comments on commit 2ab2db2

Please sign in to comment.