Skip to content

Commit

Permalink
feat(llhls): serverControl, preloadSegment, and partTargetDuration
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonocasey committed Mar 12, 2021
1 parent a8957ff commit 37ae651
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 75 deletions.
14 changes: 11 additions & 3 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1299,8 +1299,12 @@ export class MasterPlaylistController extends videojs.EventTarget {
return;
}

const suggestedPresentationDelay = this.masterPlaylistLoader_.master.suggestedPresentationDelay;
const mainSeekable = Vhs.Playlist.seekable(media, expired, suggestedPresentationDelay);
const master = this.masterPlaylistLoader_.master;
const mainSeekable = Vhs.Playlist.seekable(
media,
expired,
Vhs.Playlist.getHoldBack(master, media)
);

if (mainSeekable.length === 0) {
return;
Expand All @@ -1314,7 +1318,11 @@ export class MasterPlaylistController extends videojs.EventTarget {
return;
}

audioSeekable = Vhs.Playlist.seekable(media, expired, suggestedPresentationDelay);
audioSeekable = Vhs.Playlist.seekable(
media,
expired,
Vhs.Playlist.getHoldBack(master, media)
);

if (audioSeekable.length === 0) {
return;
Expand Down
47 changes: 30 additions & 17 deletions src/playlist-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const updateSegments = (original, update, offset) => {
};

export const resolveSegmentUris = (segment, baseUri) => {
// preloadSegments will not have a uri at all
// preloadSegment 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);
Expand Down Expand Up @@ -147,6 +147,15 @@ export const updateMaster = (master, media, unchangedCheck = isPlaylistUnchanged

const mergedPlaylist = mergeOptions(playlist, media);

media.segments = media.segments || [];

// a preloadSegment with only preloadHints is not currently
// a usable segment, only include a preloadSegment that has
// parts.
if (media.preloadSegment && media.preloadSegment.parts) {
media.segments.push(media.preloadSegment);
}

// if the update could overlap existing segment information, merge the two segment lists
if (playlist.segments) {
mergedPlaylist.segments = updateSegments(
Expand Down Expand Up @@ -188,16 +197,16 @@ export const updateMaster = (master, media, unchangedCheck = isPlaylistUnchanged
*/
export const refreshDelay = (media, update) => {
const lastSegment = media.segments[media.segments.length - 1];
let delay;

if (update && lastSegment && lastSegment.duration) {
delay = lastSegment.duration * 1000;
} else {
// if the playlist is unchanged since the last reload or last segment duration
// cannot be determined, try again after half the target duration
delay = (media.targetDuration || 10) * 500;
const lastPart = lastSegment && lastSegment.parts && lastSegment.parts[lastSegment.parts - 1];
const lastDuration = lastPart && lastPart.duration || lastSegment && lastSegment.duration;

if (update && lastDuration) {
return lastDuration * 1000;
}
return delay;

// if the playlist is unchanged since the last reload or last segment duration
// cannot be determined, try again after half the target duration
return (media.partTargetDuration || media.targetDuration || 10) * 500;
};

/**
Expand Down Expand Up @@ -326,7 +335,7 @@ export default class PlaylistLoader extends EventTarget {
// merge this playlist into the master
const update = updateMaster(this.master, playlist);

this.targetDuration = playlist.targetDuration;
this.targetDuration = playlist.partTargetDuration || playlist.targetDuration;

if (update) {
this.master = update;
Expand Down Expand Up @@ -405,7 +414,7 @@ export default class PlaylistLoader extends EventTarget {
window.clearTimeout(this.finalRenditionTimeout);

if (shouldDelay) {
const delay = (playlist.targetDuration / 2) * 1000 || 5 * 1000;
const delay = ((playlist.partTargetDuration || playlist.targetDuration) / 2) * 1000 || 5 * 1000;

this.finalRenditionTimeout =
window.setTimeout(this.media.bind(this, playlist, false), delay);
Expand Down Expand Up @@ -538,7 +547,7 @@ export default class PlaylistLoader extends EventTarget {
const media = this.media();

if (shouldDelay) {
const delay = media ? (media.targetDuration / 2) * 1000 : 5 * 1000;
const delay = media ? ((media.partTargetDuration || media.targetDuration) / 2) * 1000 : 5 * 1000;

this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay);
return;
Expand Down Expand Up @@ -660,11 +669,15 @@ export default class PlaylistLoader extends EventTarget {
// then resolve URIs in advance, as they are usually done after a playlist request,
// which may not happen if the playlist is resolved.
manifest.playlists.forEach((playlist) => {
if (playlist.segments) {
playlist.segments.forEach((segment) => {
resolveSegmentUris(segment, playlist.resolvedUri);
});
playlist.segments = playlist.segments || [];

if (playlist.preloadSegment && playlist.preloadSegment.parts) {
playlist.segments.push(playlist.preloadSegment);
}

playlist.segments.forEach((segment) => {
resolveSegmentUris(segment, playlist.resolvedUri);
});
});
this.trigger('loadedplaylist');
if (!this.request) {
Expand Down
138 changes: 109 additions & 29 deletions src/playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,80 @@ import {TIME_FUDGE_FACTOR} from './ranges.js';

const {createTimeRange} = videojs;

/**
* A function to get a combined list of parts and segments with durations
* and indexes.
*
* @param {Playlist} playlist the playlist to get the list for.
*
* @return {Array} The part/segment list.
*/
const getPartSegments = (playlist) => (playlist.segments || []).reduce((acc, segment, si) => {
if (segment.parts) {
segment.parts.forEach(function(part, pi) {
acc.push({duration: part.duration, segmentIndex: si, partIndex: pi});

});
} else {
acc.push({duration: segment.duration, segmentIndex: si, partIndex: null});
}
return acc;
}, []);

/**
* Get the number of seconds to hold back from the end of a
* live playlist.
*
* @param {Playlist} master the master playlist
* @param {Playlist} media the media playlist
* @return {number} the hold back in seconds.
*/
export const getHoldBack = (master, media) => {
if (media.endList) {
return 0;
}

const partSegments = getPartSegments(media);
const hasParts = partSegments.length &&
typeof partSegments[partSegments.length - 1].partIndex === 'number';

let lastThreeDurations = 0;

if (partSegments.length >= 3) {
for (let i = 0; i < 3; i++) {
lastThreeDurations += partSegments[partSegments.length - 1 - i].duration;
}
}

// dash suggestedPresentationDelay trumps everything
if (master && master.suggestedPresentationDelay) {
return master.suggestedPresentationDelay;

// look for "part" delays from ll-hls first
} else if (hasParts && media.serverControl && media.serverControl.partHoldBack) {
return media.serverControl.partHoldBack;
} else if (hasParts && media.partTargetDuration) {
return media.partTargetDuration * 3;
} else if (hasParts && lastThreeDurations) {
return lastThreeDurations;

// finally look for full segment delays
} else if (media.serverControl && media.serverControl.holdBack) {
return media.serverControl.holdBack;
} else if (media.targetDuration) {
// TODO: this should probably be targetDuration * 3
// but we use this for backwards compatability.
const lastPartSegment = partSegments[partSegments.length - 1];
const lastPartDuration = lastPartSegment && lastPartSegment.duration || media.targetDuration;

return lastPartDuration + media.targetDuration * 2;
} else if (lastThreeDurations) {
return lastThreeDurations;
}

return 0;
};

/**
* walk backward until we find a duration we can use
* or return a failure
Expand Down Expand Up @@ -233,31 +307,29 @@ export const sumDurations = function(playlist, startIndex, endIndex) {
* @function safeLiveIndex
*/
export const safeLiveIndex = function(playlist, liveEdgePadding) {
if (!playlist.segments.length) {
const partSegments = getPartSegments(playlist);

if (!partSegments.length) {
return 0;
}

let i = playlist.segments.length;
const lastSegmentDuration = playlist.segments[i - 1].duration || playlist.targetDuration;
const safeDistance = typeof liveEdgePadding === 'number' ?
liveEdgePadding :
lastSegmentDuration + playlist.targetDuration * 2;

if (safeDistance === 0) {
return i;
if (typeof liveEdgePadding !== 'number') {
liveEdgePadding = getHoldBack(null, playlist);
}

let i = partSegments.length;
let distanceFromEnd = 0;

while (i--) {
distanceFromEnd += playlist.segments[i].duration;
distanceFromEnd += partSegments[i].duration;

if (distanceFromEnd >= safeDistance) {
break;
if (distanceFromEnd >= liveEdgePadding) {
return partSegments[i].segmentIndex;
}
}

return Math.max(0, i);
// there is nowhere in the playlist that is a safe distance from live.
return 0;
};

/**
Expand Down Expand Up @@ -347,31 +419,34 @@ export const getMediaInfoForTime = function(
startIndex,
startTime
) {
let i;
let segment;
const numSegments = playlist.segments.length;

const partSegments = getPartSegments(playlist);
let time = currentTime - startTime;

if (time < 0) {
// Walk backward from startIndex in the playlist, adding durations
// until we find a segment that contains `time` and return it
if (startIndex > 0) {
for (i = startIndex - 1; i >= 0; i--) {
segment = playlist.segments[i];
for (let i = startIndex - 1; i >= 0; i--) {
const segment = partSegments[i];

time += (segment.duration + TIME_FUDGE_FACTOR);

if (time > 0) {
return {
mediaIndex: i,
startTime: startTime - sumDurations(playlist, startIndex, i)
mediaIndex: segment.segmentIndex,
startTime: startTime - sumDurations(playlist, startIndex, segment.segmentIndex),
partIndex: segment.partIndex
};
}
}
}

// We were unable to find a good segment within the playlist
// so select the first segment
return {
mediaIndex: 0,
mediaIndex: partSegments[0] && partSegments[0].segmentIndex || 0,
partIndex: partSegments[0] && partSegments[0].partIndex || null,
startTime: currentTime
};
}
Expand All @@ -380,11 +455,11 @@ export const getMediaInfoForTime = function(
// adding target durations. If we "run out of time" before getting to
// the first segment, return the first segment
if (startIndex < 0) {
for (i = startIndex; i < 0; i++) {
for (let i = startIndex; i < 0; i++) {
time -= playlist.targetDuration;
if (time < 0) {
return {
mediaIndex: 0,
mediaIndex: partSegments[0].segmentIndex,
startTime: currentTime
};
}
Expand All @@ -394,20 +469,24 @@ export const getMediaInfoForTime = function(

// Walk forward from startIndex in the playlist, subtracting durations
// until we find a segment that contains `time` and return it
for (i = startIndex; i < numSegments; i++) {
segment = playlist.segments[i];
time -= segment.duration + TIME_FUDGE_FACTOR;
for (let i = startIndex; i < partSegments.length; i++) {
const partSegment = partSegments[i];

time -= partSegment.duration + TIME_FUDGE_FACTOR;

if (time < 0) {
return {
mediaIndex: i,
startTime: startTime + sumDurations(playlist, startIndex, i)
mediaIndex: partSegment.segmentIndex,
startTime: startTime + sumDurations(playlist, startIndex, partSegment.segmentIndex),
partIndex: partSegment.partIndex
};
}
}

// We are out of possible candidates so load the last one...
return {
mediaIndex: numSegments - 1,
mediaIndex: partSegments[partSegments.length - 1].segmentIndex,
partIndex: partSegments[partSegments.length - 1].partIndex,
startTime: currentTime
};
};
Expand Down Expand Up @@ -543,6 +622,7 @@ export const isLowestEnabledRendition = (master, media) => {

// exports
export default {
getHoldBack,
duration,
seekable,
safeLiveIndex,
Expand Down
Loading

0 comments on commit 37ae651

Please sign in to comment.