Skip to content

Commit

Permalink
feat: support serverControl and preloadSegment behind experimentalLLH…
Browse files Browse the repository at this point in the history
…LS flag (#1078)
  • Loading branch information
brandonocasey authored Apr 5, 2021
1 parent 1e94680 commit fa1b6b5
Show file tree
Hide file tree
Showing 7 changed files with 642 additions and 408 deletions.
13 changes: 13 additions & 0 deletions src/manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ export const parseManifest = ({
});
}
}
if (!manifest.targetDuration) {
let targetDuration = 10;

if (manifest.segments && manifest.segments.length) {
targetDuration = manifest
.segments.reduce((acc, s) => Math.max(acc, s.duration), 0);
}

if (onwarn) {
onwarn(`manifest has no targetDuration defaulting to ${targetDuration}`);
}
manifest.targetDuration = targetDuration;
}

return manifest;
};
Expand Down
14 changes: 11 additions & 3 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1311,8 +1311,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.liveEdgeDelay(master, media)
);

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

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

if (audioSeekable.length === 0) {
return;
Expand Down
51 changes: 33 additions & 18 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 @@ -113,6 +113,19 @@ export const resolveSegmentUris = (segment, baseUri) => {
}
};

const getAllSegments = function(media) {
const 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) {
segments.push(media.preloadSegment);
}

return segments;
};

// consider the playlist unchanged if the playlist object is the same or
// the number of segments is equal, the media sequence number is unchanged,
// and this playlist hasn't become the end of the playlist
Expand Down Expand Up @@ -147,6 +160,8 @@ export const updateMaster = (master, media, unchangedCheck = isPlaylistUnchanged

const mergedPlaylist = mergeOptions(playlist, media);

media.segments = getAllSegments(media);

// 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 +203,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.length - 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 +341,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 +420,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 +553,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 +675,11 @@ 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 = getAllSegments(playlist);

playlist.segments.forEach((segment) => {
resolveSegmentUris(segment, playlist.resolvedUri);
});
});
this.trigger('loadedplaylist');
if (!this.request) {
Expand Down
106 changes: 81 additions & 25 deletions src/playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,62 @@ 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 getPartsAndSegments = (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 delay 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 liveEdgeDelay = (master, media) => {
if (media.endList) {
return 0;
}

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

const lastSegment = media.segments && media.segments.length && media.segments[media.segments.length - 1];
const hasParts = lastSegment && lastSegment.parts && lastSegment.parts.length;

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

// finally look for full segment delays
} else if (media.serverControl && media.serverControl.holdBack) {
return media.serverControl.holdBack;
} else if (media.targetDuration) {
return media.targetDuration * 3;
}

return 0;
};

/**
* walk backward until we find a duration we can use
* or return a failure
Expand Down Expand Up @@ -252,17 +308,9 @@ export const playlistEnd = function(playlist, expired, useSafeLiveEnd, liveEdgeP
expired
);

// "The client SHALL choose which Media Segment to play first from the
// Media Playlist when playback starts. If the EXT-X-ENDLIST tag is not
// present and the client intends to play the media normally, the client
// SHOULD NOT choose a segment which starts less than three target
// durations from the end of the Playlist file. Doing so can trigger
// playback stalls."
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.3.3
if (useSafeLiveEnd) {
liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : playlist.targetDuration * 3;
liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist);
lastSegmentTime -= liveEdgePadding;

}

// don't return a time less than zero
Expand Down Expand Up @@ -314,31 +362,34 @@ export const getMediaInfoForTime = function(
startIndex,
startTime
) {
let i;
let segment;
const numSegments = playlist.segments.length;

const partsAndSegments = getPartsAndSegments(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 = partsAndSegments[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: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0,
partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null,
startTime: currentTime
};
}
Expand All @@ -347,11 +398,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: partsAndSegments[0].segmentIndex,
startTime: currentTime
};
}
Expand All @@ -361,20 +412,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 < partsAndSegments.length; i++) {
const partSegment = partsAndSegments[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: partsAndSegments[partsAndSegments.length - 1].segmentIndex,
partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex,
startTime: currentTime
};
};
Expand Down Expand Up @@ -510,6 +565,7 @@ export const isLowestEnabledRendition = (master, media) => {

// exports
export default {
liveEdgeDelay,
duration,
seekable,
getMediaInfoForTime,
Expand Down
Loading

0 comments on commit fa1b6b5

Please sign in to comment.