Skip to content

Commit

Permalink
feat: use serverControl and preloadSegment llhls features behind a flag
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonocasey committed Feb 22, 2021
1 parent 745697a commit ec781dc
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 70 deletions.
19 changes: 16 additions & 3 deletions src/master-playlist-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -1287,8 +1287,21 @@ export class MasterPlaylistController extends videojs.EventTarget {
return;
}

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

if (this.masterPlaylistLoader_.master.hasOwnProperty('suggestedPresentationDelay')) {
delay = this.masterPlaylistLoader_.master.suggestedPresentationDelay;
} else if (media.serverControl && media.serverControl['PART-HOLD-BACK'] && media.partTargetDuration) {
delay = media.serverControl['PART-HOLD-BACK'] * media.partTargetDuration;
} else if (media.serverControl && media.serverControl['HOLD-BACK'] && media.targetDuration) {
delay = media.serverControl['HOLD-BACK'] * media.targetDuration;
} else if (media.partTargetDuration) {
delay = media.partTargetDuration * 3;
} else if (media.targetDuration) {
delay = media.targetDuration * 3;
}

const mainSeekable = Vhs.Playlist.seekable(media, expired, delay);

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

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

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 @@ -61,7 +61,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 @@ -125,6 +125,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 @@ -166,16 +175,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 @@ -304,7 +313,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 @@ -383,7 +392,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 @@ -516,7 +525,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 @@ -638,11 +647,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
71 changes: 45 additions & 26 deletions src/playlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export const safeLiveIndex = function(playlist, liveEdgePadding) {
distanceFromEnd += playlist.segments[i].duration;

if (distanceFromEnd >= safeDistance) {
i++;
break;
}
}
Expand Down Expand Up @@ -347,44 +348,58 @@ export const getMediaInfoForTime = function(
startIndex,
startTime
) {
let i;
let segment;
const numSegments = playlist.segments.length;

const partSegments = 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;
}, []);

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];
time += (segment.duration + TIME_FUDGE_FACTOR);
if (time > 0) {
return {
mediaIndex: i,
startTime: startTime - sumDurations(playlist, startIndex, i)
};
}
if (startIndex <= 0) {
// We were unable to find a good segment within the playlist
// so select the first segment
return {
mediaIndex: partSegments[0].segmentIndex,
partIndex: partSegments[0].partIndex,
startTime: currentTime
};
}

for (let i = startIndex - 1; i >= 0; i--) {
const segment = partSegments[i];

time += (segment.duration + TIME_FUDGE_FACTOR);

if (time > 0) {
return {
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,
startTime: currentTime
};
}

// When startIndex is negative, we first walk forward to first segment
// 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,
partIndex: partSegments[0].partIndex,
startTime: currentTime
};
}
Expand All @@ -394,20 +409,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];
for (let i = startIndex; i < partSegments.length; 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 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
51 changes: 27 additions & 24 deletions src/segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,20 +94,28 @@ const segmentInfoString = (segmentInfo) => {
const {
segment: {
start,
end
end,
parts
},
playlist: {
mediaSequence: seq,
id,
segments = []
},
mediaIndex: index,
partIndex,
timeline
} = segmentInfo;

const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment';

return [
`appending [${index}] of [${seq}, ${seq + segments.length}] from playlist [${id}]`,
`[${start} => ${end}] in timeline [${timeline}]`
`${name} [${index}/${segments.length - 1}]`,
(partIndex ? `part [${partIndex}/${parts.length - 1}]` : ''),
`msn [${seq}/${seq + segments.length - 1}]`,
`playlist [${id}]`,
`start/end [${start} => ${end}]`,
`timeline [${timeline}]`
].join(' ');
};

Expand Down Expand Up @@ -1326,7 +1334,7 @@ export default class SegmentLoader extends videojs.EventTarget {
return null;
}

let nextPartIndex = typeof currentPartIndex === 'number' ? currentPartIndex + 1 : 0;
let nextPartIndex = null;
let nextMediaIndex = null;
let startOfSegment;
let isSyncRequest = false;
Expand All @@ -1347,6 +1355,7 @@ export default class SegmentLoader extends videojs.EventTarget {
} else {
startOfSegment = lastBufferedEnd;
}
nextPartIndex = typeof currentPartIndex === 'number' ? currentPartIndex + 1 : 0;

if (!segment || !segment.parts || !segment.parts.length || !segment.parts[nextPartIndex]) {
nextMediaIndex = currentMediaIndex + 1;
Expand All @@ -1358,28 +1367,22 @@ export default class SegmentLoader extends videojs.EventTarget {
// There is a sync-point but the lack of a mediaIndex indicates that
// we need to make a good conservative guess about which segment to
// fetch
} else if (this.fetchAtBuffer_) {
// Find the segment containing the end of the buffer
const mediaSourceInfo = Playlist.getMediaInfoForTime(
playlist,
lastBufferedEnd,
syncPoint.segmentIndex,
syncPoint.time
);

nextMediaIndex = mediaSourceInfo.mediaIndex;
startOfSegment = mediaSourceInfo.startTime;
} else {
// Find the segment containing currentTime
// Find the segment containing the end of the buffer or current time.
const mediaSourceInfo = Playlist.getMediaInfoForTime(
playlist,
currentTime,
this.fetchAtBuffer_ ? lastBufferedEnd : currentTime,
syncPoint.segmentIndex,
syncPoint.time
);

nextMediaIndex = mediaSourceInfo.mediaIndex;
startOfSegment = mediaSourceInfo.startTime;
nextPartIndex = mediaSourceInfo.partIndex;
}

if (typeof nextPartIndex !== 'number' && playlist.segments[nextMediaIndex] && playlist.segments[nextMediaIndex].parts) {
nextPartIndex = 0;
}

const segmentInfo = this.generateSegmentInfo_(playlist, nextMediaIndex, startOfSegment, isSyncRequest, nextPartIndex);
Expand All @@ -1399,11 +1402,8 @@ export default class SegmentLoader extends videojs.EventTarget {

this.logger_(`checkBuffer_ returning ${segmentInfo.uri}`, {
segmentInfo,
playlist,
currentMediaIndex,
nextMediaIndex,
startOfSegment,
isSyncRequest
currentPartIndex
});

return segmentInfo;
Expand Down Expand Up @@ -1599,7 +1599,8 @@ export default class SegmentLoader extends videojs.EventTarget {
this.trigger('earlyabort');
}

handleAbort_() {
handleAbort_(segmentInfo) {
this.logger_(`Aborting ${segmentInfoString(segmentInfo)}`);
this.mediaRequestsAborted += 1;
}

Expand Down Expand Up @@ -2263,13 +2264,15 @@ export default class SegmentLoader extends videojs.EventTarget {
segmentInfo.timeline > 0;
const isEndOfTimeline = isEndOfStream || (isWalkingForward && isDiscontinuity);

this.logger_(`Requesting ${segmentInfoString(segmentInfo)}`);

segmentInfo.abortRequests = mediaSegmentRequest({
xhr: this.vhs_.xhr,
xhrOptions: this.xhrOptions_,
decryptionWorker: this.decrypter_,
segment: simpleSegment,
handlePartialData: this.handlePartialData_,
abortFn: this.handleAbort_.bind(this),
abortFn: this.handleAbort_.bind(this, segmentInfo),
progressFn: this.handleProgress_.bind(this),
trackInfoFn: this.handleTrackInfo_.bind(this),
timingInfoFn: this.handleTimingInfo_.bind(this),
Expand Down Expand Up @@ -2770,7 +2773,7 @@ export default class SegmentLoader extends videojs.EventTarget {
});
}

this.logger_(segmentInfoString(segmentInfo));
this.logger_(`Appended ${segmentInfoString(segmentInfo)}`);

const segmentDurationMessage =
getTroublesomeSegmentDurationMessage(segmentInfo, this.sourceType_);
Expand Down

0 comments on commit ec781dc

Please sign in to comment.