d2c53be3 by David LaPalomento

Merge pull request #411 from dmlap/next-segment-calculation

Determine the segment to load by looking at buffered
2 parents e3c7a1bc 0de3d793
1 (function(window) { 1 (function(window) {
2 var textRange = function(range, i) {
3 return range.start(i) + '-' + range.end(i);
4 };
2 var module = { 5 var module = {
3 hexDump: function(data) { 6 hexDump: function(data) {
4 var 7 var
...@@ -26,6 +29,13 @@ ...@@ -26,6 +29,13 @@
26 }, 29 },
27 tagDump: function(tag) { 30 tagDump: function(tag) {
28 return module.hexDump(tag.bytes); 31 return module.hexDump(tag.bytes);
32 },
33 textRanges: function(ranges) {
34 var result = '', i;
35 for (i = 0; i < ranges.length; i++) {
36 result += textRange(ranges, i) + ' ';
37 }
38 return result;
29 } 39 }
30 }; 40 };
31 41
......
...@@ -2,13 +2,7 @@ ...@@ -2,13 +2,7 @@
2 * playlist-loader 2 * playlist-loader
3 * 3 *
4 * A state machine that manages the loading, caching, and updating of 4 * A state machine that manages the loading, caching, and updating of
5 * M3U8 playlists. When tracking a live playlist, loaders will keep 5 * M3U8 playlists.
6 * track of the duration of content that expired since the loader was
7 * initialized and when the current discontinuity sequence was
8 * encountered. A complete media timeline for a live playlist with
9 * expiring segments looks like this:
10 *
11 * |-- expired --|-- segments --|
12 * 6 *
13 */ 7 */
14 (function(window, videojs) { 8 (function(window, videojs) {
...@@ -16,7 +10,6 @@ ...@@ -16,7 +10,6 @@
16 var 10 var
17 resolveUrl = videojs.Hls.resolveUrl, 11 resolveUrl = videojs.Hls.resolveUrl,
18 xhr = videojs.Hls.xhr, 12 xhr = videojs.Hls.xhr,
19 Playlist = videojs.Hls.Playlist,
20 mergeOptions = videojs.mergeOptions, 13 mergeOptions = videojs.mergeOptions,
21 14
22 /** 15 /**
...@@ -158,14 +151,6 @@ ...@@ -158,14 +151,6 @@
158 // initialize the loader state 151 // initialize the loader state
159 loader.state = 'HAVE_NOTHING'; 152 loader.state = 'HAVE_NOTHING';
160 153
161 // The total duration of all segments that expired and have been
162 // removed from the current playlist, in seconds. This property
163 // should always be zero for non-live playlists. In a live
164 // playlist, this is the total amount of time that has been
165 // removed from the stream since the playlist loader began
166 // tracking it.
167 loader.expired_ = 0;
168
169 // capture the prototype dispose function 154 // capture the prototype dispose function
170 dispose = this.dispose; 155 dispose = this.dispose;
171 156
...@@ -187,20 +172,20 @@ ...@@ -187,20 +172,20 @@
187 * active media playlist. When called with a single argument, 172 * active media playlist. When called with a single argument,
188 * triggers the playlist loader to asynchronously switch to the 173 * triggers the playlist loader to asynchronously switch to the
189 * specified media playlist. Calling this method while the 174 * specified media playlist. Calling this method while the
190 * loader is in the HAVE_NOTHING or HAVE_MASTER states causes an 175 * loader is in the HAVE_NOTHING causes an error to be emitted
191 * error to be emitted but otherwise has no effect. 176 * but otherwise has no effect.
192 * @param playlist (optional) {object} the parsed media playlist 177 * @param playlist (optional) {object} the parsed media playlist
193 * object to switch to 178 * object to switch to
194 */ 179 */
195 loader.media = function(playlist) { 180 loader.media = function(playlist) {
196 var mediaChange = false; 181 var startingState = loader.state, mediaChange;
197 // getter 182 // getter
198 if (!playlist) { 183 if (!playlist) {
199 return loader.media_; 184 return loader.media_;
200 } 185 }
201 186
202 // setter 187 // setter
203 if (loader.state === 'HAVE_NOTHING' || loader.state === 'HAVE_MASTER') { 188 if (loader.state === 'HAVE_NOTHING') {
204 throw new Error('Cannot switch media playlist from ' + loader.state); 189 throw new Error('Cannot switch media playlist from ' + loader.state);
205 } 190 }
206 191
...@@ -213,7 +198,7 @@ ...@@ -213,7 +198,7 @@
213 playlist = loader.master.playlists[playlist]; 198 playlist = loader.master.playlists[playlist];
214 } 199 }
215 200
216 mediaChange = playlist.uri !== loader.media_.uri; 201 mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri;
217 202
218 // switch to fully loaded playlists immediately 203 // switch to fully loaded playlists immediately
219 if (loader.master.playlists[playlist.uri].endList) { 204 if (loader.master.playlists[playlist.uri].endList) {
...@@ -258,7 +243,17 @@ ...@@ -258,7 +243,17 @@
258 withCredentials: withCredentials 243 withCredentials: withCredentials
259 }, function(error, request) { 244 }, function(error, request) {
260 haveMetadata(error, request, playlist.uri); 245 haveMetadata(error, request, playlist.uri);
246
247 if (error) {
248 return;
249 }
250
251 // fire loadedmetadata the first time a media playlist is loaded
252 if (startingState === 'HAVE_MASTER') {
253 loader.trigger('loadedmetadata');
254 } else {
261 loader.trigger('mediachange'); 255 loader.trigger('mediachange');
256 }
262 }); 257 });
263 }; 258 };
264 259
...@@ -320,19 +315,13 @@ ...@@ -320,19 +315,13 @@
320 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; 315 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i];
321 } 316 }
322 317
323 request = xhr({ 318 loader.trigger('loadedplaylist');
324 uri: resolveUrl(srcUrl, parser.manifest.playlists[0].uri), 319 if (!request) {
325 withCredentials: withCredentials 320 // no media playlist was specifically selected so start
326 }, function(error, request) { 321 // from the first listed one
327 // pass along the URL specified in the master playlist 322 loader.media(parser.manifest.playlists[0]);
328 haveMetadata(error,
329 request,
330 parser.manifest.playlists[0].uri);
331 if (!error) {
332 loader.trigger('loadedmetadata');
333 } 323 }
334 }); 324 return;
335 return loader.trigger('loadedplaylist');
336 } 325 }
337 326
338 // loaded a media playlist 327 // loaded a media playlist
...@@ -356,43 +345,10 @@ ...@@ -356,43 +345,10 @@
356 * @param update {object} the updated media playlist object 345 * @param update {object} the updated media playlist object
357 */ 346 */
358 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { 347 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
359 var expiredCount;
360
361 if (this.media_) {
362 expiredCount = update.mediaSequence - this.media_.mediaSequence;
363
364 // update the expired time count
365 this.expired_ += Playlist.duration(this.media_,
366 this.media_.mediaSequence,
367 update.mediaSequence);
368 }
369
370 this.media_ = this.master.playlists[update.uri]; 348 this.media_ = this.master.playlists[update.uri];
371 }; 349 };
372 350
373 /** 351 /**
374 * When switching variant playlists in a live stream, the player may
375 * discover that the new set of available segments is shifted in
376 * time relative to the old playlist. If that is the case, you can
377 * call this method to synchronize the playlist loader so that
378 * subsequent calls to getMediaIndexForTime_() return values
379 * appropriate for the new playlist.
380 *
381 * @param mediaIndex {integer} the index of the segment that will be
382 * the used to base timeline calculations on
383 * @param startTime {number} the media timeline position of the
384 * first moment of video data for the specified segment. That is,
385 * data from the specified segment will first be displayed when
386 * `currentTime` is equal to `startTime`.
387 */
388 PlaylistLoader.prototype.updateTimelineOffset = function(mediaIndex, startingTime) {
389 var segmentOffset = Playlist.duration(this.media_,
390 this.media_.mediaSequence,
391 this.media_.mediaSequence + mediaIndex);
392 this.expired_ = startingTime - segmentOffset;
393 };
394
395 /**
396 * Determine the index of the segment that contains a specified 352 * Determine the index of the segment that contains a specified
397 * playback position in the current media playlist. Early versions 353 * playback position in the current media playlist. Early versions
398 * of the HLS specification require segment durations to be rounded 354 * of the HLS specification require segment durations to be rounded
...@@ -411,7 +367,7 @@ ...@@ -411,7 +367,7 @@
411 * closest playback position that is currently available. 367 * closest playback position that is currently available.
412 */ 368 */
413 PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { 369 PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) {
414 var i; 370 var i, j, segment, targetDuration;
415 371
416 if (!this.media_) { 372 if (!this.media_) {
417 return 0; 373 return 0;
...@@ -419,28 +375,61 @@ ...@@ -419,28 +375,61 @@
419 375
420 // when the requested position is earlier than the current set of 376 // when the requested position is earlier than the current set of
421 // segments, return the earliest segment index 377 // segments, return the earliest segment index
422 time -= this.expired_;
423 if (time < 0) { 378 if (time < 0) {
424 return 0; 379 return 0;
425 } 380 }
426 381
427 for (i = 0; i < this.media_.segments.length; i++) { 382 // 1) Walk backward until we find the latest segment with timeline
428 time -= Playlist.duration(this.media_, 383 // information that is earlier than `time`
429 this.media_.mediaSequence + i, 384 targetDuration = this.media_.targetDuration || 10;
430 this.media_.mediaSequence + i + 1, 385 i = this.media_.segments.length;
431 false); 386 while (i--) {
387 segment = this.media_.segments[i];
388 if (segment.end !== undefined && segment.end <= time) {
389 time -= segment.end;
390 break;
391 }
392 if (segment.start !== undefined && segment.start < time) {
393
394 if (segment.end !== undefined && segment.end > time) {
395 // we've found the target segment exactly
396 return i;
397 }
432 398
433 // HLS version 3 and lower round segment durations to the 399 time -= segment.start;
434 // nearest decimal integer. When the correct media index is 400 time -= segment.duration || targetDuration;
435 // ambiguous, prefer the higher one. 401 if (time < 0) {
436 if (time <= 0) { 402 // the segment with start information is also our best guess
403 // for the momment
437 return i; 404 return i;
438 } 405 }
406 break;
407 }
408 }
409 i++;
410
411 // 2) Walk forward, testing each segment to see if `time` falls within it
412 for (j = i; j < this.media_.segments.length; j++) {
413 segment = this.media_.segments[j];
414 time -= segment.duration || targetDuration;
415
416 if (time < 0) {
417 return j;
418 }
419
420 // 2a) If we discover a segment that has timeline information
421 // before finding the result segment, the playlist information
422 // must have been inaccurate. Start a binary search for the
423 // segment which contains `time`. If the guess turns out to be
424 // incorrect, we'll have more info to work with next time.
425 if (segment.start !== undefined || segment.end !== undefined) {
426 return Math.floor((j - i) * 0.5);
427 }
439 } 428 }
440 429
441 // the playback position is outside the range of available 430 // the playback position is outside the range of available
442 // segments so return the last one 431 // segments so return the length
443 return this.media_.segments.length - 1; 432 return this.media_.segments.length;
444 }; 433 };
445 434
446 videojs.Hls.PlaylistLoader = PlaylistLoader; 435 videojs.Hls.PlaylistLoader = PlaylistLoader;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
5 'use strict'; 5 'use strict';
6 6
7 var DEFAULT_TARGET_DURATION = 10; 7 var DEFAULT_TARGET_DURATION = 10;
8 var accumulateDuration, ascendingNumeric, duration, intervalDuration, optionalMin, optionalMax, rangeDuration, seekable; 8 var duration, intervalDuration, optionalMin, optionalMax, seekable;
9 9
10 // Math.min that will return the alternative input if one of its 10 // Math.min that will return the alternative input if one of its
11 // parameters in undefined 11 // parameters in undefined
...@@ -23,170 +23,49 @@ ...@@ -23,170 +23,49 @@
23 return Math.max(left, right); 23 return Math.max(left, right);
24 }; 24 };
25 25
26 // Array.sort comparator to sort numbers in ascending order
27 ascendingNumeric = function(left, right) {
28 return left - right;
29 };
30
31 /**
32 * Returns the media duration for the segments between a start and
33 * exclusive end index. The start and end parameters are interpreted
34 * as indices into the currently available segments. This method
35 * does not calculate durations for segments that have expired.
36 * @param playlist {object} a media playlist object
37 * @param start {number} an inclusive lower boundary for the
38 * segments to examine.
39 * @param end {number} an exclusive upper boundary for the segments
40 * to examine.
41 * @param includeTrailingTime {boolean} if false, the interval between
42 * the final segment and the subsequent segment will not be included
43 * in the result
44 * @return {number} the duration between the start index and end
45 * index in seconds.
46 */
47 accumulateDuration = function(playlist, start, end, includeTrailingTime) {
48 var
49 ranges = [],
50 rangeEnds = (playlist.discontinuityStarts || []).concat(end),
51 result = 0,
52 i;
53
54 // short circuit if start and end don't specify a non-empty range
55 // of segments
56 if (start >= end) {
57 return 0;
58 }
59
60 // create a range object for each discontinuity sequence
61 rangeEnds.sort(ascendingNumeric);
62 for (i = 0; i < rangeEnds.length; i++) {
63 if (rangeEnds[i] > start) {
64 ranges.push({ start: start, end: rangeEnds[i] });
65 i++;
66 break;
67 }
68 }
69 for (; i < rangeEnds.length; i++) {
70 // ignore times ranges later than end
71 if (rangeEnds[i] >= end) {
72 ranges.push({ start: rangeEnds[i - 1], end: end });
73 break;
74 }
75 ranges.push({ start: ranges[ranges.length - 1].end, end: rangeEnds[i] });
76 }
77
78 // add up the durations for each of the ranges
79 for (i = 0; i < ranges.length; i++) {
80 result += rangeDuration(playlist,
81 ranges[i],
82 i === ranges.length - 1 && includeTrailingTime);
83 }
84
85 return result;
86 };
87
88 /**
89 * Returns the duration of the specified range of segments. The
90 * range *must not* cross a discontinuity.
91 * @param playlist {object} a media playlist object
92 * @param range {object} an object that specifies a starting and
93 * ending index into the available segments.
94 * @param includeTrailingTime {boolean} if false, the interval between
95 * the final segment and the subsequent segment will not be included
96 * in the result
97 * @return {number} the duration of the range in seconds.
98 */
99 rangeDuration = function(playlist, range, includeTrailingTime) {
100 var
101 result = 0,
102 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION,
103 segment,
104 left, right;
105
106 // accumulate while searching for the earliest segment with
107 // available PTS information
108 for (left = range.start; left < range.end; left++) {
109 segment = playlist.segments[left];
110 if (segment.minVideoPts !== undefined ||
111 segment.minAudioPts !== undefined) {
112 break;
113 }
114 result += segment.duration || targetDuration;
115 }
116
117 // see if there's enough information to include the trailing time
118 if (includeTrailingTime) {
119 segment = playlist.segments[range.end];
120 if (segment &&
121 (segment.minVideoPts !== undefined ||
122 segment.minAudioPts !== undefined)) {
123 result += 0.001 *
124 (optionalMin(segment.minVideoPts, segment.minAudioPts) -
125 optionalMin(playlist.segments[left].minVideoPts,
126 playlist.segments[left].minAudioPts));
127 return result;
128 }
129 }
130
131 // do the same thing while finding the latest segment
132 for (right = range.end - 1; right >= left; right--) {
133 segment = playlist.segments[right];
134 if (segment.maxVideoPts !== undefined ||
135 segment.maxAudioPts !== undefined) {
136 break;
137 }
138 result += segment.duration || targetDuration;
139 }
140
141 // add in the PTS interval in seconds between them
142 if (right >= left) {
143 result += 0.001 *
144 (optionalMax(playlist.segments[right].maxVideoPts,
145 playlist.segments[right].maxAudioPts) -
146 optionalMin(playlist.segments[left].minVideoPts,
147 playlist.segments[left].minAudioPts));
148 }
149
150 return result;
151 };
152
153 /** 26 /**
154 * Calculate the media duration from the segments associated with a 27 * Calculate the media duration from the segments associated with a
155 * playlist. The duration of a subinterval of the available segments 28 * playlist. The duration of a subinterval of the available segments
156 * may be calculated by specifying a start and end index. 29 * may be calculated by specifying an end index.
157 * 30 *
158 * @param playlist {object} a media playlist object 31 * @param playlist {object} a media playlist object
159 * @param startSequence {number} (optional) an inclusive lower
160 * boundary for the playlist. Defaults to 0.
161 * @param endSequence {number} (optional) an exclusive upper boundary 32 * @param endSequence {number} (optional) an exclusive upper boundary
162 * for the playlist. Defaults to playlist length. 33 * for the playlist. Defaults to playlist length.
163 * @param includeTrailingTime {boolean} if false, the interval between
164 * the final segment and the subsequent segment will not be included
165 * in the result
166 * @return {number} the duration between the start index and end 34 * @return {number} the duration between the start index and end
167 * index. 35 * index.
168 */ 36 */
169 intervalDuration = function(playlist, startSequence, endSequence, includeTrailingTime) { 37 intervalDuration = function(playlist, endSequence) {
170 var result = 0, targetDuration, expiredSegmentCount; 38 var result = 0, segment, targetDuration, i;
171 39
172 if (startSequence === undefined) {
173 startSequence = playlist.mediaSequence || 0;
174 }
175 if (endSequence === undefined) { 40 if (endSequence === undefined) {
176 endSequence = startSequence + (playlist.segments || []).length; 41 endSequence = playlist.mediaSequence + (playlist.segments || []).length;
42 }
43 if (endSequence < 0) {
44 return 0;
177 } 45 }
178 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; 46 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
179 47
180 // estimate expired segment duration using the target duration 48 i = endSequence - playlist.mediaSequence;
181 expiredSegmentCount = optionalMax(playlist.mediaSequence - startSequence, 0); 49 // if a start time is available for segment immediately following
182 result += expiredSegmentCount * targetDuration; 50 // the interval, use it
51 segment = playlist.segments[i];
52 // Walk backward until we find the latest segment with timeline
53 // information that is earlier than endSequence
54 if (segment && segment.start !== undefined) {
55 return segment.start;
56 }
57 while (i--) {
58 segment = playlist.segments[i];
59 if (segment.end !== undefined) {
60 return result + segment.end;
61 }
183 62
184 // accumulate the segment durations into the result 63 result += (segment.duration || targetDuration);
185 result += accumulateDuration(playlist,
186 startSequence + expiredSegmentCount - playlist.mediaSequence,
187 endSequence - playlist.mediaSequence,
188 includeTrailingTime);
189 64
65 if (segment.start !== undefined) {
66 return result + segment.start;
67 }
68 }
190 return result; 69 return result;
191 }; 70 };
192 71
...@@ -196,17 +75,16 @@ ...@@ -196,17 +75,16 @@
196 * timeline between those two indices. The total duration for live 75 * timeline between those two indices. The total duration for live
197 * playlists is always Infinity. 76 * playlists is always Infinity.
198 * @param playlist {object} a media playlist object 77 * @param playlist {object} a media playlist object
199 * @param startSequence {number} (optional) an inclusive lower 78 * @param endSequence {number} (optional) an exclusive upper
200 * boundary for the playlist. Defaults to 0. 79 * boundary for the playlist. Defaults to the playlist media
201 * @param endSequence {number} (optional) an exclusive upper boundary 80 * sequence number plus its length.
202 * for the playlist. Defaults to playlist length. 81 * @param includeTrailingTime {boolean} (optional) if false, the
203 * @param includeTrailingTime {boolean} (optional) if false, the interval between 82 * interval between the final segment and the subsequent segment
204 * the final segment and the subsequent segment will not be included 83 * will not be included in the result
205 * in the result
206 * @return {number} the duration between the start index and end 84 * @return {number} the duration between the start index and end
207 * index. 85 * index.
208 */ 86 */
209 duration = function(playlist, startSequence, endSequence, includeTrailingTime) { 87 duration = function(playlist, endSequence, includeTrailingTime) {
210 if (!playlist) { 88 if (!playlist) {
211 return 0; 89 return 0;
212 } 90 }
...@@ -217,7 +95,7 @@ ...@@ -217,7 +95,7 @@
217 95
218 // if a slice of the total duration is not requested, use 96 // if a slice of the total duration is not requested, use
219 // playlist-level duration indicators when they're present 97 // playlist-level duration indicators when they're present
220 if (startSequence === undefined && endSequence === undefined) { 98 if (endSequence === undefined) {
221 // if present, use the duration specified in the playlist 99 // if present, use the duration specified in the playlist
222 if (playlist.totalDuration) { 100 if (playlist.totalDuration) {
223 return playlist.totalDuration; 101 return playlist.totalDuration;
...@@ -231,7 +109,6 @@ ...@@ -231,7 +109,6 @@
231 109
232 // calculate the total duration based on the segment durations 110 // calculate the total duration based on the segment durations
233 return intervalDuration(playlist, 111 return intervalDuration(playlist,
234 startSequence,
235 endSequence, 112 endSequence,
236 includeTrailingTime); 113 includeTrailingTime);
237 }; 114 };
...@@ -248,7 +125,7 @@ ...@@ -248,7 +125,7 @@
248 * for seeking 125 * for seeking
249 */ 126 */
250 seekable = function(playlist) { 127 seekable = function(playlist) {
251 var start, end, liveBuffer, targetDuration, segment, pending, i; 128 var start, end;
252 129
253 // without segments, there are no seekable ranges 130 // without segments, there are no seekable ranges
254 if (!playlist.segments) { 131 if (!playlist.segments) {
...@@ -259,33 +136,14 @@ ...@@ -259,33 +136,14 @@
259 return videojs.createTimeRange(0, duration(playlist)); 136 return videojs.createTimeRange(0, duration(playlist));
260 } 137 }
261 138
262 start = 0;
263 end = intervalDuration(playlist,
264 playlist.mediaSequence,
265 playlist.mediaSequence + playlist.segments.length);
266 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
267
268 // live playlists should not expose three segment durations worth 139 // live playlists should not expose three segment durations worth
269 // of content from the end of the playlist 140 // of content from the end of the playlist
270 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3 141 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
271 if (!playlist.endList) { 142 start = intervalDuration(playlist, playlist.mediaSequence);
272 liveBuffer = targetDuration * 3; 143 end = intervalDuration(playlist,
273 // walk backward from the last available segment and track how 144 playlist.mediaSequence + playlist.segments.length);
274 // much media time has elapsed until three target durations have 145 end -= (playlist.targetDuration || DEFAULT_TARGET_DURATION) * 3;
275 // been traversed. if a segment is part of the interval being 146 end = Math.max(0, end);
276 // reported, subtract the overlapping portion of its duration
277 // from the result.
278 for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) {
279 segment = playlist.segments[i];
280 pending = optionalMin(duration(playlist,
281 playlist.mediaSequence + i,
282 playlist.mediaSequence + i + 1),
283 liveBuffer);
284 liveBuffer -= pending;
285 end -= pending;
286 }
287 }
288
289 return videojs.createTimeRange(start, end); 147 return videojs.createTimeRange(start, end);
290 }; 148 };
291 149
......
...@@ -15,7 +15,6 @@ var ...@@ -15,7 +15,6 @@ var
15 // the amount of time to wait between checking the state of the buffer 15 // the amount of time to wait between checking the state of the buffer
16 bufferCheckInterval = 500, 16 bufferCheckInterval = 500,
17 17
18 keyXhr,
19 keyFailed, 18 keyFailed,
20 resolveUrl; 19 resolveUrl;
21 20
...@@ -46,6 +45,8 @@ videojs.Hls = videojs.extend(Component, { ...@@ -46,6 +45,8 @@ videojs.Hls = videojs.extend(Component, {
46 this.tech_ = tech; 45 this.tech_ = tech;
47 this.source_ = options.source; 46 this.source_ = options.source;
48 this.mode_ = options.mode; 47 this.mode_ = options.mode;
48 // the segment info object for a segment that is in the process of
49 // being downloaded or processed
49 this.pendingSegment_ = null; 50 this.pendingSegment_ = null;
50 51
51 this.bytesReceived = 0; 52 this.bytesReceived = 0;
...@@ -61,9 +62,6 @@ videojs.Hls = videojs.extend(Component, { ...@@ -61,9 +62,6 @@ videojs.Hls = videojs.extend(Component, {
61 this.loadingState_ = 'meta'; 62 this.loadingState_ = 'meta';
62 } 63 }
63 64
64 // a queue of segments that need to be transmuxed and processed,
65 // and then fed to the source buffer
66 this.segmentBuffer_ = [];
67 // periodically check if new data needs to be downloaded or 65 // periodically check if new data needs to be downloaded or
68 // buffered data should be appended to the source buffer 66 // buffered data should be appended to the source buffer
69 this.startCheckingBuffer_(); 67 this.startCheckingBuffer_();
...@@ -71,6 +69,9 @@ videojs.Hls = videojs.extend(Component, { ...@@ -71,6 +69,9 @@ videojs.Hls = videojs.extend(Component, {
71 this.on(this.tech_, 'seeking', function() { 69 this.on(this.tech_, 'seeking', function() {
72 this.setCurrentTime(this.tech_.currentTime()); 70 this.setCurrentTime(this.tech_.currentTime());
73 }); 71 });
72 this.on(this.tech_, 'error', function() {
73 this.stopCheckingBuffer_();
74 });
74 75
75 this.on(this.tech_, 'play', this.play); 76 this.on(this.tech_, 'play', this.play);
76 } 77 }
...@@ -137,21 +138,10 @@ videojs.Hls.prototype.src = function(src) { ...@@ -137,21 +138,10 @@ videojs.Hls.prototype.src = function(src) {
137 } 138 }
138 139
139 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); 140 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ });
140 this.segmentBuffer_ = [];
141
142 // if the stream contains ID3 metadata, expose that as a metadata
143 // text track
144 //this.setupMetadataCueTranslation_();
145 141
146 // load the MediaSource into the player 142 // load the MediaSource into the player
147 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); 143 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
148 144
149 // The index of the next segment to be downloaded in the current
150 // media playlist. When the current media playlist is live with
151 // expiring segments, it may be a different value from the media
152 // sequence number for a segment.
153 this.mediaIndex = 0;
154
155 this.options_ = {}; 145 this.options_ = {};
156 if (this.source_.withCredentials !== undefined) { 146 if (this.source_.withCredentials !== undefined) {
157 this.options_.withCredentials = this.source_.withCredentials; 147 this.options_.withCredentials = this.source_.withCredentials;
...@@ -161,9 +151,6 @@ videojs.Hls.prototype.src = function(src) { ...@@ -161,9 +151,6 @@ videojs.Hls.prototype.src = function(src) {
161 this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials); 151 this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials);
162 152
163 this.playlists.on('loadedmetadata', function() { 153 this.playlists.on('loadedmetadata', function() {
164 var selectedPlaylist, loaderHandler, oldBitrate, newBitrate, segmentDuration,
165 segmentDlTime, threshold;
166
167 oldMediaPlaylist = this.playlists.media(); 154 oldMediaPlaylist = this.playlists.media();
168 155
169 // if this isn't a live video and preload permits, start 156 // if this isn't a live video and preload permits, start
...@@ -174,56 +161,10 @@ videojs.Hls.prototype.src = function(src) { ...@@ -174,56 +161,10 @@ videojs.Hls.prototype.src = function(src) {
174 this.loadingState_ = 'segments'; 161 this.loadingState_ = 'segments';
175 } 162 }
176 163
177 // the bandwidth estimate for the first segment is based on round
178 // trip time for the master playlist. the master playlist is
179 // almost always tiny so the round-trip time is dominated by
180 // latency and the computed bandwidth is much lower than
181 // steady-state. if the the downstream developer has a better way
182 // of detecting bandwidth and provided a number, use that instead.
183 if (this.bandwidth === undefined) {
184 // we're going to have to estimate initial bandwidth
185 // ourselves. scale the bandwidth estimate to account for the
186 // relatively high round-trip time from the master playlist.
187 this.setBandwidth({
188 bandwidth: this.playlists.bandwidth * 5
189 });
190 }
191
192 this.setupSourceBuffer_(); 164 this.setupSourceBuffer_();
193
194 selectedPlaylist = this.selectPlaylist();
195 oldBitrate = oldMediaPlaylist.attributes &&
196 oldMediaPlaylist.attributes.BANDWIDTH || 0;
197 newBitrate = selectedPlaylist.attributes &&
198 selectedPlaylist.attributes.BANDWIDTH || 0;
199 segmentDuration = oldMediaPlaylist.segments &&
200 oldMediaPlaylist.segments[this.mediaIndex].duration ||
201 oldMediaPlaylist.targetDuration;
202
203 segmentDlTime = (segmentDuration * newBitrate) / this.bandwidth;
204
205 if (!segmentDlTime) {
206 segmentDlTime = Infinity;
207 }
208
209 // this threshold is to account for having a high latency on the manifest
210 // request which is a somewhat small file.
211 threshold = 10;
212
213 if (newBitrate > oldBitrate && segmentDlTime <= threshold) {
214 this.playlists.media(selectedPlaylist);
215 loaderHandler = function() {
216 this.setupFirstPlay(); 165 this.setupFirstPlay();
217 this.fillBuffer(); 166 this.fillBuffer();
218 this.tech_.trigger('loadedmetadata'); 167 this.tech_.trigger('loadedmetadata');
219 this.playlists.off('loadedplaylist', loaderHandler);
220 }.bind(this);
221 this.playlists.on('loadedplaylist', loaderHandler);
222 } else {
223 this.setupFirstPlay();
224 this.fillBuffer();
225 this.tech_.trigger('loadedmetadata');
226 }
227 }.bind(this)); 168 }.bind(this));
228 169
229 this.playlists.on('error', function() { 170 this.playlists.on('error', function() {
...@@ -242,24 +183,20 @@ videojs.Hls.prototype.src = function(src) { ...@@ -242,24 +183,20 @@ videojs.Hls.prototype.src = function(src) {
242 var updatedPlaylist = this.playlists.media(); 183 var updatedPlaylist = this.playlists.media();
243 184
244 if (!updatedPlaylist) { 185 if (!updatedPlaylist) {
245 // do nothing before an initial media playlist has been activated 186 // select the initial variant
187 this.playlists.media(this.selectPlaylist());
246 return; 188 return;
247 } 189 }
248 190
249 this.updateDuration(this.playlists.media()); 191 this.updateDuration(this.playlists.media());
250 this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist);
251 oldMediaPlaylist = updatedPlaylist; 192 oldMediaPlaylist = updatedPlaylist;
252
253 this.fetchKeys_();
254 }.bind(this)); 193 }.bind(this));
255 194
256 this.playlists.on('mediachange', function() { 195 this.playlists.on('mediachange', function() {
257 // abort outstanding key requests and check if new keys need to be retrieved 196 this.tech_.trigger({
258 if (keyXhr) { 197 type: 'mediachange',
259 this.cancelKeyXhr(); 198 bubbles: true
260 } 199 });
261
262 this.tech_.trigger({ type: 'mediachange', bubbles: true });
263 }.bind(this)); 200 }.bind(this));
264 201
265 // do nothing if the tech has been disposed already 202 // do nothing if the tech has been disposed already
...@@ -271,26 +208,6 @@ videojs.Hls.prototype.src = function(src) { ...@@ -271,26 +208,6 @@ videojs.Hls.prototype.src = function(src) {
271 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); 208 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
272 }; 209 };
273 210
274 /* Returns the media index for the live point in the current playlist, and updates
275 the current time to go along with it.
276 */
277 videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) {
278 if (!selectedPlaylist.segments) {
279 return 0;
280 }
281
282 var tailIterator = selectedPlaylist.segments.length,
283 tailDuration = 0,
284 targetTail = (selectedPlaylist.targetDuration || 10) * 3;
285
286 while (tailDuration < targetTail && tailIterator > 0) {
287 tailDuration += selectedPlaylist.segments[tailIterator - 1].duration;
288 tailIterator--;
289 }
290
291 return tailIterator;
292 };
293
294 videojs.Hls.prototype.handleSourceOpen = function() { 211 videojs.Hls.prototype.handleSourceOpen = function() {
295 // Only attempt to create the source buffer if none already exist. 212 // Only attempt to create the source buffer if none already exist.
296 // handleSourceOpen is also called when we are "re-opening" a source buffer 213 // handleSourceOpen is also called when we are "re-opening" a source buffer
...@@ -310,6 +227,63 @@ videojs.Hls.prototype.handleSourceOpen = function() { ...@@ -310,6 +227,63 @@ videojs.Hls.prototype.handleSourceOpen = function() {
310 } 227 }
311 }; 228 };
312 229
230 // Returns the array of time range edge objects that were additively
231 // modified between two TimeRanges.
232 videojs.Hls.bufferedAdditions_ = function(original, update) {
233 var result = [], edges = [],
234 i, inOriginalRanges;
235
236 // if original or update are falsey, return an empty list of
237 // additions
238 if (!original || !update) {
239 return result;
240 }
241
242 // create a sorted array of time range start and end times
243 for (i = 0; i < original.length; i++) {
244 edges.push({ original: true, start: original.start(i) });
245 edges.push({ original: true, end: original.end(i) });
246 }
247 for (i = 0; i < update.length; i++) {
248 edges.push({ start: update.start(i) });
249 edges.push({ end: update.end(i) });
250 }
251 edges.sort(function(left, right) {
252 var leftTime, rightTime;
253 leftTime = left.start !== undefined ? left.start : left.end;
254 rightTime = right.start !== undefined ? right.start : right.end;
255
256 // when two times are equal, ensure the original edge covers the
257 // update
258 if (leftTime === rightTime) {
259 if (left.original) {
260 return left.start !== undefined ? -1 : 1;
261 }
262 return right.start !== undefined ? -1 : 1;
263 }
264 return leftTime - rightTime;
265 });
266
267 // filter out all time range edges that occur during a period that
268 // was already covered by `original`
269 inOriginalRanges = false;
270 for (i = 0; i < edges.length; i++) {
271 // if this is a transition point for `original`, track whether
272 // subsequent edges are additions
273 if (edges[i].original) {
274 inOriginalRanges = edges[i].start !== undefined;
275 continue;
276 }
277 // if we're in a time range that was in `original`, ignore this edge
278 if (inOriginalRanges) {
279 continue;
280 }
281 // this edge occurred outside the range of `original`
282 result.push(edges[i]);
283 }
284 return result;
285 };
286
313 videojs.Hls.prototype.setupSourceBuffer_ = function() { 287 videojs.Hls.prototype.setupSourceBuffer_ = function() {
314 var media = this.playlists.media(), mimeType; 288 var media = this.playlists.media(), mimeType;
315 289
...@@ -330,117 +304,42 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { ...@@ -330,117 +304,42 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
330 // transition the sourcebuffer to the ended state if we've hit the end of 304 // transition the sourcebuffer to the ended state if we've hit the end of
331 // the playlist 305 // the playlist
332 this.sourceBuffer.addEventListener('updateend', function() { 306 this.sourceBuffer.addEventListener('updateend', function() {
333 var segmentInfo = this.pendingSegment_, i, currentBuffered; 307 var segmentInfo = this.pendingSegment_, segment, currentBuffered, timelineUpdates;
334 308
335 this.pendingSegment_ = null; 309 this.pendingSegment_ = null;
336 310
337 if (this.duration() !== Infinity && 311 // if we've buffered to the end of the video, let the MediaSource know
338 this.mediaIndex === this.playlists.media().segments.length) {
339 this.mediaSource.endOfStream();
340 }
341
342 // When switching renditions or seeking, we may misjudge the media
343 // index to request to continue playback. Check after each append
344 // that a gap hasn't appeared in the buffered region and adjust
345 // the media index to fill it if necessary
346 if (this.tech_.buffered().length === 2 &&
347 segmentInfo.playlist === this.playlists.media()) {
348 i = this.tech_.buffered().length;
349 while (i--) {
350 if (this.tech_.currentTime() < this.tech_.buffered().start(i)) {
351 // found the misidentified segment's buffered time range
352 // adjust the media index to fill the gap
353 currentBuffered = this.findCurrentBuffered_(); 312 currentBuffered = this.findCurrentBuffered_();
354 this.playlists.updateTimelineOffset(segmentInfo.mediaIndex, this.tech_.buffered().start(i)); 313 if (currentBuffered.length && this.duration() === currentBuffered.end(0)) {
355 this.mediaIndex = this.playlists.getMediaIndexForTime_(currentBuffered.end(0) + 1); 314 this.mediaSource.endOfStream();
356 break;
357 }
358 }
359 }
360 }.bind(this));
361 };
362
363 // register event listeners to transform in-band metadata events into
364 // VTTCues on a text track
365 videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
366 var
367 metadataStream = this.segmentParser_.metadataStream,
368 textTrack;
369
370 // add a metadata cue whenever a metadata event is triggered during
371 // segment parsing
372 metadataStream.on('data', function(metadata) {
373 var i, hexDigit;
374
375 // create the metadata track if this is the first ID3 tag we've
376 // seen
377 if (!textTrack) {
378 textTrack = this.tech_.addTextTrack('metadata', 'Timed Metadata');
379
380 // build the dispatch type from the stream descriptor
381 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
382 textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase();
383 for (i = 0; i < metadataStream.descriptor.length; i++) {
384 hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2);
385 textTrack.inBandMetadataTrackDispatchType += hexDigit;
386 }
387 } 315 }
388 316
389 // store this event for processing once the muxing has finished 317 // stop here if the update errored or was aborted
390 this.tech_.segmentBuffer_[0].pendingMetadata.push({ 318 if (!segmentInfo) {
391 textTrack: textTrack,
392 metadata: metadata
393 });
394 }.bind(this));
395
396 // when seeking, clear out all cues ahead of the earliest position
397 // in the new segment. keep earlier cues around so they can still be
398 // programmatically inspected even though they've already fired
399 this.on(this.tech_, 'seeking', function() {
400 var media, startTime, i;
401 if (!textTrack) {
402 return; 319 return;
403 } 320 }
404 media = this.playlists.media();
405 startTime = this.tech_.playlists.expired_;
406 startTime += videojs.Hls.Playlist.duration(media,
407 media.mediaSequence,
408 media.mediaSequence + this.tech_.mediaIndex);
409 321
410 i = textTrack.cues.length; 322 // annotate the segment with any start and end time information
411 while (i--) { 323 // added by the media processing
412 if (textTrack.cues[i].startTime >= startTime) { 324 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
413 textTrack.removeCue(textTrack.cues[i]); 325 timelineUpdates = videojs.Hls.bufferedAdditions_(segmentInfo.buffered,
326 this.tech_.buffered());
327 timelineUpdates.forEach(function(update) {
328 if (update.start !== undefined) {
329 segment.start = update.start;
414 } 330 }
331 if (update.end !== undefined) {
332 segment.end = update.end;
415 } 333 }
416 }); 334 });
417 };
418
419 videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
420 var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time;
421 segmentOffset = this.playlists.expired_;
422 segmentOffset += videojs.Hls.Playlist.duration(segmentInfo.playlist,
423 segmentInfo.playlist.mediaSequence,
424 segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex);
425 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
426 minPts = Math.min(isFinite(segment.minVideoPts) ? segment.minVideoPts : Infinity,
427 isFinite(segment.minAudioPts) ? segment.minAudioPts : Infinity);
428 335
429 while (segmentInfo.pendingMetadata.length) { 336 if (timelineUpdates.length) {
430 metadata = segmentInfo.pendingMetadata[0].metadata; 337 this.updateDuration(segmentInfo.playlist);
431 textTrack = segmentInfo.pendingMetadata[0].textTrack;
432
433 // create cue points for all the ID3 frames in this metadata event
434 for (i = 0; i < metadata.frames.length; i++) {
435 frame = metadata.frames[i];
436 time = segmentOffset + ((metadata.pts - minPts) * 0.001);
437 cue = new window.VTTCue(time, time, frame.value || frame.url || '');
438 cue.frame = frame;
439 cue.pts_ = metadata.pts;
440 textTrack.addCue(cue);
441 }
442 segmentInfo.pendingMetadata.shift();
443 } 338 }
339
340 // check if it's time to download the next segment
341 this.checkBuffer_();
342 }.bind(this));
444 }; 343 };
445 344
446 /** 345 /**
...@@ -475,14 +374,13 @@ videojs.Hls.prototype.setupFirstPlay = function() { ...@@ -475,14 +374,13 @@ videojs.Hls.prototype.setupFirstPlay = function() {
475 }; 374 };
476 375
477 /** 376 /**
478 * Reset the mediaIndex if play() is called after the video has 377 * Begin playing the video.
479 * ended.
480 */ 378 */
481 videojs.Hls.prototype.play = function() { 379 videojs.Hls.prototype.play = function() {
482 this.loadingState_ = 'segments'; 380 this.loadingState_ = 'segments';
483 381
484 if (this.tech_.ended()) { 382 if (this.tech_.ended()) {
485 this.mediaIndex = 0; 383 this.tech_.setCurrentTime(0);
486 } 384 }
487 385
488 if (this.tech_.played().length === 0) { 386 if (this.tech_.played().length === 0) {
...@@ -519,23 +417,20 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { ...@@ -519,23 +417,20 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
519 return currentTime; 417 return currentTime;
520 } 418 }
521 419
522 // determine the requested segment
523 this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime);
524
525 // cancel outstanding requests and buffer appends 420 // cancel outstanding requests and buffer appends
526 this.cancelSegmentXhr(); 421 this.cancelSegmentXhr();
527 422
528 // abort outstanding key requests, if necessary 423 // abort outstanding key requests, if necessary
529 if (keyXhr) { 424 if (this.keyXhr_) {
530 keyXhr.aborted = true; 425 this.keyXhr_.aborted = true;
531 this.cancelKeyXhr(); 426 this.cancelKeyXhr();
532 } 427 }
533 428
534 // clear out any buffered segments 429 // clear out the segment being processed
535 this.segmentBuffer_ = []; 430 this.pendingSegment_ = null;
536 431
537 // begin filling the buffer at the new position 432 // begin filling the buffer at the new position
538 this.fillBuffer(currentTime * 1000); 433 this.fillBuffer(currentTime);
539 }; 434 };
540 435
541 videojs.Hls.prototype.duration = function() { 436 videojs.Hls.prototype.duration = function() {
...@@ -547,7 +442,7 @@ videojs.Hls.prototype.duration = function() { ...@@ -547,7 +442,7 @@ videojs.Hls.prototype.duration = function() {
547 }; 442 };
548 443
549 videojs.Hls.prototype.seekable = function() { 444 videojs.Hls.prototype.seekable = function() {
550 var currentSeekable, startOffset, media; 445 var media;
551 446
552 if (!this.playlists) { 447 if (!this.playlists) {
553 return videojs.createTimeRanges(); 448 return videojs.createTimeRanges();
...@@ -557,17 +452,7 @@ videojs.Hls.prototype.seekable = function() { ...@@ -557,17 +452,7 @@ videojs.Hls.prototype.seekable = function() {
557 return videojs.createTimeRanges(); 452 return videojs.createTimeRanges();
558 } 453 }
559 454
560 // report the seekable range relative to the earliest possible 455 return videojs.Hls.Playlist.seekable(media);
561 // position when the stream was first loaded
562 currentSeekable = videojs.Hls.Playlist.seekable(media);
563
564 if (!currentSeekable.length) {
565 return currentSeekable;
566 }
567
568 startOffset = this.playlists.expired_;
569 return videojs.createTimeRanges(startOffset,
570 startOffset + (currentSeekable.end(0) - currentSeekable.start(0)));
571 }; 456 };
572 457
573 /** 458 /**
...@@ -608,10 +493,10 @@ videojs.Hls.prototype.resetSrc_ = function() { ...@@ -608,10 +493,10 @@ videojs.Hls.prototype.resetSrc_ = function() {
608 }; 493 };
609 494
610 videojs.Hls.prototype.cancelKeyXhr = function() { 495 videojs.Hls.prototype.cancelKeyXhr = function() {
611 if (keyXhr) { 496 if (this.keyXhr_) {
612 keyXhr.onreadystatechange = null; 497 this.keyXhr_.onreadystatechange = null;
613 keyXhr.abort(); 498 this.keyXhr_.abort();
614 keyXhr = null; 499 this.keyXhr_ = null;
615 } 500 }
616 }; 501 };
617 502
...@@ -790,7 +675,7 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { ...@@ -790,7 +675,7 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() {
790 675
791 if (buffered && buffered.length) { 676 if (buffered && buffered.length) {
792 // Search for a range containing the play-head 677 // Search for a range containing the play-head
793 for (i = 0;i < buffered.length; i++) { 678 for (i = 0; i < buffered.length; i++) {
794 if (buffered.start(i) <= currentTime && 679 if (buffered.start(i) <= currentTime &&
795 buffered.end(i) >= currentTime) { 680 buffered.end(i) >= currentTime) {
796 ranges = videojs.createTimeRanges(buffered.start(i), buffered.end(i)); 681 ranges = videojs.createTimeRanges(buffered.start(i), buffered.end(i));
...@@ -810,16 +695,17 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { ...@@ -810,16 +695,17 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() {
810 * Determines whether there is enough video data currently in the buffer 695 * Determines whether there is enough video data currently in the buffer
811 * and downloads a new segment if the buffered time is less than the goal. 696 * and downloads a new segment if the buffered time is less than the goal.
812 * @param seekToTime (optional) {number} the offset into the downloaded segment 697 * @param seekToTime (optional) {number} the offset into the downloaded segment
813 * to seek to, in milliseconds 698 * to seek to, in seconds
814 */ 699 */
815 videojs.Hls.prototype.fillBuffer = function(seekToTime) { 700 videojs.Hls.prototype.fillBuffer = function(seekToTime) {
816 var 701 var
817 tech = this.tech_, 702 tech = this.tech_,
818 currentTime = tech.currentTime(), 703 currentTime = tech.currentTime(),
819 buffered = this.findCurrentBuffered_(), 704 currentBuffered = this.findCurrentBuffered_(),
820 bufferedTime = 0, 705 bufferedTime = 0,
706 mediaIndex = 0,
821 segment, 707 segment,
822 segmentUri; 708 segmentInfo;
823 709
824 // if preload is set to "none", do not download segments until playback is requested 710 // if preload is set to "none", do not download segments until playback is requested
825 if (this.loadingState_ !== 'segments') { 711 if (this.loadingState_ !== 'segments') {
...@@ -836,6 +722,11 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { ...@@ -836,6 +722,11 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) {
836 return; 722 return;
837 } 723 }
838 724
725 // wait until the buffer is up to date
726 if (this.pendingSegment_) {
727 return;
728 }
729
839 // if no segments are available, do nothing 730 // if no segments are available, do nothing
840 if (this.playlists.state === "HAVE_NOTHING" || 731 if (this.playlists.state === "HAVE_NOTHING" ||
841 !this.playlists.media() || 732 !this.playlists.media() ||
...@@ -848,28 +739,52 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { ...@@ -848,28 +739,52 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) {
848 return; 739 return;
849 } 740 }
850 741
742 // find the next segment to download
743 if (typeof seekToTime === 'number') {
744 mediaIndex = this.playlists.getMediaIndexForTime_(seekToTime);
745 } else if (currentBuffered && currentBuffered.length) {
746 mediaIndex = this.playlists.getMediaIndexForTime_(currentBuffered.end(0));
747 bufferedTime = Math.max(0, currentBuffered.end(0) - currentTime);
748 } else {
749 mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime());
750 }
751 segment = this.playlists.media().segments[mediaIndex];
752
851 // if the video has finished downloading, stop trying to buffer 753 // if the video has finished downloading, stop trying to buffer
852 segment = this.playlists.media().segments[this.mediaIndex];
853 if (!segment) { 754 if (!segment) {
854 return; 755 return;
855 } 756 }
856 757
857 // To determine how much is buffered, we need to find the buffered region we
858 // are currently playing in and measure it's length
859 if (buffered && buffered.length) {
860 bufferedTime = Math.max(0, buffered.end(0) - currentTime);
861 }
862
863 // if there is plenty of content in the buffer and we're not 758 // if there is plenty of content in the buffer and we're not
864 // seeking, relax for awhile 759 // seeking, relax for awhile
865 if (typeof seekToTime !== 'number' && bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { 760 if (typeof seekToTime !== 'number' &&
761 bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) {
866 return; 762 return;
867 } 763 }
868 764
765 // package up all the work to append the segment
766 segmentInfo = {
869 // resolve the segment URL relative to the playlist 767 // resolve the segment URL relative to the playlist
870 segmentUri = this.playlistUriToUrl(segment.uri); 768 uri: this.playlistUriToUrl(segment.uri),
769 // the segment's mediaIndex at the time it was received
770 mediaIndex: mediaIndex,
771 // the segment's playlist
772 playlist: this.playlists.media(),
773 // optionally, a time offset to seek to within the segment
774 offset: seekToTime,
775 // unencrypted bytes of the segment
776 bytes: null,
777 // when a key is defined for this segment, the encrypted bytes
778 encryptedBytes: null,
779 // optionally, the decrypter that is unencrypting the segment
780 decrypter: null,
781 // the state of the buffer before a segment is appended will be
782 // stored here so that the actual segment duration can be
783 // determined after it has been appended
784 buffered: null
785 };
871 786
872 this.loadSegment(segmentUri, seekToTime); 787 this.loadSegment(segmentInfo);
873 }; 788 };
874 789
875 videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) { 790 videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) {
...@@ -900,17 +815,22 @@ videojs.Hls.prototype.setBandwidth = function(xhr) { ...@@ -900,17 +815,22 @@ videojs.Hls.prototype.setBandwidth = function(xhr) {
900 this.tech_.trigger('bandwidthupdate'); 815 this.tech_.trigger('bandwidthupdate');
901 }; 816 };
902 817
903 videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { 818 videojs.Hls.prototype.loadSegment = function(segmentInfo) {
904 var self = this; 819 var
820 self = this,
821 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
822
823 // if the segment is encrypted, request the key
824 if (segment.key) {
825 this.fetchKey_(segment);
826 }
905 827
906 // request the next segment 828 // request the next segment
907 this.segmentXhr_ = videojs.Hls.xhr({ 829 this.segmentXhr_ = videojs.Hls.xhr({
908 uri: segmentUri, 830 uri: segmentInfo.uri,
909 responseType: 'arraybuffer', 831 responseType: 'arraybuffer',
910 withCredentials: this.source_.withCredentials 832 withCredentials: this.source_.withCredentials
911 }, function(error, request) { 833 }, function(error, request) {
912 var segmentInfo;
913
914 // the segment request is no longer outstanding 834 // the segment request is no longer outstanding
915 self.segmentXhr_ = null; 835 self.segmentXhr_ = null;
916 836
...@@ -920,17 +840,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { ...@@ -920,17 +840,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) {
920 return self.playlists.media(self.selectPlaylist()); 840 return self.playlists.media(self.selectPlaylist());
921 } 841 }
922 842
843 // otherwise, trigger a network error
923 if (!request.aborted && error) { 844 if (!request.aborted && error) {
924 // otherwise, try jumping ahead to the next segment
925 self.error = { 845 self.error = {
926 status: request.status, 846 status: request.status,
927 message: 'HLS segment request error at URL: ' + segmentUri, 847 message: 'HLS segment request error at URL: ' + segmentInfo.uri,
928 code: (request.status >= 500) ? 4 : 2 848 code: (request.status >= 500) ? 4 : 2
929 }; 849 };
930 850
931 // try moving on to the next segment 851 return self.mediaSource.endOfStream('network');
932 self.mediaIndex++;
933 return;
934 } 852 }
935 853
936 // stop processing if the request was aborted 854 // stop processing if the request was aborted
...@@ -940,35 +858,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { ...@@ -940,35 +858,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) {
940 858
941 self.setBandwidth(request); 859 self.setBandwidth(request);
942 860
943 // package up all the work to append the segment 861 if (segment.key) {
944 segmentInfo = {
945 // the segment's mediaIndex at the time it was received
946 mediaIndex: self.mediaIndex,
947 // the segment's playlist
948 playlist: self.playlists.media(),
949 // optionally, a time offset to seek to within the segment
950 offset: seekToTime,
951 // unencrypted bytes of the segment
952 bytes: null,
953 // when a key is defined for this segment, the encrypted bytes
954 encryptedBytes: null,
955 // optionally, the decrypter that is unencrypting the segment
956 decrypter: null,
957 // metadata events discovered during muxing that need to be
958 // translated into cue points
959 pendingMetadata: []
960 };
961 if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) {
962 segmentInfo.encryptedBytes = new Uint8Array(request.response); 862 segmentInfo.encryptedBytes = new Uint8Array(request.response);
963 } else { 863 } else {
964 segmentInfo.bytes = new Uint8Array(request.response); 864 segmentInfo.bytes = new Uint8Array(request.response);
965 } 865 }
966 self.segmentBuffer_.push(segmentInfo); 866 self.pendingSegment_ = segmentInfo;
967 self.tech_.trigger('progress'); 867 self.tech_.trigger('progress');
968 self.drainBuffer(); 868 self.drainBuffer();
969 869
970 self.mediaIndex++;
971
972 // figure out what stream the next segment should be downloaded from 870 // figure out what stream the next segment should be downloaded from
973 // with the updated bandwidth information 871 // with the updated bandwidth information
974 self.playlists.media(self.selectPlaylist()); 872 self.playlists.media(self.selectPlaylist());
...@@ -988,13 +886,11 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -988,13 +886,11 @@ videojs.Hls.prototype.drainBuffer = function(event) {
988 segmentTimestampOffset = 0, 886 segmentTimestampOffset = 0,
989 hasBufferedContent = (this.tech_.buffered().length !== 0), 887 hasBufferedContent = (this.tech_.buffered().length !== 0),
990 currentBuffered = this.findCurrentBuffered_(), 888 currentBuffered = this.findCurrentBuffered_(),
991 outsideBufferedRanges = !(currentBuffered && currentBuffered.length), 889 outsideBufferedRanges = !(currentBuffered && currentBuffered.length);
992 // ptsTime,
993 segmentBuffer = this.segmentBuffer_;
994 890
995 // if the buffer is empty or the source buffer hasn't been created 891 // if the buffer is empty or the source buffer hasn't been created
996 // yet, do nothing 892 // yet, do nothing
997 if (!segmentBuffer.length || !this.sourceBuffer) { 893 if (!this.pendingSegment_ || !this.sourceBuffer) {
998 return; 894 return;
999 } 895 }
1000 896
...@@ -1004,7 +900,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1004,7 +900,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1004 return; 900 return;
1005 } 901 }
1006 902
1007 segmentInfo = segmentBuffer[0]; 903 segmentInfo = this.pendingSegment_;
1008 mediaIndex = segmentInfo.mediaIndex; 904 mediaIndex = segmentInfo.mediaIndex;
1009 playlist = segmentInfo.playlist; 905 playlist = segmentInfo.playlist;
1010 offset = segmentInfo.offset; 906 offset = segmentInfo.offset;
...@@ -1017,18 +913,19 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1017,18 +913,19 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1017 // if the key download failed, we want to skip this segment 913 // if the key download failed, we want to skip this segment
1018 // but if the key hasn't downloaded yet, we want to try again later 914 // but if the key hasn't downloaded yet, we want to try again later
1019 if (keyFailed(segment.key)) { 915 if (keyFailed(segment.key)) {
1020 return segmentBuffer.shift(); 916 videojs.log.warn('Network error retrieving key from "' +
917 segment.key.uri + '"');
918 return this.mediaSource.endOfStream('network');
1021 } else if (!segment.key.bytes) { 919 } else if (!segment.key.bytes) {
1022 920
1023 // trigger a key request if one is not already in-flight 921 // waiting for the key bytes, try again later
1024 return this.fetchKeys_(); 922 return;
1025
1026 } else if (segmentInfo.decrypter) { 923 } else if (segmentInfo.decrypter) {
1027 924
1028 // decryption is in progress, try again later 925 // decryption is in progress, try again later
1029 return; 926 return;
1030
1031 } else { 927 } else {
928
1032 // if the media sequence is greater than 2^32, the IV will be incorrect 929 // if the media sequence is greater than 2^32, the IV will be incorrect
1033 // assuming 10s segments, that would be about 1300 years 930 // assuming 10s segments, that would be about 1300 years
1034 segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); 931 segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]);
...@@ -1047,32 +944,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1047,32 +944,6 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1047 944
1048 event = event || {}; 945 event = event || {};
1049 946
1050 // if (this.segmentParser_.tagsAvailable()) {
1051 // // record PTS information for the segment so we can calculate
1052 // // accurate durations and seek reliably
1053 // if (this.segmentParser_.stats.h264Tags()) {
1054 // segment.minVideoPts = this.segmentParser_.stats.minVideoPts();
1055 // segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts();
1056 // }
1057 // if (this.segmentParser_.stats.aacTags()) {
1058 // segment.minAudioPts = this.segmentParser_.stats.minAudioPts();
1059 // segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts();
1060 // }
1061 // }
1062
1063 // while (this.segmentParser_.tagsAvailable()) {
1064 // tags.push(this.segmentParser_.getNextTag());
1065 // }
1066
1067 this.addCuesForMetadata_(segmentInfo);
1068 //this.updateDuration(this.playlists.media());
1069
1070 // // when we're crossing a discontinuity, inject metadata to indicate
1071 // // that the decoder should be reset appropriately
1072 // if (segment.discontinuity && tags.length) {
1073 // this.tech_.el().vjs_discontinuity();
1074 // }
1075
1076 // If we have seeked into a non-buffered time-range, remove all buffered 947 // If we have seeked into a non-buffered time-range, remove all buffered
1077 // time-ranges because they could have been incorrectly placed originally 948 // time-ranges because they could have been incorrectly placed originally
1078 if (this.tech_.seeking() && outsideBufferedRanges) { 949 if (this.tech_.seeking() && outsideBufferedRanges) {
...@@ -1088,7 +959,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1088,7 +959,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1088 // anew on every seek 959 // anew on every seek
1089 if (segmentInfo.playlist.discontinuityStarts.length) { 960 if (segmentInfo.playlist.discontinuityStarts.length) {
1090 if (segmentInfo.mediaIndex > 0) { 961 if (segmentInfo.mediaIndex > 0) {
1091 segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, 0, segmentInfo.mediaIndex); 962 segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, segmentInfo.mediaIndex);
1092 } 963 }
1093 964
1094 // Now that the forward buffer is clear, we have to set timestamp offset to 965 // Now that the forward buffer is clear, we have to set timestamp offset to
...@@ -1102,44 +973,46 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1102,44 +973,46 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1102 this.sourceBuffer.timestampOffset = currentBuffered.end(0); 973 this.sourceBuffer.timestampOffset = currentBuffered.end(0);
1103 } 974 }
1104 975
976 if (currentBuffered.length) {
977 // Chrome 45 stalls if appends overlap the playhead
978 this.sourceBuffer.appendWindowStart = Math.min(this.tech_.currentTime(), currentBuffered.end(0));
979 } else {
980 this.sourceBuffer.appendWindowStart = 0;
981 }
982 this.pendingSegment_.buffered = this.tech_.buffered();
983
1105 // the segment is asynchronously added to the current buffered data 984 // the segment is asynchronously added to the current buffered data
1106 this.sourceBuffer.appendBuffer(bytes); 985 this.sourceBuffer.appendBuffer(bytes);
1107 this.pendingSegment_ = segmentBuffer.shift();
1108 }; 986 };
1109 987
1110 /** 988 /**
1111 * Attempt to retrieve keys starting at a particular media 989 * Attempt to retrieve the key for a particular media segment.
1112 * segment. This method has no effect if segments are not yet
1113 * available or a key request is already in progress.
1114 *
1115 * @param playlist {object} the media playlist to fetch keys for
1116 * @param index {number} the media segment index to start from
1117 */ 990 */
1118 videojs.Hls.prototype.fetchKeys_ = function() { 991 videojs.Hls.prototype.fetchKey_ = function(segment) {
1119 var i, key, tech, player, settings, segment, view, receiveKey; 992 var key, self, settings, receiveKey;
1120 993
1121 // if there is a pending XHR or no segments, don't do anything 994 // if there is a pending XHR or no segments, don't do anything
1122 if (keyXhr || !this.segmentBuffer_.length) { 995 if (this.keyXhr_) {
1123 return; 996 return;
1124 } 997 }
1125 998
1126 tech = this; 999 self = this;
1127 player = this.player();
1128 settings = this.options_; 1000 settings = this.options_;
1129 1001
1130 /** 1002 /**
1131 * Handle a key XHR response. This function needs to lookup the 1003 * Handle a key XHR response.
1132 */ 1004 */
1133 receiveKey = function(key) { 1005 receiveKey = function(key) {
1134 return function(error, request) { 1006 return function(error, request) {
1135 keyXhr = null; 1007 var view;
1008 self.keyXhr_ = null;
1136 1009
1137 if (error || !request.response || request.response.byteLength !== 16) { 1010 if (error || !request.response || request.response.byteLength !== 16) {
1138 key.retries = key.retries || 0; 1011 key.retries = key.retries || 0;
1139 key.retries++; 1012 key.retries++;
1140 if (!request.aborted) { 1013 if (!request.aborted) {
1141 // try fetching again 1014 // try fetching again
1142 tech.fetchKeys_(); 1015 self.fetchKey_(segment);
1143 } 1016 }
1144 return; 1017 return;
1145 } 1018 }
...@@ -1153,28 +1026,25 @@ videojs.Hls.prototype.fetchKeys_ = function() { ...@@ -1153,28 +1026,25 @@ videojs.Hls.prototype.fetchKeys_ = function() {
1153 ]); 1026 ]);
1154 1027
1155 // check to see if this allows us to make progress buffering now 1028 // check to see if this allows us to make progress buffering now
1156 tech.checkBuffer_(); 1029 self.checkBuffer_();
1157 }; 1030 };
1158 }; 1031 };
1159 1032
1160 for (i = 0; i < tech.segmentBuffer_.length; i++) {
1161 segment = tech.segmentBuffer_[i].playlist.segments[tech.segmentBuffer_[i].mediaIndex];
1162 key = segment.key; 1033 key = segment.key;
1163 1034
1164 // continue looking if this segment is unencrypted 1035 // nothing to do if this segment is unencrypted
1165 if (!key) { 1036 if (!key) {
1166 continue; 1037 return;
1167 } 1038 }
1168 1039
1169 // request the key if the retry limit hasn't been reached 1040 // request the key if the retry limit hasn't been reached
1170 if (!key.bytes && !keyFailed(key)) { 1041 if (!key.bytes && !keyFailed(key)) {
1171 keyXhr = videojs.Hls.xhr({ 1042 this.keyXhr_ = videojs.Hls.xhr({
1172 uri: this.playlistUriToUrl(key.uri), 1043 uri: this.playlistUriToUrl(key.uri),
1173 responseType: 'arraybuffer', 1044 responseType: 'arraybuffer',
1174 withCredentials: settings.withCredentials 1045 withCredentials: settings.withCredentials
1175 }, receiveKey(key)); 1046 }, receiveKey(key));
1176 break; 1047 return;
1177 }
1178 } 1048 }
1179 }; 1049 };
1180 1050
...@@ -1206,83 +1076,6 @@ videojs.Hls.isSupported = function() { ...@@ -1206,83 +1076,6 @@ videojs.Hls.isSupported = function() {
1206 }; 1076 };
1207 1077
1208 /** 1078 /**
1209 * Calculate the duration of a playlist from a given start index to a given
1210 * end index.
1211 * @param playlist {object} a media playlist object
1212 * @param startIndex {number} an inclusive lower boundary for the playlist.
1213 * Defaults to 0.
1214 * @param endIndex {number} an exclusive upper boundary for the playlist.
1215 * Defaults to playlist length.
1216 * @return {number} the duration between the start index and end index.
1217 */
1218 videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) {
1219 videojs.log.warn('videojs.Hls.getPlaylistDuration is deprecated. ' +
1220 'Use videojs.Hls.Playlist.duration instead');
1221 return videojs.Hls.Playlist.duration(playlist, startIndex, endIndex);
1222 };
1223
1224 /**
1225 * Calculate the total duration for a playlist based on segment metadata.
1226 * @param playlist {object} a media playlist object
1227 * @return {number} the currently known duration, in seconds
1228 */
1229 videojs.Hls.getPlaylistTotalDuration = function(playlist) {
1230 videojs.log.warn('videojs.Hls.getPlaylistTotalDuration is deprecated. ' +
1231 'Use videojs.Hls.Playlist.duration instead');
1232 return videojs.Hls.Playlist.duration(playlist);
1233 };
1234
1235 /**
1236 * Determine the media index in one playlist that corresponds to a
1237 * specified media index in another. This function can be used to
1238 * calculate a new segment position when a playlist is reloaded or a
1239 * variant playlist is becoming active.
1240 * @param mediaIndex {number} the index into the original playlist
1241 * to translate
1242 * @param original {object} the playlist to translate the media
1243 * index from
1244 * @param update {object} the playlist to translate the media index
1245 * to
1246 * @param {number} the corresponding media index in the updated
1247 * playlist
1248 */
1249 videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) {
1250 var translatedMediaIndex;
1251
1252 // no segments have been loaded from the original playlist
1253 if (mediaIndex === 0) {
1254 return 0;
1255 }
1256
1257 if (!(update && update.segments)) {
1258 // let the media index be zero when there are no segments defined
1259 return 0;
1260 }
1261
1262 // translate based on media sequence numbers. syncing up across
1263 // bitrate switches should be happening here.
1264 translatedMediaIndex = (mediaIndex + (original.mediaSequence - update.mediaSequence));
1265
1266 if (translatedMediaIndex > update.segments.length || translatedMediaIndex < 0) {
1267 // recalculate the live point if the streams are too far out of sync
1268 return videojs.Hls.getMediaIndexForLive_(update) + 1;
1269 }
1270
1271 return translatedMediaIndex;
1272 };
1273
1274 /**
1275 * Deprecated.
1276 *
1277 * @deprecated use player.hls.playlists.getMediaIndexForTime_() instead
1278 */
1279 videojs.Hls.getMediaIndexByTime = function() {
1280 videojs.log.warn('getMediaIndexByTime is deprecated. ' +
1281 'Use PlaylistLoader.getMediaIndexForTime_ instead.');
1282 return 0;
1283 };
1284
1285 /**
1286 * A comparator function to sort two playlist object by bandwidth. 1079 * A comparator function to sort two playlist object by bandwidth.
1287 * @param left {object} a media playlist object 1080 * @param left {object} a media playlist object
1288 * @param right {object} a media playlist object 1081 * @param right {object} a media playlist object
......
...@@ -53,15 +53,6 @@ ...@@ -53,15 +53,6 @@
53 strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); 53 strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
54 }); 54 });
55 55
56 test('starts with no expired time', function() {
57 var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
58 requests.pop().respond(200, null,
59 '#EXTM3U\n' +
60 '#EXTINF:10,\n' +
61 '0.ts\n');
62 equal(loader.expired_, 0, 'zero seconds expired');
63 });
64
65 test('requests the initial playlist immediately', function() { 56 test('requests the initial playlist immediately', function() {
66 new videojs.Hls.PlaylistLoader('master.m3u8'); 57 new videojs.Hls.PlaylistLoader('master.m3u8');
67 strictEqual(requests.length, 1, 'made a request'); 58 strictEqual(requests.length, 1, 'made a request');
...@@ -69,13 +60,16 @@ ...@@ -69,13 +60,16 @@
69 }); 60 });
70 61
71 test('moves to HAVE_MASTER after loading a master playlist', function() { 62 test('moves to HAVE_MASTER after loading a master playlist', function() {
72 var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); 63 var loader = new videojs.Hls.PlaylistLoader('master.m3u8'), state;
64 loader.on('loadedplaylist', function() {
65 state = loader.state;
66 });
73 requests.pop().respond(200, null, 67 requests.pop().respond(200, null,
74 '#EXTM3U\n' + 68 '#EXTM3U\n' +
75 '#EXT-X-STREAM-INF:\n' + 69 '#EXT-X-STREAM-INF:\n' +
76 'media.m3u8\n'); 70 'media.m3u8\n');
77 ok(loader.master, 'the master playlist is available'); 71 ok(loader.master, 'the master playlist is available');
78 strictEqual(loader.state, 'HAVE_MASTER', 'the state is correct'); 72 strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct');
79 }); 73 });
80 74
81 test('jumps to HAVE_METADATA when initialized with a media playlist', function() { 75 test('jumps to HAVE_METADATA when initialized with a media playlist', function() {
...@@ -172,101 +166,6 @@ ...@@ -172,101 +166,6 @@
172 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); 166 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
173 }); 167 });
174 168
175 test('increments expired seconds after a segment is removed', function() {
176 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
177 requests.pop().respond(200, null,
178 '#EXTM3U\n' +
179 '#EXT-X-MEDIA-SEQUENCE:0\n' +
180 '#EXTINF:10,\n' +
181 '0.ts\n' +
182 '#EXTINF:10,\n' +
183 '1.ts\n' +
184 '#EXTINF:10,\n' +
185 '2.ts\n' +
186 '#EXTINF:10,\n' +
187 '3.ts\n');
188 clock.tick(10 * 1000); // 10s, one target duration
189 requests.pop().respond(200, null,
190 '#EXTM3U\n' +
191 '#EXT-X-MEDIA-SEQUENCE:1\n' +
192 '#EXTINF:10,\n' +
193 '1.ts\n' +
194 '#EXTINF:10,\n' +
195 '2.ts\n' +
196 '#EXTINF:10,\n' +
197 '3.ts\n' +
198 '#EXTINF:10,\n' +
199 '4.ts\n');
200 equal(loader.expired_, 10, 'expired one segment');
201 });
202
203 test('increments expired seconds after a discontinuity', function() {
204 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
205 requests.pop().respond(200, null,
206 '#EXTM3U\n' +
207 '#EXT-X-MEDIA-SEQUENCE:0\n' +
208 '#EXTINF:10,\n' +
209 '0.ts\n' +
210 '#EXTINF:3,\n' +
211 '1.ts\n' +
212 '#EXT-X-DISCONTINUITY\n' +
213 '#EXTINF:4,\n' +
214 '2.ts\n');
215 clock.tick(10 * 1000); // 10s, one target duration
216 requests.pop().respond(200, null,
217 '#EXTM3U\n' +
218 '#EXT-X-MEDIA-SEQUENCE:1\n' +
219 '#EXTINF:3,\n' +
220 '1.ts\n' +
221 '#EXT-X-DISCONTINUITY\n' +
222 '#EXTINF:4,\n' +
223 '2.ts\n');
224 equal(loader.expired_, 10, 'expired one segment');
225
226 clock.tick(10 * 1000); // 10s, one target duration
227 requests.pop().respond(200, null,
228 '#EXTM3U\n' +
229 '#EXT-X-MEDIA-SEQUENCE:2\n' +
230 '#EXT-X-DISCONTINUITY\n' +
231 '#EXTINF:4,\n' +
232 '2.ts\n');
233 equal(loader.expired_, 13, 'no expirations after the discontinuity yet');
234
235 clock.tick(10 * 1000); // 10s, one target duration
236 requests.pop().respond(200, null,
237 '#EXTM3U\n' +
238 '#EXT-X-MEDIA-SEQUENCE:3\n' +
239 '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' +
240 '#EXTINF:10,\n' +
241 '3.ts\n');
242 equal(loader.expired_, 13 + 4, 'tracked expired prior to the discontinuity');
243 });
244
245 test('tracks expired seconds properly when two discontinuities expire at once', function() {
246 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
247 requests.pop().respond(200, null,
248 '#EXTM3U\n' +
249 '#EXT-X-MEDIA-SEQUENCE:0\n' +
250 '#EXTINF:4,\n' +
251 '0.ts\n' +
252 '#EXT-X-DISCONTINUITY\n' +
253 '#EXTINF:5,\n' +
254 '1.ts\n' +
255 '#EXT-X-DISCONTINUITY\n' +
256 '#EXTINF:6,\n' +
257 '2.ts\n' +
258 '#EXTINF:7,\n' +
259 '3.ts\n');
260 clock.tick(10 * 1000);
261 requests.pop().respond(200, null,
262 '#EXTM3U\n' +
263 '#EXT-X-MEDIA-SEQUENCE:3\n' +
264 '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' +
265 '#EXTINF:7,\n' +
266 '3.ts\n');
267 equal(loader.expired_, 4 + 5 + 6, 'tracked both expired discontinuities');
268 });
269
270 test('emits an error when an initial playlist request fails', function() { 169 test('emits an error when an initial playlist request fails', function() {
271 var 170 var
272 errors = [], 171 errors = [],
...@@ -453,6 +352,20 @@ ...@@ -453,6 +352,20 @@
453 'updated the active media'); 352 'updated the active media');
454 }); 353 });
455 354
355 test('can switch playlists immediately after the master is downloaded', function() {
356 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
357 loader.on('loadedplaylist', function() {
358 loader.media('high.m3u8');
359 });
360 requests.pop().respond(200, null,
361 '#EXTM3U\n' +
362 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
363 'low.m3u8\n' +
364 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
365 'high.m3u8\n');
366 equal(requests[0].url, urlTo('high.m3u8'), 'switched variants immediately');
367 });
368
456 test('can switch media playlists based on URI', function() { 369 test('can switch media playlists based on URI', function() {
457 var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); 370 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
458 requests.pop().respond(200, null, 371 requests.pop().respond(200, null,
...@@ -624,9 +537,6 @@ ...@@ -624,9 +537,6 @@
624 'low.m3u8\n' + 537 'low.m3u8\n' +
625 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + 538 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
626 'high.m3u8\n'); 539 'high.m3u8\n');
627 throws(function() {
628 loader.media('high.m3u8');
629 }, 'throws an error from HAVE_MASTER');
630 }); 540 });
631 541
632 test('throws an error if a switch to an unrecognized playlist is requested', function() { 542 test('throws an error if a switch to an unrecognized playlist is requested', function() {
...@@ -743,8 +653,8 @@ ...@@ -743,8 +653,8 @@
743 equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); 653 equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero');
744 equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); 654 equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2');
745 equal(loader.getMediaIndexForTime_(22), 655 equal(loader.getMediaIndexForTime_(22),
746 2, 656 3,
747 'the index is never greater than the length'); 657 'time greater than the length is index 3');
748 }); 658 });
749 659
750 test('returns the lower index when calculating for a segment boundary', function() { 660 test('returns the lower index when calculating for a segment boundary', function() {
...@@ -757,10 +667,8 @@ ...@@ -757,10 +667,8 @@
757 '#EXTINF:5,\n' + 667 '#EXTINF:5,\n' +
758 '1.ts\n' + 668 '1.ts\n' +
759 '#EXT-X-ENDLIST\n'); 669 '#EXT-X-ENDLIST\n');
760 equal(loader.getMediaIndexForTime_(4), 0, 'rounds down exact matches'); 670 equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches');
761 equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); 671 equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down');
762 // FIXME: the test below should pass for HLSv3
763 //equal(loader.getMediaIndexForTime_(4.2), 0, 'rounds down');
764 equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); 672 equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5');
765 }); 673 });
766 674
...@@ -773,7 +681,7 @@ ...@@ -773,7 +681,7 @@
773 '1001.ts\n' + 681 '1001.ts\n' +
774 '#EXTINF:5,\n' + 682 '#EXTINF:5,\n' +
775 '1002.ts\n'); 683 '1002.ts\n');
776 loader.expired_ = 150; 684 loader.media().segments[0].start = 150;
777 685
778 equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); 686 equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero');
779 equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); 687 equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero');
...@@ -785,30 +693,6 @@ ...@@ -785,30 +693,6 @@
785 equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); 693 equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment');
786 }); 694 });
787 695
788 test('updating the timeline offset adjusts results from getMediaIndexForTime_', function() {
789 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
790 requests.pop().respond(200, null,
791 '#EXTM3U\n' +
792 '#EXT-X-MEDIA-SEQUENCE:23\n' +
793 '#EXTINF:4,\n' +
794 '23.ts\n' +
795 '#EXTINF:5,\n' +
796 '24.ts\n' +
797 '#EXTINF:6,\n' +
798 '25.ts\n' +
799 '#EXTINF:7,\n' +
800 '26.ts\n');
801 loader.updateTimelineOffset(0, 150);
802 equal(loader.getMediaIndexForTime_(150), 0, 'translated the first segment');
803 equal(loader.getMediaIndexForTime_(130), 0, 'clamps the index to zero');
804 equal(loader.getMediaIndexForTime_(155), 1, 'translated the second segment');
805
806 loader.updateTimelineOffset(2, 30);
807 equal(loader.getMediaIndexForTime_(30 - 5 - 1), 0, 'translated the first segment');
808 equal(loader.getMediaIndexForTime_(30 + 7), 3, 'translated the last segment');
809 equal(loader.getMediaIndexForTime_(30 - 3), 1, 'translated an earlier segment');
810 });
811
812 test('does not misintrepret playlists missing newlines at the end', function() { 696 test('does not misintrepret playlists missing newlines at the end', function() {
813 var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); 697 var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
814 requests.shift().respond(200, null, 698 requests.shift().respond(200, null,
......
...@@ -18,27 +18,6 @@ ...@@ -18,27 +18,6 @@
18 18
19 module('Playlist Interval Duration'); 19 module('Playlist Interval Duration');
20 20
21 test('accounts expired duration for live playlists', function() {
22 var duration = Playlist.duration({
23 mediaSequence: 10,
24 segments: [{
25 duration: 10,
26 uri: '10.ts'
27 }, {
28 duration: 10,
29 uri: '11.ts'
30 }, {
31 duration: 10,
32 uri: '12.ts'
33 }, {
34 duration: 10,
35 uri: '13.ts'
36 }]
37 }, 0, 14);
38
39 equal(duration, 14 * 10, 'duration includes dropped segments');
40 });
41
42 test('accounts for non-zero starting VOD media sequences', function() { 21 test('accounts for non-zero starting VOD media sequences', function() {
43 var duration = Playlist.duration({ 22 var duration = Playlist.duration({
44 mediaSequence: 10, 23 mediaSequence: 10,
...@@ -61,47 +40,37 @@ ...@@ -61,47 +40,37 @@
61 equal(duration, 4 * 10, 'includes only listed segments'); 40 equal(duration, 4 * 10, 'includes only listed segments');
62 }); 41 });
63 42
64 test('uses PTS values when available', function() { 43 test('uses timeline values when available', function() {
65 var duration = Playlist.duration({ 44 var duration = Playlist.duration({
66 mediaSequence: 0, 45 mediaSequence: 0,
67 endList: true, 46 endList: true,
68 segments: [{ 47 segments: [{
69 minVideoPts: 1, 48 start: 0,
70 minAudioPts: 2,
71 uri: '0.ts' 49 uri: '0.ts'
72 }, { 50 }, {
73 duration: 10, 51 duration: 10,
74 maxVideoPts: 2 * 10 * 1000 + 1, 52 end: 2 * 10 + 2,
75 maxAudioPts: 2 * 10 * 1000 + 2,
76 uri: '1.ts' 53 uri: '1.ts'
77 }, { 54 }, {
78 duration: 10, 55 duration: 10,
79 maxVideoPts: 3 * 10 * 1000 + 1, 56 end: 3 * 10 + 2,
80 maxAudioPts: 3 * 10 * 1000 + 2,
81 uri: '2.ts' 57 uri: '2.ts'
82 }, { 58 }, {
83 duration: 10, 59 duration: 10,
84 maxVideoPts: 4 * 10 * 1000 + 1, 60 end: 4 * 10 + 2,
85 maxAudioPts: 4 * 10 * 1000 + 2,
86 uri: '3.ts' 61 uri: '3.ts'
87 }] 62 }]
88 }, 0, 4); 63 }, 4);
89 64
90 equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values'); 65 equal(duration, 4 * 10 + 2, 'used timeline values');
91 }); 66 });
92 67
93 test('works when partial PTS information is available', function() { 68 test('works when partial timeline information is available', function() {
94 var duration = Playlist.duration({ 69 var duration = Playlist.duration({
95 mediaSequence: 0, 70 mediaSequence: 0,
96 endList: true, 71 endList: true,
97 segments: [{ 72 segments: [{
98 minVideoPts: 1, 73 start: 0,
99 minAudioPts: 2,
100 maxVideoPts: 10 * 1000 + 1,
101
102 // intentionally less duration than video
103 // the max stream duration should be used
104 maxAudioPts: 10 * 1000 + 1,
105 uri: '0.ts' 74 uri: '0.ts'
106 }, { 75 }, {
107 duration: 9, 76 duration: 9,
...@@ -111,67 +80,17 @@ ...@@ -111,67 +80,17 @@
111 uri: '2.ts' 80 uri: '2.ts'
112 }, { 81 }, {
113 duration: 10, 82 duration: 10,
114 minVideoPts: 30 * 1000 + 7, 83 start: 30.007,
115 minAudioPts: 30 * 1000 + 10, 84 end: 40.002,
116 maxVideoPts: 40 * 1000 + 1,
117 maxAudioPts: 40 * 1000 + 2,
118 uri: '3.ts' 85 uri: '3.ts'
119 }, { 86 }, {
120 duration: 10, 87 duration: 10,
121 maxVideoPts: 50 * 1000 + 1, 88 end: 50.0002,
122 maxAudioPts: 50 * 1000 + 2,
123 uri: '4.ts' 89 uri: '4.ts'
124 }] 90 }]
125 }, 0, 5); 91 }, 5);
126 92
127 equal(duration, 93 equal(duration, 50.0002, 'calculated with mixed intervals');
128 ((50 * 1000 + 2) - 1) * 0.001,
129 'calculated with mixed intervals');
130 });
131
132 test('ignores segments before the start', function() {
133 var duration = Playlist.duration({
134 mediaSequence: 0,
135 segments: [{
136 duration: 10,
137 uri: '0.ts'
138 }, {
139 duration: 10,
140 uri: '1.ts'
141 }, {
142 duration: 10,
143 uri: '2.ts'
144 }]
145 }, 1, 3);
146
147 equal(duration, 10 + 10, 'ignored the first segment');
148 });
149
150 test('ignores discontinuity sequences earlier than the start', function() {
151 var duration = Playlist.duration({
152 mediaSequence: 0,
153 discontinuityStarts: [1, 3],
154 segments: [{
155 minVideoPts: 0,
156 minAudioPts: 0,
157 maxVideoPts: 10 * 1000,
158 maxAudioPts: 10 * 1000,
159 uri: '0.ts'
160 }, {
161 discontinuity: true,
162 duration: 9,
163 uri: '1.ts'
164 }, {
165 duration: 10,
166 uri: '2.ts'
167 }, {
168 discontinuity: true,
169 duration: 10,
170 uri: '3.ts'
171 }]
172 }, 2, 4);
173
174 equal(duration, 10 + 10, 'excluded the earlier segments');
175 }); 94 });
176 95
177 test('ignores discontinuity sequences later than the end', function() { 96 test('ignores discontinuity sequences later than the end', function() {
...@@ -196,20 +115,19 @@ ...@@ -196,20 +115,19 @@
196 duration: 10, 115 duration: 10,
197 uri: '3.ts' 116 uri: '3.ts'
198 }] 117 }]
199 }, 0, 2); 118 }, 2);
200 119
201 equal(duration, 19, 'excluded the later segments'); 120 equal(duration, 19, 'excluded the later segments');
202 }); 121 });
203 122
204 test('handles trailing segments without PTS information', function() { 123 test('handles trailing segments without timeline information', function() {
205 var duration = Playlist.duration({ 124 var playlist, duration;
125 playlist = {
206 mediaSequence: 0, 126 mediaSequence: 0,
207 endList: true, 127 endList: true,
208 segments: [{ 128 segments: [{
209 minVideoPts: 0, 129 start: 0,
210 minAudioPts: 0, 130 end: 10.5,
211 maxVideoPts: 10 * 1000,
212 maxAudioPts: 10 * 1000,
213 uri: '0.ts' 131 uri: '0.ts'
214 }, { 132 }, {
215 duration: 9, 133 duration: 9,
...@@ -218,107 +136,43 @@ ...@@ -218,107 +136,43 @@
218 duration: 10, 136 duration: 10,
219 uri: '2.ts' 137 uri: '2.ts'
220 }, { 138 }, {
221 minVideoPts: 29.5 * 1000, 139 start: 29.45,
222 minAudioPts: 29.5 * 1000, 140 end: 39.5,
223 maxVideoPts: 39.5 * 1000,
224 maxAudioPts: 39.5 * 1000,
225 uri: '3.ts' 141 uri: '3.ts'
226 }] 142 }]
227 }, 0, 3); 143 };
144
145 duration = Playlist.duration(playlist, 3);
146 equal(duration, 29.45, 'calculated duration');
228 147
229 equal(duration, 29.5, 'calculated duration'); 148 duration = Playlist.duration(playlist, 2);
149 equal(duration, 19.5, 'calculated duration');
230 }); 150 });
231 151
232 test('uses PTS intervals when the start and end segment have them', function() { 152 test('uses timeline intervals when segments have them', function() {
233 var playlist, duration; 153 var playlist, duration;
234 playlist = { 154 playlist = {
235 mediaSequence: 0, 155 mediaSequence: 0,
236 segments: [{ 156 segments: [{
237 minVideoPts: 0, 157 start: 0,
238 minAudioPts: 0, 158 end: 10,
239 maxVideoPts: 10 * 1000,
240 maxAudioPts: 10 * 1000,
241 uri: '0.ts' 159 uri: '0.ts'
242 }, { 160 }, {
243 duration: 9, 161 duration: 9,
244 uri: '1.ts' 162 uri: '1.ts'
245 },{ 163 },{
246 minVideoPts: 20 * 1000 + 100, 164 start: 20.1,
247 minAudioPts: 20 * 1000 + 100, 165 end: 30.1,
248 maxVideoPts: 30 * 1000 + 100,
249 maxAudioPts: 30 * 1000 + 100,
250 duration: 10, 166 duration: 10,
251 uri: '2.ts' 167 uri: '2.ts'
252 }] 168 }]
253 }; 169 };
254 duration = Playlist.duration(playlist, 0, 2); 170 duration = Playlist.duration(playlist, 2);
255 171
256 equal(duration, 20.1, 'used the PTS-based interval'); 172 equal(duration, 20.1, 'used the timeline-based interval');
257 173
258 duration = Playlist.duration(playlist, 0, 3); 174 duration = Playlist.duration(playlist, 3);
259 equal(duration, 30.1, 'used the PTS-based interval'); 175 equal(duration, 30.1, 'used the timeline-based interval');
260 });
261
262 test('works for media without audio', function() {
263 equal(Playlist.duration({
264 mediaSequence: 0,
265 endList: true,
266 segments: [{
267 minVideoPts: 0,
268 maxVideoPts: 9 * 1000,
269 uri: 'no-audio.ts'
270 }]
271 }), 9, 'used video PTS values');
272 });
273
274 test('works for media without video', function() {
275 equal(Playlist.duration({
276 mediaSequence: 0,
277 endList: true,
278 segments: [{
279 minAudioPts: 0,
280 maxAudioPts: 9 * 1000,
281 uri: 'no-video.ts'
282 }]
283 }), 9, 'used video PTS values');
284 });
285
286 test('uses the largest continuous available PTS ranges', function() {
287 var playlist = {
288 mediaSequence: 0,
289 segments: [{
290 minVideoPts: 0,
291 minAudioPts: 0,
292 maxVideoPts: 10 * 1000,
293 maxAudioPts: 10 * 1000,
294 uri: '0.ts'
295 }, {
296 duration: 10,
297 uri: '1.ts'
298 }, {
299 // starts 0.5s earlier than the previous segment indicates
300 minVideoPts: 19.5 * 1000,
301 minAudioPts: 19.5 * 1000,
302 maxVideoPts: 29.5 * 1000,
303 maxAudioPts: 29.5 * 1000,
304 uri: '2.ts'
305 }, {
306 duration: 10,
307 uri: '3.ts'
308 }, {
309 // ... but by the last segment, there is actual 0.5s more
310 // content than duration indicates
311 minVideoPts: 40.5 * 1000,
312 minAudioPts: 40.5 * 1000,
313 maxVideoPts: 50.5 * 1000,
314 maxAudioPts: 50.5 * 1000,
315 uri: '4.ts'
316 }]
317 };
318
319 equal(Playlist.duration(playlist, 0, 5),
320 50.5,
321 'calculated across the larger PTS interval');
322 }); 176 });
323 177
324 test('counts the time between segments as part of the earlier segment\'s duration', function() { 178 test('counts the time between segments as part of the earlier segment\'s duration', function() {
...@@ -326,22 +180,18 @@ ...@@ -326,22 +180,18 @@
326 mediaSequence: 0, 180 mediaSequence: 0,
327 endList: true, 181 endList: true,
328 segments: [{ 182 segments: [{
329 minVideoPts: 0, 183 start: 0,
330 minAudioPts: 0, 184 end: 10,
331 maxVideoPts: 1 * 10 * 1000,
332 maxAudioPts: 1 * 10 * 1000,
333 uri: '0.ts' 185 uri: '0.ts'
334 }, { 186 }, {
335 minVideoPts: 1 * 10 * 1000 + 100, 187 start: 10.1,
336 minAudioPts: 1 * 10 * 1000 + 100, 188 end: 20.1,
337 maxVideoPts: 2 * 10 * 1000 + 100,
338 maxAudioPts: 2 * 10 * 1000 + 100,
339 duration: 10, 189 duration: 10,
340 uri: '1.ts' 190 uri: '1.ts'
341 }] 191 }]
342 }, 0, 1); 192 }, 1);
343 193
344 equal(duration, (1 * 10 * 1000 + 100) * 0.001, 'included the segment gap'); 194 equal(duration, 10.1, 'included the segment gap');
345 }); 195 });
346 196
347 test('accounts for discontinuities', function() { 197 test('accounts for discontinuities', function() {
...@@ -364,7 +214,7 @@ ...@@ -364,7 +214,7 @@
364 duration: 10, 214 duration: 10,
365 uri: '1.ts' 215 uri: '1.ts'
366 }] 216 }]
367 }, 0, 2); 217 }, 2);
368 218
369 equal(duration, 10 + 10, 'handles discontinuities'); 219 equal(duration, 10 + 10, 'handles discontinuities');
370 }); 220 });
...@@ -389,7 +239,7 @@ ...@@ -389,7 +239,7 @@
389 duration: 10, 239 duration: 10,
390 uri: '1.ts' 240 uri: '1.ts'
391 }] 241 }]
392 }, 0, 1); 242 }, 1);
393 243
394 equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); 244 equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap');
395 }); 245 });
...@@ -412,7 +262,7 @@ ...@@ -412,7 +262,7 @@
412 duration: 10, 262 duration: 10,
413 uri: '1.ts' 263 uri: '1.ts'
414 }] 264 }]
415 }, 0, 1, false); 265 }, 1, false);
416 266
417 equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); 267 equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap');
418 }); 268 });
...@@ -431,10 +281,9 @@ ...@@ -431,10 +281,9 @@
431 }] 281 }]
432 }; 282 };
433 283
434 equal(Playlist.duration(playlist, 0, 0), 0, 'zero-length duration is zero'); 284 equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero');
435 equal(Playlist.duration(playlist, 0, 0, false), 0, 'zero-length duration is zero'); 285 equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero');
436 equal(Playlist.duration(playlist, 0, -1), 0, 'negative length duration is zero'); 286 equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero');
437 equal(Playlist.duration(playlist, 2, 1, false), 0, 'negative length duration is zero');
438 }); 287 });
439 288
440 module('Playlist Seekable'); 289 module('Playlist Seekable');
......
...@@ -167,60 +167,6 @@ var ...@@ -167,60 +167,6 @@ var
167 window.manifests[manifestName]); 167 window.manifests[manifestName]);
168 }, 168 },
169 169
170 mockSegmentParser = function(tags) {
171 var MockSegmentParser;
172
173 if (tags === undefined) {
174 tags = [{ pts: 0, bytes: new Uint8Array(1) }];
175 }
176 MockSegmentParser = function() {
177 this.getFlvHeader = function() {
178 return 'flv';
179 };
180 this.parseSegmentBinaryData = function() {};
181 this.flushTags = function() {};
182 this.tagsAvailable = function() {
183 return tags.length;
184 };
185 this.getTags = function() {
186 return tags;
187 };
188 this.getNextTag = function() {
189 return tags.shift();
190 };
191 this.metadataStream = new videojs.Hls.Stream();
192 this.metadataStream.init();
193 this.metadataStream.descriptor = new Uint8Array([
194 1, 2, 3, 0xbb
195 ]);
196
197 this.stats = {
198 h264Tags: function() {
199 return tags.length;
200 },
201 minVideoPts: function() {
202 return tags[0].pts;
203 },
204 maxVideoPts: function() {
205 return tags[tags.length - 1].pts;
206 },
207 aacTags: function() {
208 return tags.length;
209 },
210 minAudioPts: function() {
211 return tags[0].pts;
212 },
213 maxAudioPts: function() {
214 return tags[tags.length - 1].pts;
215 },
216 };
217 };
218
219 MockSegmentParser.STREAM_TYPES = videojs.Hls.SegmentParser.STREAM_TYPES;
220
221 return MockSegmentParser;
222 },
223
224 // a no-op MediaSource implementation to allow synchronous testing 170 // a no-op MediaSource implementation to allow synchronous testing
225 MockMediaSource = videojs.extend(videojs.EventTarget, { 171 MockMediaSource = videojs.extend(videojs.EventTarget, {
226 constructor: function() {}, 172 constructor: function() {},
...@@ -524,7 +470,7 @@ test('sets the duration if one is available on the playlist', function() { ...@@ -524,7 +470,7 @@ test('sets the duration if one is available on the playlist', function() {
524 equal(events, 1, 'durationchange is fired'); 470 equal(events, 1, 'durationchange is fired');
525 }); 471 });
526 472
527 QUnit.skip('calculates the duration if needed', function() { 473 test('estimates individual segment durations if needed', function() {
528 var changes = 0; 474 var changes = 0;
529 player.src({ 475 player.src({
530 src: 'http://example.com/manifest/missingExtinf.m3u8', 476 src: 'http://example.com/manifest/missingExtinf.m3u8',
...@@ -532,7 +478,7 @@ QUnit.skip('calculates the duration if needed', function() { ...@@ -532,7 +478,7 @@ QUnit.skip('calculates the duration if needed', function() {
532 }); 478 });
533 openMediaSource(player); 479 openMediaSource(player);
534 player.tech_.hls.mediaSource.duration = NaN; 480 player.tech_.hls.mediaSource.duration = NaN;
535 player.on('durationchange', function() { 481 player.tech_.on('durationchange', function() {
536 changes++; 482 changes++;
537 }); 483 });
538 484
...@@ -686,73 +632,59 @@ test('downloads media playlists after loading the master', function() { ...@@ -686,73 +632,59 @@ test('downloads media playlists after loading the master', function() {
686 }); 632 });
687 openMediaSource(player); 633 openMediaSource(player);
688 634
689 // set bandwidth to an appropriate number so we don't switch 635 player.tech_.hls.bandwidth = 20e10;
690 player.tech_.hls.bandwidth = 200000;
691 standardXHRResponse(requests[0]); 636 standardXHRResponse(requests[0]);
692 standardXHRResponse(requests[1]); 637 standardXHRResponse(requests[1]);
693 standardXHRResponse(requests[2]); 638 standardXHRResponse(requests[2]);
694 639
695 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); 640 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
696 strictEqual(requests[1].url, 641 strictEqual(requests[1].url,
697 absoluteUrl('manifest/media.m3u8'), 642 absoluteUrl('manifest/media3.m3u8'),
698 'media playlist requested'); 643 'media playlist requested');
699 strictEqual(requests[2].url, 644 strictEqual(requests[2].url,
700 absoluteUrl('manifest/media-00001.ts'), 645 absoluteUrl('manifest/media3-00001.ts'),
701 'first segment requested'); 646 'first segment requested');
702 }); 647 });
703 648
704 test('upshift if initial bandwidth is high', function() { 649 test('upshifts if the initial bandwidth hint is high', function() {
705 player.src({ 650 player.src({
706 src: 'manifest/master.m3u8', 651 src: 'manifest/master.m3u8',
707 type: 'application/vnd.apple.mpegurl' 652 type: 'application/vnd.apple.mpegurl'
708 }); 653 });
709 openMediaSource(player); 654 openMediaSource(player);
710 655
656 player.tech_.hls.bandwidth = 10e20;
711 standardXHRResponse(requests[0]); 657 standardXHRResponse(requests[0]);
712
713 player.tech_.hls.playlists.setBandwidth = function() {
714 player.tech_.hls.playlists.bandwidth = 1000000000;
715 };
716
717 standardXHRResponse(requests[1]); 658 standardXHRResponse(requests[1]);
718 standardXHRResponse(requests[2]); 659 standardXHRResponse(requests[2]);
719 660
720 standardXHRResponse(requests[3]);
721
722 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); 661 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
723 strictEqual(requests[1].url, 662 strictEqual(requests[1].url,
724 absoluteUrl('manifest/media.m3u8'),
725 'media playlist requested');
726 strictEqual(requests[2].url,
727 absoluteUrl('manifest/media3.m3u8'), 663 absoluteUrl('manifest/media3.m3u8'),
728 'media playlist requested'); 664 'media playlist requested');
729 strictEqual(requests[3].url, 665 strictEqual(requests[2].url,
730 absoluteUrl('manifest/media3-00001.ts'), 666 absoluteUrl('manifest/media3-00001.ts'),
731 'first segment requested'); 667 'first segment requested');
732 }); 668 });
733 669
734 test('dont downshift if bandwidth is low', function() { 670 test('downshifts if the initial bandwidth hint is low', function() {
735 player.src({ 671 player.src({
736 src: 'manifest/master.m3u8', 672 src: 'manifest/master.m3u8',
737 type: 'application/vnd.apple.mpegurl' 673 type: 'application/vnd.apple.mpegurl'
738 }); 674 });
739 openMediaSource(player); 675 openMediaSource(player);
740 676
677 player.tech_.hls.bandwidth = 100;
741 standardXHRResponse(requests[0]); 678 standardXHRResponse(requests[0]);
742
743 player.tech_.hls.playlists.setBandwidth = function() {
744 player.tech_.hls.playlists.bandwidth = 100;
745 };
746
747 standardXHRResponse(requests[1]); 679 standardXHRResponse(requests[1]);
748 standardXHRResponse(requests[2]); 680 standardXHRResponse(requests[2]);
749 681
750 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); 682 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
751 strictEqual(requests[1].url, 683 strictEqual(requests[1].url,
752 absoluteUrl('manifest/media.m3u8'), 684 absoluteUrl('manifest/media1.m3u8'),
753 'media playlist requested'); 685 'media playlist requested');
754 strictEqual(requests[2].url, 686 strictEqual(requests[2].url,
755 absoluteUrl('manifest/media-00001.ts'), 687 absoluteUrl('manifest/media1-00001.ts'),
756 'first segment requested'); 688 'first segment requested');
757 }); 689 });
758 690
...@@ -822,7 +754,7 @@ test('buffer checks are noops when only the master is ready', function() { ...@@ -822,7 +754,7 @@ test('buffer checks are noops when only the master is ready', function() {
822 754
823 strictEqual(1, requests.length, 'one request was made'); 755 strictEqual(1, requests.length, 'one request was made');
824 strictEqual(requests[0].url, 756 strictEqual(requests[0].url,
825 absoluteUrl('manifest/media.m3u8'), 757 absoluteUrl('manifest/media1.m3u8'),
826 'media playlist requested'); 758 'media playlist requested');
827 }); 759 });
828 760
...@@ -876,11 +808,9 @@ test('selects a playlist after segment downloads', function() { ...@@ -876,11 +808,9 @@ test('selects a playlist after segment downloads', function() {
876 return player.tech_.hls.playlists.master.playlists[0]; 808 return player.tech_.hls.playlists.master.playlists[0];
877 }; 809 };
878 810
879 standardXHRResponse(requests[0]); 811 standardXHRResponse(requests[0]); // master
880 812 standardXHRResponse(requests[1]); // media
881 player.tech_.hls.bandwidth = 3000000; 813 standardXHRResponse(requests[2]); // segment
882 standardXHRResponse(requests[1]);
883 standardXHRResponse(requests[2]);
884 814
885 strictEqual(calls, 2, 'selects after the initial segment'); 815 strictEqual(calls, 2, 'selects after the initial segment');
886 player.currentTime = function() { 816 player.currentTime = function() {
...@@ -889,6 +819,7 @@ test('selects a playlist after segment downloads', function() { ...@@ -889,6 +819,7 @@ test('selects a playlist after segment downloads', function() {
889 player.buffered = function() { 819 player.buffered = function() {
890 return videojs.createTimeRange(0, 2); 820 return videojs.createTimeRange(0, 2);
891 }; 821 };
822 player.tech_.hls.sourceBuffer.trigger('updateend');
892 player.tech_.hls.checkBuffer_(); 823 player.tech_.hls.checkBuffer_();
893 824
894 standardXHRResponse(requests[3]); 825 standardXHRResponse(requests[3]);
...@@ -896,9 +827,7 @@ test('selects a playlist after segment downloads', function() { ...@@ -896,9 +827,7 @@ test('selects a playlist after segment downloads', function() {
896 strictEqual(calls, 3, 'selects after additional segments'); 827 strictEqual(calls, 3, 'selects after additional segments');
897 }); 828 });
898 829
899 test('moves to the next segment if there is a network error', function() { 830 test('reports an error if a segment is unreachable', function() {
900 var mediaIndex;
901
902 player.src({ 831 player.src({
903 src: 'manifest/master.m3u8', 832 src: 'manifest/master.m3u8',
904 type: 'application/vnd.apple.mpegurl' 833 type: 'application/vnd.apple.mpegurl'
...@@ -906,65 +835,11 @@ test('moves to the next segment if there is a network error', function() { ...@@ -906,65 +835,11 @@ test('moves to the next segment if there is a network error', function() {
906 openMediaSource(player); 835 openMediaSource(player);
907 836
908 player.tech_.hls.bandwidth = 20000; 837 player.tech_.hls.bandwidth = 20000;
909 standardXHRResponse(requests[0]); 838 standardXHRResponse(requests[0]); // master
910 standardXHRResponse(requests[1]); 839 standardXHRResponse(requests[1]); // media
911 840
912 mediaIndex = player.tech_.hls.mediaIndex; 841 requests[2].respond(400); // segment
913 player.trigger('timeupdate'); 842 strictEqual(player.tech_.hls.mediaSource.error_, 'network', 'network error is triggered');
914
915 requests[2].respond(400);
916 strictEqual(mediaIndex + 1, player.tech_.hls.mediaIndex, 'media index is incremented');
917 });
918
919 test('updates playlist timeline offsets if it detects a desynchronization', function() {
920 var buffered = [], currentTime = 0;
921
922 player.src({
923 src: 'manifest/master.m3u8',
924 type: 'application/vnd.apple.mpegurl'
925 });
926 openMediaSource(player);
927 standardXHRResponse(requests.shift()); // master
928 requests.shift().respond(200, null,
929 '#EXTM3U\n' +
930 '#EXT-X-MEDIA-SEQUENCE:2\n' +
931 '#EXTINF:10,\n' +
932 '2.ts\n' +
933 '#EXTINF:10,\n' +
934 '3.ts\n'); // media
935 player.tech_.buffered = function() { return videojs.createTimeRange(buffered); };
936 player.tech_.currentTime = function() { return currentTime; };
937 player.tech_.paused = function() { return false; };
938 player.tech_.trigger('play');
939 clock.tick(1);
940 standardXHRResponse(requests.shift()); // segment 0
941 equal(player.tech_.hls.mediaIndex, 1, 'incremented mediaIndex');
942
943 player.tech_.hls.sourceBuffer.trigger('updateend');
944 buffered.push([0, 10]);
945
946 // force a playlist switch
947 player.tech_.hls.playlists.media('media1.m3u8');
948 requests = requests.filter(function(request) {
949 return !request.aborted;
950 });
951 requests.shift().respond(200, null,
952 '#EXTM3U\n' +
953 '#EXT-X-MEDIA-SEQUENCE:9999\n' +
954 '#EXTINF:10,\n' +
955 '3.ts\n' +
956 '#EXTINF:10,\n' +
957 '4.ts\n' +
958 '#EXTINF:10,\n' +
959 '5.ts\n'); // media1
960 player.tech_.hls.checkBuffer_();
961 standardXHRResponse(requests.shift());
962
963 buffered.push([20, 30]);
964 currentTime = 8;
965
966 player.tech_.hls.sourceBuffer.trigger('updateend');
967 equal(player.tech_.hls.mediaIndex, 0, 'prepared to request the missing segment');
968 }); 843 });
969 844
970 test('updates the duration after switching playlists', function() { 845 test('updates the duration after switching playlists', function() {
...@@ -974,19 +849,22 @@ test('updates the duration after switching playlists', function() { ...@@ -974,19 +849,22 @@ test('updates the duration after switching playlists', function() {
974 type: 'application/vnd.apple.mpegurl' 849 type: 'application/vnd.apple.mpegurl'
975 }); 850 });
976 openMediaSource(player); 851 openMediaSource(player);
852
853 player.tech_.hls.bandwidth = 1e20;
854 standardXHRResponse(requests[0]); // master
855 standardXHRResponse(requests[1]); // media3
856
977 player.tech_.hls.selectPlaylist = function() { 857 player.tech_.hls.selectPlaylist = function() {
978 selectedPlaylist = true; 858 selectedPlaylist = true;
979 859
980 // this duraiton should be overwritten by the playlist change 860 // this duration should be overwritten by the playlist change
981 player.tech_.hls.mediaSource.duration = -Infinity; 861 player.tech_.hls.mediaSource.duration = -Infinity;
982 862
983 return player.tech_.hls.playlists.master.playlists[1]; 863 return player.tech_.hls.playlists.master.playlists[1];
984 }; 864 };
985 865
986 standardXHRResponse(requests[0]); 866 standardXHRResponse(requests[2]); // segment 0
987 standardXHRResponse(requests[1]); 867 standardXHRResponse(requests[3]); // media1
988 standardXHRResponse(requests[2]);
989 standardXHRResponse(requests[3]);
990 ok(selectedPlaylist, 'selected playlist'); 868 ok(selectedPlaylist, 'selected playlist');
991 ok(player.tech_.hls.mediaSource.duration !== -Infinity, 'updates the duration'); 869 ok(player.tech_.hls.mediaSource.duration !== -Infinity, 'updates the duration');
992 }); 870 });
...@@ -1058,21 +936,6 @@ test('selects a playlist below the current bandwidth', function() { ...@@ -1058,21 +936,6 @@ test('selects a playlist below the current bandwidth', function() {
1058 'the low bitrate stream is selected'); 936 'the low bitrate stream is selected');
1059 }); 937 });
1060 938
1061 test('scales the bandwidth estimate for the first segment', function() {
1062 player.src({
1063 src: 'manifest/master.m3u8',
1064 type: 'application/vnd.apple.mpegurl'
1065 });
1066 openMediaSource(player);
1067
1068 requests[0].bandwidth = 500;
1069 requests.shift().respond(200, null,
1070 '#EXTM3U\n' +
1071 '#EXT-X-PLAYLIST-TYPE:VOD\n' +
1072 '#EXT-X-TARGETDURATION:10\n');
1073 equal(player.tech_.hls.bandwidth, 500 * 5, 'scaled the bandwidth estimate by 5');
1074 });
1075
1076 test('allows initial bandwidth to be provided', function() { 939 test('allows initial bandwidth to be provided', function() {
1077 player.src({ 940 player.src({
1078 src: 'manifest/master.m3u8', 941 src: 'manifest/master.m3u8',
...@@ -1242,6 +1105,7 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -1242,6 +1105,7 @@ test('downloads the next segment if the buffer is getting low', function() {
1242 player.tech_.buffered = function() { 1105 player.tech_.buffered = function() {
1243 return videojs.createTimeRange(0, 19.999); 1106 return videojs.createTimeRange(0, 19.999);
1244 }; 1107 };
1108 player.tech_.hls.sourceBuffer.trigger('updateend');
1245 player.tech_.hls.checkBuffer_(); 1109 player.tech_.hls.checkBuffer_();
1246 1110
1247 standardXHRResponse(requests[2]); 1111 standardXHRResponse(requests[2]);
...@@ -1253,46 +1117,41 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -1253,46 +1117,41 @@ test('downloads the next segment if the buffer is getting low', function() {
1253 }); 1117 });
1254 1118
1255 test('buffers based on the correct TimeRange if multiple ranges exist', function() { 1119 test('buffers based on the correct TimeRange if multiple ranges exist', function() {
1256 player.tech_.currentTime = function() { 1120 var currentTime, buffered;
1257 return 8;
1258 };
1259
1260 player.src({ 1121 player.src({
1261 src: 'manifest/media.m3u8', 1122 src: 'manifest/media.m3u8',
1262 type: 'application/vnd.apple.mpegurl' 1123 type: 'application/vnd.apple.mpegurl'
1263 }); 1124 });
1264 openMediaSource(player); 1125 openMediaSource(player);
1126
1127 player.tech_.currentTime = function() {
1128 return currentTime;
1129 };
1265 player.tech_.buffered = function() { 1130 player.tech_.buffered = function() {
1266 return videojs.createTimeRange([[0, 10], [50, 160]]); 1131 return videojs.createTimeRange(buffered);
1267 }; 1132 };
1133 currentTime = 8;
1134 buffered = [[0, 10], [20, 40]];
1268 1135
1269 standardXHRResponse(requests[0]); 1136 standardXHRResponse(requests[0]);
1270 standardXHRResponse(requests[1]); 1137 standardXHRResponse(requests[1]);
1271 1138
1272 strictEqual(requests.length, 2, 'made two requests'); 1139 strictEqual(requests.length, 2, 'made two requests');
1273 strictEqual(requests[1].url, 1140 strictEqual(requests[1].url,
1274 absoluteUrl('manifest/media-00001.ts'), 1141 absoluteUrl('manifest/media-00002.ts'),
1275 'made segment request'); 1142 'made segment request');
1276 1143
1277 player.tech_.currentTime = function() { 1144 currentTime = 22;
1278 return 55; 1145 player.tech_.hls.sourceBuffer.trigger('updateend');
1279 };
1280
1281 player.tech_.hls.checkBuffer_(); 1146 player.tech_.hls.checkBuffer_();
1282
1283 strictEqual(requests.length, 2, 'made no additional requests'); 1147 strictEqual(requests.length, 2, 'made no additional requests');
1284 1148
1285 player.tech_.currentTime = function() { 1149 buffered = [[0, 10], [20, 30]];
1286 return 134;
1287 };
1288
1289 player.tech_.hls.checkBuffer_(); 1150 player.tech_.hls.checkBuffer_();
1290 standardXHRResponse(requests[2]); 1151 standardXHRResponse(requests[2]);
1291
1292 strictEqual(requests.length, 3, 'made three requests'); 1152 strictEqual(requests.length, 3, 'made three requests');
1293
1294 strictEqual(requests[2].url, 1153 strictEqual(requests[2].url,
1295 absoluteUrl('manifest/media-00002.ts'), 1154 absoluteUrl('manifest/media-00004.ts'),
1296 'made segment request'); 1155 'made segment request');
1297 }); 1156 });
1298 1157
...@@ -1325,7 +1184,6 @@ test('only makes one segment request at a time', function() { ...@@ -1325,7 +1184,6 @@ test('only makes one segment request at a time', function() {
1325 }); 1184 });
1326 1185
1327 test('only appends one segment at a time', function() { 1186 test('only appends one segment at a time', function() {
1328 var appends = 0;
1329 player.src({ 1187 player.src({
1330 src: 'manifest/media.m3u8', 1188 src: 'manifest/media.m3u8',
1331 type: 'application/vnd.apple.mpegurl' 1189 type: 'application/vnd.apple.mpegurl'
...@@ -1334,97 +1192,11 @@ test('only appends one segment at a time', function() { ...@@ -1334,97 +1192,11 @@ test('only appends one segment at a time', function() {
1334 standardXHRResponse(requests.pop()); // media.m3u8 1192 standardXHRResponse(requests.pop()); // media.m3u8
1335 standardXHRResponse(requests.pop()); // segment 0 1193 standardXHRResponse(requests.pop()); // segment 0
1336 1194
1337 player.tech_.hls.sourceBuffer.updating = true;
1338 player.tech_.hls.sourceBuffer.appendBuffer = function() {
1339 appends++;
1340 };
1341
1342 player.tech_.hls.checkBuffer_(); 1195 player.tech_.hls.checkBuffer_();
1343 standardXHRResponse(requests.pop()); // segment 1 1196 equal(requests.length, 0, 'did not request while updating');
1344 player.tech_.hls.checkBuffer_(); // should be a no-op
1345 equal(appends, 0, 'did not append while updating');
1346 });
1347
1348 QUnit.skip('records the min and max PTS values for a segment', function() {
1349 var tags = [];
1350 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1351 player.src({
1352 src: 'manifest/media.m3u8',
1353 type: 'application/vnd.apple.mpegurl'
1354 });
1355 openMediaSource(player);
1356 standardXHRResponse(requests.pop()); // media.m3u8
1357
1358 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1359 tags.push({ pts: 10, bytes: new Uint8Array(1) });
1360 standardXHRResponse(requests.pop()); // segment 0
1361
1362 equal(player.tech_.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts');
1363 equal(player.tech_.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts');
1364 equal(player.tech_.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts');
1365 equal(player.tech_.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts');
1366 });
1367
1368 QUnit.skip('records PTS values for video-only segments', function() {
1369 var tags = [];
1370 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1371 player.src({
1372 src: 'manifest/media.m3u8',
1373 type: 'application/vnd.apple.mpegurl'
1374 });
1375 openMediaSource(player);
1376 standardXHRResponse(requests.pop()); // media.m3u8
1377
1378 player.tech_.hls.segmentParser_.stats.aacTags = function() {
1379 return 0;
1380 };
1381 player.tech_.hls.segmentParser_.stats.minAudioPts = function() {
1382 throw new Error('No audio tags');
1383 };
1384 player.tech_.hls.segmentParser_.stats.maxAudioPts = function() {
1385 throw new Error('No audio tags');
1386 };
1387 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1388 tags.push({ pts: 10, bytes: new Uint8Array(1) });
1389 standardXHRResponse(requests.pop()); // segment 0
1390
1391 equal(player.tech_.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts');
1392 equal(player.tech_.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts');
1393 strictEqual(player.tech_.hls.playlists.media().segments[0].minAudioPts, undefined, 'min audio pts is undefined');
1394 strictEqual(player.tech_.hls.playlists.media().segments[0].maxAudioPts, undefined, 'max audio pts is undefined');
1395 });
1396
1397 QUnit.skip('records PTS values for audio-only segments', function() {
1398 var tags = [];
1399 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1400 player.src({
1401 src: 'manifest/media.m3u8',
1402 type: 'application/vnd.apple.mpegurl'
1403 });
1404 openMediaSource(player);
1405 standardXHRResponse(requests.pop()); // media.m3u8
1406
1407 player.tech_.hls.segmentParser_.stats.h264Tags = function() {
1408 return 0;
1409 };
1410 player.tech_.hls.segmentParser_.stats.minVideoPts = function() {
1411 throw new Error('No video tags');
1412 };
1413 player.tech_.hls.segmentParser_.stats.maxVideoPts = function() {
1414 throw new Error('No video tags');
1415 };
1416 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1417 tags.push({ pts: 10, bytes: new Uint8Array(1) });
1418 standardXHRResponse(requests.pop()); // segment 0
1419
1420 equal(player.tech_.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts');
1421 equal(player.tech_.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts');
1422 strictEqual(player.tech_.hls.playlists.media().segments[0].minVideoPts, undefined, 'min video pts is undefined');
1423 strictEqual(player.tech_.hls.playlists.media().segments[0].maxVideoPts, undefined, 'max video pts is undefined');
1424 }); 1197 });
1425 1198
1426 test('waits to download new segments until the media playlist is stable', function() { 1199 test('waits to download new segments until the media playlist is stable', function() {
1427 var media;
1428 player.src({ 1200 player.src({
1429 src: 'manifest/master.m3u8', 1201 src: 'manifest/master.m3u8',
1430 type: 'application/vnd.apple.mpegurl' 1202 type: 'application/vnd.apple.mpegurl'
...@@ -1432,22 +1204,19 @@ test('waits to download new segments until the media playlist is stable', functi ...@@ -1432,22 +1204,19 @@ test('waits to download new segments until the media playlist is stable', functi
1432 openMediaSource(player); 1204 openMediaSource(player);
1433 standardXHRResponse(requests.shift()); // master 1205 standardXHRResponse(requests.shift()); // master
1434 player.tech_.hls.bandwidth = 1; // make sure we stay on the lowest variant 1206 player.tech_.hls.bandwidth = 1; // make sure we stay on the lowest variant
1435 standardXHRResponse(requests.shift()); // media 1207 standardXHRResponse(requests.shift()); // media1
1436 1208
1437 // mock a playlist switch 1209 // force a playlist switch
1438 media = player.tech_.hls.playlists.media(); 1210 player.tech_.hls.playlists.media('media3.m3u8');
1439 player.tech_.hls.playlists.media = function() {
1440 return media;
1441 };
1442 player.tech_.hls.playlists.state = 'SWITCHING_MEDIA';
1443 1211
1444 standardXHRResponse(requests.shift()); // segment 0 1212 standardXHRResponse(requests.shift()); // segment 0
1213 player.tech_.hls.sourceBuffer.trigger('updateend');
1445 1214
1446 equal(requests.length, 0, 'no requests outstanding'); 1215 equal(requests.length, 1, 'only the playlist request outstanding');
1447 player.tech_.hls.checkBuffer_(); 1216 player.tech_.hls.checkBuffer_();
1448 equal(requests.length, 0, 'delays segment fetching'); 1217 equal(requests.length, 1, 'delays segment fetching');
1449 1218
1450 player.tech_.hls.playlists.state = 'LOADED_METADATA'; 1219 standardXHRResponse(requests.shift()); // media3
1451 player.tech_.hls.checkBuffer_(); 1220 player.tech_.hls.checkBuffer_();
1452 equal(requests.length, 1, 'resumes segment fetching'); 1221 equal(requests.length, 1, 'resumes segment fetching');
1453 }); 1222 });
...@@ -1536,359 +1305,6 @@ test('segmentXhr is properly nulled out when dispose is called', function() { ...@@ -1536,359 +1305,6 @@ test('segmentXhr is properly nulled out when dispose is called', function() {
1536 Flash.prototype.dispose = oldDispose; 1305 Flash.prototype.dispose = oldDispose;
1537 }); 1306 });
1538 1307
1539 QUnit.skip('exposes in-band metadata events as cues', function() {
1540 var track;
1541 videojs.Hls.SegmentParser = mockSegmentParser();
1542 player.src({
1543 src: 'manifest/media.m3u8',
1544 type: 'application/vnd.apple.mpegurl'
1545 });
1546 openMediaSource(player);
1547
1548 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1549 // trigger a metadata event
1550 player.tech_.hls.segmentParser_.metadataStream.trigger('data', {
1551 pts: 2000,
1552 data: new Uint8Array([]),
1553 frames: [{
1554 id: 'TXXX',
1555 value: 'cue text'
1556 }, {
1557 id: 'WXXX',
1558 url: 'http://example.com'
1559 }, {
1560 id: 'PRIV',
1561 owner: 'owner@example.com',
1562 privateData: new Uint8Array([1, 2, 3])
1563 }]
1564 });
1565 };
1566
1567 standardXHRResponse(requests[0]);
1568 standardXHRResponse(requests[1]);
1569
1570 equal(player.textTracks().length, 1, 'created a text track');
1571 track = player.textTracks()[0];
1572 equal(track.kind, 'metadata', 'kind is metadata');
1573 equal(track.inBandMetadataTrackDispatchType, '15010203BB', 'set the dispatch type');
1574 equal(track.cues.length, 3, 'created three cues');
1575 equal(track.cues[0].startTime, 2, 'cue starts at 2 seconds');
1576 equal(track.cues[0].endTime, 2, 'cue ends at 2 seconds');
1577 equal(track.cues[0].pauseOnExit, false, 'cue does not pause on exit');
1578 equal(track.cues[0].text, 'cue text', 'set cue text');
1579
1580 equal(track.cues[1].startTime, 2, 'cue starts at 2 seconds');
1581 equal(track.cues[1].endTime, 2, 'cue ends at 2 seconds');
1582 equal(track.cues[1].pauseOnExit, false, 'cue does not pause on exit');
1583 equal(track.cues[1].text, 'http://example.com', 'set cue text');
1584
1585 equal(track.cues[2].startTime, 2, 'cue starts at 2 seconds');
1586 equal(track.cues[2].endTime, 2, 'cue ends at 2 seconds');
1587 equal(track.cues[2].pauseOnExit, false, 'cue does not pause on exit');
1588 equal(track.cues[2].text, '', 'did not set cue text');
1589 equal(track.cues[2].frame.owner, 'owner@example.com', 'set the owner');
1590 deepEqual(track.cues[2].frame.privateData,
1591 new Uint8Array([1, 2, 3]),
1592 'set the private data');
1593 });
1594
1595 QUnit.skip('only adds in-band cues the first time they are encountered', function() {
1596 var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track;
1597 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1598 player.src({
1599 src: 'manifest/media.m3u8',
1600 type: 'application/vnd.apple.mpegurl'
1601 });
1602 openMediaSource(player);
1603
1604 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1605 // trigger a metadata event
1606 player.tech_.hls.segmentParser_.metadataStream.trigger('data', {
1607 pts: 2000,
1608 data: new Uint8Array([]),
1609 frames: [{
1610 id: 'TXXX',
1611 value: 'cue text'
1612 }]
1613 });
1614 };
1615 standardXHRResponse(requests.shift());
1616 standardXHRResponse(requests.shift());
1617 // seek back to the first segment
1618 player.currentTime(0);
1619 player.tech_.hls.trigger('seeking');
1620 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1621 standardXHRResponse(requests.shift());
1622
1623 track = player.textTracks()[0];
1624 equal(track.cues.length, 1, 'only added the cue once');
1625 });
1626
1627 QUnit.skip('clears in-band cues ahead of current time on seek', function() {
1628 var
1629 tags = [],
1630 events = [],
1631 track;
1632 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1633 player.src({
1634 src: 'manifest/media.m3u8',
1635 type: 'application/vnd.apple.mpegurl'
1636 });
1637 openMediaSource(player);
1638
1639 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1640 // trigger a metadata event
1641 while (events.length) {
1642 player.tech_.hls.segmentParser_.metadataStream.trigger('data', events.shift());
1643 }
1644 };
1645 standardXHRResponse(requests.shift()); // media
1646 tags.push({ pts: 0, bytes: new Uint8Array(1) },
1647 { pts: 10 * 1000, bytes: new Uint8Array(1) });
1648 events.push({
1649 pts: 9.9 * 1000,
1650 data: new Uint8Array([]),
1651 frames: [{
1652 id: 'TXXX',
1653 value: 'cue 1'
1654 }]
1655 });
1656 events.push({
1657 pts: 20 * 1000,
1658 data: new Uint8Array([]),
1659 frames: [{
1660 id: 'TXXX',
1661 value: 'cue 3'
1662 }]
1663 });
1664 standardXHRResponse(requests.shift()); // segment 0
1665 tags.push({ pts: 10 * 1000 + 1, bytes: new Uint8Array(1) },
1666 { pts: 20 * 1000, bytes: new Uint8Array(1) });
1667 events.push({
1668 pts: 19.9 * 1000,
1669 data: new Uint8Array([]),
1670 frames: [{
1671 id: 'TXXX',
1672 value: 'cue 2'
1673 }]
1674 });
1675 player.tech_.hls.checkBuffer_();
1676 standardXHRResponse(requests.shift()); // segment 1
1677
1678 track = player.textTracks()[0];
1679 equal(track.cues.length, 3, 'added the cues');
1680
1681 // seek into segment 1
1682 player.currentTime(11);
1683 player.trigger('seeking');
1684 equal(track.cues.length, 1, 'removed later cues');
1685 equal(track.cues[0].startTime, 9.9, 'retained the earlier cue');
1686 });
1687
1688 QUnit.skip('translates ID3 PTS values to cue media timeline positions', function() {
1689 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
1690 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1691 player.src({
1692 src: 'manifest/media.m3u8',
1693 type: 'application/vnd.apple.mpegurl'
1694 });
1695 openMediaSource(player);
1696
1697 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1698 // trigger a metadata event
1699 player.tech_.hls.segmentParser_.metadataStream.trigger('data', {
1700 pts: 5 * 1000,
1701 data: new Uint8Array([]),
1702 frames: [{
1703 id: 'TXXX',
1704 value: 'cue text'
1705 }]
1706 });
1707 };
1708 standardXHRResponse(requests.shift()); // media
1709 standardXHRResponse(requests.shift()); // segment 0
1710
1711 track = player.textTracks()[0];
1712 equal(track.cues[0].startTime, 1, 'translated startTime');
1713 equal(track.cues[0].endTime, 1, 'translated startTime');
1714 });
1715
1716 QUnit.skip('translates ID3 PTS values with expired segments', function() {
1717 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
1718 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1719 player.src({
1720 src: 'live.m3u8',
1721 type: 'application/vnd.apple.mpegurl'
1722 });
1723 openMediaSource(player);
1724 player.play();
1725
1726 // 20.9 seconds of content have expired
1727 player.hls.playlists.expiredPostDiscontinuity_ = 20.9;
1728
1729 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1730 // trigger a metadata event
1731 player.hls.segmentParser_.metadataStream.trigger('data', {
1732 pts: 5 * 1000,
1733 data: new Uint8Array([]),
1734 frames: [{
1735 id: 'TXXX',
1736 value: 'cue text'
1737 }]
1738 });
1739 };
1740 requests.shift().respond(200, null,
1741 '#EXTM3U\n' +
1742 '#EXT-X-MEDIA-SEQUENCE:2\n' +
1743 '#EXTINF:10,\n' +
1744 '2.ts\n' +
1745 '#EXTINF:10,\n' +
1746 '3.ts\n'); // media
1747 standardXHRResponse(requests.shift()); // segment 0
1748
1749 track = player.textTracks()[0];
1750 equal(track.cues[0].startTime, 20.9 + 1, 'translated startTime');
1751 equal(track.cues[0].endTime, 20.9 + 1, 'translated startTime');
1752 });
1753
1754 QUnit.skip('translates id3 PTS values for audio-only media', function() {
1755 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
1756 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1757 player.src({
1758 src: 'manifest/media.m3u8',
1759 type: 'application/vnd.apple.mpegurl'
1760 });
1761 openMediaSource(player);
1762
1763 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1764 // trigger a metadata event
1765 player.hls.segmentParser_.metadataStream.trigger('data', {
1766 pts: 5 * 1000,
1767 data: new Uint8Array([]),
1768 frames: [{
1769 id: 'TXXX',
1770 value: 'cue text'
1771 }]
1772 });
1773 };
1774 player.hls.segmentParser_.stats.h264Tags = function() { return 0; };
1775 player.hls.segmentParser_.stats.minVideoPts = null;
1776 standardXHRResponse(requests.shift()); // media
1777 standardXHRResponse(requests.shift()); // segment 0
1778
1779 track = player.textTracks()[0];
1780 equal(track.cues[0].startTime, 1, 'translated startTime');
1781 });
1782
1783 QUnit.skip('translates ID3 PTS values across discontinuities', function() {
1784 var tags = [], events = [], track;
1785 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1786 player.src({
1787 src: 'cues-and-discontinuities.m3u8',
1788 type: 'application/vnd.apple.mpegurl'
1789 });
1790 openMediaSource(player);
1791
1792 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1793 // trigger a metadata event
1794 if (events.length) {
1795 player.tech_.hls.segmentParser_.metadataStream.trigger('data', events.shift());
1796 }
1797 };
1798
1799 // media playlist
1800 player.trigger('play');
1801 requests.shift().respond(200, null,
1802 '#EXTM3U\n' +
1803 '#EXTINF:10,\n' +
1804 '0.ts\n' +
1805 '#EXT-X-DISCONTINUITY\n' +
1806 '#EXTINF:10,\n' +
1807 '1.ts\n');
1808
1809 // segment 0 starts at PTS 14000 and has a cue point at 15000
1810 tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) },
1811 { pts: 24 * 1000, bytes: new Uint8Array(1) });
1812 events.push({
1813 pts: 15 * 1000,
1814 data: new Uint8Array([]),
1815 frames: [{
1816 id: 'TXXX',
1817 value: 'cue 0'
1818 }]
1819 });
1820 standardXHRResponse(requests.shift()); // segment 0
1821
1822 // segment 1 is after a discontinuity, starts at PTS 22000
1823 // and has a cue point at 23000
1824 tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) });
1825 events.push({
1826 pts: 23 * 1000,
1827 data: new Uint8Array([]),
1828 frames: [{
1829 id: 'TXXX',
1830 value: 'cue 1'
1831 }]
1832 });
1833 player.tech_.hls.checkBuffer_();
1834 standardXHRResponse(requests.shift());
1835
1836 track = player.textTracks()[0];
1837 equal(track.cues.length, 2, 'created cues');
1838 equal(track.cues[0].startTime, 1, 'first cue started at the correct time');
1839 equal(track.cues[0].endTime, 1, 'first cue ended at the correct time');
1840 equal(track.cues[1].startTime, 11, 'second cue started at the correct time');
1841 equal(track.cues[1].endTime, 11, 'second cue ended at the correct time');
1842 });
1843
1844 test('adjusts the segment offsets for out-of-buffer seeking', function() {
1845 player.src({
1846 src: 'manifest/media.m3u8',
1847 type: 'application/vnd.apple.mpegurl'
1848 });
1849 openMediaSource(player);
1850 standardXHRResponse(requests.shift()); // media
1851 player.tech_.hls.sourceBuffer.buffered = function() {
1852 return videojs.createTimeRange(0, 20);
1853 };
1854 equal(player.tech_.hls.mediaIndex, 0, 'starts at zero');
1855
1856 player.tech_.setCurrentTime(35);
1857 clock.tick(1);
1858 // drop the aborted segment
1859 requests.shift();
1860 equal(player.tech_.hls.mediaIndex, 3, 'moved the mediaIndex');
1861 standardXHRResponse(requests.shift());
1862 });
1863
1864 test('seeks between buffered time ranges', function() {
1865 player.src({
1866 src: 'manifest/media.m3u8',
1867 type: 'application/vnd.apple.mpegurl'
1868 });
1869 openMediaSource(player);
1870 standardXHRResponse(requests.shift()); // media
1871 player.tech_.buffered = function() {
1872 return {
1873 length: 2,
1874 ranges_: [[0, 10], [20, 30]],
1875 start: function(i) {
1876 return this.ranges_[i][0];
1877 },
1878 end: function(i) {
1879 return this.ranges_[i][1];
1880 }
1881 };
1882 };
1883
1884 player.tech_.setCurrentTime(15);
1885 clock.tick(1);
1886 // drop the aborted segment
1887 requests.shift();
1888 equal(player.tech_.hls.mediaIndex, 1, 'updated the mediaIndex');
1889 standardXHRResponse(requests.shift());
1890 });
1891
1892 test('does not modify the media index for in-buffer seeking', function() { 1308 test('does not modify the media index for in-buffer seeking', function() {
1893 var mediaIndex; 1309 var mediaIndex;
1894 player.src({ 1310 player.src({
...@@ -1979,40 +1395,6 @@ test('duration is Infinity for live playlists', function() { ...@@ -1979,40 +1395,6 @@ test('duration is Infinity for live playlists', function() {
1979 'duration is infinity'); 1395 'duration is infinity');
1980 }); 1396 });
1981 1397
1982 test('updates the media index when a playlist reloads', function() {
1983 player.src({
1984 src: 'http://example.com/live-updating.m3u8',
1985 type: 'application/vnd.apple.mpegurl'
1986 });
1987 openMediaSource(player);
1988 player.tech_.trigger('play');
1989
1990 requests[0].respond(200, null,
1991 '#EXTM3U\n' +
1992 '#EXTINF:10,\n' +
1993 '0.ts\n' +
1994 '#EXTINF:10,\n' +
1995 '1.ts\n' +
1996 '#EXTINF:10,\n' +
1997 '2.ts\n');
1998 standardXHRResponse(requests[1]);
1999 // play the stream until 2.ts is playing
2000 player.tech_.hls.mediaIndex = 3;
2001 // trigger a playlist refresh
2002 player.tech_.hls.playlists.trigger('mediaupdatetimeout');
2003 requests[2].respond(200, null,
2004 '#EXTM3U\n' +
2005 '#EXT-X-MEDIA-SEQUENCE:1\n' +
2006 '#EXTINF:10,\n' +
2007 '1.ts\n' +
2008 '#EXTINF:10,\n' +
2009 '2.ts\n' +
2010 '#EXTINF:10,\n' +
2011 '3.ts\n');
2012
2013 strictEqual(player.tech_.hls.mediaIndex, 2, 'mediaIndex is updated after the reload');
2014 });
2015
2016 test('live playlist starts three target durations before live', function() { 1398 test('live playlist starts three target durations before live', function() {
2017 var mediaPlaylist; 1399 var mediaPlaylist;
2018 player.src({ 1400 player.src({
...@@ -2040,30 +1422,11 @@ test('live playlist starts three target durations before live', function() { ...@@ -2040,30 +1422,11 @@ test('live playlist starts three target durations before live', function() {
2040 player.tech_.trigger('play'); 1422 player.tech_.trigger('play');
2041 clock.tick(1); 1423 clock.tick(1);
2042 mediaPlaylist = player.tech_.hls.playlists.media(); 1424 mediaPlaylist = player.tech_.hls.playlists.media();
2043 equal(player.tech_.hls.mediaIndex, 1, 'mediaIndex is updated at play');
2044 equal(player.currentTime(), player.tech_.hls.seekable().end(0), 'seeked to the seekable end'); 1425 equal(player.currentTime(), player.tech_.hls.seekable().end(0), 'seeked to the seekable end');
2045 1426
2046 equal(requests.length, 1, 'begins buffering'); 1427 equal(requests.length, 1, 'begins buffering');
2047 }); 1428 });
2048 1429
2049 test('does not reset live currentTime if mediaIndex is one beyond the last available segment', function() {
2050 var playlist = {
2051 mediaSequence: 20,
2052 targetDuration: 9,
2053 segments: [{
2054 duration: 3
2055 }, {
2056 duration: 3
2057 }, {
2058 duration: 3
2059 }]
2060 };
2061
2062 equal(playlist.segments.length,
2063 videojs.Hls.translateMediaIndex(playlist.segments.length, playlist, playlist),
2064 'did not change mediaIndex');
2065 });
2066
2067 test('live playlist starts with correct currentTime value', function() { 1430 test('live playlist starts with correct currentTime value', function() {
2068 player.src({ 1431 player.src({
2069 src: 'http://example.com/manifest/liveStart30sBefore.m3u8', 1432 src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
...@@ -2117,39 +1480,6 @@ test('resets the time to a seekable position when resuming a live stream ' + ...@@ -2117,39 +1480,6 @@ test('resets the time to a seekable position when resuming a live stream ' +
2117 player.tech_.trigger('seeked'); 1480 player.tech_.trigger('seeked');
2118 }); 1481 });
2119 1482
2120 test('mediaIndex is zero before the first segment loads', function() {
2121 window.manifests['first-seg-load'] =
2122 '#EXTM3U\n' +
2123 '#EXTINF:10,\n' +
2124 '0.ts\n';
2125 player.src({
2126 src: 'http://example.com/first-seg-load.m3u8',
2127 type: 'application/vnd.apple.mpegurl'
2128 });
2129 openMediaSource(player);
2130
2131 strictEqual(player.tech_.hls.mediaIndex, 0, 'mediaIndex is zero');
2132 });
2133
2134 test('mediaIndex returns correctly at playlist boundaries', function() {
2135 player.src({
2136 src: 'http://example.com/master.m3u8',
2137 type: 'application/vnd.apple.mpegurl'
2138 });
2139
2140 openMediaSource(player);
2141 standardXHRResponse(requests.shift()); // master
2142 standardXHRResponse(requests.shift()); // media
2143
2144 strictEqual(player.tech_.hls.mediaIndex, 0, 'mediaIndex is zero at first segment');
2145
2146 // seek to end
2147 player.tech_.setCurrentTime(40);
2148 clock.tick(1);
2149
2150 strictEqual(player.tech_.hls.mediaIndex, 3, 'mediaIndex is 3 at last segment');
2151 });
2152
2153 test('reloads out-of-date live playlists when switching variants', function() { 1483 test('reloads out-of-date live playlists when switching variants', function() {
2154 player.src({ 1484 player.src({
2155 src: 'http://example.com/master.m3u8', 1485 src: 'http://example.com/master.m3u8',
...@@ -2248,26 +1578,21 @@ test('does not break if the playlist has no segments', function() { ...@@ -2248,26 +1578,21 @@ test('does not break if the playlist has no segments', function() {
2248 strictEqual(requests.length, 1, 'no requests for non-existent segments were queued'); 1578 strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
2249 }); 1579 });
2250 1580
2251 test('clears the segment buffer on seek', function() { 1581 test('aborts segment processing on seek', function() {
2252 var currentTime, oldCurrentTime; 1582 var currentTime = 0;
2253
2254 player.src({ 1583 player.src({
2255 src: 'discontinuity.m3u8', 1584 src: 'discontinuity.m3u8',
2256 type: 'application/vnd.apple.mpegurl' 1585 type: 'application/vnd.apple.mpegurl'
2257 }); 1586 });
2258 openMediaSource(player); 1587 openMediaSource(player);
2259 oldCurrentTime = player.currentTime; 1588 player.tech_.currentTime = function() {
2260 player.currentTime = function(time) {
2261 if (time !== undefined) {
2262 return oldCurrentTime.call(player, time);
2263 }
2264 return currentTime; 1589 return currentTime;
2265 }; 1590 };
2266 player.tech_.buffered = function() { 1591 player.tech_.buffered = function() {
2267 return videojs.createTimeRange(); 1592 return videojs.createTimeRange();
2268 }; 1593 };
2269 1594
2270 requests.pop().respond(200, null, 1595 requests.shift().respond(200, null,
2271 '#EXTM3U\n' + 1596 '#EXTM3U\n' +
2272 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + 1597 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
2273 '#EXTINF:10,0\n' + 1598 '#EXTINF:10,0\n' +
...@@ -2275,23 +1600,19 @@ test('clears the segment buffer on seek', function() { ...@@ -2275,23 +1600,19 @@ test('clears the segment buffer on seek', function() {
2275 '#EXT-X-DISCONTINUITY\n' + 1600 '#EXT-X-DISCONTINUITY\n' +
2276 '#EXTINF:10,0\n' + 1601 '#EXTINF:10,0\n' +
2277 '2.ts\n' + 1602 '2.ts\n' +
2278 '#EXT-X-ENDLIST\n'); 1603 '#EXT-X-ENDLIST\n'); // media
2279 standardXHRResponse(requests.pop()); // 1.ts 1604 standardXHRResponse(requests.shift()); // 1.ts
2280 1605 standardXHRResponse(requests.shift()); // key.php
2281 // play to 6s to trigger the next segment request 1606 ok(player.tech_.hls.pendingSegment_, 'decrypting the segment');
2282 currentTime = 6;
2283 clock.tick(6000);
2284
2285 standardXHRResponse(requests.pop()); // 2.ts
2286 equal(player.tech_.hls.segmentBuffer_.length, 2, 'started fetching segments');
2287 1607
2288 // seek back to the beginning 1608 // seek back to the beginning
2289 player.currentTime(0); 1609 player.currentTime(0);
2290 clock.tick(1); 1610 clock.tick(1);
2291 equal(player.tech_.hls.segmentBuffer_.length, 0, 'cleared the segment buffer'); 1611 ok(!player.tech_.hls.pendingSegment_, 'aborted processing');
2292 }); 1612 });
2293 1613
2294 test('calls mediaSource\'s timestampOffset on discontinuity', function() { 1614 test('calls mediaSource\'s timestampOffset on discontinuity', function() {
1615 var buffered = [[]];
2295 player.src({ 1616 player.src({
2296 src: 'discontinuity.m3u8', 1617 src: 'discontinuity.m3u8',
2297 type: 'application/vnd.apple.mpegurl' 1618 type: 'application/vnd.apple.mpegurl'
...@@ -2299,10 +1620,10 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { ...@@ -2299,10 +1620,10 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() {
2299 openMediaSource(player); 1620 openMediaSource(player);
2300 player.play(); 1621 player.play();
2301 player.tech_.buffered = function() { 1622 player.tech_.buffered = function() {
2302 return videojs.createTimeRange(0, 10); 1623 return videojs.createTimeRange(buffered);
2303 }; 1624 };
2304 1625
2305 requests.pop().respond(200, null, 1626 requests.shift().respond(200, null,
2306 '#EXTM3U\n' + 1627 '#EXTM3U\n' +
2307 '#EXTINF:10,0\n' + 1628 '#EXTINF:10,0\n' +
2308 '1.ts\n' + 1629 '1.ts\n' +
...@@ -2311,14 +1632,14 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { ...@@ -2311,14 +1632,14 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() {
2311 '2.ts\n' + 1632 '2.ts\n' +
2312 '#EXT-X-ENDLIST\n'); 1633 '#EXT-X-ENDLIST\n');
2313 player.tech_.hls.sourceBuffer.timestampOffset = 0; 1634 player.tech_.hls.sourceBuffer.timestampOffset = 0;
2314 standardXHRResponse(requests.pop()); // 1.ts 1635 standardXHRResponse(requests.shift()); // 1.ts
2315 1636 equal(player.tech_.hls.sourceBuffer.timestampOffset,
2316 equal(player.tech_.hls.sourceBuffer.timestampOffset, 0, 'timestampOffset starts at zero'); 1637 0,
2317 1638 'timestampOffset starts at zero');
2318 // play to 6s to trigger the next segment request
2319 clock.tick(6000);
2320 1639
2321 standardXHRResponse(requests.pop()); // 2.ts 1640 buffered = [[0, 10]];
1641 player.tech_.hls.sourceBuffer.trigger('updateend');
1642 standardXHRResponse(requests.shift()); // 2.ts
2322 equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity'); 1643 equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity');
2323 }); 1644 });
2324 1645
...@@ -2410,7 +1731,7 @@ QUnit.skip('sets the timestampOffset after seeking to discontinuity', function() ...@@ -2410,7 +1731,7 @@ QUnit.skip('sets the timestampOffset after seeking to discontinuity', function()
2410 'set the timestamp offset'); 1731 'set the timestamp offset');
2411 }); 1732 });
2412 1733
2413 QUnit.skip('tracks segment end times as they are buffered', function() { 1734 test('tracks segment end times as they are buffered', function() {
2414 var bufferEnd = 0; 1735 var bufferEnd = 0;
2415 player.src({ 1736 player.src({
2416 src: 'media.m3u8', 1737 src: 'media.m3u8',
...@@ -2437,8 +1758,7 @@ QUnit.skip('tracks segment end times as they are buffered', function() { ...@@ -2437,8 +1758,7 @@ QUnit.skip('tracks segment end times as they are buffered', function() {
2437 bufferEnd = 9.5; 1758 bufferEnd = 9.5;
2438 player.tech_.hls.sourceBuffer.trigger('update'); 1759 player.tech_.hls.sourceBuffer.trigger('update');
2439 player.tech_.hls.sourceBuffer.trigger('updateend'); 1760 player.tech_.hls.sourceBuffer.trigger('updateend');
2440 equal(player.tech_.duration(), 10 + 9.5, 'updated duration'); 1761 equal(player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration');
2441 equal(player.tech_.hls.appendingSegmentInfo_, null, 'cleared the appending segment');
2442 }); 1762 });
2443 1763
2444 QUnit.skip('seeking does not fail when targeted between segments', function() { 1764 QUnit.skip('seeking does not fail when targeted between segments', function() {
...@@ -2486,7 +1806,7 @@ test('resets the switching algorithm if a request times out', function() { ...@@ -2486,7 +1806,7 @@ test('resets the switching algorithm if a request times out', function() {
2486 type: 'application/vnd.apple.mpegurl' 1806 type: 'application/vnd.apple.mpegurl'
2487 }); 1807 });
2488 openMediaSource(player); 1808 openMediaSource(player);
2489 player.tech_.hls.bandwidth = 20000; 1809 player.tech_.hls.bandwidth = 1e20;
2490 1810
2491 standardXHRResponse(requests.shift()); // master 1811 standardXHRResponse(requests.shift()); // master
2492 standardXHRResponse(requests.shift()); // media.m3u8 1812 standardXHRResponse(requests.shift()); // media.m3u8
...@@ -2667,6 +1987,7 @@ test('tracks the bytes downloaded', function() { ...@@ -2667,6 +1987,7 @@ test('tracks the bytes downloaded', function() {
2667 // transmit some segment bytes 1987 // transmit some segment bytes
2668 requests[0].response = new ArrayBuffer(17); 1988 requests[0].response = new ArrayBuffer(17);
2669 requests.shift().respond(200, null, ''); 1989 requests.shift().respond(200, null, '');
1990 player.tech_.hls.sourceBuffer.trigger('updateend');
2670 1991
2671 strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received'); 1992 strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received');
2672 1993
...@@ -2721,12 +2042,15 @@ test('can be disposed before finishing initialization', function() { ...@@ -2721,12 +2042,15 @@ test('can be disposed before finishing initialization', function() {
2721 }); 2042 });
2722 2043
2723 test('calls ended() on the media source at the end of a playlist', function() { 2044 test('calls ended() on the media source at the end of a playlist', function() {
2724 var endOfStreams = 0; 2045 var endOfStreams = 0, buffered = [[]];
2725 player.src({ 2046 player.src({
2726 src: 'http://example.com/media.m3u8', 2047 src: 'http://example.com/media.m3u8',
2727 type: 'application/vnd.apple.mpegurl' 2048 type: 'application/vnd.apple.mpegurl'
2728 }); 2049 });
2729 openMediaSource(player); 2050 openMediaSource(player);
2051 player.tech_.buffered = function() {
2052 return videojs.createTimeRanges(buffered);
2053 };
2730 player.tech_.hls.mediaSource.endOfStream = function() { 2054 player.tech_.hls.mediaSource.endOfStream = function() {
2731 endOfStreams++; 2055 endOfStreams++;
2732 }; 2056 };
...@@ -2741,70 +2065,61 @@ test('calls ended() on the media source at the end of a playlist', function() { ...@@ -2741,70 +2065,61 @@ test('calls ended() on the media source at the end of a playlist', function() {
2741 requests.shift().respond(200, null, ''); 2065 requests.shift().respond(200, null, '');
2742 strictEqual(endOfStreams, 0, 'waits for the buffer update to finish'); 2066 strictEqual(endOfStreams, 0, 'waits for the buffer update to finish');
2743 2067
2068 buffered =[[0, 10]];
2744 player.tech_.hls.sourceBuffer.trigger('updateend'); 2069 player.tech_.hls.sourceBuffer.trigger('updateend');
2745 strictEqual(endOfStreams, 1, 'ended media source'); 2070 strictEqual(endOfStreams, 1, 'ended media source');
2746 }); 2071 });
2747 2072
2748 test('calling play() at the end of a video resets the media index', function() { 2073 test('calling play() at the end of a video replays', function() {
2074 var seekTime = -1;
2749 player.src({ 2075 player.src({
2750 src: 'http://example.com/media.m3u8', 2076 src: 'http://example.com/media.m3u8',
2751 type: 'application/vnd.apple.mpegurl' 2077 type: 'application/vnd.apple.mpegurl'
2752 }); 2078 });
2753 openMediaSource(player); 2079 openMediaSource(player);
2080 player.tech_.setCurrentTime = function(time) {
2081 if (time !== undefined) {
2082 seekTime = time;
2083 }
2084 return 0;
2085 };
2754 requests.shift().respond(200, null, 2086 requests.shift().respond(200, null,
2755 '#EXTM3U\n' + 2087 '#EXTM3U\n' +
2756 '#EXTINF:10,\n' + 2088 '#EXTINF:10,\n' +
2757 '0.ts\n' + 2089 '0.ts\n' +
2758 '#EXT-X-ENDLIST\n'); 2090 '#EXT-X-ENDLIST\n');
2759 standardXHRResponse(requests.shift()); 2091 standardXHRResponse(requests.shift());
2760
2761 strictEqual(player.tech_.hls.mediaIndex, 1, 'index is 1 after the first segment');
2762 player.tech_.ended = function() { 2092 player.tech_.ended = function() {
2763 return true; 2093 return true;
2764 }; 2094 };
2765 2095
2766 player.tech_.trigger('play'); 2096 player.tech_.trigger('play');
2767 strictEqual(player.tech_.hls.mediaIndex, 0, 'index is 0 after the first segment'); 2097 equal(seekTime, 0, 'seeked to the beginning');
2768 }); 2098 });
2769 2099
2770 test('drainBuffer will not proceed with empty source buffer', function() { 2100 test('segments remain pending without a source buffer', function() {
2771 var oldMedia, newMedia, compareBuffer;
2772 player.src({ 2101 player.src({
2773 src: 'https://example.com/encrypted-media.m3u8', 2102 src: 'https://example.com/encrypted-media.m3u8',
2774 type: 'application/vnd.apple.mpegurl' 2103 type: 'application/vnd.apple.mpegurl'
2775 }); 2104 });
2776 openMediaSource(player); 2105 openMediaSource(player);
2777 2106
2778 oldMedia = player.tech_.hls.playlists.media; 2107 requests.shift().respond(200, null,
2779 newMedia = {segments: [{ 2108 '#EXTM3U\n' +
2780 key: { 2109 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=52"\n' +
2781 'retries': 5 2110 '#EXTINF:10,\n' +
2782 }, 2111 'http://media.example.com/fileSequence52-A.ts' +
2783 uri: 'http://media.example.com/fileSequence52-A.ts' 2112 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=53"\n' +
2784 }, { 2113 '#EXTINF:10,\n' +
2785 key: { 2114 'http://media.example.com/fileSequence53-B.ts\n' +
2786 'method': 'AES-128', 2115 '#EXT-X-ENDLIST\n');
2787 'uri': 'https://priv.example.com/key.php?r=53'
2788 },
2789 uri: 'http://media.example.com/fileSequence53-B.ts'
2790 }]};
2791 player.tech_.hls.playlists.media = function() {
2792 return newMedia;
2793 };
2794 2116
2795 player.tech_.hls.sourceBuffer = undefined; 2117 player.tech_.hls.sourceBuffer = undefined;
2796 compareBuffer = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}];
2797 player.tech_.hls.segmentBuffer_ = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}];
2798
2799 player.tech_.hls.drainBuffer();
2800
2801 /* Normally, drainBuffer() calls segmentBuffer.shift(), removing a segment from the stack.
2802 * Comparing two buffers to ensure no segment was popped verifies that we returned early
2803 * from drainBuffer() because sourceBuffer was empty.
2804 */
2805 deepEqual(player.tech_.hls.segmentBuffer_, compareBuffer, 'playlist remains unchanged');
2806 2118
2807 player.tech_.hls.playlists.media = oldMedia; 2119 standardXHRResponse(requests.shift()); // key
2120 standardXHRResponse(requests.shift()); // segment
2121 player.tech_.hls.checkBuffer_();
2122 ok(player.tech_.hls.pendingSegment_, 'waiting for the source buffer');
2808 }); 2123 });
2809 2124
2810 test('keys are requested when an encrypted segment is loaded', function() { 2125 test('keys are requested when an encrypted segment is loaded', function() {
...@@ -2815,12 +2130,14 @@ test('keys are requested when an encrypted segment is loaded', function() { ...@@ -2815,12 +2130,14 @@ test('keys are requested when an encrypted segment is loaded', function() {
2815 openMediaSource(player); 2130 openMediaSource(player);
2816 player.tech_.trigger('play'); 2131 player.tech_.trigger('play');
2817 standardXHRResponse(requests.shift()); // playlist 2132 standardXHRResponse(requests.shift()); // playlist
2818 standardXHRResponse(requests.shift()); // first segment
2819 2133
2820 strictEqual(requests.length, 1, 'a key XHR is created'); 2134 strictEqual(requests.length, 2, 'a key XHR is created');
2821 strictEqual(requests[0].url, 2135 strictEqual(requests[0].url,
2822 player.tech_.hls.playlists.media().segments[0].key.uri, 2136 player.tech_.hls.playlists.media().segments[0].key.uri,
2823 'a key XHR is created with correct uri'); 2137 'key XHR is created with correct uri');
2138 strictEqual(requests[1].url,
2139 player.tech_.hls.playlists.media().segments[0].uri,
2140 'segment XHR is created with correct uri');
2824 }); 2141 });
2825 2142
2826 test('keys are resolved relative to the master playlist', function() { 2143 test('keys are resolved relative to the master playlist', function() {
...@@ -2841,10 +2158,9 @@ test('keys are resolved relative to the master playlist', function() { ...@@ -2841,10 +2158,9 @@ test('keys are resolved relative to the master playlist', function() {
2841 '#EXTINF:2.833,\n' + 2158 '#EXTINF:2.833,\n' +
2842 'http://media.example.com/fileSequence1.ts\n' + 2159 'http://media.example.com/fileSequence1.ts\n' +
2843 '#EXT-X-ENDLIST\n'); 2160 '#EXT-X-ENDLIST\n');
2844 2161 equal(requests.length, 2, 'requested the key');
2845 standardXHRResponse(requests.shift()); 2162 equal(requests[0].url,
2846 equal(requests.length, 1, 'requested the key'); 2163 absoluteUrl('video/playlist/keys/key.php'),
2847 ok((/video\/playlist\/keys\/key\.php$/).test(requests[0].url),
2848 'resolves multiple relative paths'); 2164 'resolves multiple relative paths');
2849 }); 2165 });
2850 2166
...@@ -2861,13 +2177,13 @@ test('keys are resolved relative to their containing playlist', function() { ...@@ -2861,13 +2177,13 @@ test('keys are resolved relative to their containing playlist', function() {
2861 '#EXTINF:2.833,\n' + 2177 '#EXTINF:2.833,\n' +
2862 'http://media.example.com/fileSequence1.ts\n' + 2178 'http://media.example.com/fileSequence1.ts\n' +
2863 '#EXT-X-ENDLIST\n'); 2179 '#EXT-X-ENDLIST\n');
2864 standardXHRResponse(requests.shift()); 2180 equal(requests.length, 2, 'requested a key');
2865 equal(requests.length, 1, 'requested a key'); 2181 equal(requests[0].url,
2866 ok((/video\/keys\/key\.php$/).test(requests[0].url), 2182 absoluteUrl('video/keys/key.php'),
2867 'resolves multiple relative paths'); 2183 'resolves multiple relative paths');
2868 }); 2184 });
2869 2185
2870 test('a new key XHR is created when a the segment is received', function() { 2186 test('a new key XHR is created when a the segment is requested', function() {
2871 player.src({ 2187 player.src({
2872 src: 'https://example.com/encrypted-media.m3u8', 2188 src: 'https://example.com/encrypted-media.m3u8',
2873 type: 'application/vnd.apple.mpegurl' 2189 type: 'application/vnd.apple.mpegurl'
...@@ -2884,15 +2200,17 @@ test('a new key XHR is created when a the segment is received', function() { ...@@ -2884,15 +2200,17 @@ test('a new key XHR is created when a the segment is received', function() {
2884 '#EXTINF:2.833,\n' + 2200 '#EXTINF:2.833,\n' +
2885 'http://media.example.com/fileSequence2.ts\n' + 2201 'http://media.example.com/fileSequence2.ts\n' +
2886 '#EXT-X-ENDLIST\n'); 2202 '#EXT-X-ENDLIST\n');
2887 standardXHRResponse(requests.shift()); // segment 1
2888 standardXHRResponse(requests.shift()); // key 1 2203 standardXHRResponse(requests.shift()); // key 1
2204 standardXHRResponse(requests.shift()); // segment 1
2889 // "finish" decrypting segment 1 2205 // "finish" decrypting segment 1
2890 player.tech_.hls.segmentBuffer_[0].bytes = new Uint8Array(16); 2206 player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16);
2891 player.tech_.hls.checkBuffer_(); 2207 player.tech_.hls.checkBuffer_();
2208 player.tech_.buffered = function() {
2209 return videojs.createTimeRange(0, 2.833);
2210 };
2211 player.tech_.hls.sourceBuffer.trigger('updateend');
2892 2212
2893 standardXHRResponse(requests.shift()); // segment 2 2213 strictEqual(requests.length, 2, 'a key XHR is created');
2894
2895 strictEqual(requests.length, 1, 'a key XHR is created');
2896 strictEqual(requests[0].url, 2214 strictEqual(requests[0].url,
2897 'https://example.com/' + 2215 'https://example.com/' +
2898 player.tech_.hls.playlists.media().segments[1].key.uri, 2216 player.tech_.hls.playlists.media().segments[1].key.uri,
...@@ -2916,16 +2234,14 @@ test('seeking should abort an outstanding key request and create a new one', fun ...@@ -2916,16 +2234,14 @@ test('seeking should abort an outstanding key request and create a new one', fun
2916 '#EXTINF:9,\n' + 2234 '#EXTINF:9,\n' +
2917 'http://media.example.com/fileSequence2.ts\n' + 2235 'http://media.example.com/fileSequence2.ts\n' +
2918 '#EXT-X-ENDLIST\n'); 2236 '#EXT-X-ENDLIST\n');
2919 standardXHRResponse(requests.shift()); // segment 1 2237 standardXHRResponse(requests.pop()); // segment 1
2920 2238
2921 player.currentTime(11); 2239 player.currentTime(11);
2922 clock.tick(1); 2240 clock.tick(1);
2923 ok(requests[0].aborted, 'the key XHR should be aborted'); 2241 ok(requests[0].aborted, 'the key XHR should be aborted');
2924 requests.shift(); // aborted key 1 2242 requests.shift(); // aborted key 1
2925 2243
2926 equal(requests.length, 1, 'requested the new segment'); 2244 equal(requests.length, 2, 'requested the new key');
2927 standardXHRResponse(requests.shift()); // segment 2
2928 equal(requests.length, 1, 'requested the new key');
2929 equal(requests[0].url, 2245 equal(requests[0].url,
2930 'https://example.com/' + 2246 'https://example.com/' +
2931 player.tech_.hls.playlists.media().segments[1].key.uri, 2247 player.tech_.hls.playlists.media().segments[1].key.uri,
...@@ -2948,7 +2264,7 @@ test('retries key requests once upon failure', function() { ...@@ -2948,7 +2264,7 @@ test('retries key requests once upon failure', function() {
2948 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' + 2264 '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
2949 '#EXTINF:15.0,\n' + 2265 '#EXTINF:15.0,\n' +
2950 'http://media.example.com/fileSequence53-A.ts\n'); 2266 'http://media.example.com/fileSequence53-A.ts\n');
2951 standardXHRResponse(requests.shift()); // segment 2267 standardXHRResponse(requests.pop()); // segment
2952 requests[0].respond(404); 2268 requests[0].respond(404);
2953 equal(requests.length, 2, 'create a new XHR for the same key'); 2269 equal(requests.length, 2, 'create a new XHR for the same key');
2954 equal(requests[1].url, requests[0].url, 'should be the same key'); 2270 equal(requests[1].url, requests[0].url, 'should be the same key');
...@@ -2957,7 +2273,7 @@ test('retries key requests once upon failure', function() { ...@@ -2957,7 +2273,7 @@ test('retries key requests once upon failure', function() {
2957 equal(requests.length, 2, 'gives up after one retry'); 2273 equal(requests.length, 2, 'gives up after one retry');
2958 }); 2274 });
2959 2275
2960 test('skip segments if key requests fail more than once', function() { 2276 test('errors if key requests fail more than once', function() {
2961 var bytes = []; 2277 var bytes = [];
2962 2278
2963 player.src({ 2279 player.src({
...@@ -2978,23 +2294,14 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2978,23 +2294,14 @@ test('skip segments if key requests fail more than once', function() {
2978 player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) { 2294 player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) {
2979 bytes.push(chunk); 2295 bytes.push(chunk);
2980 }; 2296 };
2981 standardXHRResponse(requests.shift()); // segment 1 2297 standardXHRResponse(requests.pop()); // segment 1
2982 requests.shift().respond(404); // fail key 2298 requests.shift().respond(404); // fail key
2983 requests.shift().respond(404); // fail key, again 2299 requests.shift().respond(404); // fail key, again
2984
2985 player.tech_.hls.checkBuffer_(); 2300 player.tech_.hls.checkBuffer_();
2986 standardXHRResponse(requests.shift()); // segment 2
2987 equal(bytes.length, 0, 'did not append encrypted bytes');
2988 2301
2989 // key for second segment 2302 equal(player.tech_.hls.mediaSource.error_,
2990 requests[0].response = new Uint32Array([0,0,0,0]).buffer; 2303 'network',
2991 requests.shift().respond(200, null, ''); 2304 'triggered a network error');
2992 // "finish" decryption
2993 player.tech_.hls.segmentBuffer_[0].bytes = new Uint8Array(16);
2994 player.tech_.hls.checkBuffer_();
2995
2996 equal(bytes.length, 1, 'appended cleartext bytes from the second segment');
2997 deepEqual(bytes[0], new Uint8Array(16), 'appended bytes from the second segment, not the first');
2998 }); 2305 });
2999 2306
3000 test('the key is supplied to the decrypter in the correct format', function() { 2307 test('the key is supplied to the decrypter in the correct format', function() {
...@@ -3016,12 +2323,11 @@ test('the key is supplied to the decrypter in the correct format', function() { ...@@ -3016,12 +2323,11 @@ test('the key is supplied to the decrypter in the correct format', function() {
3016 '#EXTINF:15.0,\n' + 2323 '#EXTINF:15.0,\n' +
3017 'http://media.example.com/fileSequence52-B.ts\n'); 2324 'http://media.example.com/fileSequence52-B.ts\n');
3018 2325
3019
3020 videojs.Hls.Decrypter = function(encrypted, key) { 2326 videojs.Hls.Decrypter = function(encrypted, key) {
3021 keys.push(key); 2327 keys.push(key);
3022 }; 2328 };
3023 2329
3024 standardXHRResponse(requests.shift()); // segment 2330 standardXHRResponse(requests.pop()); // segment
3025 requests[0].response = new Uint32Array([0,1,2,3]).buffer; 2331 requests[0].response = new Uint32Array([0,1,2,3]).buffer;
3026 requests[0].respond(200, null, ''); 2332 requests[0].respond(200, null, '');
3027 requests.shift(); // key 2333 requests.shift(); // key
...@@ -3068,6 +2374,7 @@ test('supplies the media sequence of current segment as the IV by default, if no ...@@ -3068,6 +2374,7 @@ test('supplies the media sequence of current segment as the IV by default, if no
3068 }); 2374 });
3069 2375
3070 test('switching playlists with an outstanding key request does not stall playback', function() { 2376 test('switching playlists with an outstanding key request does not stall playback', function() {
2377 var buffered = [];
3071 var media = '#EXTM3U\n' + 2378 var media = '#EXTM3U\n' +
3072 '#EXT-X-MEDIA-SEQUENCE:5\n' + 2379 '#EXT-X-MEDIA-SEQUENCE:5\n' +
3073 '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' + 2380 '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' +
...@@ -3082,31 +2389,37 @@ test('switching playlists with an outstanding key request does not stall playbac ...@@ -3082,31 +2389,37 @@ test('switching playlists with an outstanding key request does not stall playbac
3082 openMediaSource(player); 2389 openMediaSource(player);
3083 player.tech_.trigger('play'); 2390 player.tech_.trigger('play');
3084 2391
2392 player.tech_.hls.bandwidth = 1;
2393 player.tech_.buffered = function() {
2394 return videojs.createTimeRange(buffered);
2395 };
3085 // master playlist 2396 // master playlist
3086 standardXHRResponse(requests.shift()); 2397 standardXHRResponse(requests.shift());
3087 // media playlist 2398 // media playlist
3088 requests.shift().respond(200, null, media); 2399 requests.shift().respond(200, null, media);
3089 // mock out media switching from this point on 2400 // mock out media switching from this point on
3090 player.tech_.hls.playlists.media = function() { 2401 player.tech_.hls.playlists.media = function() {
3091 return player.tech_.hls.playlists.master.playlists[0]; 2402 return player.tech_.hls.playlists.master.playlists[1];
3092 }; 2403 };
3093 // first segment of the original media playlist 2404 // first segment of the original media playlist
3094 standardXHRResponse(requests.shift()); 2405 standardXHRResponse(requests.pop());
3095 // don't respond to the initial key request
3096 requests.shift();
3097 2406
3098 // "switch" media 2407 // "switch" media
3099 player.tech_.hls.playlists.trigger('mediachange'); 2408 player.tech_.hls.playlists.trigger('mediachange');
2409 ok(!requests[0].aborted, 'did not abort the key request');
3100 2410
2411 // "finish" decrypting segment 1
2412 standardXHRResponse(requests.shift()); // key
2413 player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16);
2414 player.tech_.hls.checkBuffer_();
2415 buffered = [[0, 2.833]];
2416 player.tech_.hls.sourceBuffer.trigger('updateend');
3101 player.tech_.hls.checkBuffer_(); 2417 player.tech_.hls.checkBuffer_();
3102 2418
3103 ok(requests.length, 'made a request'); 2419 equal(requests.length, 1, 'made a request');
3104 equal(requests[0].url, 2420 equal(requests[0].url,
3105 'http://media.example.com/fileSequence52-B.ts', 2421 'http://media.example.com/fileSequence52-B.ts',
3106 'requested the segment'); 2422 'requested the segment');
3107 equal(requests[1].url,
3108 'https://priv.example.com/key.php?r=52',
3109 'requested the key');
3110 }); 2423 });
3111 2424
3112 test('resolves relative key URLs against the playlist', function() { 2425 test('resolves relative key URLs against the playlist', function() {
...@@ -3123,8 +2436,6 @@ test('resolves relative key URLs against the playlist', function() { ...@@ -3123,8 +2436,6 @@ test('resolves relative key URLs against the playlist', function() {
3123 '#EXTINF:2.833,\n' + 2436 '#EXTINF:2.833,\n' +
3124 'http://media.example.com/fileSequence52-A.ts\n' + 2437 'http://media.example.com/fileSequence52-A.ts\n' +
3125 '#EXT-X-ENDLIST\n'); 2438 '#EXT-X-ENDLIST\n');
3126 standardXHRResponse(requests.shift()); // segment
3127
3128 equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL'); 2439 equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL');
3129 }); 2440 });
3130 2441
...@@ -3149,7 +2460,7 @@ test('treats invalid keys as a key request failure', function() { ...@@ -3149,7 +2460,7 @@ test('treats invalid keys as a key request failure', function() {
3149 bytes.push(chunk); 2460 bytes.push(chunk);
3150 }; 2461 };
3151 // segment request 2462 // segment request
3152 standardXHRResponse(requests.shift()); 2463 standardXHRResponse(requests.pop());
3153 // keys should be 16 bytes long 2464 // keys should be 16 bytes long
3154 requests[0].response = new Uint8Array(1).buffer; 2465 requests[0].response = new Uint8Array(1).buffer;
3155 requests.shift().respond(200, null, ''); 2466 requests.shift().respond(200, null, '');
...@@ -3159,17 +2470,12 @@ test('treats invalid keys as a key request failure', function() { ...@@ -3159,17 +2470,12 @@ test('treats invalid keys as a key request failure', function() {
3159 // the retried response is invalid, too 2470 // the retried response is invalid, too
3160 requests[0].response = new Uint8Array(1); 2471 requests[0].response = new Uint8Array(1);
3161 requests.shift().respond(200, null, ''); 2472 requests.shift().respond(200, null, '');
3162
3163 // the first segment should be dropped and playback moves on
3164 player.tech_.hls.checkBuffer_(); 2473 player.tech_.hls.checkBuffer_();
3165 equal(bytes.length, 0, 'did not append bytes');
3166
3167 // second segment request
3168 requests[0].response = new Uint8Array([1, 2]);
3169 requests.shift().respond(200, null, '');
3170 2474
3171 equal(bytes.length, 1, 'appended bytes'); 2475 // two failed attempts is a network error
3172 deepEqual(bytes[0], new Uint8Array([1, 2]), 'skipped to the second segment'); 2476 equal(player.tech_.hls.mediaSource.error_,
2477 'network',
2478 'triggered a network error');
3173 }); 2479 });
3174 2480
3175 test('live stream should not call endOfStream', function(){ 2481 test('live stream should not call endOfStream', function(){
...@@ -3209,4 +2515,49 @@ test('does not download segments if preload option set to none', function() { ...@@ -3209,4 +2515,49 @@ test('does not download segments if preload option set to none', function() {
3209 equal(requests.length, 0, 'did not download any segments'); 2515 equal(requests.length, 0, 'did not download any segments');
3210 }); 2516 });
3211 2517
2518 module('Buffer Inspection');
2519
2520 test('detects time range edges added by updates', function() {
2521 var edges;
2522
2523 edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]),
2524 videojs.createTimeRange([[0, 11]]));
2525 deepEqual(edges, [{ end: 11 }], 'detected a forward addition');
2526
2527 edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[5, 10]]),
2528 videojs.createTimeRange([[0, 10]]));
2529 deepEqual(edges, [{ start: 0 }], 'detected a backward addition');
2530
2531 edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[5, 10]]),
2532 videojs.createTimeRange([[0, 11]]));
2533 deepEqual(edges, [
2534 { start: 0 }, { end: 11 }
2535 ], 'detected forward and backward additions');
2536
2537 edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]),
2538 videojs.createTimeRange([[0, 10]]));
2539 deepEqual(edges, [], 'detected no addition');
2540
2541 edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([]),
2542 videojs.createTimeRange([[0, 10]]));
2543 deepEqual(edges, [
2544 { start: 0 },
2545 { end: 10 }
2546 ], 'detected an initial addition');
2547
2548 edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]),
2549 videojs.createTimeRange([[0, 10], [20, 30]]));
2550 deepEqual(edges, [
2551 { start: 20 },
2552 { end: 30}
2553 ], 'detected a non-contiguous addition');
2554 });
2555
2556 test('treats null buffered ranges as no addition', function() {
2557 var edges = videojs.Hls.bufferedAdditions_(null,
2558 videojs.createTimeRange([[0, 11]]));
2559
2560 equal(edges.length, 0, 'no additions');
2561 });
2562
3212 })(window, window.videojs); 2563 })(window, window.videojs);
......