55303704 by David LaPalomento

Fix seeks between segments. Improve duration calculation. Closes #339

2 parents af92753d e81de1a7
Showing 49 changed files with 659 additions and 230 deletions
...@@ -2,7 +2,7 @@ CHANGELOG ...@@ -2,7 +2,7 @@ CHANGELOG
2 ========= 2 =========
3 3
4 ## HEAD (Unreleased) 4 ## HEAD (Unreleased)
5 _(none)_ 5 * Fix seeks between segments. Improve duration calculation. ([view](https://github.com/videojs/videojs-contrib-hls/pull/339))
6 6
7 -------------------- 7 --------------------
8 8
......
...@@ -375,7 +375,8 @@ ...@@ -375,7 +375,8 @@
375 375
376 // the manifest is empty until the parse stream begins delivering data 376 // the manifest is empty until the parse stream begins delivering data
377 this.manifest = { 377 this.manifest = {
378 allowCache: true 378 allowCache: true,
379 discontinuityStarts: []
379 }; 380 };
380 381
381 // update the manifest with the m3u8 entry from the parse stream 382 // update the manifest with the m3u8 entry from the parse stream
...@@ -513,6 +514,7 @@ ...@@ -513,6 +514,7 @@
513 }, 514 },
514 'discontinuity': function() { 515 'discontinuity': function() {
515 currentUri.discontinuity = true; 516 currentUri.discontinuity = true;
517 this.manifest.discontinuityStarts.push(uris.length);
516 }, 518 },
517 'targetduration': function() { 519 'targetduration': function() {
518 if (!isFinite(entry.duration) || entry.duration < 0) { 520 if (!isFinite(entry.duration) || entry.duration < 0) {
......
...@@ -431,11 +431,12 @@ ...@@ -431,11 +431,12 @@
431 for (i = 0; i < this.media_.segments.length; i++) { 431 for (i = 0; i < this.media_.segments.length; i++) {
432 time -= Playlist.duration(this.media_, 432 time -= Playlist.duration(this.media_,
433 this.media_.mediaSequence + i, 433 this.media_.mediaSequence + i,
434 this.media_.mediaSequence + i + 1); 434 this.media_.mediaSequence + i + 1,
435 false);
435 436
436 // HLS version 3 and lower round segment durations to the 437 // HLS version 3 and lower round segment durations to the
437 // nearest decimal integer. When the correct media index is 438 // nearest decimal integer. When the correct media index is
438 // ambiguous, prefer the lower one. 439 // ambiguous, prefer the higher one.
439 if (time <= 0) { 440 if (time <= 0) {
440 return i; 441 return i;
441 } 442 }
......
...@@ -5,7 +5,130 @@ ...@@ -5,7 +5,130 @@
5 'use strict'; 5 'use strict';
6 6
7 var DEFAULT_TARGET_DURATION = 10; 7 var DEFAULT_TARGET_DURATION = 10;
8 var duration, seekable, segmentsDuration; 8 var accumulateDuration, ascendingNumeric, duration, intervalDuration, rangeDuration, seekable;
9
10 // Array.sort comparator to sort numbers in ascending order
11 ascendingNumeric = function(left, right) {
12 return left - right;
13 };
14
15 /**
16 * Returns the media duration for the segments between a start and
17 * exclusive end index. The start and end parameters are interpreted
18 * as indices into the currently available segments. This method
19 * does not calculate durations for segments that have expired.
20 * @param playlist {object} a media playlist object
21 * @param start {number} an inclusive lower boundary for the
22 * segments to examine.
23 * @param end {number} an exclusive upper boundary for the segments
24 * to examine.
25 * @param includeTrailingTime {boolean} if false, the interval between
26 * the final segment and the subsequent segment will not be included
27 * in the result
28 * @return {number} the duration between the start index and end
29 * index in seconds.
30 */
31 accumulateDuration = function(playlist, start, end, includeTrailingTime) {
32 var
33 ranges = [],
34 rangeEnds = (playlist.discontinuityStarts || []).concat(end),
35 result = 0,
36 i;
37
38 // short circuit if start and end don't specify a non-empty range
39 // of segments
40 if (start >= end) {
41 return 0;
42 }
43
44 // create a range object for each discontinuity sequence
45 rangeEnds.sort(ascendingNumeric);
46 for (i = 0; i < rangeEnds.length; i++) {
47 if (rangeEnds[i] > start) {
48 ranges.push({ start: start, end: rangeEnds[i] });
49 i++;
50 break;
51 }
52 }
53 for (; i < rangeEnds.length; i++) {
54 // ignore times ranges later than end
55 if (rangeEnds[i] >= end) {
56 ranges.push({ start: rangeEnds[i - 1], end: end });
57 break;
58 }
59 ranges.push({ start: ranges[ranges.length - 1].end, end: rangeEnds[i] });
60 }
61
62 // add up the durations for each of the ranges
63 for (i = 0; i < ranges.length; i++) {
64 result += rangeDuration(playlist,
65 ranges[i],
66 i === ranges.length - 1 && includeTrailingTime);
67 }
68
69 return result;
70 };
71
72 /**
73 * Returns the duration of the specified range of segments. The
74 * range *must not* cross a discontinuity.
75 * @param playlist {object} a media playlist object
76 * @param range {object} an object that specifies a starting and
77 * ending index into the available segments.
78 * @param includeTrailingTime {boolean} if false, the interval between
79 * the final segment and the subsequent segment will not be included
80 * in the result
81 * @return {number} the duration of the range in seconds.
82 */
83 rangeDuration = function(playlist, range, includeTrailingTime) {
84 var
85 result = 0,
86 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION,
87 segment,
88 left, right;
89
90 // accumulate while searching for the earliest segment with
91 // available PTS information
92 for (left = range.start; left < range.end; left++) {
93 segment = playlist.segments[left];
94 if (segment.minVideoPts !== undefined) {
95 break;
96 }
97 result += segment.duration || targetDuration;
98 }
99
100 // see if there's enough information to include the trailing time
101 if (includeTrailingTime) {
102 segment = playlist.segments[range.end];
103 if (segment && segment.minVideoPts !== undefined) {
104 result += 0.001 *
105 (Math.min(segment.minVideoPts, segment.minAudioPts) -
106 Math.min(playlist.segments[left].minVideoPts,
107 playlist.segments[left].minAudioPts));
108 return result;
109 }
110 }
111
112 // do the same thing while finding the latest segment
113 for (right = range.end - 1; right >= left; right--) {
114 segment = playlist.segments[right];
115 if (segment.maxVideoPts !== undefined) {
116 break;
117 }
118 result += segment.duration || targetDuration;
119 }
120
121 // add in the PTS interval in seconds between them
122 if (right >= left) {
123 result += 0.001 *
124 (Math.max(playlist.segments[right].maxVideoPts,
125 playlist.segments[right].maxAudioPts) -
126 Math.min(playlist.segments[left].minVideoPts,
127 playlist.segments[left].minAudioPts));
128 }
129
130 return result;
131 };
9 132
10 /** 133 /**
11 * Calculate the media duration from the segments associated with a 134 * Calculate the media duration from the segments associated with a
...@@ -17,47 +140,28 @@ ...@@ -17,47 +140,28 @@
17 * boundary for the playlist. Defaults to 0. 140 * boundary for the playlist. Defaults to 0.
18 * @param endSequence {number} (optional) an exclusive upper boundary 141 * @param endSequence {number} (optional) an exclusive upper boundary
19 * for the playlist. Defaults to playlist length. 142 * for the playlist. Defaults to playlist length.
143 * @param includeTrailingTime {boolean} if false, the interval between
144 * the final segment and the subsequent segment will not be included
145 * in the result
20 * @return {number} the duration between the start index and end 146 * @return {number} the duration between the start index and end
21 * index. 147 * index.
22 */ 148 */
23 segmentsDuration = function(playlist, startSequence, endSequence) { 149 intervalDuration = function(playlist, startSequence, endSequence, includeTrailingTime) {
24 var targetDuration, i, j, segment, endSegment, expiredSegmentCount, result = 0; 150 var result = 0, targetDuration, expiredSegmentCount;
25 151
26 startSequence = startSequence || 0; 152 startSequence = startSequence || 0;
27 i = startSequence;
28 endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length; 153 endSequence = endSequence !== undefined ? endSequence : (playlist.segments || []).length;
29 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; 154 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
30 155
31 // estimate expired segment duration using the target duration 156 // estimate expired segment duration using the target duration
32 expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0); 157 expiredSegmentCount = Math.max(playlist.mediaSequence - startSequence, 0);
33 result += expiredSegmentCount * targetDuration; 158 result += expiredSegmentCount * targetDuration;
34 i += expiredSegmentCount;
35 159
36 // accumulate the segment durations into the result 160 // accumulate the segment durations into the result
37 for (; i < endSequence; i++) { 161 result += accumulateDuration(playlist,
38 segment = playlist.segments[i - playlist.mediaSequence]; 162 startSequence + expiredSegmentCount - playlist.mediaSequence,
39 163 endSequence - playlist.mediaSequence,
40 // when PTS values aren't available, use information from the playlist 164 includeTrailingTime);
41 if (segment.minVideoPts === undefined) {
42 result += segment.duration ||
43 targetDuration;
44 continue;
45 }
46
47 // find the last segment with PTS info and use that to calculate
48 // the interval duration
49 for(j = i; j < endSequence - 1; j++) {
50 endSegment = playlist.segments[j - playlist.mediaSequence + 1];
51 if (endSegment.maxVideoPts === undefined ||
52 endSegment.discontinuity) {
53 break;
54 }
55 }
56 endSegment = playlist.segments[j - playlist.mediaSequence];
57 result += (Math.max(endSegment.maxVideoPts, endSegment.maxAudioPts) -
58 Math.min(segment.minVideoPts, segment.minAudioPts)) * 0.001;
59 i = j;
60 }
61 165
62 return result; 166 return result;
63 }; 167 };
...@@ -72,14 +176,21 @@ ...@@ -72,14 +176,21 @@
72 * boundary for the playlist. Defaults to 0. 176 * boundary for the playlist. Defaults to 0.
73 * @param endSequence {number} (optional) an exclusive upper boundary 177 * @param endSequence {number} (optional) an exclusive upper boundary
74 * for the playlist. Defaults to playlist length. 178 * for the playlist. Defaults to playlist length.
179 * @param includeTrailingTime {boolean} (optional) if false, the interval between
180 * the final segment and the subsequent segment will not be included
181 * in the result
75 * @return {number} the duration between the start index and end 182 * @return {number} the duration between the start index and end
76 * index. 183 * index.
77 */ 184 */
78 duration = function(playlist, startSequence, endSequence) { 185 duration = function(playlist, startSequence, endSequence, includeTrailingTime) {
79 if (!playlist) { 186 if (!playlist) {
80 return 0; 187 return 0;
81 } 188 }
82 189
190 if (includeTrailingTime === undefined) {
191 includeTrailingTime = true;
192 }
193
83 // if a slice of the total duration is not requested, use 194 // if a slice of the total duration is not requested, use
84 // playlist-level duration indicators when they're present 195 // playlist-level duration indicators when they're present
85 if (startSequence === undefined && endSequence === undefined) { 196 if (startSequence === undefined && endSequence === undefined) {
...@@ -95,9 +206,10 @@ ...@@ -95,9 +206,10 @@
95 } 206 }
96 207
97 // calculate the total duration based on the segment durations 208 // calculate the total duration based on the segment durations
98 return segmentsDuration(playlist, 209 return intervalDuration(playlist,
99 startSequence, 210 startSequence,
100 endSequence); 211 endSequence,
212 includeTrailingTime);
101 }; 213 };
102 214
103 /** 215 /**
...@@ -119,8 +231,8 @@ ...@@ -119,8 +231,8 @@
119 return videojs.createTimeRange(0, duration(playlist)); 231 return videojs.createTimeRange(0, duration(playlist));
120 } 232 }
121 233
122 start = segmentsDuration(playlist, 0, playlist.mediaSequence); 234 start = intervalDuration(playlist, 0, playlist.mediaSequence);
123 end = start + segmentsDuration(playlist, 235 end = start + intervalDuration(playlist,
124 playlist.mediaSequence, 236 playlist.mediaSequence,
125 playlist.mediaSequence + playlist.segments.length); 237 playlist.mediaSequence + playlist.segments.length);
126 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; 238 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
......
...@@ -31,15 +31,6 @@ ...@@ -31,15 +31,6 @@
31 // allow in-band metadata to be observed 31 // allow in-band metadata to be observed
32 self.metadataStream = new MetadataStream(); 32 self.metadataStream = new MetadataStream();
33 33
34 this.mediaTimelineOffset = null;
35
36 // The first timestamp value encountered during parsing. This
37 // value can be used to determine the relative timing between
38 // frames and the start of the current timestamp sequence. It
39 // should be reset to null before parsing a segment with
40 // discontinuous timestamp values from previous segments.
41 self.timestampOffset = null;
42
43 // For information on the FLV format, see 34 // For information on the FLV format, see
44 // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf. 35 // http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf.
45 // Technically, this function returns the header and a metadata FLV tag 36 // Technically, this function returns the header and a metadata FLV tag
...@@ -360,13 +351,6 @@ ...@@ -360,13 +351,6 @@
360 // Skip past "optional" portion of PTS header 351 // Skip past "optional" portion of PTS header
361 offset += pesHeaderLength; 352 offset += pesHeaderLength;
362 353
363 // keep track of the earliest encounted PTS value so
364 // external parties can align timestamps across
365 // discontinuities
366 if (self.timestampOffset === null) {
367 self.timestampOffset = pts;
368 }
369
370 if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) { 354 if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
371 h264Stream.setNextTimeStamp(pts, 355 h264Stream.setNextTimeStamp(pts,
372 dts, 356 dts,
......
...@@ -260,7 +260,7 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { ...@@ -260,7 +260,7 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
260 // add a metadata cue whenever a metadata event is triggered during 260 // add a metadata cue whenever a metadata event is triggered during
261 // segment parsing 261 // segment parsing
262 metadataStream.on('data', function(metadata) { 262 metadataStream.on('data', function(metadata) {
263 var i, cue, frame, time, media, segmentOffset, hexDigit; 263 var i, hexDigit;
264 264
265 // create the metadata track if this is the first ID3 tag we've 265 // create the metadata track if this is the first ID3 tag we've
266 // seen 266 // seen
...@@ -276,19 +276,11 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { ...@@ -276,19 +276,11 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
276 } 276 }
277 } 277 }
278 278
279 // calculate the start time for the segment that is currently being parsed 279 // store this event for processing once the muxing has finished
280 media = tech.playlists.media(); 280 tech.segmentBuffer_[0].pendingMetadata.push({
281 segmentOffset = tech.playlists.expiredPreDiscontinuity_ + tech.playlists.expiredPostDiscontinuity_; 281 textTrack: textTrack,
282 segmentOffset += videojs.Hls.Playlist.duration(media, media.mediaSequence, media.mediaSequence + tech.mediaIndex); 282 metadata: metadata
283 283 });
284 // create cue points for all the ID3 frames in this metadata event
285 for (i = 0; i < metadata.frames.length; i++) {
286 frame = metadata.frames[i];
287 time = tech.segmentParser_.mediaTimelineOffset + ((metadata.pts - tech.segmentParser_.timestampOffset) * 0.001);
288 cue = new window.VTTCue(time, time, frame.value || frame.url || '');
289 cue.frame = frame;
290 textTrack.addCue(cue);
291 }
292 }); 284 });
293 285
294 // when seeking, clear out all cues ahead of the earliest position 286 // when seeking, clear out all cues ahead of the earliest position
...@@ -312,6 +304,30 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { ...@@ -312,6 +304,30 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
312 }); 304 });
313 }; 305 };
314 306
307 videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
308 var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time;
309 segmentOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist,
310 segmentInfo.playlist.mediaSequence,
311 segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex);
312 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
313 minPts = Math.min(segment.minVideoPts, segment.minAudioPts);
314
315 while (segmentInfo.pendingMetadata.length) {
316 metadata = segmentInfo.pendingMetadata[0].metadata;
317 textTrack = segmentInfo.pendingMetadata[0].textTrack;
318
319 // create cue points for all the ID3 frames in this metadata event
320 for (i = 0; i < metadata.frames.length; i++) {
321 frame = metadata.frames[i];
322 time = segmentOffset + ((metadata.pts - minPts) * 0.001);
323 cue = new window.VTTCue(time, time, frame.value || frame.url || '');
324 cue.frame = frame;
325 textTrack.addCue(cue);
326 }
327 segmentInfo.pendingMetadata.shift();
328 }
329 };
330
315 /** 331 /**
316 * Reset the mediaIndex if play() is called after the video has 332 * Reset the mediaIndex if play() is called after the video has
317 * ended. 333 * ended.
...@@ -780,7 +796,10 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) { ...@@ -780,7 +796,10 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, offset) {
780 // when a key is defined for this segment, the encrypted bytes 796 // when a key is defined for this segment, the encrypted bytes
781 encryptedBytes: null, 797 encryptedBytes: null,
782 // optionally, the decrypter that is unencrypting the segment 798 // optionally, the decrypter that is unencrypting the segment
783 decrypter: null 799 decrypter: null,
800 // metadata events discovered during muxing that need to be
801 // translated into cue points
802 pendingMetadata: []
784 }; 803 };
785 if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) { 804 if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) {
786 segmentInfo.encryptedBytes = new Uint8Array(this.response); 805 segmentInfo.encryptedBytes = new Uint8Array(this.response);
...@@ -870,20 +889,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -870,20 +889,6 @@ videojs.Hls.prototype.drainBuffer = function(event) {
870 } 889 }
871 890
872 event = event || {}; 891 event = event || {};
873 segmentOffset = this.playlists.expiredPreDiscontinuity_;
874 segmentOffset += this.playlists.expiredPostDiscontinuity_;
875 segmentOffset += videojs.Hls.Playlist.duration(playlist, playlist.mediaSequence, playlist.mediaSequence + mediaIndex);
876 segmentOffset *= 1000;
877
878 // if this segment starts is the start of a new discontinuity
879 // sequence, the segment parser's timestamp offset must be
880 // re-calculated
881 if (segment.discontinuity) {
882 this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001;
883 this.segmentParser_.timestampOffset = null;
884 } else if (this.segmentParser_.mediaTimelineOffset === null) {
885 this.segmentParser_.mediaTimelineOffset = segmentOffset * 0.001;
886 }
887 892
888 // transmux the segment data from MP2T to FLV 893 // transmux the segment data from MP2T to FLV
889 this.segmentParser_.parseSegmentBinaryData(bytes); 894 this.segmentParser_.parseSegmentBinaryData(bytes);
...@@ -904,20 +909,27 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -904,20 +909,27 @@ videojs.Hls.prototype.drainBuffer = function(event) {
904 tags.push(this.segmentParser_.getNextTag()); 909 tags.push(this.segmentParser_.getNextTag());
905 } 910 }
906 911
912 this.addCuesForMetadata_(segmentInfo);
907 this.updateDuration(this.playlists.media()); 913 this.updateDuration(this.playlists.media());
908 914
909 // if we're refilling the buffer after a seek, scan through the muxed 915 // if we're refilling the buffer after a seek, scan through the muxed
910 // FLV tags until we find the one that is closest to the desired 916 // FLV tags until we find the one that is closest to the desired
911 // playback time 917 // playback time
912 if (typeof offset === 'number') { 918 if (typeof offset === 'number') {
913 ptsTime = offset - segmentOffset + tags[0].pts; 919 // determine the offset within this segment we're seeking to
914 920 segmentOffset = this.playlists.expiredPostDiscontinuity_ + this.playlists.expiredPreDiscontinuity_;
915 while (tags[i].pts < ptsTime) { 921 segmentOffset += videojs.Hls.Playlist.duration(playlist,
922 playlist.mediaSequence,
923 playlist.mediaSequence + mediaIndex);
924 segmentOffset = offset - (segmentOffset * 1000);
925 ptsTime = segmentOffset + tags[0].pts;
926
927 while (tags[i + 1] && tags[i].pts < ptsTime) {
916 i++; 928 i++;
917 } 929 }
918 930
919 // tell the SWF where we will be seeking to 931 // tell the SWF the media position of the first tag we'll be delivering
920 this.el().vjs_setProperty('currentTime', (tags[i].pts - tags[0].pts + segmentOffset) * 0.001); 932 this.el().vjs_setProperty('currentTime', ((tags[i].pts - ptsTime + offset) * 0.001));
921 933
922 tags = tags.slice(i); 934 tags = tags.slice(i);
923 935
...@@ -1139,29 +1151,6 @@ videojs.Hls.getMediaIndexByTime = function() { ...@@ -1139,29 +1151,6 @@ videojs.Hls.getMediaIndexByTime = function() {
1139 }; 1151 };
1140 1152
1141 /** 1153 /**
1142 * Determine the current time in seconds in one playlist by a media index. This
1143 * function iterates through the segments of a playlist up to the specified index
1144 * and then returns the time up to that point.
1145 *
1146 * @param playlist {object} The playlist of the segments being searched.
1147 * @param mediaIndex {number} The index of the target segment in the playlist.
1148 * @returns {number} The current time to that point, or 0 if none appropriate.
1149 */
1150 videojs.Hls.prototype.getCurrentTimeByMediaIndex_ = function(playlist, mediaIndex) {
1151 var index, time = 0;
1152
1153 if (!playlist.segments || mediaIndex === 0) {
1154 return 0;
1155 }
1156
1157 for (index = 0; index < mediaIndex; index++) {
1158 time += playlist.segments[index].duration;
1159 }
1160
1161 return time;
1162 };
1163
1164 /**
1165 * A comparator function to sort two playlist object by bandwidth. 1154 * A comparator function to sort two playlist object by bandwidth.
1166 * @param left {object} a media playlist object 1155 * @param left {object} a media playlist object
1167 * @param right {object} a media playlist object 1156 * @param right {object} a media playlist object
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 10, 23 "targetDuration": 10,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -142,5 +142,6 @@ ...@@ -142,5 +142,6 @@
142 ], 142 ],
143 "targetDuration": 10, 143 "targetDuration": 10,
144 "endList": true, 144 "endList": true,
145 "discontinuitySequence": 0 145 "discontinuitySequence": 0,
146 } 146 "discontinuityStarts": []
147 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -14,5 +14,6 @@ ...@@ -14,5 +14,6 @@
14 ], 14 ],
15 "targetDuration": 10, 15 "targetDuration": 10,
16 "endList": true, 16 "endList": true,
17 "discontinuitySequence": 0 17 "discontinuitySequence": 0,
18 } 18 "discontinuityStarts": []
19 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -41,5 +41,6 @@ ...@@ -41,5 +41,6 @@
41 }, 41 },
42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" 42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
43 } 43 }
44 ] 44 ],
45 } 45 "discontinuityStarts": []
46 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -138,5 +138,6 @@ ...@@ -138,5 +138,6 @@
138 ], 138 ],
139 "targetDuration": 10, 139 "targetDuration": 10,
140 "endList": true, 140 "endList": true,
141 "discontinuitySequence": 0 141 "discontinuitySequence": 0,
142 } 142 "discontinuityStarts": []
143 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -14,5 +14,6 @@ ...@@ -14,5 +14,6 @@
14 ], 14 ],
15 "targetDuration": 10, 15 "targetDuration": 10,
16 "endList": true, 16 "endList": true,
17 "discontinuitySequence": 0 17 "discontinuitySequence": 0,
18 } 18 "discontinuityStarts": []
19 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 } 22 }
23 ], 23 ],
24 "targetDuration": 19, 24 "targetDuration": 19,
25 "endList": true 25 "endList": true,
26 "discontinuityStarts": [2]
26 } 27 }
......
...@@ -44,5 +44,6 @@ ...@@ -44,5 +44,6 @@
44 } 44 }
45 ], 45 ],
46 "targetDuration": 19, 46 "targetDuration": 19,
47 "endList": true 47 "endList": true,
48 "discontinuityStarts": [2, 4, 7]
48 } 49 }
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 10, 23 "targetDuration": 10,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -14,5 +14,6 @@ ...@@ -14,5 +14,6 @@
14 ], 14 ],
15 "targetDuration": 10, 15 "targetDuration": 10,
16 "endList": true, 16 "endList": true,
17 "discontinuitySequence": 0 17 "discontinuitySequence": 0,
18 } 18 "discontinuityStarts": []
19 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -29,5 +29,6 @@ ...@@ -29,5 +29,6 @@
29 ], 29 ],
30 "targetDuration": 10, 30 "targetDuration": 10,
31 "endList": true, 31 "endList": true,
32 "discontinuitySequence": 0 32 "discontinuitySequence": 0,
33 } 33 "discontinuityStarts": []
34 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -41,5 +41,6 @@ ...@@ -41,5 +41,6 @@
41 }, 41 },
42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001" 42 "uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
43 } 43 }
44 ] 44 ],
45 } 45 "discontinuityStarts": []
46 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
2 "allowCache": true, 2 "allowCache": true,
3 "mediaSequence": 7794, 3 "mediaSequence": 7794,
4 "discontinuitySequence": 0, 4 "discontinuitySequence": 0,
5 "discontinuityStarts": [],
5 "segments": [ 6 "segments": [
6 { 7 {
7 "duration": 2.833, 8 "duration": 2.833,
......
...@@ -30,5 +30,6 @@ ...@@ -30,5 +30,6 @@
30 ], 30 ],
31 "targetDuration": 10, 31 "targetDuration": 10,
32 "endList": true, 32 "endList": true,
33 "discontinuitySequence": 0 33 "discontinuitySequence": 0,
34 } 34 "discontinuityStarts": []
35 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -9,5 +9,6 @@ ...@@ -9,5 +9,6 @@
9 ], 9 ],
10 "targetDuration": 8, 10 "targetDuration": 8,
11 "endList": true, 11 "endList": true,
12 "discontinuitySequence": 0 12 "discontinuitySequence": 0,
13 } 13 "discontinuityStarts": []
14 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -142,5 +142,6 @@ ...@@ -142,5 +142,6 @@
142 ], 142 ],
143 "targetDuration": 10, 143 "targetDuration": 10,
144 "endList": true, 144 "endList": true,
145 "discontinuitySequence": 0 145 "discontinuitySequence": 0,
146 } 146 "discontinuityStarts": []
147 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -14,5 +14,6 @@ ...@@ -14,5 +14,6 @@
14 ], 14 ],
15 "targetDuration": 10, 15 "targetDuration": 10,
16 "endList": true, 16 "endList": true,
17 "discontinuitySequence": 0 17 "discontinuitySequence": 0,
18 } 18 "discontinuityStarts": []
19 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -29,5 +29,6 @@ ...@@ -29,5 +29,6 @@
29 ], 29 ],
30 "targetDuration": 10, 30 "targetDuration": 10,
31 "endList": true, 31 "endList": true,
32 "discontinuitySequence": 0 32 "discontinuitySequence": 0,
33 } 33 "discontinuityStarts": []
34 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -141,5 +141,6 @@ ...@@ -141,5 +141,6 @@
141 } 141 }
142 ], 142 ],
143 "endList": true, 143 "endList": true,
144 "discontinuitySequence": 0 144 "discontinuitySequence": 0,
145 } 145 "discontinuityStarts": []
146 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -17,5 +17,6 @@ ...@@ -17,5 +17,6 @@
17 } 17 }
18 ], 18 ],
19 "targetDuration": 8, 19 "targetDuration": 8,
20 "discontinuitySequence": 0 20 "discontinuitySequence": 0,
21 } 21 "discontinuityStarts": []
22 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -40,5 +40,6 @@ ...@@ -40,5 +40,6 @@
40 } 40 }
41 ], 41 ],
42 "targetDuration": 10, 42 "targetDuration": 10,
43 "discontinuitySequence": 0 43 "discontinuitySequence": 0,
44 } 44 "discontinuityStarts": []
45 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -8,5 +8,6 @@ ...@@ -8,5 +8,6 @@
8 } 8 }
9 ], 9 ],
10 "endList": true, 10 "endList": true,
11 "discontinuitySequence": 0 11 "discontinuitySequence": 0,
12 } 12 "discontinuityStarts": []
13 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -25,5 +25,6 @@ ...@@ -25,5 +25,6 @@
25 ], 25 ],
26 "targetDuration": 10, 26 "targetDuration": 10,
27 "endList": true, 27 "endList": true,
28 "discontinuitySequence": 0 28 "discontinuitySequence": 0,
29 } 29 "discontinuityStarts": []
30 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -9,5 +9,6 @@ ...@@ -9,5 +9,6 @@
9 ], 9 ],
10 "targetDuration": 10, 10 "targetDuration": 10,
11 "endList": true, 11 "endList": true,
12 "discontinuitySequence": 0 12 "discontinuitySequence": 0,
13 } 13 "discontinuityStarts": []
14 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -41,5 +41,6 @@ ...@@ -41,5 +41,6 @@
41 }, 41 },
42 "uri": "media3.m3u8" 42 "uri": "media3.m3u8"
43 } 43 }
44 ] 44 ],
45 } 45 "discontinuityStarts": []
46 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 10, 23 "targetDuration": 10,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -12,5 +12,6 @@ ...@@ -12,5 +12,6 @@
12 } 12 }
13 ], 13 ],
14 "targetDuration": 10, 14 "targetDuration": 10,
15 "discontinuitySequence": 0 15 "discontinuitySequence": 0,
16 } 16 "discontinuityStarts": []
17 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -18,5 +18,6 @@ ...@@ -18,5 +18,6 @@
18 ], 18 ],
19 "targetDuration": 10, 19 "targetDuration": 10,
20 "endList": true, 20 "endList": true,
21 "discontinuitySequence": 0 21 "discontinuitySequence": 0,
22 } 22 "discontinuityStarts": []
23 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -19,5 +19,6 @@ ...@@ -19,5 +19,6 @@
19 "duration": 10 19 "duration": 10
20 } 20 }
21 ], 21 ],
22 "discontinuitySequence": 0 22 "discontinuitySequence": 0,
23 } 23 "discontinuityStarts": []
24 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -142,5 +142,6 @@ ...@@ -142,5 +142,6 @@
142 ], 142 ],
143 "targetDuration": 10, 143 "targetDuration": 10,
144 "endList": true, 144 "endList": true,
145 "discontinuitySequence": 0 145 "discontinuitySequence": 0,
146 } 146 "discontinuityStarts": []
147 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -10,5 +10,6 @@ ...@@ -10,5 +10,6 @@
10 ], 10 ],
11 "targetDuration": 8, 11 "targetDuration": 8,
12 "endList": true, 12 "endList": true,
13 "discontinuitySequence": 0 13 "discontinuitySequence": 0,
14 } 14 "discontinuityStarts": []
15 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -10,5 +10,6 @@ ...@@ -10,5 +10,6 @@
10 { 10 {
11 "uri": "media1.m3u8" 11 "uri": "media1.m3u8"
12 } 12 }
13 ] 13 ],
14 } 14 "discontinuityStarts": []
15 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 8, 23 "targetDuration": 8,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -10,5 +10,6 @@ ...@@ -10,5 +10,6 @@
10 ], 10 ],
11 "targetDuration": 10, 11 "targetDuration": 10,
12 "endList": true, 12 "endList": true,
13 "discontinuitySequence": 0 13 "discontinuitySequence": 0,
14 } 14 "discontinuityStarts": []
15 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,5 +22,6 @@ ...@@ -22,5 +22,6 @@
22 ], 22 ],
23 "targetDuration": 10, 23 "targetDuration": 10,
24 "endList": true, 24 "endList": true,
25 "discontinuitySequence": 0 25 "discontinuitySequence": 0,
26 } 26 "discontinuityStarts": []
27 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
3 'use strict'; 3 'use strict';
4 var Playlist = videojs.Hls.Playlist; 4 var Playlist = videojs.Hls.Playlist;
5 5
6 module('Playlist Utilities'); 6 module('Playlist Duration');
7 7
8 test('total duration for live playlists is Infinity', function() { 8 test('total duration for live playlists is Infinity', function() {
9 var duration = Playlist.duration({ 9 var duration = Playlist.duration({
...@@ -16,7 +16,9 @@ ...@@ -16,7 +16,9 @@
16 equal(duration, Infinity, 'duration is infinity'); 16 equal(duration, Infinity, 'duration is infinity');
17 }); 17 });
18 18
19 test('interval duration accounts for media sequences', function() { 19 module('Playlist Interval Duration');
20
21 test('accounts for media sequences', function() {
20 var duration = Playlist.duration({ 22 var duration = Playlist.duration({
21 mediaSequence: 10, 23 mediaSequence: 10,
22 endList: true, 24 endList: true,
...@@ -38,7 +40,7 @@ ...@@ -38,7 +40,7 @@
38 equal(duration, 14 * 10, 'duration includes dropped segments'); 40 equal(duration, 14 * 10, 'duration includes dropped segments');
39 }); 41 });
40 42
41 test('interval duration uses PTS values when available', function() { 43 test('uses PTS values when available', function() {
42 var duration = Playlist.duration({ 44 var duration = Playlist.duration({
43 mediaSequence: 0, 45 mediaSequence: 0,
44 endList: true, 46 endList: true,
...@@ -67,52 +69,244 @@ ...@@ -67,52 +69,244 @@
67 equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values'); 69 equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values');
68 }); 70 });
69 71
70 test('interval duration works when partial PTS information is available', function() { 72 test('works when partial PTS information is available', function() {
71 var firstInterval, secondInterval, duration = Playlist.duration({ 73 var duration = Playlist.duration({
72 mediaSequence: 0, 74 mediaSequence: 0,
73 endList: true, 75 endList: true,
74 segments: [{ 76 segments: [{
75 minVideoPts: 1, 77 minVideoPts: 1,
76 minAudioPts: 2, 78 minAudioPts: 2,
77 maxVideoPts: 1 * 10 * 1000 + 1, 79 maxVideoPts: 10 * 1000 + 1,
78 80
79 // intentionally less duration than video 81 // intentionally less duration than video
80 // the max stream duration should be used 82 // the max stream duration should be used
81 maxAudioPts: 1 * 10 * 1000 + 1, 83 maxAudioPts: 10 * 1000 + 1,
84 uri: '0.ts'
85 }, {
86 duration: 9,
87 uri: '1.ts'
88 }, {
89 duration: 10,
90 uri: '2.ts'
91 }, {
92 duration: 10,
93 minVideoPts: 30 * 1000 + 7,
94 minAudioPts: 30 * 1000 + 10,
95 maxVideoPts: 40 * 1000 + 1,
96 maxAudioPts: 40 * 1000 + 2,
97 uri: '3.ts'
98 }, {
99 duration: 10,
100 maxVideoPts: 50 * 1000 + 1,
101 maxAudioPts: 50 * 1000 + 2,
102 uri: '4.ts'
103 }]
104 }, 0, 5);
105
106 equal(duration,
107 ((50 * 1000 + 2) - 1) * 0.001,
108 'calculated with mixed intervals');
109 });
110
111 test('ignores segments before the start', function() {
112 var duration = Playlist.duration({
113 mediaSequence: 0,
114 segments: [{
115 duration: 10,
82 uri: '0.ts' 116 uri: '0.ts'
83 }, { 117 }, {
84 duration: 10, 118 duration: 10,
85 uri: '1.ts' 119 uri: '1.ts'
86 }, { 120 }, {
87 duration: 10, 121 duration: 10,
88 minVideoPts: 2 * 10 * 1000 + 7, 122 uri: '2.ts'
89 minAudioPts: 2 * 10 * 1000 + 10, 123 }]
90 maxVideoPts: 3 * 10 * 1000 + 1, 124 }, 1, 3);
91 maxAudioPts: 3 * 10 * 1000 + 2, 125
126 equal(duration, 10 + 10, 'ignored the first segment');
127 });
128
129 test('ignores discontinuity sequences earlier than the start', function() {
130 var duration = Playlist.duration({
131 mediaSequence: 0,
132 discontinuityStarts: [1, 3],
133 segments: [{
134 minVideoPts: 0,
135 minAudioPts: 0,
136 maxVideoPts: 10 * 1000,
137 maxAudioPts: 10 * 1000,
138 uri: '0.ts'
139 }, {
140 discontinuity: true,
141 duration: 9,
142 uri: '1.ts'
143 }, {
144 duration: 10,
92 uri: '2.ts' 145 uri: '2.ts'
93 }, { 146 }, {
147 discontinuity: true,
94 duration: 10, 148 duration: 10,
95 maxVideoPts: 4 * 10 * 1000 + 1,
96 maxAudioPts: 4 * 10 * 1000 + 2,
97 uri: '3.ts' 149 uri: '3.ts'
98 }] 150 }]
99 }, 0, 4); 151 }, 2, 4);
152
153 equal(duration, 10 + 10, 'excluded the earlier segments');
154 });
100 155
101 firstInterval = (1 * 10 * 1000 + 1) - 1; 156 test('ignores discontinuity sequences later than the end', function() {
102 firstInterval *= 0.001; 157 var duration = Playlist.duration({
103 secondInterval = (4 * 10 * 1000 + 2) - (2 * 10 * 1000 + 7); 158 mediaSequence: 0,
104 secondInterval *= 0.001; 159 discontinuityStarts: [1, 3],
160 segments: [{
161 minVideoPts: 0,
162 minAudioPts: 0,
163 maxVideoPts: 10 * 1000,
164 maxAudioPts: 10 * 1000,
165 uri: '0.ts'
166 }, {
167 discontinuity: true,
168 duration: 9,
169 uri: '1.ts'
170 }, {
171 duration: 10,
172 uri: '2.ts'
173 }, {
174 discontinuity: true,
175 duration: 10,
176 uri: '3.ts'
177 }]
178 }, 0, 2);
105 179
106 equal(duration, firstInterval + 10 + secondInterval, 'calculated with mixed intervals'); 180 equal(duration, 19, 'excluded the later segments');
107 }); 181 });
108 182
109 test('interval duration accounts for discontinuities', function() { 183 test('handles trailing segments without PTS information', function() {
110 var duration = Playlist.duration({ 184 var duration = Playlist.duration({
111 mediaSequence: 0, 185 mediaSequence: 0,
112 endList: true, 186 endList: true,
113 segments: [{ 187 segments: [{
114 minVideoPts: 0, 188 minVideoPts: 0,
115 minAudioPts: 0, 189 minAudioPts: 0,
190 maxVideoPts: 10 * 1000,
191 maxAudioPts: 10 * 1000,
192 uri: '0.ts'
193 }, {
194 duration: 9,
195 uri: '1.ts'
196 }, {
197 duration: 10,
198 uri: '2.ts'
199 }, {
200 minVideoPts: 29.5 * 1000,
201 minAudioPts: 29.5 * 1000,
202 maxVideoPts: 39.5 * 1000,
203 maxAudioPts: 39.5 * 1000,
204 uri: '3.ts'
205 }]
206 }, 0, 3);
207
208 equal(duration, 29.5, 'calculated duration');
209 });
210
211 test('uses PTS intervals when the start and end segment have them', function() {
212 var playlist, duration;
213 playlist = {
214 mediaSequence: 0,
215 segments: [{
216 minVideoPts: 0,
217 minAudioPts: 0,
218 maxVideoPts: 10 * 1000,
219 maxAudioPts: 10 * 1000,
220 uri: '0.ts'
221 }, {
222 duration: 9,
223 uri: '1.ts'
224 },{
225 minVideoPts: 20 * 1000 + 100,
226 minAudioPts: 20 * 1000 + 100,
227 maxVideoPts: 30 * 1000 + 100,
228 maxAudioPts: 30 * 1000 + 100,
229 duration: 10,
230 uri: '2.ts'
231 }]
232 };
233 duration = Playlist.duration(playlist, 0, 2);
234
235 equal(duration, 20.1, 'used the PTS-based interval');
236
237 duration = Playlist.duration(playlist, 0, 3);
238 equal(duration, 30.1, 'used the PTS-based interval');
239 });
240
241 test('uses the largest continuous available PTS ranges', function() {
242 var playlist = {
243 mediaSequence: 0,
244 segments: [{
245 minVideoPts: 0,
246 minAudioPts: 0,
247 maxVideoPts: 10 * 1000,
248 maxAudioPts: 10 * 1000,
249 uri: '0.ts'
250 }, {
251 duration: 10,
252 uri: '1.ts'
253 }, {
254 // starts 0.5s earlier than the previous segment indicates
255 minVideoPts: 19.5 * 1000,
256 minAudioPts: 19.5 * 1000,
257 maxVideoPts: 29.5 * 1000,
258 maxAudioPts: 29.5 * 1000,
259 uri: '2.ts'
260 }, {
261 duration: 10,
262 uri: '3.ts'
263 }, {
264 // ... but by the last segment, there is actual 0.5s more
265 // content than duration indicates
266 minVideoPts: 40.5 * 1000,
267 minAudioPts: 40.5 * 1000,
268 maxVideoPts: 50.5 * 1000,
269 maxAudioPts: 50.5 * 1000,
270 uri: '4.ts'
271 }]
272 };
273
274 equal(Playlist.duration(playlist, 0, 5),
275 50.5,
276 'calculated across the larger PTS interval');
277 });
278
279 test('counts the time between segments as part of the earlier segment\'s duration', function() {
280 var duration = Playlist.duration({
281 mediaSequence: 0,
282 endList: true,
283 segments: [{
284 minVideoPts: 0,
285 minAudioPts: 0,
286 maxVideoPts: 1 * 10 * 1000,
287 maxAudioPts: 1 * 10 * 1000,
288 uri: '0.ts'
289 }, {
290 minVideoPts: 1 * 10 * 1000 + 100,
291 minAudioPts: 1 * 10 * 1000 + 100,
292 maxVideoPts: 2 * 10 * 1000 + 100,
293 maxAudioPts: 2 * 10 * 1000 + 100,
294 duration: 10,
295 uri: '1.ts'
296 }]
297 }, 0, 1);
298
299 equal(duration, (1 * 10 * 1000 + 100) * 0.001, 'included the segment gap');
300 });
301
302 test('accounts for discontinuities', function() {
303 var duration = Playlist.duration({
304 mediaSequence: 0,
305 endList: true,
306 discontinuityStarts: [1],
307 segments: [{
308 minVideoPts: 0,
309 minAudioPts: 0,
116 maxVideoPts: 1 * 10 * 1000, 310 maxVideoPts: 1 * 10 * 1000,
117 maxAudioPts: 1 * 10 * 1000, 311 maxAudioPts: 1 * 10 * 1000,
118 uri: '0.ts' 312 uri: '0.ts'
...@@ -130,6 +324,76 @@ ...@@ -130,6 +324,76 @@
130 equal(duration, 10 + 10, 'handles discontinuities'); 324 equal(duration, 10 + 10, 'handles discontinuities');
131 }); 325 });
132 326
327 test('does not count ending segment gaps across a discontinuity', function() {
328 var duration = Playlist.duration({
329 mediaSequence: 0,
330 discontinuityStarts: [1],
331 endList: true,
332 segments: [{
333 minVideoPts: 0,
334 minAudioPts: 0,
335 maxVideoPts: 1 * 10 * 1000,
336 maxAudioPts: 1 * 10 * 1000,
337 uri: '0.ts'
338 }, {
339 discontinuity: true,
340 minVideoPts: 1 * 10 * 1000 + 100,
341 minAudioPts: 1 * 10 * 1000 + 100,
342 maxVideoPts: 2 * 10 * 1000 + 100,
343 maxAudioPts: 2 * 10 * 1000 + 100,
344 duration: 10,
345 uri: '1.ts'
346 }]
347 }, 0, 1);
348
349 equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap');
350 });
351
352 test('trailing duration on the final segment can be excluded', function() {
353 var duration = Playlist.duration({
354 mediaSequence: 0,
355 endList: true,
356 segments: [{
357 minVideoPts: 0,
358 minAudioPts: 0,
359 maxVideoPts: 1 * 10 * 1000,
360 maxAudioPts: 1 * 10 * 1000,
361 uri: '0.ts'
362 }, {
363 minVideoPts: 1 * 10 * 1000 + 100,
364 minAudioPts: 1 * 10 * 1000 + 100,
365 maxVideoPts: 2 * 10 * 1000 + 100,
366 maxAudioPts: 2 * 10 * 1000 + 100,
367 duration: 10,
368 uri: '1.ts'
369 }]
370 }, 0, 1, false);
371
372 equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap');
373 });
374
375 test('a non-positive length interval has zero duration', function() {
376 var playlist = {
377 mediaSequence: 0,
378 discontinuityStarts: [1],
379 segments: [{
380 duration: 10,
381 uri: '0.ts'
382 }, {
383 discontinuity: true,
384 duration: 10,
385 uri: '1.ts'
386 }]
387 };
388
389 equal(Playlist.duration(playlist, 0, 0), 0, 'zero-length duration is zero');
390 equal(Playlist.duration(playlist, 0, 0, false), 0, 'zero-length duration is zero');
391 equal(Playlist.duration(playlist, 0, -1), 0, 'negative length duration is zero');
392 equal(Playlist.duration(playlist, 2, 1, false), 0, 'negative length duration is zero');
393 });
394
395 module('Playlist Seekable');
396
133 test('calculates seekable time ranges from the available segments', function() { 397 test('calculates seekable time ranges from the available segments', function() {
134 var playlist = { 398 var playlist = {
135 mediaSequence: 0, 399 mediaSequence: 0,
......
...@@ -97,15 +97,13 @@ var ...@@ -97,15 +97,13 @@ var
97 var MockSegmentParser; 97 var MockSegmentParser;
98 98
99 if (tags === undefined) { 99 if (tags === undefined) {
100 tags = []; 100 tags = [{ pts: 0, bytes: new Uint8Array(1) }];
101 } 101 }
102 MockSegmentParser = function() { 102 MockSegmentParser = function() {
103 this.getFlvHeader = function() { 103 this.getFlvHeader = function() {
104 return 'flv'; 104 return 'flv';
105 }; 105 };
106 this.parseSegmentBinaryData = function() {}; 106 this.parseSegmentBinaryData = function() {};
107 this.timestampOffset = 0;
108 this.mediaTimelineOffset = 0;
109 this.flushTags = function() {}; 107 this.flushTags = function() {};
110 this.tagsAvailable = function() { 108 this.tagsAvailable = function() {
111 return tags.length; 109 return tags.length;
...@@ -1287,30 +1285,32 @@ test('clears in-band cues ahead of current time on seek', function() { ...@@ -1287,30 +1285,32 @@ test('clears in-band cues ahead of current time on seek', function() {
1287 1285
1288 player.hls.segmentParser_.parseSegmentBinaryData = function() { 1286 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1289 // trigger a metadata event 1287 // trigger a metadata event
1290 if (events.length) { 1288 while (events.length) {
1291 player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); 1289 player.hls.segmentParser_.metadataStream.trigger('data', events.shift());
1292 } 1290 }
1293 }; 1291 };
1294 standardXHRResponse(requests.shift()); // media 1292 standardXHRResponse(requests.shift()); // media
1295 tags.push({ pts: 10 * 1000, bytes: new Uint8Array(1) }); 1293 tags.push({ pts: 0, bytes: new Uint8Array(1) },
1294 { pts: 10 * 1000, bytes: new Uint8Array(1) });
1296 events.push({ 1295 events.push({
1297 pts: 20 * 1000, 1296 pts: 9.9 * 1000,
1298 data: new Uint8Array([]), 1297 data: new Uint8Array([]),
1299 frames: [{ 1298 frames: [{
1300 id: 'TXXX', 1299 id: 'TXXX',
1301 value: 'cue 3' 1300 value: 'cue 1'
1302 }] 1301 }]
1303 }); 1302 });
1304 events.push({ 1303 events.push({
1305 pts: 9.9 * 1000, 1304 pts: 20 * 1000,
1306 data: new Uint8Array([]), 1305 data: new Uint8Array([]),
1307 frames: [{ 1306 frames: [{
1308 id: 'TXXX', 1307 id: 'TXXX',
1309 value: 'cue 1' 1308 value: 'cue 3'
1310 }] 1309 }]
1311 }); 1310 });
1312 standardXHRResponse(requests.shift()); // segment 0 1311 standardXHRResponse(requests.shift()); // segment 0
1313 tags.push({ pts: 20 * 1000, bytes: new Uint8Array(1) }); 1312 tags.push({ pts: 10 * 1000 + 1, bytes: new Uint8Array(1) },
1313 { pts: 20 * 1000, bytes: new Uint8Array(1) });
1314 events.push({ 1314 events.push({
1315 pts: 19.9 * 1000, 1315 pts: 19.9 * 1000,
1316 data: new Uint8Array([]), 1316 data: new Uint8Array([]),
...@@ -1323,12 +1323,12 @@ test('clears in-band cues ahead of current time on seek', function() { ...@@ -1323,12 +1323,12 @@ test('clears in-band cues ahead of current time on seek', function() {
1323 standardXHRResponse(requests.shift()); // segment 1 1323 standardXHRResponse(requests.shift()); // segment 1
1324 1324
1325 track = player.textTracks()[0]; 1325 track = player.textTracks()[0];
1326 equal(track.cues.length, 2, 'added the cues'); 1326 equal(track.cues.length, 3, 'added the cues');
1327 1327
1328 // seek into segment 1 1328 // seek into segment 1
1329 player.currentTime(11); 1329 player.currentTime(11);
1330 player.trigger('seeking'); 1330 player.trigger('seeking');
1331 equal(track.cues.length, 1, 'removed a cue'); 1331 equal(track.cues.length, 1, 'removed later cues');
1332 equal(track.cues[0].startTime, 9.9, 'retained the earlier cue'); 1332 equal(track.cues[0].startTime, 9.9, 'retained the earlier cue');
1333 }); 1333 });
1334 1334
...@@ -1342,9 +1342,6 @@ test('translates ID3 PTS values to cue media timeline positions', function() { ...@@ -1342,9 +1342,6 @@ test('translates ID3 PTS values to cue media timeline positions', function() {
1342 openMediaSource(player); 1342 openMediaSource(player);
1343 1343
1344 player.hls.segmentParser_.parseSegmentBinaryData = function() { 1344 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1345 // setup the timestamp offset
1346 this.timestampOffset = tags[0].pts;
1347
1348 // trigger a metadata event 1345 // trigger a metadata event
1349 player.hls.segmentParser_.metadataStream.trigger('data', { 1346 player.hls.segmentParser_.metadataStream.trigger('data', {
1350 pts: 5 * 1000, 1347 pts: 5 * 1000,
...@@ -1373,9 +1370,6 @@ test('translates ID3 PTS values across discontinuities', function() { ...@@ -1373,9 +1370,6 @@ test('translates ID3 PTS values across discontinuities', function() {
1373 openMediaSource(player); 1370 openMediaSource(player);
1374 1371
1375 player.hls.segmentParser_.parseSegmentBinaryData = function() { 1372 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1376 if (this.timestampOffset === null) {
1377 this.timestampOffset = tags[0].pts;
1378 }
1379 // trigger a metadata event 1373 // trigger a metadata event
1380 if (events.length) { 1374 if (events.length) {
1381 player.hls.segmentParser_.metadataStream.trigger('data', events.shift()); 1375 player.hls.segmentParser_.metadataStream.trigger('data', events.shift());
...@@ -1393,7 +1387,6 @@ test('translates ID3 PTS values across discontinuities', function() { ...@@ -1393,7 +1387,6 @@ test('translates ID3 PTS values across discontinuities', function() {
1393 '1.ts\n'); 1387 '1.ts\n');
1394 1388
1395 // segment 0 starts at PTS 14000 and has a cue point at 15000 1389 // segment 0 starts at PTS 14000 and has a cue point at 15000
1396 player.hls.segmentParser_.timestampOffset = 14 * 1000;
1397 tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }, 1390 tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) },
1398 { pts: 24 * 1000, bytes: new Uint8Array(1) }); 1391 { pts: 24 * 1000, bytes: new Uint8Array(1) });
1399 events.push({ 1392 events.push({
...@@ -2010,6 +2003,48 @@ test('continues playing after seek to discontinuity', function() { ...@@ -2010,6 +2003,48 @@ test('continues playing after seek to discontinuity', function() {
2010 strictEqual(aborts, 1, 'cleared the segment buffer on a seek'); 2003 strictEqual(aborts, 1, 'cleared the segment buffer on a seek');
2011 }); 2004 });
2012 2005
2006 test('seeking does not fail when targeted between segments', function() {
2007 var tags = [], currentTime, segmentUrl;
2008 videojs.Hls.SegmentParser = mockSegmentParser(tags);
2009 player.src({
2010 src: 'media.m3u8',
2011 type: 'application/vnd.apple.mpegurl'
2012 });
2013 openMediaSource(player);
2014
2015 // mock out the currentTime callbacks
2016 player.hls.el().vjs_setProperty = function(property, value) {
2017 if (property === 'currentTime') {
2018 currentTime = value;
2019 }
2020 };
2021 player.hls.el().vjs_getProperty = function(property) {
2022 if (property === 'currentTime') {
2023 return currentTime;
2024 }
2025 };
2026
2027 standardXHRResponse(requests.shift()); // media
2028 tags.push({ pts: 100, bytes: new Uint8Array(1) },
2029 { pts: 9 * 1000 + 100, bytes: new Uint8Array(1) });
2030 standardXHRResponse(requests.shift()); // segment 0
2031 player.hls.checkBuffer_();
2032 tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) },
2033 { pts: 20 * 1000 + 100, bytes: new Uint8Array(1) });
2034 segmentUrl = requests[0].url;
2035 standardXHRResponse(requests.shift()); // segment 1
2036
2037 // seek to a time that is greater than the last tag in segment 0 but
2038 // less than the first in segment 1
2039 player.currentTime(9.4);
2040 equal(requests[0].url, segmentUrl, 'requested the later segment');
2041
2042 tags.push({ pts: 9.5 * 1000 + 100, bytes: new Uint8Array(1) },
2043 { pts: 20 * 1000 + 100, bytes: new Uint8Array(1) });
2044 standardXHRResponse(requests.shift()); // segment 1
2045 equal(player.currentTime(), 9.5, 'seeked to the later time');
2046 });
2047
2013 test('resets the switching algorithm if a request times out', function() { 2048 test('resets the switching algorithm if a request times out', function() {
2014 player.src({ 2049 player.src({
2015 src: 'master.m3u8', 2050 src: 'master.m3u8',
...@@ -2666,12 +2701,13 @@ test('treats invalid keys as a key request failure', function() { ...@@ -2666,12 +2701,13 @@ test('treats invalid keys as a key request failure', function() {
2666 equal(bytes[0], 'flv', 'appended the flv header'); 2701 equal(bytes[0], 'flv', 'appended the flv header');
2667 2702
2668 tags.length = 0; 2703 tags.length = 0;
2669 tags.push({ pts: 1, bytes: new Uint8Array([1]) }); 2704 tags.push({ pts: 2833, bytes: new Uint8Array([1]) },
2705 { pts: 4833, bytes: new Uint8Array([2]) });
2670 // second segment request 2706 // second segment request
2671 standardXHRResponse(requests.shift()); 2707 standardXHRResponse(requests.shift());
2672 2708
2673 equal(bytes.length, 2, 'appended bytes'); 2709 equal(bytes.length, 2, 'appended bytes');
2674 deepEqual(new Uint8Array([1]), bytes[1], 'skipped to the second segment'); 2710 deepEqual(bytes[1], new Uint8Array([1, 2]), 'skipped to the second segment');
2675 }); 2711 });
2676 2712
2677 test('live stream should not call endOfStream', function(){ 2713 test('live stream should not call endOfStream', function(){
......