-
Notifications
You must be signed in to change notification settings - Fork 100
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
feat: Add llhls preloadSegment, add parts to segments, and fix byterange for parts/preloadHints #137
Changes from 7 commits
62a4cbd
d179d9c
cb66a5f
1701757
739e2e0
4c9e603
77f6138
8c9b3ea
b18ce3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,23 @@ import Stream from '@videojs/vhs-utils/es/stream.js'; | |
|
||
const TAB = String.fromCharCode(0x09); | ||
|
||
const parseByterange = function(byterangeString) { | ||
// 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)* | ||
|
@@ -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; | ||
} | ||
|
@@ -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
|
||
} | ||
} | ||
|
||
|
@@ -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); | ||
|
@@ -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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is a bit different as we have attributes for both |
||
|
||
event.attributes.byterange = event.attributes.byterange || {}; | ||
event.attributes.byterange[subkey] = event.attributes[key]; | ||
} | ||
}); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,6 +92,7 @@ export default class Parser extends Stream { | |
let currentMap; | ||
// if specified, the active decryption key | ||
let key; | ||
let hasParts = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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': {}, | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. build preload segment if we get |
||
}); | ||
|
||
// update the manifest with the m3u8 entry from the parse stream | ||
this.parseStream.on('data', function(entry) { | ||
|
@@ -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) { | ||
|
@@ -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(', ')}` | ||
}); | ||
} | ||
|
||
|
@@ -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 = []; | ||
|
||
|
@@ -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 || []; | ||
|
@@ -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'); | ||
} | ||
|
||
|
@@ -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 = {}; | ||
}, | ||
|
@@ -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 | ||
|
There was a problem hiding this comment.
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
, andpart