diff --git a/src/m3u8/m3u8-parser.js b/src/m3u8/m3u8-parser.js index 2eb537c2b..4d7df0bc5 100644 --- a/src/m3u8/m3u8-parser.js +++ b/src/m3u8/m3u8-parser.js @@ -375,7 +375,8 @@ // the manifest is empty until the parse stream begins delivering data this.manifest = { - allowCache: true + allowCache: true, + discontinuityStarts: [] }; // update the manifest with the m3u8 entry from the parse stream @@ -513,6 +514,7 @@ }, 'discontinuity': function() { currentUri.discontinuity = true; + this.manifest.discontinuityStarts.push(uris.length); }, 'targetduration': function() { if (!isFinite(entry.duration) || entry.duration < 0) { diff --git a/src/playlist-loader.js b/src/playlist-loader.js index 91d76037f..d91217124 100644 --- a/src/playlist-loader.js +++ b/src/playlist-loader.js @@ -431,11 +431,12 @@ for (i = 0; i < this.media_.segments.length; i++) { time -= Playlist.duration(this.media_, this.media_.mediaSequence + i, - this.media_.mediaSequence + i + 1); + this.media_.mediaSequence + i + 1, + false); // HLS version 3 and lower round segment durations to the // nearest decimal integer. When the correct media index is - // ambiguous, prefer the lower one. + // ambiguous, prefer the higher one. if (time <= 0) { return i; } diff --git a/src/playlist.js b/src/playlist.js index 4d33ba8dd..9329f054a 100644 --- a/src/playlist.js +++ b/src/playlist.js @@ -5,7 +5,130 @@ 'use strict'; var DEFAULT_TARGET_DURATION = 10; - var duration, seekable, segmentsDuration; + var accumulateDuration, ascendingNumeric, duration, intervalDuration, rangeDuration, seekable; + + // Array.sort comparator to sort numbers in ascending order + ascendingNumeric = function(left, right) { + return left - right; + }; + + /** + * Returns the media duration for the segments between a start and + * exclusive end index. The start and end parameters are interpreted + * as indices into the currently available segments. This method + * does not calculate durations for segments that have expired. + * @param playlist {object} a media playlist object + * @param start {number} an inclusive lower boundary for the + * segments to examine. + * @param end {number} an exclusive upper boundary for the segments + * to examine. + * @param includeTrailingTime {boolean} if false, the interval between + * the final segment and the subsequent segment will not be included + * in the result + * @return {number} the duration between the start index and end + * index in seconds. + */ + accumulateDuration = function(playlist, start, end, includeTrailingTime) { + var + ranges = [], + rangeEnds = (playlist.discontinuityStarts || []).concat(end), + result = 0, + i; + + // short circuit if start and end don't specify a non-empty range + // of segments + if (start >= end) { + return 0; + } + + // create a range object for each discontinuity sequence + rangeEnds.sort(ascendingNumeric); + for (i = 0; i < rangeEnds.length; i++) { + if (rangeEnds[i] > start) { + ranges.push({ start: start, end: rangeEnds[i] }); + i++; + break; + } + } + for (; i < rangeEnds.length; i++) { + // ignore times ranges later than end + if (rangeEnds[i] >= end) { + ranges.push({ start: rangeEnds[i - 1], end: end }); + break; + } + ranges.push({ start: ranges[ranges.length - 1].end, end: rangeEnds[i] }); + } + + // add up the durations for each of the ranges + for (i = 0; i < ranges.length; i++) { + result += rangeDuration(playlist, + ranges[i], + i === ranges.length - 1 && includeTrailingTime); + } + + return result; + }; + + /** + * Returns the duration of the specified range of segments. The + * range *must not* cross a discontinuity. + * @param playlist {object} a media playlist object + * @param range {object} an object that specifies a starting and + * ending index into the available segments. + * @param includeTrailingTime {boolean} if false, the interval between + * the final segment and the subsequent segment will not be included + * in the result + * @return {number} the duration of the range in seconds. + */ + rangeDuration = function(playlist, range, includeTrailingTime) { + var + result = 0, + targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION, + segment, + left, right; + + // accumulate while searching for the earliest segment with + // available PTS information + for (left = range.start; left < range.end; left++) { + segment = playlist.segments[left]; + if (segment.minVideoPts !== undefined) { + break; + } + result += segment.duration || targetDuration; + } + + // see if there's enough information to include the trailing time + if (includeTrailingTime) { + segment = playlist.segments[range.end]; + if (segment && segment.minVideoPts !== undefined) { + result += 0.001 * + (Math.min(segment.minVideoPts, segment.minAudioPts) - + Math.min(playlist.segments[left].minVideoPts, + playlist.segments[left].minAudioPts)); + return result; + } + } + + // do the same thing while finding the latest segment + for (right = range.end - 1; right >= left; right--) { + segment = playlist.segments[right]; + if (segment.maxVideoPts !== undefined) { + break; + } + result += segment.duration || targetDuration; + } + + // add in the PTS interval in seconds between them + if (right >= left) { + result += 0.001 * + (Math.max(playlist.segments[right].maxVideoPts, + playlist.segments[right].maxAudioPts) - + Math.min(playlist.segments[left].minVideoPts, + playlist.segments[left].minAudioPts)); + } + + return result; + }; /** * Calculate the media duration from the segments associated with a @@ -17,47 +140,28 @@ * boundary for the playlist. Defaults to 0. * @param endSequence {number} (optional) an exclusive upper boundary * for the playlist. Defaults to playlist length. + * @param includeTrailingTime {boolean} if false, the interval between + * the final segment and the subsequent segment will not be included + * in the result * @return {number} the duration between the start index and end * index. */ - segmentsDuration = function(playlist, startSequence, endSequence) { - var targetDuration, i, j, segment, endSegment, expiredSegmentCount, result = 0; + intervalDuration = function(playlist, startSequence, endSequence, includeTrailingTime) { + var result = 0, targetDuration, expiredSegmentCount; startSequence = startSequence || 0; - i = startSequence; endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length; targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; // estimate expired segment duration using the target duration expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0); result += expiredSegmentCount * targetDuration; - i += expiredSegmentCount; // accumulate the segment durations into the result - for (; i < endSequence; i++) { - segment = playlist.segments[i - playlist.mediaSequence]; - - // when PTS values aren't available, use information from the playlist - if (segment.minVideoPts === undefined) { - result += segment.duration || - targetDuration; - continue; - } - - // find the last segment with PTS info and use that to calculate - // the interval duration - for(j = i; j < endSequence - 1; j++) { - endSegment = playlist.segments[j - playlist.mediaSequence + 1]; - if (endSegment.maxVideoPts === undefined || - endSegment.discontinuity) { - break; - } - } - endSegment = playlist.segments[j - playlist.mediaSequence]; - result += (Math.max(endSegment.maxVideoPts, endSegment.maxAudioPts) - - Math.min(segment.minVideoPts, segment.minAudioPts)) * 0.001; - i = j; - } + result += accumulateDuration(playlist, + startSequence + expiredSegmentCount - playlist.mediaSequence, + endSequence - playlist.mediaSequence, + includeTrailingTime); return result; }; @@ -72,14 +176,21 @@ * boundary for the playlist. Defaults to 0. * @param endSequence {number} (optional) an exclusive upper boundary * for the playlist. Defaults to playlist length. + * @param includeTrailingTime {boolean} (optional) if false, the interval between + * the final segment and the subsequent segment will not be included + * in the result * @return {number} the duration between the start index and end * index. */ - duration = function(playlist, startSequence, endSequence) { + duration = function(playlist, startSequence, endSequence, includeTrailingTime) { if (!playlist) { return 0; } + if (includeTrailingTime === undefined) { + includeTrailingTime = true; + } + // if a slice of the total duration is not requested, use // playlist-level duration indicators when they're present if (startSequence === undefined && endSequence === undefined) { @@ -95,9 +206,10 @@ } // calculate the total duration based on the segment durations - return segmentsDuration(playlist, + return intervalDuration(playlist, startSequence, - endSequence); + endSequence, + includeTrailingTime); }; /** @@ -119,8 +231,8 @@ return videojs.createTimeRange(0, duration(playlist)); } - start = segmentsDuration(playlist, 0, playlist.mediaSequence); - end = start + segmentsDuration(playlist, + start = intervalDuration(playlist, 0, playlist.mediaSequence); + end = start + intervalDuration(playlist, playlist.mediaSequence, playlist.mediaSequence + playlist.segments.length); targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; diff --git a/src/segment-parser.js b/src/segment-parser.js index 6b51791bd..209260b04 100644 --- a/src/segment-parser.js +++ b/src/segment-parser.js @@ -31,15 +31,6 @@ // allow in-band metadata to be observed self.metadataStream = new MetadataStream(); - this.mediaTimelineOffset = null; - - // The first timestamp value encountered during parsing. This - // value can be used to determine the relative timing between - // frames and the start of the current timestamp sequence. It - // should be reset to null before parsing a segment with - // discontinuous timestamp values from previous segments. - self.timestampOffset = null; - // For information on the FLV format, see // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf. // Technically, this function returns the header and a metadata FLV tag @@ -360,13 +351,6 @@ // Skip past "optional" portion of PTS header offset += pesHeaderLength; - // keep track of the earliest encounted PTS value so - // external parties can align timestamps across - // discontinuities - if (self.timestampOffset === null) { - self.timestampOffset = pts; - } - if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) { h264Stream.setNextTimeStamp(pts, dts, diff --git a/src/videojs-hls.js b/src/videojs-hls.js index bb5b13558..9c56aa060 100644 --- a/src/videojs-hls.js +++ b/src/videojs-hls.js @@ -260,7 +260,7 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { // add a metadata cue whenever a metadata event is triggered during // segment parsing metadataStream.on('data', function(metadata) { - var i, cue, frame, time, media, segmentOffset, hexDigit; + var i, hexDigit; // create the metadata track if this is the first ID3 tag we've // seen @@ -276,19 +276,11 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { } } - // calculate the start time for the segment that is currently being parsed - media = tech.playlists.media(); - segmentOffset = tech.playlists.expiredPreDiscontinuity_ + tech.playlists.expiredPostDiscontinuity_; - segmentOffset += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + tech.mediaIndex); - - // create cue points for all the ID3 frames in this metadata event - for (i = 0; i < metadata.frames.length; i++) { - frame = metadata.frames[i]; - time = tech.segmentParser_.mediaTimelineOffset + ((metadata.pts - tech.segmentParser_.timestampOffset) * 0.001); - cue = new window.VTTCue(time, time, frame.value || frame.url || ''); - cue.frame = frame; - textTrack.addCue(cue); - } + // store this event for processing once the muxing has finished + tech.segmentBuffer_[0].pendingMetadata.push({ + textTrack: textTrack, + metadata: metadata + }); }); // when seeking, clear out all cues ahead of the earliest position @@ -312,6 +304,30 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { }); }; +videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { + var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time; + segmentOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, + segmentInfo.playlist.mediaSequence, + segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex); + segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; + minPts = Math.min(segment.minVideoPts, segment.minAudioPts); + + while (segmentInfo.pendingMetadata.length) { + metadata = segmentInfo.pendingMetadata[0].metadata; + textTrack = segmentInfo.pendingMetadata[0].textTrack; + + // create cue points for all the ID3 frames in this metadata event + for (i = 0; i < metadata.frames.length; i++) { + frame = metadata.frames[i]; + time = segmentOffset + ((metadata.pts - minPts) * 0.001); + cue = new window.VTTCue(time, time, frame.value || frame.url || ''); + cue.frame = frame; + textTrack.addCue(cue); + } + segmentInfo.pendingMetadata.shift(); + } +}; + /** * Reset the mediaIndex if play() is called after the video has * ended. @@ -780,7 +796,10 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { // when a key is defined for this segment, the encrypted bytes encryptedBytes: null, // optionally, the decrypter that is unencrypting the segment - decrypter: null + decrypter: null, + // metadata events discovered during muxing that need to be + // translated into cue points + pendingMetadata: [] }; if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) { segmentInfo.encryptedBytes = new Uint8Array(this.response); @@ -870,20 +889,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { } event = event || {}; - segmentOffset = this.playlists.expiredPreDiscontinuity_; - segmentOffset += this.playlists.expiredPostDiscontinuity_; - segmentOffset += videojs.Hls.Playlist.duration(playlist, playlist.mediaSequence, playlist.mediaSequence + mediaIndex); - segmentOffset *= 1000; - - // if this segment starts is the start of a new discontinuity - // sequence, the segment parser's timestamp offset must be - // re-calculated - if (segment.discontinuity) { - this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001; - this.segmentParser_.timestampOffset = null; - } else if (this.segmentParser_.mediaTimelineOffset === null) { - this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001; - } // transmux the segment data from MP2T to FLV this.segmentParser_.parseSegmentBinaryData(bytes); @@ -904,20 +909,27 @@ videojs.Hls.prototype.drainBuffer = function(event) { tags.push(this.segmentParser_.getNextTag()); } + this.addCuesForMetadata_(segmentInfo); this.updateDuration(this.playlists.media()); // if we're refilling the buffer after a seek, scan through the muxed // FLV tags until we find the one that is closest to the desired // playback time if (typeof offset === 'number') { - ptsTime = offset - segmentOffset + tags[0].pts; - - while (tags[i].pts < ptsTime) { + // determine the offset within this segment we're seeking to + segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_; + segmentOffset += videojs.Hls.Playlist.duration(playlist, + playlist.mediaSequence, + playlist.mediaSequence + mediaIndex); + segmentOffset = offset - (segmentOffset * 1000); + ptsTime = segmentOffset + tags[0].pts; + + while (tags[i + 1] && tags[i].pts < ptsTime) { i++; } - // tell the SWF where we will be seeking to - this.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001); + // tell the SWF the media position of the first tag we'll be delivering + this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001)); tags = tags.slice(i); @@ -1138,29 +1150,6 @@ videojs.Hls.getMediaIndexByTime = function() { return 0; }; -/** - * Determine the current time in seconds in one playlist by a media index. This - * function iterates through the segments of a playlist up to the specified index - * and then returns the time up to that point. - * - * @param playlist {object} The playlist of the segments being searched. - * @param mediaIndex {number} The index of the target segment in the playlist. - * @returns {number} The current time to that point, or 0 if none appropriate. - */ -videojs.Hls.prototype.getCurrentTimeByMediaIndex_ = function(playlist, mediaIndex) { - var index, time = 0; - - if (!playlist.segments || mediaIndex === 0) { - return 0; - } - - for (index = 0; index < mediaIndex; index++) { - time += playlist.segments[index].duration; - } - - return time; -}; - /** * A comparator function to sort two playlist object by bandwidth. * @param left {object} a media playlist object diff --git a/test/manifest/absoluteUris.js b/test/manifest/absoluteUris.js index 208de13f4..808a44a1a 100644 --- a/test/manifest/absoluteUris.js +++ b/test/manifest/absoluteUris.js @@ -22,5 +22,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/allowCache.js b/test/manifest/allowCache.js index 58e49317a..9164d8575 100644 --- a/test/manifest/allowCache.js +++ b/test/manifest/allowCache.js @@ -142,5 +142,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/allowCacheInvalid.js b/test/manifest/allowCacheInvalid.js index b721a9c8c..4092cc02a 100644 --- a/test/manifest/allowCacheInvalid.js +++ b/test/manifest/allowCacheInvalid.js @@ -14,5 +14,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/brightcove.js b/test/manifest/brightcove.js index fbf9428bd..ee3d5f766 100644 --- a/test/manifest/brightcove.js +++ b/test/manifest/brightcove.js @@ -41,5 +41,6 @@ }, "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" } - ] -} + ], + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/byteRange.js b/test/manifest/byteRange.js index 324e829e0..7a426d0db 100644 --- a/test/manifest/byteRange.js +++ b/test/manifest/byteRange.js @@ -138,5 +138,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/disallowCache.js b/test/manifest/disallowCache.js index a297080ff..deda65eec 100644 --- a/test/manifest/disallowCache.js +++ b/test/manifest/disallowCache.js @@ -14,5 +14,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/disc-sequence.js b/test/manifest/disc-sequence.js index 4c84a97bb..f30e86656 100644 --- a/test/manifest/disc-sequence.js +++ b/test/manifest/disc-sequence.js @@ -22,5 +22,6 @@ } ], "targetDuration": 19, - "endList": true + "endList": true, + "discontinuityStarts": [2] } diff --git a/test/manifest/discontinuity.js b/test/manifest/discontinuity.js index 8b9d874df..06b84d924 100644 --- a/test/manifest/discontinuity.js +++ b/test/manifest/discontinuity.js @@ -44,5 +44,6 @@ } ], "targetDuration": 19, - "endList": true + "endList": true, + "discontinuityStarts": [2, 4, 7] } diff --git a/test/manifest/domainUris.js b/test/manifest/domainUris.js index fc9504696..3501a5c44 100644 --- a/test/manifest/domainUris.js +++ b/test/manifest/domainUris.js @@ -22,5 +22,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/emptyAllowCache.js b/test/manifest/emptyAllowCache.js index b721a9c8c..4092cc02a 100644 --- a/test/manifest/emptyAllowCache.js +++ b/test/manifest/emptyAllowCache.js @@ -14,5 +14,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/emptyMediaSequence.js b/test/manifest/emptyMediaSequence.js index b5f53cd01..bb928c09c 100644 --- a/test/manifest/emptyMediaSequence.js +++ b/test/manifest/emptyMediaSequence.js @@ -22,5 +22,6 @@ ], "targetDuration": 8, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/emptyPlaylistType.js b/test/manifest/emptyPlaylistType.js index fc4c2aec5..2b864d953 100644 --- a/test/manifest/emptyPlaylistType.js +++ b/test/manifest/emptyPlaylistType.js @@ -29,5 +29,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/emptyTargetDuration.js b/test/manifest/emptyTargetDuration.js index fbf9428bd..ee3d5f766 100644 --- a/test/manifest/emptyTargetDuration.js +++ b/test/manifest/emptyTargetDuration.js @@ -41,5 +41,6 @@ }, "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" } - ] -} + ], + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/encrypted.js b/test/manifest/encrypted.js index 80ad8eaa8..f94752be2 100644 --- a/test/manifest/encrypted.js +++ b/test/manifest/encrypted.js @@ -2,6 +2,7 @@ "allowCache": true, "mediaSequence": 7794, "discontinuitySequence": 0, + "discontinuityStarts": [], "segments": [ { "duration": 2.833, diff --git a/test/manifest/event.js b/test/manifest/event.js index 9ec9248b6..49bcda6dd 100644 --- a/test/manifest/event.js +++ b/test/manifest/event.js @@ -30,5 +30,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/extXPlaylistTypeInvalidPlaylist.js b/test/manifest/extXPlaylistTypeInvalidPlaylist.js index 760713d15..5ca2b8996 100644 --- a/test/manifest/extXPlaylistTypeInvalidPlaylist.js +++ b/test/manifest/extXPlaylistTypeInvalidPlaylist.js @@ -9,5 +9,6 @@ ], "targetDuration": 8, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/extinf.js b/test/manifest/extinf.js index be5f64887..89b6b537b 100644 --- a/test/manifest/extinf.js +++ b/test/manifest/extinf.js @@ -142,5 +142,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/invalidAllowCache.js b/test/manifest/invalidAllowCache.js index b721a9c8c..4092cc02a 100644 --- a/test/manifest/invalidAllowCache.js +++ b/test/manifest/invalidAllowCache.js @@ -14,5 +14,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/invalidMediaSequence.js b/test/manifest/invalidMediaSequence.js index b5f53cd01..bb928c09c 100644 --- a/test/manifest/invalidMediaSequence.js +++ b/test/manifest/invalidMediaSequence.js @@ -22,5 +22,6 @@ ], "targetDuration": 8, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/invalidPlaylistType.js b/test/manifest/invalidPlaylistType.js index fc4c2aec5..2b864d953 100644 --- a/test/manifest/invalidPlaylistType.js +++ b/test/manifest/invalidPlaylistType.js @@ -29,5 +29,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/invalidTargetDuration.js b/test/manifest/invalidTargetDuration.js index 3686308d6..09b4953ad 100644 --- a/test/manifest/invalidTargetDuration.js +++ b/test/manifest/invalidTargetDuration.js @@ -141,5 +141,6 @@ } ], "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/liveMissingSegmentDuration.js b/test/manifest/liveMissingSegmentDuration.js index 95bc58972..3a94b5fa0 100644 --- a/test/manifest/liveMissingSegmentDuration.js +++ b/test/manifest/liveMissingSegmentDuration.js @@ -17,5 +17,6 @@ } ], "targetDuration": 8, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/liveStart30sBefore.js b/test/manifest/liveStart30sBefore.js index 068f7d06c..cd5274ece 100644 --- a/test/manifest/liveStart30sBefore.js +++ b/test/manifest/liveStart30sBefore.js @@ -40,5 +40,6 @@ } ], "targetDuration": 10, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/manifestExtTTargetdurationNegative.js b/test/manifest/manifestExtTTargetdurationNegative.js index f34e556e1..39c44b244 100644 --- a/test/manifest/manifestExtTTargetdurationNegative.js +++ b/test/manifest/manifestExtTTargetdurationNegative.js @@ -8,5 +8,6 @@ } ], "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/manifestExtXEndlistEarly.js b/test/manifest/manifestExtXEndlistEarly.js index ff8dc2339..426d7bf99 100644 --- a/test/manifest/manifestExtXEndlistEarly.js +++ b/test/manifest/manifestExtXEndlistEarly.js @@ -25,5 +25,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/manifestNoExtM3u.js b/test/manifest/manifestNoExtM3u.js index 25c129e3a..4cca9db7b 100644 --- a/test/manifest/manifestNoExtM3u.js +++ b/test/manifest/manifestNoExtM3u.js @@ -9,5 +9,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/master.js b/test/manifest/master.js index d089e4360..5bdc0aadc 100644 --- a/test/manifest/master.js +++ b/test/manifest/master.js @@ -41,5 +41,6 @@ }, "uri": "media3.m3u8" } - ] -} + ], + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/media.js b/test/manifest/media.js index 9b68ea124..25e653048 100644 --- a/test/manifest/media.js +++ b/test/manifest/media.js @@ -22,5 +22,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/mediaSequence.js b/test/manifest/mediaSequence.js index b5f53cd01..bb928c09c 100644 --- a/test/manifest/mediaSequence.js +++ b/test/manifest/mediaSequence.js @@ -22,5 +22,6 @@ ], "targetDuration": 8, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/missingEndlist.js b/test/manifest/missingEndlist.js index 6d9894876..8dbd5e997 100644 --- a/test/manifest/missingEndlist.js +++ b/test/manifest/missingEndlist.js @@ -12,5 +12,6 @@ } ], "targetDuration": 10, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/missingExtinf.js b/test/manifest/missingExtinf.js index a82e9a58c..791ebd7ee 100644 --- a/test/manifest/missingExtinf.js +++ b/test/manifest/missingExtinf.js @@ -18,5 +18,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/missingMediaSequence.js b/test/manifest/missingMediaSequence.js index b5f53cd01..bb928c09c 100644 --- a/test/manifest/missingMediaSequence.js +++ b/test/manifest/missingMediaSequence.js @@ -22,5 +22,6 @@ ], "targetDuration": 8, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/missingSegmentDuration.js b/test/manifest/missingSegmentDuration.js index b34d052f1..6af2ba5ed 100644 --- a/test/manifest/missingSegmentDuration.js +++ b/test/manifest/missingSegmentDuration.js @@ -22,5 +22,6 @@ ], "targetDuration": 8, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/multipleTargetDurations.js b/test/manifest/multipleTargetDurations.js index ca77d2817..576178397 100644 --- a/test/manifest/multipleTargetDurations.js +++ b/test/manifest/multipleTargetDurations.js @@ -19,5 +19,6 @@ "duration": 10 } ], - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/negativeMediaSequence.js b/test/manifest/negativeMediaSequence.js index 0e1cd7c41..4306f60ee 100644 --- a/test/manifest/negativeMediaSequence.js +++ b/test/manifest/negativeMediaSequence.js @@ -22,5 +22,6 @@ ], "targetDuration": 8, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/playlist.js b/test/manifest/playlist.js index 58e49317a..9164d8575 100644 --- a/test/manifest/playlist.js +++ b/test/manifest/playlist.js @@ -142,5 +142,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/playlistMediaSequenceHigher.js b/test/manifest/playlistMediaSequenceHigher.js index a1526c630..1b4ebd701 100644 --- a/test/manifest/playlistMediaSequenceHigher.js +++ b/test/manifest/playlistMediaSequenceHigher.js @@ -10,5 +10,6 @@ ], "targetDuration": 8, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/streamInfInvalid.js b/test/manifest/streamInfInvalid.js index 1ab386d51..5b3ce60bd 100644 --- a/test/manifest/streamInfInvalid.js +++ b/test/manifest/streamInfInvalid.js @@ -10,5 +10,6 @@ { "uri": "media1.m3u8" } - ] -} + ], + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/twoMediaSequences.js b/test/manifest/twoMediaSequences.js index af7dd8a6d..00be4f01c 100644 --- a/test/manifest/twoMediaSequences.js +++ b/test/manifest/twoMediaSequences.js @@ -22,5 +22,6 @@ ], "targetDuration": 8, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/versionInvalid.js b/test/manifest/versionInvalid.js index a100a159b..f3c393020 100644 --- a/test/manifest/versionInvalid.js +++ b/test/manifest/versionInvalid.js @@ -10,5 +10,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/manifest/whiteSpace.js b/test/manifest/whiteSpace.js index 208de13f4..808a44a1a 100644 --- a/test/manifest/whiteSpace.js +++ b/test/manifest/whiteSpace.js @@ -22,5 +22,6 @@ ], "targetDuration": 10, "endList": true, - "discontinuitySequence": 0 -} + "discontinuitySequence": 0, + "discontinuityStarts": [] +} \ No newline at end of file diff --git a/test/playlist_test.js b/test/playlist_test.js index 97bd236ea..e19b626af 100644 --- a/test/playlist_test.js +++ b/test/playlist_test.js @@ -3,7 +3,7 @@ 'use strict'; var Playlist = videojs.Hls.Playlist; - module('Playlist Utilities'); + module('Playlist Duration'); test('total duration for live playlists is Infinity', function() { var duration = Playlist.duration({ @@ -16,7 +16,9 @@ equal(duration, Infinity, 'duration is infinity'); }); - test('interval duration accounts for media sequences', function() { + module('Playlist Interval Duration'); + + test('accounts for media sequences', function() { var duration = Playlist.duration({ mediaSequence: 10, endList: true, @@ -38,7 +40,7 @@ equal(duration, 14 * 10, 'duration includes dropped segments'); }); - test('interval duration uses PTS values when available', function() { + test('uses PTS values when available', function() { var duration = Playlist.duration({ mediaSequence: 0, endList: true, @@ -67,49 +69,241 @@ equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values'); }); - test('interval duration works when partial PTS information is available', function() { - var firstInterval, secondInterval, duration = Playlist.duration({ + test('works when partial PTS information is available', function() { + var duration = Playlist.duration({ mediaSequence: 0, endList: true, segments: [{ minVideoPts: 1, minAudioPts: 2, - maxVideoPts: 1 * 10 * 1000 + 1, + maxVideoPts: 10 * 1000 + 1, // intentionally less duration than video // the max stream duration should be used - maxAudioPts: 1 * 10 * 1000 + 1, + maxAudioPts: 10 * 1000 + 1, + uri: '0.ts' + }, { + duration: 9, + uri: '1.ts' + }, { + duration: 10, + uri: '2.ts' + }, { + duration: 10, + minVideoPts: 30 * 1000 + 7, + minAudioPts: 30 * 1000 + 10, + maxVideoPts: 40 * 1000 + 1, + maxAudioPts: 40 * 1000 + 2, + uri: '3.ts' + }, { + duration: 10, + maxVideoPts: 50 * 1000 + 1, + maxAudioPts: 50 * 1000 + 2, + uri: '4.ts' + }] + }, 0, 5); + + equal(duration, + ((50 * 1000 + 2) - 1) * 0.001, + 'calculated with mixed intervals'); + }); + + test('ignores segments before the start', function() { + var duration = Playlist.duration({ + mediaSequence: 0, + segments: [{ + duration: 10, uri: '0.ts' }, { duration: 10, uri: '1.ts' }, { duration: 10, - minVideoPts: 2 * 10 * 1000 + 7, - minAudioPts: 2 * 10 * 1000 + 10, - maxVideoPts: 3 * 10 * 1000 + 1, - maxAudioPts: 3 * 10 * 1000 + 2, + uri: '2.ts' + }] + }, 1, 3); + + equal(duration, 10 + 10, 'ignored the first segment'); + }); + + test('ignores discontinuity sequences earlier than the start', function() { + var duration = Playlist.duration({ + mediaSequence: 0, + discontinuityStarts: [1, 3], + segments: [{ + minVideoPts: 0, + minAudioPts: 0, + maxVideoPts: 10 * 1000, + maxAudioPts: 10 * 1000, + uri: '0.ts' + }, { + discontinuity: true, + duration: 9, + uri: '1.ts' + }, { + duration: 10, uri: '2.ts' }, { + discontinuity: true, duration: 10, - maxVideoPts: 4 * 10 * 1000 + 1, - maxAudioPts: 4 * 10 * 1000 + 2, uri: '3.ts' }] - }, 0, 4); + }, 2, 4); + + equal(duration, 10 + 10, 'excluded the earlier segments'); + }); - firstInterval = (1 * 10 * 1000 + 1) - 1; - firstInterval *= 0.001; - secondInterval = (4 * 10 * 1000 + 2) - (2 * 10 * 1000 + 7); - secondInterval *= 0.001; + test('ignores discontinuity sequences later than the end', function() { + var duration = Playlist.duration({ + mediaSequence: 0, + discontinuityStarts: [1, 3], + segments: [{ + minVideoPts: 0, + minAudioPts: 0, + maxVideoPts: 10 * 1000, + maxAudioPts: 10 * 1000, + uri: '0.ts' + }, { + discontinuity: true, + duration: 9, + uri: '1.ts' + }, { + duration: 10, + uri: '2.ts' + }, { + discontinuity: true, + duration: 10, + uri: '3.ts' + }] + }, 0, 2); - equal(duration, firstInterval + 10 + secondInterval, 'calculated with mixed intervals'); + equal(duration, 19, 'excluded the later segments'); }); - test('interval duration accounts for discontinuities', function() { + test('handles trailing segments without PTS information', function() { var duration = Playlist.duration({ mediaSequence: 0, endList: true, + segments: [{ + minVideoPts: 0, + minAudioPts: 0, + maxVideoPts: 10 * 1000, + maxAudioPts: 10 * 1000, + uri: '0.ts' + }, { + duration: 9, + uri: '1.ts' + }, { + duration: 10, + uri: '2.ts' + }, { + minVideoPts: 29.5 * 1000, + minAudioPts: 29.5 * 1000, + maxVideoPts: 39.5 * 1000, + maxAudioPts: 39.5 * 1000, + uri: '3.ts' + }] + }, 0, 3); + + equal(duration, 29.5, 'calculated duration'); + }); + + test('uses PTS intervals when the start and end segment have them', function() { + var playlist, duration; + playlist = { + mediaSequence: 0, + segments: [{ + minVideoPts: 0, + minAudioPts: 0, + maxVideoPts: 10 * 1000, + maxAudioPts: 10 * 1000, + uri: '0.ts' + }, { + duration: 9, + uri: '1.ts' + },{ + minVideoPts: 20 * 1000 + 100, + minAudioPts: 20 * 1000 + 100, + maxVideoPts: 30 * 1000 + 100, + maxAudioPts: 30 * 1000 + 100, + duration: 10, + uri: '2.ts' + }] + }; + duration = Playlist.duration(playlist, 0, 2); + + equal(duration, 20.1, 'used the PTS-based interval'); + + duration = Playlist.duration(playlist, 0, 3); + equal(duration, 30.1, 'used the PTS-based interval'); + }); + + test('uses the largest continuous available PTS ranges', function() { + var playlist = { + mediaSequence: 0, + segments: [{ + minVideoPts: 0, + minAudioPts: 0, + maxVideoPts: 10 * 1000, + maxAudioPts: 10 * 1000, + uri: '0.ts' + }, { + duration: 10, + uri: '1.ts' + }, { + // starts 0.5s earlier than the previous segment indicates + minVideoPts: 19.5 * 1000, + minAudioPts: 19.5 * 1000, + maxVideoPts: 29.5 * 1000, + maxAudioPts: 29.5 * 1000, + uri: '2.ts' + }, { + duration: 10, + uri: '3.ts' + }, { + // ... but by the last segment, there is actual 0.5s more + // content than duration indicates + minVideoPts: 40.5 * 1000, + minAudioPts: 40.5 * 1000, + maxVideoPts: 50.5 * 1000, + maxAudioPts: 50.5 * 1000, + uri: '4.ts' + }] + }; + + equal(Playlist.duration(playlist, 0, 5), + 50.5, + 'calculated across the larger PTS interval'); + }); + + test('counts the time between segments as part of the earlier segment\'s duration', function() { + var duration = Playlist.duration({ + mediaSequence: 0, + endList: true, + segments: [{ + minVideoPts: 0, + minAudioPts: 0, + maxVideoPts: 1 * 10 * 1000, + maxAudioPts: 1 * 10 * 1000, + uri: '0.ts' + }, { + minVideoPts: 1 * 10 * 1000 + 100, + minAudioPts: 1 * 10 * 1000 + 100, + maxVideoPts: 2 * 10 * 1000 + 100, + maxAudioPts: 2 * 10 * 1000 + 100, + duration: 10, + uri: '1.ts' + }] + }, 0, 1); + + equal(duration, (1 * 10 * 1000 + 100) * 0.001, 'included the segment gap'); + }); + + test('accounts for discontinuities', function() { + var duration = Playlist.duration({ + mediaSequence: 0, + endList: true, + discontinuityStarts: [1], segments: [{ minVideoPts: 0, minAudioPts: 0, @@ -130,6 +324,76 @@ equal(duration, 10 + 10, 'handles discontinuities'); }); + test('does not count ending segment gaps across a discontinuity', function() { + var duration = Playlist.duration({ + mediaSequence: 0, + discontinuityStarts: [1], + endList: true, + segments: [{ + minVideoPts: 0, + minAudioPts: 0, + maxVideoPts: 1 * 10 * 1000, + maxAudioPts: 1 * 10 * 1000, + uri: '0.ts' + }, { + discontinuity: true, + minVideoPts: 1 * 10 * 1000 + 100, + minAudioPts: 1 * 10 * 1000 + 100, + maxVideoPts: 2 * 10 * 1000 + 100, + maxAudioPts: 2 * 10 * 1000 + 100, + duration: 10, + uri: '1.ts' + }] + }, 0, 1); + + equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); + }); + + test('trailing duration on the final segment can be excluded', function() { + var duration = Playlist.duration({ + mediaSequence: 0, + endList: true, + segments: [{ + minVideoPts: 0, + minAudioPts: 0, + maxVideoPts: 1 * 10 * 1000, + maxAudioPts: 1 * 10 * 1000, + uri: '0.ts' + }, { + minVideoPts: 1 * 10 * 1000 + 100, + minAudioPts: 1 * 10 * 1000 + 100, + maxVideoPts: 2 * 10 * 1000 + 100, + maxAudioPts: 2 * 10 * 1000 + 100, + duration: 10, + uri: '1.ts' + }] + }, 0, 1, false); + + equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); + }); + + test('a non-positive length interval has zero duration', function() { + var playlist = { + mediaSequence: 0, + discontinuityStarts: [1], + segments: [{ + duration: 10, + uri: '0.ts' + }, { + discontinuity: true, + duration: 10, + uri: '1.ts' + }] + }; + + equal(Playlist.duration(playlist, 0, 0), 0, 'zero-length duration is zero'); + equal(Playlist.duration(playlist, 0, 0, false), 0, 'zero-length duration is zero'); + equal(Playlist.duration(playlist, 0, -1), 0, 'negative length duration is zero'); + equal(Playlist.duration(playlist, 2, 1, false), 0, 'negative length duration is zero'); + }); + + module('Playlist Seekable'); + test('calculates seekable time ranges from the available segments', function() { var playlist = { mediaSequence: 0, diff --git a/test/videojs-hls_test.js b/test/videojs-hls_test.js index c865bb010..6b6c3b90d 100644 --- a/test/videojs-hls_test.js +++ b/test/videojs-hls_test.js @@ -97,15 +97,13 @@ var var MockSegmentParser; if (tags === undefined) { - tags = []; + tags = [{ pts: 0, bytes: new Uint8Array(1) }]; } MockSegmentParser = function() { this.getFlvHeader = function() { return 'flv'; }; this.parseSegmentBinaryData = function() {}; - this.timestampOffset = 0; - this.mediaTimelineOffset = 0; this.flushTags = function() {}; this.tagsAvailable = function() { return tags.length; @@ -1287,30 +1285,32 @@ test('clears in-band cues ahead of current time on seek', function() { player.hls.segmentParser_.parseSegmentBinaryData = function() { // trigger a metadata event - if (events.length) { + while (events.length) { player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); } }; standardXHRResponse(requests.shift()); // media - tags.push({ pts: 10 * 1000, bytes: new Uint8Array(1) }); + tags.push({ pts: 0, bytes: new Uint8Array(1) }, + { pts: 10 * 1000, bytes: new Uint8Array(1) }); events.push({ - pts: 20 * 1000, + pts: 9.9 * 1000, data: new Uint8Array([]), frames: [{ id: 'TXXX', - value: 'cue 3' + value: 'cue 1' }] }); events.push({ - pts: 9.9 * 1000, + pts: 20 * 1000, data: new Uint8Array([]), frames: [{ id: 'TXXX', - value: 'cue 1' + value: 'cue 3' }] }); standardXHRResponse(requests.shift()); // segment 0 - tags.push({ pts: 20 * 1000, bytes: new Uint8Array(1) }); + tags.push({ pts: 10 * 1000 + 1, bytes: new Uint8Array(1) }, + { pts: 20 * 1000, bytes: new Uint8Array(1) }); events.push({ pts: 19.9 * 1000, data: new Uint8Array([]), @@ -1323,12 +1323,12 @@ test('clears in-band cues ahead of current time on seek', function() { standardXHRResponse(requests.shift()); // segment 1 track = player.textTracks()[0]; - equal(track.cues.length, 2, 'added the cues'); + equal(track.cues.length, 3, 'added the cues'); // seek into segment 1 player.currentTime(11); player.trigger('seeking'); - equal(track.cues.length, 1, 'removed a cue'); + equal(track.cues.length, 1, 'removed later cues'); equal(track.cues[0].startTime, 9.9, 'retained the earlier cue'); }); @@ -1342,9 +1342,6 @@ test('translates ID3 PTS values to cue media timeline positions', function() { openMediaSource(player); player.hls.segmentParser_.parseSegmentBinaryData = function() { - // setup the timestamp offset - this.timestampOffset = tags[0].pts; - // trigger a metadata event player.hls.segmentParser_.metadataStream.trigger('data', { pts: 5 * 1000, @@ -1373,9 +1370,6 @@ test('translates ID3 PTS values across discontinuities', function() { openMediaSource(player); player.hls.segmentParser_.parseSegmentBinaryData = function() { - if (this.timestampOffset === null) { - this.timestampOffset = tags[0].pts; - } // trigger a metadata event if (events.length) { player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); @@ -1393,7 +1387,6 @@ test('translates ID3 PTS values across discontinuities', function() { '1.ts\n'); // segment 0 starts at PTS 14000 and has a cue point at 15000 - player.hls.segmentParser_.timestampOffset = 14 * 1000; tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }, { pts: 24 * 1000, bytes: new Uint8Array(1) }); events.push({ @@ -2010,6 +2003,48 @@ test('continues playing after seek to discontinuity', function() { strictEqual(aborts, 1, 'cleared the segment buffer on a seek'); }); +test('seeking does not fail when targeted between segments', function() { + var tags = [], currentTime, segmentUrl; + videojs.Hls.SegmentParser = mockSegmentParser(tags); + player.src({ + src: 'media.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + openMediaSource(player); + + // mock out the currentTime callbacks + player.hls.el().vjs_setProperty = function(property, value) { + if (property === 'currentTime') { + currentTime = value; + } + }; + player.hls.el().vjs_getProperty = function(property) { + if (property === 'currentTime') { + return currentTime; + } + }; + + standardXHRResponse(requests.shift()); // media + tags.push({ pts: 100, bytes: new Uint8Array(1) }, + { pts: 9 * 1000 + 100, bytes: new Uint8Array(1) }); + standardXHRResponse(requests.shift()); // segment 0 + player.hls.checkBuffer_(); + tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) }, + { pts: 20 * 1000 + 100, bytes: new Uint8Array(1) }); + segmentUrl = requests[0].url; + standardXHRResponse(requests.shift()); // segment 1 + + // seek to a time that is greater than the last tag in segment 0 but + // less than the first in segment 1 + player.currentTime(9.4); + equal(requests[0].url, segmentUrl, 'requested the later segment'); + + tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) }, + { pts: 20 * 1000 + 100, bytes: new Uint8Array(1) }); + standardXHRResponse(requests.shift()); // segment 1 + equal(player.currentTime(), 9.5, 'seeked to the later time'); +}); + test('resets the switching algorithm if a request times out', function() { player.src({ src: 'master.m3u8', @@ -2666,12 +2701,13 @@ test('treats invalid keys as a key request failure', function() { equal(bytes[0], 'flv', 'appended the flv header'); tags.length = 0; - tags.push({ pts: 1, bytes: new Uint8Array([1]) }); + tags.push({ pts: 2833, bytes: new Uint8Array([1]) }, + { pts: 4833, bytes: new Uint8Array([2]) }); // second segment request standardXHRResponse(requests.shift()); equal(bytes.length, 2, 'appended bytes'); - deepEqual(new Uint8Array([1]), bytes[1], 'skipped to the second segment'); + deepEqual(bytes[1], new Uint8Array([1, 2]), 'skipped to the second segment'); }); test('live stream should not call endOfStream', function(){