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: use EXT-X-PART for LL-HLS #1055

Merged
merged 12 commits into from
Mar 19, 2021
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
brandonocasey marked this conversation as resolved.
Show resolved Hide resolved
// 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,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we don't actually use uri anywhere 🤷 but for completeness sake I changed it.

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