Skip to content

Commit

Permalink
feat: Add EXT-X-PART support behind a flag for LL-HLS (#1055)
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonocasey authored Mar 19, 2021
1 parent 87947fc commit b33e109
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 65 deletions.
4 changes: 4 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ <h3>Options</h3>
<input id=partial type="checkbox">
Handle Partial (reloads player)
</label>
<label>
<input id=llhls type="checkbox">
[EXPERIMENTAL] Enables support for ll-hls (reloads player)
</label>
<label>
<input id=buffer-water type="checkbox">
[EXPERIMENTAL] Use Buffer Level for ABR (reloads player)
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"@videojs/vhs-utils": "^3.0.0",
"aes-decrypter": "3.1.2",
"global": "^4.4.0",
"m3u8-parser": "4.5.2",
"m3u8-parser": "4.6.0",
"mpd-parser": "0.15.4",
"mux.js": "5.10.0",
"video.js": "^6 || ^7"
Expand Down
11 changes: 10 additions & 1 deletion scripts/index-demo-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@
'sync-workers',
'liveui',
'partial',
'llhls',
'url',
'type',
'keysystems',
Expand Down Expand Up @@ -300,6 +301,13 @@
window.videojs.log.level(event.target.checked ? 'debug' : 'info');
});

stateEls.llhls.addEventListener('change', function(event) {
saveState();

// reload the player and scripts
stateEls.minified.dispatchEvent(newEvent('change'));
});

stateEls.partial.addEventListener('change', function(event) {
saveState();

Expand Down Expand Up @@ -377,7 +385,8 @@
vhs: {
overrideNative: getInputValue(stateEls['override-native']),
handlePartialData: getInputValue(stateEls.partial),
experimentalBufferBasedABR: getInputValue(stateEls['buffer-water'])
experimentalBufferBasedABR: getInputValue(stateEls['buffer-water']),
experimentalLLHLS: getInputValue(stateEls.llhls)
}
}
});
Expand Down
42 changes: 39 additions & 3 deletions src/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ export const createPlaylistID = (index, uri) => {
/**
* Parses a given m3u8 playlist
*
* @param {Function} [onwarn]
* a function to call when the parser triggers a warning event.
* @param {Function} [oninfo]
* a function to call when the parser triggers an info event.
* @param {string} manifestString
* The downloaded manifest string
* @param {Object[]} [customTagParsers]
* An array of custom tag parsers for the m3u8-parser instance
* @param {Object[]} [customTagMappers]
* An array of custom tag mappers for the m3u8-parser instance
* An array of custom tag mappers for the m3u8-parser instance
* @param {boolean} [experimentalLLHLS=false]
* Whether to keep ll-hls features in the manifest after parsing.
* @return {Object}
* The manifest object
*/
Expand All @@ -26,7 +32,8 @@ export const parseManifest = ({
oninfo,
manifestString,
customTagParsers = [],
customTagMappers = []
customTagMappers = [],
experimentalLLHLS
}) => {
const parser = new M3u8Parser();

Expand All @@ -43,7 +50,36 @@ export const parseManifest = ({
parser.push(manifestString);
parser.end();

return parser.manifest;
const manifest = parser.manifest;

// remove llhls features from the parsed manifest
// if we don't want llhls support.
if (!experimentalLLHLS) {
[
'preloadSegment',
'skip',
'serverControl',
'renditionReports',
'partInf',
'partTargetDuration'
].forEach(function(k) {
if (manifest.hasOwnProperty(k)) {
delete manifest[k];
}
});

if (manifest.segments) {
manifest.segments.forEach(function(segment) {
['parts', 'preloadHints'].forEach(function(k) {
if (segment.hasOwnProperty(k)) {
delete segment[k];
}
});
});
}
}

return manifest;
};

/**
Expand Down
2 changes: 1 addition & 1 deletion src/media-segment-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,7 @@ export const mediaSegmentRequest = ({
}

const segmentRequestOptions = videojs.mergeOptions(xhrOptions, {
uri: segment.resolvedUri,
uri: segment.part && segment.part.resolvedUri || segment.resolvedUri,
responseType: 'arraybuffer',
headers: segmentXhrHeaders(segment)
});
Expand Down
95 changes: 77 additions & 18 deletions src/playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,34 +19,73 @@ import {
const { mergeOptions, EventTarget } = videojs;

/**
* Returns a new array of segments that is the result of merging
* properties from an older list of segments onto an updated
* list. No properties on the updated playlist will be overridden.
*
* @param {Array} original the outdated list of segments
* @param {Array} update the updated list of segments
* @param {number=} offset the index of the first update
* segment in the original segment list. For non-live playlists,
* this should always be zero and does not need to be
* specified. For live playlists, it should be the difference
* between the media sequence numbers in the original and updated
* playlists.
* @return a list of merged segment objects
*/
* Returns a new segment object with properties and
* the parts array merged.
*
* @param {Object} a the old segment
* @param {Object} b the new segment
*
* @return {Object} the merged segment
*/
export const updateSegment = (a, b) => {
if (!a) {
return b;
}

const result = mergeOptions(a, b);

// if only the old segment has parts
// then the parts are no longer valid
if (a.parts && !b.parts) {
delete result.parts;
// if both segments have parts
// copy part propeties from the old segment
// to the new one.
} else if (a.parts && b.parts) {
for (let i = 0; i < b.parts.length; i++) {
if (a.parts && a.parts[i]) {
result.parts[i] = mergeOptions(a.parts[i], b.parts[i]);
}
}
}

return result;
};

/**
* Returns a new array of segments that is the result of merging
* properties from an older list of segments onto an updated
* list. No properties on the updated playlist will be ovewritten.
*
* @param {Array} original the outdated list of segments
* @param {Array} update the updated list of segments
* @param {number=} offset the index of the first update
* segment in the original segment list. For non-live playlists,
* this should always be zero and does not need to be
* specified. For live playlists, it should be the difference
* between the media sequence numbers in the original and updated
* playlists.
* @return {Array} a list of merged segment objects
*/
export const updateSegments = (original, update, offset) => {
const oldSegments = original.slice();
const result = update.slice();

offset = offset || 0;
const length = Math.min(original.length, update.length + offset);

for (let i = offset; i < length; i++) {
result[i - offset] = mergeOptions(original[i], result[i - offset]);
const newIndex = i - offset;

result[newIndex] = updateSegment(oldSegments[i], result[newIndex]);
}
return result;
};

export const resolveSegmentUris = (segment, baseUri) => {
if (!segment.resolvedUri) {
// preloadSegments will not have a uri at all
// as the segment isn't actually in the manifest yet, only parts
if (!segment.resolvedUri && segment.uri) {
segment.resolvedUri = resolveUrl(baseUri, segment.uri);
}
if (segment.key && !segment.key.resolvedUri) {
Expand All @@ -55,6 +94,23 @@ export const resolveSegmentUris = (segment, baseUri) => {
if (segment.map && !segment.map.resolvedUri) {
segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri);
}
if (segment.parts && segment.parts.length) {
segment.parts.forEach((p) => {
if (p.resolvedUri) {
return;
}
p.resolvedUri = resolveUrl(baseUri, p.uri);
});
}

if (segment.preloadHints && segment.preloadHints.length) {
segment.preloadHints.forEach((p) => {
if (p.resolvedUri) {
return;
}
p.resolvedUri = resolveUrl(baseUri, p.uri);
});
}
};

// consider the playlist unchanged if the playlist object is the same or
Expand Down Expand Up @@ -173,6 +229,7 @@ export default class PlaylistLoader extends EventTarget {

this.customTagParsers = (vhsOptions && vhsOptions.customTagParsers) || [];
this.customTagMappers = (vhsOptions && vhsOptions.customTagMappers) || [];
this.experimentalLLHLS = (vhsOptions && vhsOptions.experimentalLLHLS) || false;

// initialize the loader state
this.state = 'HAVE_NOTHING';
Expand Down Expand Up @@ -254,7 +311,8 @@ export default class PlaylistLoader extends EventTarget {
oninfo: ({message}) => this.logger_(`m3u8-parser info for ${id}: ${message}`),
manifestString: playlistString,
customTagParsers: this.customTagParsers,
customTagMappers: this.customTagMappers
customTagMappers: this.customTagMappers,
experimentalLLHLS: this.experimentalLLHLS
});

playlist.lastRequest = Date.now();
Expand Down Expand Up @@ -562,7 +620,8 @@ export default class PlaylistLoader extends EventTarget {
const manifest = parseManifest({
manifestString: req.responseText,
customTagParsers: this.customTagParsers,
customTagMappers: this.customTagMappers
customTagMappers: this.customTagMappers,
llhls: this.llhls
});

this.setupInitialPlaylist(manifest);
Expand Down
Loading

0 comments on commit b33e109

Please sign in to comment.