diff --git a/src/master-playlist-controller.js b/src/master-playlist-controller.js index d82ebcda6..3fe44d70e 100644 --- a/src/master-playlist-controller.js +++ b/src/master-playlist-controller.js @@ -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; @@ -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; diff --git a/src/playlist-loader.js b/src/playlist-loader.js index 49964869b..cfbc2b465 100644 --- a/src/playlist-loader.js +++ b/src/playlist-loader.js @@ -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); @@ -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( @@ -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; }; /** @@ -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; @@ -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); @@ -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; @@ -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) { diff --git a/src/playlist.js b/src/playlist.js index 59a236157..3f124a52f 100644 --- a/src/playlist.js +++ b/src/playlist.js @@ -253,6 +253,7 @@ export const safeLiveIndex = function(playlist, liveEdgePadding) { distanceFromEnd += playlist.segments[i].duration; if (distanceFromEnd >= safeDistance) { + i++; break; } } @@ -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 }; } @@ -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 }; }; diff --git a/src/segment-loader.js b/src/segment-loader.js index c826390e8..2be981883 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -94,7 +94,8 @@ const segmentInfoString = (segmentInfo) => { const { segment: { start, - end + end, + parts }, playlist: { mediaSequence: seq, @@ -102,12 +103,19 @@ const segmentInfoString = (segmentInfo) => { 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(' '); }; @@ -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; @@ -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; @@ -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); @@ -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; @@ -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; } @@ -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), @@ -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_);