5b501b00 by David LaPalomento

Remove expired time tracking

Media sources implicitly track expired time by retaining the mapping between presentation timestamp values and the media timeline in the buffer. That allows us to simplify a good deal of code by not tracking it ourselves. Finish updating tests to work against the new timeline start and end annotations on segments instead of the old PTS values. Remove metadata cue translation because that is now handled by contrib-media-sources. Update key fetching in HLSe to occur concurrently with the segment download. All tests are now passing.
1 parent 2633a46d
...@@ -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
...@@ -360,43 +345,10 @@ ...@@ -360,43 +345,10 @@
360 * @param update {object} the updated media playlist object 345 * @param update {object} the updated media playlist object
361 */ 346 */
362 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { 347 PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) {
363 var expiredCount;
364
365 if (this.media_) {
366 expiredCount = update.mediaSequence - this.media_.mediaSequence;
367
368 // update the expired time count
369 this.expired_ += Playlist.duration(this.media_,
370 this.media_.mediaSequence,
371 update.mediaSequence);
372 }
373
374 this.media_ = this.master.playlists[update.uri]; 348 this.media_ = this.master.playlists[update.uri];
375 }; 349 };
376 350
377 /** 351 /**
378 * When switching variant playlists in a live stream, the player may
379 * discover that the new set of available segments is shifted in
380 * time relative to the old playlist. If that is the case, you can
381 * call this method to synchronize the playlist loader so that
382 * subsequent calls to getMediaIndexForTime_() return values
383 * appropriate for the new playlist.
384 *
385 * @param mediaIndex {integer} the index of the segment that will be
386 * the used to base timeline calculations on
387 * @param startTime {number} the media timeline position of the
388 * first moment of video data for the specified segment. That is,
389 * data from the specified segment will first be displayed when
390 * `currentTime` is equal to `startTime`.
391 */
392 PlaylistLoader.prototype.updateTimelineOffset = function(mediaIndex, startingTime) {
393 var segmentOffset = Playlist.duration(this.media_,
394 this.media_.mediaSequence,
395 this.media_.mediaSequence + mediaIndex);
396 this.expired_ = startingTime - segmentOffset;
397 };
398
399 /**
400 * Determine the index of the segment that contains a specified 352 * Determine the index of the segment that contains a specified
401 * playback position in the current media playlist. Early versions 353 * playback position in the current media playlist. Early versions
402 * of the HLS specification require segment durations to be rounded 354 * of the HLS specification require segment durations to be rounded
...@@ -423,7 +375,6 @@ ...@@ -423,7 +375,6 @@
423 375
424 // when the requested position is earlier than the current set of 376 // when the requested position is earlier than the current set of
425 // segments, return the earliest segment index 377 // segments, return the earliest segment index
426 time -= this.expired_;
427 if (time < 0) { 378 if (time < 0) {
428 return 0; 379 return 0;
429 } 380 }
...@@ -447,6 +398,11 @@ ...@@ -447,6 +398,11 @@
447 398
448 time -= segment.start; 399 time -= segment.start;
449 time -= segment.duration || targetDuration; 400 time -= segment.duration || targetDuration;
401 if (time < 0) {
402 // the segment with start information is also our best guess
403 // for the momment
404 return i;
405 }
450 break; 406 break;
451 } 407 }
452 } 408 }
......
...@@ -26,47 +26,46 @@ ...@@ -26,47 +26,46 @@
26 /** 26 /**
27 * Calculate the media duration from the segments associated with a 27 * Calculate the media duration from the segments associated with a
28 * playlist. The duration of a subinterval of the available segments 28 * playlist. The duration of a subinterval of the available segments
29 * may be calculated by specifying a start and end index. 29 * may be calculated by specifying an end index.
30 * 30 *
31 * @param playlist {object} a media playlist object 31 * @param playlist {object} a media playlist object
32 * @param startSequence {number} (optional) an inclusive lower
33 * boundary for the playlist. Defaults to 0.
34 * @param endSequence {number} (optional) an exclusive upper boundary 32 * @param endSequence {number} (optional) an exclusive upper boundary
35 * for the playlist. Defaults to playlist length. 33 * for the playlist. Defaults to playlist length.
36 * @return {number} the duration between the start index and end 34 * @return {number} the duration between the start index and end
37 * index. 35 * index.
38 */ 36 */
39 intervalDuration = function(playlist, startSequence, endSequence) { 37 intervalDuration = function(playlist, endSequence) {
40 var result = 0, targetDuration, i, start, end, expiredSegmentCount; 38 var result = 0, segment, targetDuration, i;
41 39
42 if (startSequence === undefined) {
43 startSequence = playlist.mediaSequence || 0;
44 }
45 if (endSequence === undefined) { 40 if (endSequence === undefined) {
46 endSequence = startSequence + (playlist.segments || []).length; 41 endSequence = playlist.mediaSequence + (playlist.segments || []).length;
42 }
43 if (endSequence < 0) {
44 return 0;
47 } 45 }
48 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; 46 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
49 47
50 // accumulate while looking for the latest known segment-timeline mapping 48 i = endSequence - playlist.mediaSequence;
51 expiredSegmentCount = optionalMax(playlist.mediaSequence - startSequence, 0); 49 // if a start time is available for segment immediately following
52 start = startSequence + expiredSegmentCount - playlist.mediaSequence; 50 // the interval, use it
53 end = endSequence - playlist.mediaSequence; 51 segment = playlist.segments[i];
54 for (i = end - 1; i >= start; i--) { 52 // Walk backward until we find the latest segment with timeline
55 if (playlist.segments[i].end !== undefined) { 53 // information that is earlier than endSequence
56 result += playlist.segments[i].end; 54 if (segment && segment.start !== undefined) {
57 return result; 55 return segment.start;
56 }
57 while (i--) {
58 segment = playlist.segments[i];
59 if (segment.end !== undefined) {
60 return result + segment.end;
58 } 61 }
59 62
60 result += playlist.segments[i].duration || targetDuration; 63 result += (segment.duration || targetDuration);
61 64
62 if (playlist.segments[i].start !== undefined) { 65 if (segment.start !== undefined) {
63 result += playlist.segments[i].start; 66 return result + segment.start;
64 return result;
65 } 67 }
66 } 68 }
67 // neither a start or end time was found in the interval so we
68 // have to estimate the expired duration
69 result += expiredSegmentCount * targetDuration;
70 return result; 69 return result;
71 }; 70 };
72 71
...@@ -76,17 +75,16 @@ ...@@ -76,17 +75,16 @@
76 * timeline between those two indices. The total duration for live 75 * timeline between those two indices. The total duration for live
77 * playlists is always Infinity. 76 * playlists is always Infinity.
78 * @param playlist {object} a media playlist object 77 * @param playlist {object} a media playlist object
79 * @param startSequence {number} (optional) an inclusive lower 78 * @param endSequence {number} (optional) an exclusive upper
80 * boundary for the playlist. Defaults to 0. 79 * boundary for the playlist. Defaults to the playlist media
81 * @param endSequence {number} (optional) an exclusive upper boundary 80 * sequence number plus its length.
82 * for the playlist. Defaults to playlist length. 81 * @param includeTrailingTime {boolean} (optional) if false, the
83 * @param includeTrailingTime {boolean} (optional) if false, the interval between 82 * interval between the final segment and the subsequent segment
84 * the final segment and the subsequent segment will not be included 83 * will not be included in the result
85 * in the result
86 * @return {number} the duration between the start index and end 84 * @return {number} the duration between the start index and end
87 * index. 85 * index.
88 */ 86 */
89 duration = function(playlist, startSequence, endSequence, includeTrailingTime) { 87 duration = function(playlist, endSequence, includeTrailingTime) {
90 if (!playlist) { 88 if (!playlist) {
91 return 0; 89 return 0;
92 } 90 }
...@@ -97,7 +95,7 @@ ...@@ -97,7 +95,7 @@
97 95
98 // if a slice of the total duration is not requested, use 96 // if a slice of the total duration is not requested, use
99 // playlist-level duration indicators when they're present 97 // playlist-level duration indicators when they're present
100 if (startSequence === undefined && endSequence === undefined) { 98 if (endSequence === undefined) {
101 // if present, use the duration specified in the playlist 99 // if present, use the duration specified in the playlist
102 if (playlist.totalDuration) { 100 if (playlist.totalDuration) {
103 return playlist.totalDuration; 101 return playlist.totalDuration;
...@@ -111,7 +109,6 @@ ...@@ -111,7 +109,6 @@
111 109
112 // calculate the total duration based on the segment durations 110 // calculate the total duration based on the segment durations
113 return intervalDuration(playlist, 111 return intervalDuration(playlist,
114 startSequence,
115 endSequence, 112 endSequence,
116 includeTrailingTime); 113 includeTrailingTime);
117 }; 114 };
...@@ -128,7 +125,7 @@ ...@@ -128,7 +125,7 @@
128 * for seeking 125 * for seeking
129 */ 126 */
130 seekable = function(playlist) { 127 seekable = function(playlist) {
131 var start, end, liveBuffer, targetDuration, segment, pending, i; 128 var start, end;
132 129
133 // without segments, there are no seekable ranges 130 // without segments, there are no seekable ranges
134 if (!playlist.segments) { 131 if (!playlist.segments) {
...@@ -139,33 +136,14 @@ ...@@ -139,33 +136,14 @@
139 return videojs.createTimeRange(0, duration(playlist)); 136 return videojs.createTimeRange(0, duration(playlist));
140 } 137 }
141 138
142 start = 0;
143 end = intervalDuration(playlist,
144 playlist.mediaSequence,
145 playlist.mediaSequence + playlist.segments.length);
146 targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION;
147
148 // live playlists should not expose three segment durations worth 139 // live playlists should not expose three segment durations worth
149 // of content from the end of the playlist 140 // of content from the end of the playlist
150 // 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
151 if (!playlist.endList) { 142 start = intervalDuration(playlist, playlist.mediaSequence);
152 liveBuffer = targetDuration * 3; 143 end = intervalDuration(playlist,
153 // walk backward from the last available segment and track how 144 playlist.mediaSequence + playlist.segments.length);
154 // much media time has elapsed until three target durations have 145 end -= (playlist.targetDuration || DEFAULT_TARGET_DURATION) * 3;
155 // been traversed. if a segment is part of the interval being 146 end = Math.max(0, end);
156 // reported, subtract the overlapping portion of its duration
157 // from the result.
158 for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) {
159 segment = playlist.segments[i];
160 pending = optionalMin(duration(playlist,
161 playlist.mediaSequence + i,
162 playlist.mediaSequence + i + 1),
163 liveBuffer);
164 liveBuffer -= pending;
165 end -= pending;
166 }
167 }
168
169 return videojs.createTimeRange(start, end); 147 return videojs.createTimeRange(start, end);
170 }; 148 };
171 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_();
...@@ -140,11 +138,6 @@ videojs.Hls.prototype.src = function(src) { ...@@ -140,11 +138,6 @@ videojs.Hls.prototype.src = function(src) {
140 } 138 }
141 139
142 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); 140 this.mediaSource = new videojs.MediaSource({ mode: this.mode_ });
143 this.segmentBuffer_ = [];
144
145 // if the stream contains ID3 metadata, expose that as a metadata
146 // text track
147 //this.setupMetadataCueTranslation_();
148 141
149 // load the MediaSource into the player 142 // load the MediaSource into the player
150 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); 143 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this));
...@@ -197,17 +190,13 @@ videojs.Hls.prototype.src = function(src) { ...@@ -197,17 +190,13 @@ videojs.Hls.prototype.src = function(src) {
197 190
198 this.updateDuration(this.playlists.media()); 191 this.updateDuration(this.playlists.media());
199 oldMediaPlaylist = updatedPlaylist; 192 oldMediaPlaylist = updatedPlaylist;
200
201 this.fetchKeys_();
202 }.bind(this)); 193 }.bind(this));
203 194
204 this.playlists.on('mediachange', function() { 195 this.playlists.on('mediachange', function() {
205 // abort outstanding key requests and check if new keys need to be retrieved 196 this.tech_.trigger({
206 if (keyXhr) { 197 type: 'mediachange',
207 this.cancelKeyXhr(); 198 bubbles: true
208 } 199 });
209
210 this.tech_.trigger({ type: 'mediachange', bubbles: true });
211 }.bind(this)); 200 }.bind(this));
212 201
213 // do nothing if the tech has been disposed already 202 // do nothing if the tech has been disposed already
...@@ -219,26 +208,6 @@ videojs.Hls.prototype.src = function(src) { ...@@ -219,26 +208,6 @@ videojs.Hls.prototype.src = function(src) {
219 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); 208 this.tech_.src(videojs.URL.createObjectURL(this.mediaSource));
220 }; 209 };
221 210
222 /* Returns the media index for the live point in the current playlist, and updates
223 the current time to go along with it.
224 */
225 videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) {
226 if (!selectedPlaylist.segments) {
227 return 0;
228 }
229
230 var tailIterator = selectedPlaylist.segments.length,
231 tailDuration = 0,
232 targetTail = (selectedPlaylist.targetDuration || 10) * 3;
233
234 while (tailDuration < targetTail && tailIterator > 0) {
235 tailDuration += selectedPlaylist.segments[tailIterator - 1].duration;
236 tailIterator--;
237 }
238
239 return tailIterator;
240 };
241
242 videojs.Hls.prototype.handleSourceOpen = function() { 211 videojs.Hls.prototype.handleSourceOpen = function() {
243 this.setupSourceBuffer_(); 212 this.setupSourceBuffer_();
244 213
...@@ -324,7 +293,7 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { ...@@ -324,7 +293,7 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
324 // transition the sourcebuffer to the ended state if we've hit the end of 293 // transition the sourcebuffer to the ended state if we've hit the end of
325 // the playlist 294 // the playlist
326 this.sourceBuffer.addEventListener('updateend', function() { 295 this.sourceBuffer.addEventListener('updateend', function() {
327 var segmentInfo = this.pendingSegment_, segment, i, currentBuffered, timelineUpdates; 296 var segmentInfo = this.pendingSegment_, segment, currentBuffered, timelineUpdates;
328 297
329 this.pendingSegment_ = null; 298 this.pendingSegment_ = null;
330 299
...@@ -334,24 +303,7 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { ...@@ -334,24 +303,7 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
334 this.mediaSource.endOfStream(); 303 this.mediaSource.endOfStream();
335 } 304 }
336 305
337 // When switching renditions or seeking, we may misjudge the media 306 // stop here if the update errored or was aborted
338 // index to request to continue playback. Check after each append
339 // that a gap hasn't appeared in the buffered region and adjust
340 // the media index to fill it if necessary
341 if (this.tech_.buffered().length === 2 &&
342 segmentInfo.playlist === this.playlists.media()) {
343 i = this.tech_.buffered().length;
344 while (i--) {
345 if (this.tech_.currentTime() < this.tech_.buffered().start(i)) {
346 // found the misidentified segment's buffered time range
347 // adjust the media index to fill the gap
348 this.playlists.updateTimelineOffset(segmentInfo.mediaIndex,
349 this.tech_.buffered().start(i));
350 break;
351 }
352 }
353 }
354
355 if (!segmentInfo) { 307 if (!segmentInfo) {
356 return; 308 return;
357 } 309 }
...@@ -370,90 +322,13 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { ...@@ -370,90 +322,13 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
370 } 322 }
371 }); 323 });
372 324
373 }.bind(this)); 325 if (timelineUpdates.length) {
374 }; 326 this.updateDuration(segmentInfo.playlist);
375
376 // register event listeners to transform in-band metadata events into
377 // VTTCues on a text track
378 videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
379 var
380 metadataStream = this.segmentParser_.metadataStream,
381 textTrack;
382
383 // add a metadata cue whenever a metadata event is triggered during
384 // segment parsing
385 metadataStream.on('data', function(metadata) {
386 var i, hexDigit;
387
388 // create the metadata track if this is the first ID3 tag we've
389 // seen
390 if (!textTrack) {
391 textTrack = this.tech_.addTextTrack('metadata', 'Timed Metadata');
392
393 // build the dispatch type from the stream descriptor
394 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track
395 textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase();
396 for (i = 0; i < metadataStream.descriptor.length; i++) {
397 hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2);
398 textTrack.inBandMetadataTrackDispatchType += hexDigit;
399 }
400 } 327 }
401 328
402 // store this event for processing once the muxing has finished 329 // check if it's time to download the next segment
403 this.tech_.segmentBuffer_[0].pendingMetadata.push({ 330 this.checkBuffer_();
404 textTrack: textTrack,
405 metadata: metadata
406 });
407 }.bind(this)); 331 }.bind(this));
408
409 // when seeking, clear out all cues ahead of the earliest position
410 // in the new segment. keep earlier cues around so they can still be
411 // programmatically inspected even though they've already fired
412 this.on(this.tech_, 'seeking', function() {
413 var media, startTime, i;
414 if (!textTrack) {
415 return;
416 }
417 media = this.playlists.media();
418 startTime = this.tech_.playlists.expired_;
419 startTime += videojs.Hls.Playlist.duration(media,
420 media.mediaSequence,
421 media.mediaSequence + this.tech_.mediaIndex);
422
423 i = textTrack.cues.length;
424 while (i--) {
425 if (textTrack.cues[i].startTime >= startTime) {
426 textTrack.removeCue(textTrack.cues[i]);
427 }
428 }
429 });
430 };
431
432 videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) {
433 var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time;
434 segmentOffset = this.playlists.expired_;
435 segmentOffset += videojs.Hls.Playlist.duration(segmentInfo.playlist,
436 segmentInfo.playlist.mediaSequence,
437 segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex);
438 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
439 minPts = Math.min(isFinite(segment.minVideoPts) ? segment.minVideoPts : Infinity,
440 isFinite(segment.minAudioPts) ? segment.minAudioPts : Infinity);
441
442 while (segmentInfo.pendingMetadata.length) {
443 metadata = segmentInfo.pendingMetadata[0].metadata;
444 textTrack = segmentInfo.pendingMetadata[0].textTrack;
445
446 // create cue points for all the ID3 frames in this metadata event
447 for (i = 0; i < metadata.frames.length; i++) {
448 frame = metadata.frames[i];
449 time = segmentOffset + ((metadata.pts - minPts) * 0.001);
450 cue = new window.VTTCue(time, time, frame.value || frame.url || '');
451 cue.frame = frame;
452 cue.pts_ = metadata.pts;
453 textTrack.addCue(cue);
454 }
455 segmentInfo.pendingMetadata.shift();
456 }
457 }; 332 };
458 333
459 /** 334 /**
...@@ -535,13 +410,13 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { ...@@ -535,13 +410,13 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) {
535 this.cancelSegmentXhr(); 410 this.cancelSegmentXhr();
536 411
537 // abort outstanding key requests, if necessary 412 // abort outstanding key requests, if necessary
538 if (keyXhr) { 413 if (this.keyXhr_) {
539 keyXhr.aborted = true; 414 this.keyXhr_.aborted = true;
540 this.cancelKeyXhr(); 415 this.cancelKeyXhr();
541 } 416 }
542 417
543 // clear out any buffered segments 418 // clear out the segment being processed
544 this.segmentBuffer_ = []; 419 this.pendingSegment_ = null;
545 420
546 // begin filling the buffer at the new position 421 // begin filling the buffer at the new position
547 this.fillBuffer(currentTime); 422 this.fillBuffer(currentTime);
...@@ -556,7 +431,7 @@ videojs.Hls.prototype.duration = function() { ...@@ -556,7 +431,7 @@ videojs.Hls.prototype.duration = function() {
556 }; 431 };
557 432
558 videojs.Hls.prototype.seekable = function() { 433 videojs.Hls.prototype.seekable = function() {
559 var currentSeekable, startOffset, media; 434 var media;
560 435
561 if (!this.playlists) { 436 if (!this.playlists) {
562 return videojs.createTimeRanges(); 437 return videojs.createTimeRanges();
...@@ -566,17 +441,7 @@ videojs.Hls.prototype.seekable = function() { ...@@ -566,17 +441,7 @@ videojs.Hls.prototype.seekable = function() {
566 return videojs.createTimeRanges(); 441 return videojs.createTimeRanges();
567 } 442 }
568 443
569 // report the seekable range relative to the earliest possible 444 return videojs.Hls.Playlist.seekable(media);
570 // position when the stream was first loaded
571 currentSeekable = videojs.Hls.Playlist.seekable(media);
572
573 if (!currentSeekable.length) {
574 return currentSeekable;
575 }
576
577 startOffset = this.playlists.expired_;
578 return videojs.createTimeRanges(startOffset,
579 startOffset + (currentSeekable.end(0) - currentSeekable.start(0)));
580 }; 445 };
581 446
582 /** 447 /**
...@@ -617,10 +482,10 @@ videojs.Hls.prototype.resetSrc_ = function() { ...@@ -617,10 +482,10 @@ videojs.Hls.prototype.resetSrc_ = function() {
617 }; 482 };
618 483
619 videojs.Hls.prototype.cancelKeyXhr = function() { 484 videojs.Hls.prototype.cancelKeyXhr = function() {
620 if (keyXhr) { 485 if (this.keyXhr_) {
621 keyXhr.onreadystatechange = null; 486 this.keyXhr_.onreadystatechange = null;
622 keyXhr.abort(); 487 this.keyXhr_.abort();
623 keyXhr = null; 488 this.keyXhr_ = null;
624 } 489 }
625 }; 490 };
626 491
...@@ -829,7 +694,7 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { ...@@ -829,7 +694,7 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) {
829 bufferedTime = 0, 694 bufferedTime = 0,
830 mediaIndex = 0, 695 mediaIndex = 0,
831 segment, 696 segment,
832 segmentUri; 697 segmentInfo;
833 698
834 // if preload is set to "none", do not download segments until playback is requested 699 // if preload is set to "none", do not download segments until playback is requested
835 if (this.loadingState_ !== 'segments') { 700 if (this.loadingState_ !== 'segments') {
...@@ -847,7 +712,7 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { ...@@ -847,7 +712,7 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) {
847 } 712 }
848 713
849 // wait until the buffer is up to date 714 // wait until the buffer is up to date
850 if (this.segmentBuffer_.length || this.pendingSegment_) { 715 if (this.pendingSegment_) {
851 return; 716 return;
852 } 717 }
853 718
...@@ -886,10 +751,29 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { ...@@ -886,10 +751,29 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) {
886 return; 751 return;
887 } 752 }
888 753
754 // package up all the work to append the segment
755 segmentInfo = {
889 // resolve the segment URL relative to the playlist 756 // resolve the segment URL relative to the playlist
890 segmentUri = this.playlistUriToUrl(segment.uri); 757 uri: this.playlistUriToUrl(segment.uri),
758 // the segment's mediaIndex at the time it was received
759 mediaIndex: mediaIndex,
760 // the segment's playlist
761 playlist: this.playlists.media(),
762 // optionally, a time offset to seek to within the segment
763 offset: seekToTime,
764 // unencrypted bytes of the segment
765 bytes: null,
766 // when a key is defined for this segment, the encrypted bytes
767 encryptedBytes: null,
768 // optionally, the decrypter that is unencrypting the segment
769 decrypter: null,
770 // the state of the buffer before a segment is appended will be
771 // stored here so that the actual segment duration can be
772 // determined after it has been appended
773 buffered: null
774 };
891 775
892 this.loadSegment(segmentUri, mediaIndex, seekToTime); 776 this.loadSegment(segmentInfo);
893 }; 777 };
894 778
895 videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) { 779 videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) {
...@@ -920,17 +804,22 @@ videojs.Hls.prototype.setBandwidth = function(xhr) { ...@@ -920,17 +804,22 @@ videojs.Hls.prototype.setBandwidth = function(xhr) {
920 this.tech_.trigger('bandwidthupdate'); 804 this.tech_.trigger('bandwidthupdate');
921 }; 805 };
922 806
923 videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime) { 807 videojs.Hls.prototype.loadSegment = function(segmentInfo) {
924 var self = this; 808 var
809 self = this,
810 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
811
812 // if the segment is encrypted, request the key
813 if (segment.key) {
814 this.fetchKey_(segment);
815 }
925 816
926 // request the next segment 817 // request the next segment
927 this.segmentXhr_ = videojs.Hls.xhr({ 818 this.segmentXhr_ = videojs.Hls.xhr({
928 uri: segmentUri, 819 uri: segmentInfo.uri,
929 responseType: 'arraybuffer', 820 responseType: 'arraybuffer',
930 withCredentials: this.source_.withCredentials 821 withCredentials: this.source_.withCredentials
931 }, function(error, request) { 822 }, function(error, request) {
932 var segmentInfo;
933
934 // the segment request is no longer outstanding 823 // the segment request is no longer outstanding
935 self.segmentXhr_ = null; 824 self.segmentXhr_ = null;
936 825
...@@ -944,7 +833,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime) ...@@ -944,7 +833,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime)
944 if (!request.aborted && error) { 833 if (!request.aborted && error) {
945 self.error = { 834 self.error = {
946 status: request.status, 835 status: request.status,
947 message: 'HLS segment request error at URL: ' + segmentUri, 836 message: 'HLS segment request error at URL: ' + segmentInfo.uri,
948 code: (request.status >= 500) ? 4 : 2 837 code: (request.status >= 500) ? 4 : 2
949 }; 838 };
950 839
...@@ -958,34 +847,12 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime) ...@@ -958,34 +847,12 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime)
958 847
959 self.setBandwidth(request); 848 self.setBandwidth(request);
960 849
961 // package up all the work to append the segment 850 if (segment.key) {
962 segmentInfo = {
963 // the segment's mediaIndex at the time it was received
964 mediaIndex: mediaIndex,
965 // the segment's playlist
966 playlist: self.playlists.media(),
967 // optionally, a time offset to seek to within the segment
968 offset: seekToTime,
969 // unencrypted bytes of the segment
970 bytes: null,
971 // when a key is defined for this segment, the encrypted bytes
972 encryptedBytes: null,
973 // optionally, the decrypter that is unencrypting the segment
974 decrypter: null,
975 // metadata events discovered during muxing that need to be
976 // translated into cue points
977 pendingMetadata: [],
978 // the state of the buffer before a segment is appended will be
979 // stored here so that the actual segment duration can be
980 // determined after it has been appended
981 buffered: null
982 };
983 if (segmentInfo.playlist.segments[mediaIndex].key) {
984 segmentInfo.encryptedBytes = new Uint8Array(request.response); 851 segmentInfo.encryptedBytes = new Uint8Array(request.response);
985 } else { 852 } else {
986 segmentInfo.bytes = new Uint8Array(request.response); 853 segmentInfo.bytes = new Uint8Array(request.response);
987 } 854 }
988 self.segmentBuffer_.push(segmentInfo); 855 self.pendingSegment_ = segmentInfo;
989 self.tech_.trigger('progress'); 856 self.tech_.trigger('progress');
990 self.drainBuffer(); 857 self.drainBuffer();
991 858
...@@ -1008,13 +875,11 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1008,13 +875,11 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1008 segmentTimestampOffset = 0, 875 segmentTimestampOffset = 0,
1009 hasBufferedContent = (this.tech_.buffered().length !== 0), 876 hasBufferedContent = (this.tech_.buffered().length !== 0),
1010 currentBuffered = this.findCurrentBuffered_(), 877 currentBuffered = this.findCurrentBuffered_(),
1011 outsideBufferedRanges = !(currentBuffered && currentBuffered.length), 878 outsideBufferedRanges = !(currentBuffered && currentBuffered.length);
1012 // ptsTime,
1013 segmentBuffer = this.segmentBuffer_;
1014 879
1015 // if the buffer is empty or the source buffer hasn't been created 880 // if the buffer is empty or the source buffer hasn't been created
1016 // yet, do nothing 881 // yet, do nothing
1017 if (!segmentBuffer.length || !this.sourceBuffer) { 882 if (!this.pendingSegment_ || !this.sourceBuffer) {
1018 return; 883 return;
1019 } 884 }
1020 885
...@@ -1024,7 +889,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1024,7 +889,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1024 return; 889 return;
1025 } 890 }
1026 891
1027 segmentInfo = segmentBuffer[0]; 892 segmentInfo = this.pendingSegment_;
1028 mediaIndex = segmentInfo.mediaIndex; 893 mediaIndex = segmentInfo.mediaIndex;
1029 playlist = segmentInfo.playlist; 894 playlist = segmentInfo.playlist;
1030 offset = segmentInfo.offset; 895 offset = segmentInfo.offset;
...@@ -1037,18 +902,19 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1037,18 +902,19 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1037 // if the key download failed, we want to skip this segment 902 // if the key download failed, we want to skip this segment
1038 // but if the key hasn't downloaded yet, we want to try again later 903 // but if the key hasn't downloaded yet, we want to try again later
1039 if (keyFailed(segment.key)) { 904 if (keyFailed(segment.key)) {
1040 return segmentBuffer.shift(); 905 videojs.log.warn('Network error retrieving key from "' +
906 segment.key.uri + '"');
907 return this.mediaSource.endOfStream('network');
1041 } else if (!segment.key.bytes) { 908 } else if (!segment.key.bytes) {
1042 909
1043 // trigger a key request if one is not already in-flight 910 // waiting for the key bytes, try again later
1044 return this.fetchKeys_(); 911 return;
1045
1046 } else if (segmentInfo.decrypter) { 912 } else if (segmentInfo.decrypter) {
1047 913
1048 // decryption is in progress, try again later 914 // decryption is in progress, try again later
1049 return; 915 return;
1050
1051 } else { 916 } else {
917
1052 // if the media sequence is greater than 2^32, the IV will be incorrect 918 // if the media sequence is greater than 2^32, the IV will be incorrect
1053 // assuming 10s segments, that would be about 1300 years 919 // assuming 10s segments, that would be about 1300 years
1054 segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); 920 segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]);
...@@ -1067,32 +933,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1067,32 +933,6 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1067 933
1068 event = event || {}; 934 event = event || {};
1069 935
1070 // if (this.segmentParser_.tagsAvailable()) {
1071 // // record PTS information for the segment so we can calculate
1072 // // accurate durations and seek reliably
1073 // if (this.segmentParser_.stats.h264Tags()) {
1074 // segment.minVideoPts = this.segmentParser_.stats.minVideoPts();
1075 // segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts();
1076 // }
1077 // if (this.segmentParser_.stats.aacTags()) {
1078 // segment.minAudioPts = this.segmentParser_.stats.minAudioPts();
1079 // segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts();
1080 // }
1081 // }
1082
1083 // while (this.segmentParser_.tagsAvailable()) {
1084 // tags.push(this.segmentParser_.getNextTag());
1085 // }
1086
1087 this.addCuesForMetadata_(segmentInfo);
1088 //this.updateDuration(this.playlists.media());
1089
1090 // // when we're crossing a discontinuity, inject metadata to indicate
1091 // // that the decoder should be reset appropriately
1092 // if (segment.discontinuity && tags.length) {
1093 // this.tech_.el().vjs_discontinuity();
1094 // }
1095
1096 // If we have seeked into a non-buffered time-range, remove all buffered 936 // If we have seeked into a non-buffered time-range, remove all buffered
1097 // time-ranges because they could have been incorrectly placed originally 937 // time-ranges because they could have been incorrectly placed originally
1098 if (this.tech_.seeking() && outsideBufferedRanges) { 938 if (this.tech_.seeking() && outsideBufferedRanges) {
...@@ -1108,7 +948,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1108,7 +948,7 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1108 // anew on every seek 948 // anew on every seek
1109 if (segmentInfo.playlist.discontinuityStarts.length) { 949 if (segmentInfo.playlist.discontinuityStarts.length) {
1110 if (segmentInfo.mediaIndex > 0) { 950 if (segmentInfo.mediaIndex > 0) {
1111 segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, 0, segmentInfo.mediaIndex); 951 segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, segmentInfo.mediaIndex);
1112 } 952 }
1113 953
1114 // Now that the forward buffer is clear, we have to set timestamp offset to 954 // Now that the forward buffer is clear, we have to set timestamp offset to
...@@ -1128,7 +968,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1128,7 +968,6 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1128 } else { 968 } else {
1129 this.sourceBuffer.appendWindowStart = 0; 969 this.sourceBuffer.appendWindowStart = 0;
1130 } 970 }
1131 this.pendingSegment_ = segmentBuffer.shift();
1132 this.pendingSegment_.buffered = this.tech_.buffered(); 971 this.pendingSegment_.buffered = this.tech_.buffered();
1133 972
1134 // the segment is asynchronously added to the current buffered data 973 // the segment is asynchronously added to the current buffered data
...@@ -1136,38 +975,33 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1136,38 +975,33 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1136 }; 975 };
1137 976
1138 /** 977 /**
1139 * Attempt to retrieve keys starting at a particular media 978 * Attempt to retrieve the key for a particular media segment.
1140 * segment. This method has no effect if segments are not yet
1141 * available or a key request is already in progress.
1142 *
1143 * @param playlist {object} the media playlist to fetch keys for
1144 * @param index {number} the media segment index to start from
1145 */ 979 */
1146 videojs.Hls.prototype.fetchKeys_ = function() { 980 videojs.Hls.prototype.fetchKey_ = function(segment) {
1147 var i, key, tech, player, settings, segment, view, receiveKey; 981 var key, self, settings, receiveKey;
1148 982
1149 // if there is a pending XHR or no segments, don't do anything 983 // if there is a pending XHR or no segments, don't do anything
1150 if (keyXhr || !this.segmentBuffer_.length) { 984 if (this.keyXhr_) {
1151 return; 985 return;
1152 } 986 }
1153 987
1154 tech = this; 988 self = this;
1155 player = this.player();
1156 settings = this.options_; 989 settings = this.options_;
1157 990
1158 /** 991 /**
1159 * Handle a key XHR response. This function needs to lookup the 992 * Handle a key XHR response.
1160 */ 993 */
1161 receiveKey = function(key) { 994 receiveKey = function(key) {
1162 return function(error, request) { 995 return function(error, request) {
1163 keyXhr = null; 996 var view;
997 self.keyXhr_ = null;
1164 998
1165 if (error || !request.response || request.response.byteLength !== 16) { 999 if (error || !request.response || request.response.byteLength !== 16) {
1166 key.retries = key.retries || 0; 1000 key.retries = key.retries || 0;
1167 key.retries++; 1001 key.retries++;
1168 if (!request.aborted) { 1002 if (!request.aborted) {
1169 // try fetching again 1003 // try fetching again
1170 tech.fetchKeys_(); 1004 self.fetchKey_(segment);
1171 } 1005 }
1172 return; 1006 return;
1173 } 1007 }
...@@ -1181,28 +1015,25 @@ videojs.Hls.prototype.fetchKeys_ = function() { ...@@ -1181,28 +1015,25 @@ videojs.Hls.prototype.fetchKeys_ = function() {
1181 ]); 1015 ]);
1182 1016
1183 // check to see if this allows us to make progress buffering now 1017 // check to see if this allows us to make progress buffering now
1184 tech.checkBuffer_(); 1018 self.checkBuffer_();
1185 }; 1019 };
1186 }; 1020 };
1187 1021
1188 for (i = 0; i < tech.segmentBuffer_.length; i++) {
1189 segment = tech.segmentBuffer_[i].playlist.segments[tech.segmentBuffer_[i].mediaIndex];
1190 key = segment.key; 1022 key = segment.key;
1191 1023
1192 // continue looking if this segment is unencrypted 1024 // nothing to do if this segment is unencrypted
1193 if (!key) { 1025 if (!key) {
1194 continue; 1026 return;
1195 } 1027 }
1196 1028
1197 // request the key if the retry limit hasn't been reached 1029 // request the key if the retry limit hasn't been reached
1198 if (!key.bytes && !keyFailed(key)) { 1030 if (!key.bytes && !keyFailed(key)) {
1199 keyXhr = videojs.Hls.xhr({ 1031 this.keyXhr_ = videojs.Hls.xhr({
1200 uri: this.playlistUriToUrl(key.uri), 1032 uri: this.playlistUriToUrl(key.uri),
1201 responseType: 'arraybuffer', 1033 responseType: 'arraybuffer',
1202 withCredentials: settings.withCredentials 1034 withCredentials: settings.withCredentials
1203 }, receiveKey(key)); 1035 }, receiveKey(key));
1204 break; 1036 return;
1205 }
1206 } 1037 }
1207 }; 1038 };
1208 1039
...@@ -1234,44 +1065,6 @@ videojs.Hls.isSupported = function() { ...@@ -1234,44 +1065,6 @@ videojs.Hls.isSupported = function() {
1234 }; 1065 };
1235 1066
1236 /** 1067 /**
1237 * Calculate the duration of a playlist from a given start index to a given
1238 * end index.
1239 * @param playlist {object} a media playlist object
1240 * @param startIndex {number} an inclusive lower boundary for the playlist.
1241 * Defaults to 0.
1242 * @param endIndex {number} an exclusive upper boundary for the playlist.
1243 * Defaults to playlist length.
1244 * @return {number} the duration between the start index and end index.
1245 */
1246 videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) {
1247 videojs.log.warn('videojs.Hls.getPlaylistDuration is deprecated. ' +
1248 'Use videojs.Hls.Playlist.duration instead');
1249 return videojs.Hls.Playlist.duration(playlist, startIndex, endIndex);
1250 };
1251
1252 /**
1253 * Calculate the total duration for a playlist based on segment metadata.
1254 * @param playlist {object} a media playlist object
1255 * @return {number} the currently known duration, in seconds
1256 */
1257 videojs.Hls.getPlaylistTotalDuration = function(playlist) {
1258 videojs.log.warn('videojs.Hls.getPlaylistTotalDuration is deprecated. ' +
1259 'Use videojs.Hls.Playlist.duration instead');
1260 return videojs.Hls.Playlist.duration(playlist);
1261 };
1262
1263 /**
1264 * Deprecated.
1265 *
1266 * @deprecated use player.hls.playlists.getMediaIndexForTime_() instead
1267 */
1268 videojs.Hls.getMediaIndexByTime = function() {
1269 videojs.log.warn('getMediaIndexByTime is deprecated. ' +
1270 'Use PlaylistLoader.getMediaIndexForTime_ instead.');
1271 return 0;
1272 };
1273
1274 /**
1275 * A comparator function to sort two playlist object by bandwidth. 1068 * A comparator function to sort two playlist object by bandwidth.
1276 * @param left {object} a media playlist object 1069 * @param left {object} a media playlist object
1277 * @param right {object} a media playlist object 1070 * @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');
...@@ -175,101 +166,6 @@ ...@@ -175,101 +166,6 @@
175 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); 166 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
176 }); 167 });
177 168
178 test('increments expired seconds after a segment is removed', function() {
179 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
180 requests.pop().respond(200, null,
181 '#EXTM3U\n' +
182 '#EXT-X-MEDIA-SEQUENCE:0\n' +
183 '#EXTINF:10,\n' +
184 '0.ts\n' +
185 '#EXTINF:10,\n' +
186 '1.ts\n' +
187 '#EXTINF:10,\n' +
188 '2.ts\n' +
189 '#EXTINF:10,\n' +
190 '3.ts\n');
191 clock.tick(10 * 1000); // 10s, one target duration
192 requests.pop().respond(200, null,
193 '#EXTM3U\n' +
194 '#EXT-X-MEDIA-SEQUENCE:1\n' +
195 '#EXTINF:10,\n' +
196 '1.ts\n' +
197 '#EXTINF:10,\n' +
198 '2.ts\n' +
199 '#EXTINF:10,\n' +
200 '3.ts\n' +
201 '#EXTINF:10,\n' +
202 '4.ts\n');
203 equal(loader.expired_, 10, 'expired one segment');
204 });
205
206 test('increments expired seconds after a discontinuity', function() {
207 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
208 requests.pop().respond(200, null,
209 '#EXTM3U\n' +
210 '#EXT-X-MEDIA-SEQUENCE:0\n' +
211 '#EXTINF:10,\n' +
212 '0.ts\n' +
213 '#EXTINF:3,\n' +
214 '1.ts\n' +
215 '#EXT-X-DISCONTINUITY\n' +
216 '#EXTINF:4,\n' +
217 '2.ts\n');
218 clock.tick(10 * 1000); // 10s, one target duration
219 requests.pop().respond(200, null,
220 '#EXTM3U\n' +
221 '#EXT-X-MEDIA-SEQUENCE:1\n' +
222 '#EXTINF:3,\n' +
223 '1.ts\n' +
224 '#EXT-X-DISCONTINUITY\n' +
225 '#EXTINF:4,\n' +
226 '2.ts\n');
227 equal(loader.expired_, 10, 'expired one segment');
228
229 clock.tick(10 * 1000); // 10s, one target duration
230 requests.pop().respond(200, null,
231 '#EXTM3U\n' +
232 '#EXT-X-MEDIA-SEQUENCE:2\n' +
233 '#EXT-X-DISCONTINUITY\n' +
234 '#EXTINF:4,\n' +
235 '2.ts\n');
236 equal(loader.expired_, 13, 'no expirations after the discontinuity yet');
237
238 clock.tick(10 * 1000); // 10s, one target duration
239 requests.pop().respond(200, null,
240 '#EXTM3U\n' +
241 '#EXT-X-MEDIA-SEQUENCE:3\n' +
242 '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' +
243 '#EXTINF:10,\n' +
244 '3.ts\n');
245 equal(loader.expired_, 13 + 4, 'tracked expired prior to the discontinuity');
246 });
247
248 test('tracks expired seconds properly when two discontinuities expire at once', function() {
249 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
250 requests.pop().respond(200, null,
251 '#EXTM3U\n' +
252 '#EXT-X-MEDIA-SEQUENCE:0\n' +
253 '#EXTINF:4,\n' +
254 '0.ts\n' +
255 '#EXT-X-DISCONTINUITY\n' +
256 '#EXTINF:5,\n' +
257 '1.ts\n' +
258 '#EXT-X-DISCONTINUITY\n' +
259 '#EXTINF:6,\n' +
260 '2.ts\n' +
261 '#EXTINF:7,\n' +
262 '3.ts\n');
263 clock.tick(10 * 1000);
264 requests.pop().respond(200, null,
265 '#EXTM3U\n' +
266 '#EXT-X-MEDIA-SEQUENCE:3\n' +
267 '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' +
268 '#EXTINF:7,\n' +
269 '3.ts\n');
270 equal(loader.expired_, 4 + 5 + 6, 'tracked both expired discontinuities');
271 });
272
273 test('emits an error when an initial playlist request fails', function() { 169 test('emits an error when an initial playlist request fails', function() {
274 var 170 var
275 errors = [], 171 errors = [],
...@@ -757,8 +653,8 @@ ...@@ -757,8 +653,8 @@
757 equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); 653 equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero');
758 equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); 654 equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2');
759 equal(loader.getMediaIndexForTime_(22), 655 equal(loader.getMediaIndexForTime_(22),
760 2, 656 3,
761 'the index is never greater than the length'); 657 'time greater than the length is index 3');
762 }); 658 });
763 659
764 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() {
...@@ -785,7 +681,7 @@ ...@@ -785,7 +681,7 @@
785 '1001.ts\n' + 681 '1001.ts\n' +
786 '#EXTINF:5,\n' + 682 '#EXTINF:5,\n' +
787 '1002.ts\n'); 683 '1002.ts\n');
788 loader.expired_ = 150; 684 loader.media().segments[0].start = 150;
789 685
790 equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); 686 equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero');
791 equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); 687 equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero');
...@@ -797,30 +693,6 @@ ...@@ -797,30 +693,6 @@
797 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');
798 }); 694 });
799 695
800 test('updating the timeline offset adjusts results from getMediaIndexForTime_', function() {
801 var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
802 requests.pop().respond(200, null,
803 '#EXTM3U\n' +
804 '#EXT-X-MEDIA-SEQUENCE:23\n' +
805 '#EXTINF:4,\n' +
806 '23.ts\n' +
807 '#EXTINF:5,\n' +
808 '24.ts\n' +
809 '#EXTINF:6,\n' +
810 '25.ts\n' +
811 '#EXTINF:7,\n' +
812 '26.ts\n');
813 loader.updateTimelineOffset(0, 150);
814 equal(loader.getMediaIndexForTime_(150), 0, 'translated the first segment');
815 equal(loader.getMediaIndexForTime_(130), 0, 'clamps the index to zero');
816 equal(loader.getMediaIndexForTime_(155), 1, 'translated the second segment');
817
818 loader.updateTimelineOffset(2, 30);
819 equal(loader.getMediaIndexForTime_(30 - 5 - 1), 0, 'translated the first segment');
820 equal(loader.getMediaIndexForTime_(30 + 7), 3, 'translated the last segment');
821 equal(loader.getMediaIndexForTime_(30 - 3), 1, 'translated an earlier segment');
822 });
823
824 test('does not misintrepret playlists missing newlines at the end', function() { 696 test('does not misintrepret playlists missing newlines at the end', function() {
825 var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); 697 var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
826 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
...@@ -889,10 +835,10 @@ test('reports an error if a segment is unreachable', function() { ...@@ -889,10 +835,10 @@ test('reports an error if a segment is unreachable', function() {
889 openMediaSource(player); 835 openMediaSource(player);
890 836
891 player.tech_.hls.bandwidth = 20000; 837 player.tech_.hls.bandwidth = 20000;
892 standardXHRResponse(requests[0]); 838 standardXHRResponse(requests[0]); // master
893 standardXHRResponse(requests[1]); 839 standardXHRResponse(requests[1]); // media
894 840
895 requests[2].respond(400); 841 requests[2].respond(400); // segment
896 strictEqual(player.tech_.hls.mediaSource.error_, 'network', 'network error is triggered'); 842 strictEqual(player.tech_.hls.mediaSource.error_, 'network', 'network error is triggered');
897 }); 843 });
898 844
...@@ -1172,18 +1118,18 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -1172,18 +1118,18 @@ test('downloads the next segment if the buffer is getting low', function() {
1172 1118
1173 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() {
1174 var currentTime, buffered; 1120 var currentTime, buffered;
1121 player.src({
1122 src: 'manifest/media.m3u8',
1123 type: 'application/vnd.apple.mpegurl'
1124 });
1125 openMediaSource(player);
1126
1175 player.tech_.currentTime = function() { 1127 player.tech_.currentTime = function() {
1176 return currentTime; 1128 return currentTime;
1177 }; 1129 };
1178 player.tech_.buffered = function() { 1130 player.tech_.buffered = function() {
1179 return videojs.createTimeRange(buffered); 1131 return videojs.createTimeRange(buffered);
1180 }; 1132 };
1181
1182 player.src({
1183 src: 'manifest/media.m3u8',
1184 type: 'application/vnd.apple.mpegurl'
1185 });
1186 openMediaSource(player);
1187 currentTime = 8; 1133 currentTime = 8;
1188 buffered = [[0, 10], [20, 40]]; 1134 buffered = [[0, 10], [20, 40]];
1189 1135
...@@ -1238,7 +1184,6 @@ test('only makes one segment request at a time', function() { ...@@ -1238,7 +1184,6 @@ test('only makes one segment request at a time', function() {
1238 }); 1184 });
1239 1185
1240 test('only appends one segment at a time', function() { 1186 test('only appends one segment at a time', function() {
1241 var appends = 0;
1242 player.src({ 1187 player.src({
1243 src: 'manifest/media.m3u8', 1188 src: 'manifest/media.m3u8',
1244 type: 'application/vnd.apple.mpegurl' 1189 type: 'application/vnd.apple.mpegurl'
...@@ -1251,84 +1196,6 @@ test('only appends one segment at a time', function() { ...@@ -1251,84 +1196,6 @@ test('only appends one segment at a time', function() {
1251 equal(requests.length, 0, 'did not request while updating'); 1196 equal(requests.length, 0, 'did not request while updating');
1252 }); 1197 });
1253 1198
1254 QUnit.skip('records the min and max PTS values for a segment', function() {
1255 var tags = [];
1256 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1257 player.src({
1258 src: 'manifest/media.m3u8',
1259 type: 'application/vnd.apple.mpegurl'
1260 });
1261 openMediaSource(player);
1262 standardXHRResponse(requests.pop()); // media.m3u8
1263
1264 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1265 tags.push({ pts: 10, bytes: new Uint8Array(1) });
1266 standardXHRResponse(requests.pop()); // segment 0
1267
1268 equal(player.tech_.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts');
1269 equal(player.tech_.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts');
1270 equal(player.tech_.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts');
1271 equal(player.tech_.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts');
1272 });
1273
1274 QUnit.skip('records PTS values for video-only segments', function() {
1275 var tags = [];
1276 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1277 player.src({
1278 src: 'manifest/media.m3u8',
1279 type: 'application/vnd.apple.mpegurl'
1280 });
1281 openMediaSource(player);
1282 standardXHRResponse(requests.pop()); // media.m3u8
1283
1284 player.tech_.hls.segmentParser_.stats.aacTags = function() {
1285 return 0;
1286 };
1287 player.tech_.hls.segmentParser_.stats.minAudioPts = function() {
1288 throw new Error('No audio tags');
1289 };
1290 player.tech_.hls.segmentParser_.stats.maxAudioPts = function() {
1291 throw new Error('No audio tags');
1292 };
1293 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1294 tags.push({ pts: 10, bytes: new Uint8Array(1) });
1295 standardXHRResponse(requests.pop()); // segment 0
1296
1297 equal(player.tech_.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts');
1298 equal(player.tech_.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts');
1299 strictEqual(player.tech_.hls.playlists.media().segments[0].minAudioPts, undefined, 'min audio pts is undefined');
1300 strictEqual(player.tech_.hls.playlists.media().segments[0].maxAudioPts, undefined, 'max audio pts is undefined');
1301 });
1302
1303 QUnit.skip('records PTS values for audio-only segments', function() {
1304 var tags = [];
1305 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1306 player.src({
1307 src: 'manifest/media.m3u8',
1308 type: 'application/vnd.apple.mpegurl'
1309 });
1310 openMediaSource(player);
1311 standardXHRResponse(requests.pop()); // media.m3u8
1312
1313 player.tech_.hls.segmentParser_.stats.h264Tags = function() {
1314 return 0;
1315 };
1316 player.tech_.hls.segmentParser_.stats.minVideoPts = function() {
1317 throw new Error('No video tags');
1318 };
1319 player.tech_.hls.segmentParser_.stats.maxVideoPts = function() {
1320 throw new Error('No video tags');
1321 };
1322 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1323 tags.push({ pts: 10, bytes: new Uint8Array(1) });
1324 standardXHRResponse(requests.pop()); // segment 0
1325
1326 equal(player.tech_.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts');
1327 equal(player.tech_.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts');
1328 strictEqual(player.tech_.hls.playlists.media().segments[0].minVideoPts, undefined, 'min video pts is undefined');
1329 strictEqual(player.tech_.hls.playlists.media().segments[0].maxVideoPts, undefined, 'max video pts is undefined');
1330 });
1331
1332 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() {
1333 player.src({ 1200 player.src({
1334 src: 'manifest/master.m3u8', 1201 src: 'manifest/master.m3u8',
...@@ -1438,331 +1305,6 @@ test('segmentXhr is properly nulled out when dispose is called', function() { ...@@ -1438,331 +1305,6 @@ test('segmentXhr is properly nulled out when dispose is called', function() {
1438 Flash.prototype.dispose = oldDispose; 1305 Flash.prototype.dispose = oldDispose;
1439 }); 1306 });
1440 1307
1441 QUnit.skip('exposes in-band metadata events as cues', function() {
1442 var track;
1443 videojs.Hls.SegmentParser = mockSegmentParser();
1444 player.src({
1445 src: 'manifest/media.m3u8',
1446 type: 'application/vnd.apple.mpegurl'
1447 });
1448 openMediaSource(player);
1449
1450 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1451 // trigger a metadata event
1452 player.tech_.hls.segmentParser_.metadataStream.trigger('data', {
1453 pts: 2000,
1454 data: new Uint8Array([]),
1455 frames: [{
1456 id: 'TXXX',
1457 value: 'cue text'
1458 }, {
1459 id: 'WXXX',
1460 url: 'http://example.com'
1461 }, {
1462 id: 'PRIV',
1463 owner: 'owner@example.com',
1464 privateData: new Uint8Array([1, 2, 3])
1465 }]
1466 });
1467 };
1468
1469 standardXHRResponse(requests[0]);
1470 standardXHRResponse(requests[1]);
1471
1472 equal(player.textTracks().length, 1, 'created a text track');
1473 track = player.textTracks()[0];
1474 equal(track.kind, 'metadata', 'kind is metadata');
1475 equal(track.inBandMetadataTrackDispatchType, '15010203BB', 'set the dispatch type');
1476 equal(track.cues.length, 3, 'created three cues');
1477 equal(track.cues[0].startTime, 2, 'cue starts at 2 seconds');
1478 equal(track.cues[0].endTime, 2, 'cue ends at 2 seconds');
1479 equal(track.cues[0].pauseOnExit, false, 'cue does not pause on exit');
1480 equal(track.cues[0].text, 'cue text', 'set cue text');
1481
1482 equal(track.cues[1].startTime, 2, 'cue starts at 2 seconds');
1483 equal(track.cues[1].endTime, 2, 'cue ends at 2 seconds');
1484 equal(track.cues[1].pauseOnExit, false, 'cue does not pause on exit');
1485 equal(track.cues[1].text, 'http://example.com', 'set cue text');
1486
1487 equal(track.cues[2].startTime, 2, 'cue starts at 2 seconds');
1488 equal(track.cues[2].endTime, 2, 'cue ends at 2 seconds');
1489 equal(track.cues[2].pauseOnExit, false, 'cue does not pause on exit');
1490 equal(track.cues[2].text, '', 'did not set cue text');
1491 equal(track.cues[2].frame.owner, 'owner@example.com', 'set the owner');
1492 deepEqual(track.cues[2].frame.privateData,
1493 new Uint8Array([1, 2, 3]),
1494 'set the private data');
1495 });
1496
1497 QUnit.skip('only adds in-band cues the first time they are encountered', function() {
1498 var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track;
1499 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1500 player.src({
1501 src: 'manifest/media.m3u8',
1502 type: 'application/vnd.apple.mpegurl'
1503 });
1504 openMediaSource(player);
1505
1506 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1507 // trigger a metadata event
1508 player.tech_.hls.segmentParser_.metadataStream.trigger('data', {
1509 pts: 2000,
1510 data: new Uint8Array([]),
1511 frames: [{
1512 id: 'TXXX',
1513 value: 'cue text'
1514 }]
1515 });
1516 };
1517 standardXHRResponse(requests.shift());
1518 standardXHRResponse(requests.shift());
1519 // seek back to the first segment
1520 player.currentTime(0);
1521 player.tech_.hls.trigger('seeking');
1522 tags.push({ pts: 0, bytes: new Uint8Array(1) });
1523 standardXHRResponse(requests.shift());
1524
1525 track = player.textTracks()[0];
1526 equal(track.cues.length, 1, 'only added the cue once');
1527 });
1528
1529 QUnit.skip('clears in-band cues ahead of current time on seek', function() {
1530 var
1531 tags = [],
1532 events = [],
1533 track;
1534 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1535 player.src({
1536 src: 'manifest/media.m3u8',
1537 type: 'application/vnd.apple.mpegurl'
1538 });
1539 openMediaSource(player);
1540
1541 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1542 // trigger a metadata event
1543 while (events.length) {
1544 player.tech_.hls.segmentParser_.metadataStream.trigger('data', events.shift());
1545 }
1546 };
1547 standardXHRResponse(requests.shift()); // media
1548 tags.push({ pts: 0, bytes: new Uint8Array(1) },
1549 { pts: 10 * 1000, bytes: new Uint8Array(1) });
1550 events.push({
1551 pts: 9.9 * 1000,
1552 data: new Uint8Array([]),
1553 frames: [{
1554 id: 'TXXX',
1555 value: 'cue 1'
1556 }]
1557 });
1558 events.push({
1559 pts: 20 * 1000,
1560 data: new Uint8Array([]),
1561 frames: [{
1562 id: 'TXXX',
1563 value: 'cue 3'
1564 }]
1565 });
1566 standardXHRResponse(requests.shift()); // segment 0
1567 tags.push({ pts: 10 * 1000 + 1, bytes: new Uint8Array(1) },
1568 { pts: 20 * 1000, bytes: new Uint8Array(1) });
1569 events.push({
1570 pts: 19.9 * 1000,
1571 data: new Uint8Array([]),
1572 frames: [{
1573 id: 'TXXX',
1574 value: 'cue 2'
1575 }]
1576 });
1577 player.tech_.hls.checkBuffer_();
1578 standardXHRResponse(requests.shift()); // segment 1
1579
1580 track = player.textTracks()[0];
1581 equal(track.cues.length, 3, 'added the cues');
1582
1583 // seek into segment 1
1584 player.currentTime(11);
1585 player.trigger('seeking');
1586 equal(track.cues.length, 1, 'removed later cues');
1587 equal(track.cues[0].startTime, 9.9, 'retained the earlier cue');
1588 });
1589
1590 QUnit.skip('translates ID3 PTS values to cue media timeline positions', function() {
1591 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
1592 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1593 player.src({
1594 src: 'manifest/media.m3u8',
1595 type: 'application/vnd.apple.mpegurl'
1596 });
1597 openMediaSource(player);
1598
1599 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1600 // trigger a metadata event
1601 player.tech_.hls.segmentParser_.metadataStream.trigger('data', {
1602 pts: 5 * 1000,
1603 data: new Uint8Array([]),
1604 frames: [{
1605 id: 'TXXX',
1606 value: 'cue text'
1607 }]
1608 });
1609 };
1610 standardXHRResponse(requests.shift()); // media
1611 standardXHRResponse(requests.shift()); // segment 0
1612
1613 track = player.textTracks()[0];
1614 equal(track.cues[0].startTime, 1, 'translated startTime');
1615 equal(track.cues[0].endTime, 1, 'translated startTime');
1616 });
1617
1618 QUnit.skip('translates ID3 PTS values with expired segments', function() {
1619 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
1620 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1621 player.src({
1622 src: 'live.m3u8',
1623 type: 'application/vnd.apple.mpegurl'
1624 });
1625 openMediaSource(player);
1626 player.play();
1627
1628 // 20.9 seconds of content have expired
1629 player.hls.playlists.expiredPostDiscontinuity_ = 20.9;
1630
1631 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1632 // trigger a metadata event
1633 player.hls.segmentParser_.metadataStream.trigger('data', {
1634 pts: 5 * 1000,
1635 data: new Uint8Array([]),
1636 frames: [{
1637 id: 'TXXX',
1638 value: 'cue text'
1639 }]
1640 });
1641 };
1642 requests.shift().respond(200, null,
1643 '#EXTM3U\n' +
1644 '#EXT-X-MEDIA-SEQUENCE:2\n' +
1645 '#EXTINF:10,\n' +
1646 '2.ts\n' +
1647 '#EXTINF:10,\n' +
1648 '3.ts\n'); // media
1649 standardXHRResponse(requests.shift()); // segment 0
1650
1651 track = player.textTracks()[0];
1652 equal(track.cues[0].startTime, 20.9 + 1, 'translated startTime');
1653 equal(track.cues[0].endTime, 20.9 + 1, 'translated startTime');
1654 });
1655
1656 QUnit.skip('translates id3 PTS values for audio-only media', function() {
1657 var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track;
1658 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1659 player.src({
1660 src: 'manifest/media.m3u8',
1661 type: 'application/vnd.apple.mpegurl'
1662 });
1663 openMediaSource(player);
1664
1665 player.hls.segmentParser_.parseSegmentBinaryData = function() {
1666 // trigger a metadata event
1667 player.hls.segmentParser_.metadataStream.trigger('data', {
1668 pts: 5 * 1000,
1669 data: new Uint8Array([]),
1670 frames: [{
1671 id: 'TXXX',
1672 value: 'cue text'
1673 }]
1674 });
1675 };
1676 player.hls.segmentParser_.stats.h264Tags = function() { return 0; };
1677 player.hls.segmentParser_.stats.minVideoPts = null;
1678 standardXHRResponse(requests.shift()); // media
1679 standardXHRResponse(requests.shift()); // segment 0
1680
1681 track = player.textTracks()[0];
1682 equal(track.cues[0].startTime, 1, 'translated startTime');
1683 });
1684
1685 QUnit.skip('translates ID3 PTS values across discontinuities', function() {
1686 var tags = [], events = [], track;
1687 videojs.Hls.SegmentParser = mockSegmentParser(tags);
1688 player.src({
1689 src: 'cues-and-discontinuities.m3u8',
1690 type: 'application/vnd.apple.mpegurl'
1691 });
1692 openMediaSource(player);
1693
1694 player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() {
1695 // trigger a metadata event
1696 if (events.length) {
1697 player.tech_.hls.segmentParser_.metadataStream.trigger('data', events.shift());
1698 }
1699 };
1700
1701 // media playlist
1702 player.trigger('play');
1703 requests.shift().respond(200, null,
1704 '#EXTM3U\n' +
1705 '#EXTINF:10,\n' +
1706 '0.ts\n' +
1707 '#EXT-X-DISCONTINUITY\n' +
1708 '#EXTINF:10,\n' +
1709 '1.ts\n');
1710
1711 // segment 0 starts at PTS 14000 and has a cue point at 15000
1712 tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) },
1713 { pts: 24 * 1000, bytes: new Uint8Array(1) });
1714 events.push({
1715 pts: 15 * 1000,
1716 data: new Uint8Array([]),
1717 frames: [{
1718 id: 'TXXX',
1719 value: 'cue 0'
1720 }]
1721 });
1722 standardXHRResponse(requests.shift()); // segment 0
1723
1724 // segment 1 is after a discontinuity, starts at PTS 22000
1725 // and has a cue point at 23000
1726 tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) });
1727 events.push({
1728 pts: 23 * 1000,
1729 data: new Uint8Array([]),
1730 frames: [{
1731 id: 'TXXX',
1732 value: 'cue 1'
1733 }]
1734 });
1735 player.tech_.hls.checkBuffer_();
1736 standardXHRResponse(requests.shift());
1737
1738 track = player.textTracks()[0];
1739 equal(track.cues.length, 2, 'created cues');
1740 equal(track.cues[0].startTime, 1, 'first cue started at the correct time');
1741 equal(track.cues[0].endTime, 1, 'first cue ended at the correct time');
1742 equal(track.cues[1].startTime, 11, 'second cue started at the correct time');
1743 equal(track.cues[1].endTime, 11, 'second cue ended at the correct time');
1744 });
1745
1746 test('seeks between buffered time ranges', function() {
1747 player.src({
1748 src: 'media.m3u8',
1749 type: 'application/vnd.apple.mpegurl'
1750 });
1751 openMediaSource(player);
1752 standardXHRResponse(requests.shift()); // media
1753 player.tech_.buffered = function() {
1754 return videojs.createTimeRange([[0, 10], [20, 30]]);
1755 };
1756
1757 player.tech_.setCurrentTime(15);
1758 clock.tick(1);
1759 // drop the aborted segment
1760 requests.shift();
1761 equal(requests[0].url,
1762 absoluteUrl('media-00002.ts'),
1763 'requested the correct segment');
1764 });
1765
1766 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() {
1767 var mediaIndex; 1309 var mediaIndex;
1768 player.src({ 1310 player.src({
...@@ -2036,26 +1578,21 @@ test('does not break if the playlist has no segments', function() { ...@@ -2036,26 +1578,21 @@ test('does not break if the playlist has no segments', function() {
2036 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');
2037 }); 1579 });
2038 1580
2039 test('clears the segment buffer on seek', function() { 1581 test('aborts segment processing on seek', function() {
2040 var currentTime, oldCurrentTime; 1582 var currentTime = 0;
2041
2042 player.src({ 1583 player.src({
2043 src: 'discontinuity.m3u8', 1584 src: 'discontinuity.m3u8',
2044 type: 'application/vnd.apple.mpegurl' 1585 type: 'application/vnd.apple.mpegurl'
2045 }); 1586 });
2046 openMediaSource(player); 1587 openMediaSource(player);
2047 oldCurrentTime = player.currentTime; 1588 player.tech_.currentTime = function() {
2048 player.currentTime = function(time) {
2049 if (time !== undefined) {
2050 return oldCurrentTime.call(player, time);
2051 }
2052 return currentTime; 1589 return currentTime;
2053 }; 1590 };
2054 player.tech_.buffered = function() { 1591 player.tech_.buffered = function() {
2055 return videojs.createTimeRange(); 1592 return videojs.createTimeRange();
2056 }; 1593 };
2057 1594
2058 requests.pop().respond(200, null, 1595 requests.shift().respond(200, null,
2059 '#EXTM3U\n' + 1596 '#EXTM3U\n' +
2060 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + 1597 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
2061 '#EXTINF:10,0\n' + 1598 '#EXTINF:10,0\n' +
...@@ -2063,23 +1600,19 @@ test('clears the segment buffer on seek', function() { ...@@ -2063,23 +1600,19 @@ test('clears the segment buffer on seek', function() {
2063 '#EXT-X-DISCONTINUITY\n' + 1600 '#EXT-X-DISCONTINUITY\n' +
2064 '#EXTINF:10,0\n' + 1601 '#EXTINF:10,0\n' +
2065 '2.ts\n' + 1602 '2.ts\n' +
2066 '#EXT-X-ENDLIST\n'); 1603 '#EXT-X-ENDLIST\n'); // media
2067 standardXHRResponse(requests.pop()); // 1.ts 1604 standardXHRResponse(requests.shift()); // 1.ts
2068 1605 standardXHRResponse(requests.shift()); // key.php
2069 // play to 6s to trigger the next segment request 1606 ok(player.tech_.hls.pendingSegment_, 'decrypting the segment');
2070 currentTime = 6;
2071 clock.tick(6000);
2072
2073 standardXHRResponse(requests.pop()); // 2.ts
2074 equal(player.tech_.hls.segmentBuffer_.length, 2, 'started fetching segments');
2075 1607
2076 // seek back to the beginning 1608 // seek back to the beginning
2077 player.currentTime(0); 1609 player.currentTime(0);
2078 clock.tick(1); 1610 clock.tick(1);
2079 equal(player.tech_.hls.segmentBuffer_.length, 0, 'cleared the segment buffer'); 1611 ok(!player.tech_.hls.pendingSegment_, 'aborted processing');
2080 }); 1612 });
2081 1613
2082 test('calls mediaSource\'s timestampOffset on discontinuity', function() { 1614 test('calls mediaSource\'s timestampOffset on discontinuity', function() {
1615 var buffered = [[]];
2083 player.src({ 1616 player.src({
2084 src: 'discontinuity.m3u8', 1617 src: 'discontinuity.m3u8',
2085 type: 'application/vnd.apple.mpegurl' 1618 type: 'application/vnd.apple.mpegurl'
...@@ -2087,10 +1620,10 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { ...@@ -2087,10 +1620,10 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() {
2087 openMediaSource(player); 1620 openMediaSource(player);
2088 player.play(); 1621 player.play();
2089 player.tech_.buffered = function() { 1622 player.tech_.buffered = function() {
2090 return videojs.createTimeRange(0, 10); 1623 return videojs.createTimeRange(buffered);
2091 }; 1624 };
2092 1625
2093 requests.pop().respond(200, null, 1626 requests.shift().respond(200, null,
2094 '#EXTM3U\n' + 1627 '#EXTM3U\n' +
2095 '#EXTINF:10,0\n' + 1628 '#EXTINF:10,0\n' +
2096 '1.ts\n' + 1629 '1.ts\n' +
...@@ -2099,14 +1632,14 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { ...@@ -2099,14 +1632,14 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() {
2099 '2.ts\n' + 1632 '2.ts\n' +
2100 '#EXT-X-ENDLIST\n'); 1633 '#EXT-X-ENDLIST\n');
2101 player.tech_.hls.sourceBuffer.timestampOffset = 0; 1634 player.tech_.hls.sourceBuffer.timestampOffset = 0;
2102 standardXHRResponse(requests.pop()); // 1.ts 1635 standardXHRResponse(requests.shift()); // 1.ts
2103 1636 equal(player.tech_.hls.sourceBuffer.timestampOffset,
2104 equal(player.tech_.hls.sourceBuffer.timestampOffset, 0, 'timestampOffset starts at zero'); 1637 0,
2105 1638 'timestampOffset starts at zero');
2106 // play to 6s to trigger the next segment request
2107 clock.tick(6000);
2108 1639
2109 standardXHRResponse(requests.pop()); // 2.ts 1640 buffered = [[0, 10]];
1641 player.tech_.hls.sourceBuffer.trigger('updateend');
1642 standardXHRResponse(requests.shift()); // 2.ts
2110 equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity'); 1643 equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity');
2111 }); 1644 });
2112 1645
...@@ -2198,7 +1731,7 @@ QUnit.skip('sets the timestampOffset after seeking to discontinuity', function() ...@@ -2198,7 +1731,7 @@ QUnit.skip('sets the timestampOffset after seeking to discontinuity', function()
2198 'set the timestamp offset'); 1731 'set the timestamp offset');
2199 }); 1732 });
2200 1733
2201 QUnit.skip('tracks segment end times as they are buffered', function() { 1734 test('tracks segment end times as they are buffered', function() {
2202 var bufferEnd = 0; 1735 var bufferEnd = 0;
2203 player.src({ 1736 player.src({
2204 src: 'media.m3u8', 1737 src: 'media.m3u8',
...@@ -2225,8 +1758,7 @@ QUnit.skip('tracks segment end times as they are buffered', function() { ...@@ -2225,8 +1758,7 @@ QUnit.skip('tracks segment end times as they are buffered', function() {
2225 bufferEnd = 9.5; 1758 bufferEnd = 9.5;
2226 player.tech_.hls.sourceBuffer.trigger('update'); 1759 player.tech_.hls.sourceBuffer.trigger('update');
2227 player.tech_.hls.sourceBuffer.trigger('updateend'); 1760 player.tech_.hls.sourceBuffer.trigger('updateend');
2228 equal(player.tech_.duration(), 10 + 9.5, 'updated duration'); 1761 equal(player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration');
2229 equal(player.tech_.hls.appendingSegmentInfo_, null, 'cleared the appending segment');
2230 }); 1762 });
2231 1763
2232 QUnit.skip('seeking does not fail when targeted between segments', function() { 1764 QUnit.skip('seeking does not fail when targeted between segments', function() {
...@@ -2274,7 +1806,7 @@ test('resets the switching algorithm if a request times out', function() { ...@@ -2274,7 +1806,7 @@ test('resets the switching algorithm if a request times out', function() {
2274 type: 'application/vnd.apple.mpegurl' 1806 type: 'application/vnd.apple.mpegurl'
2275 }); 1807 });
2276 openMediaSource(player); 1808 openMediaSource(player);
2277 player.tech_.hls.bandwidth = 20000; 1809 player.tech_.hls.bandwidth = 1e20;
2278 1810
2279 standardXHRResponse(requests.shift()); // master 1811 standardXHRResponse(requests.shift()); // master
2280 standardXHRResponse(requests.shift()); // media.m3u8 1812 standardXHRResponse(requests.shift()); // media.m3u8
...@@ -2455,6 +1987,7 @@ test('tracks the bytes downloaded', function() { ...@@ -2455,6 +1987,7 @@ test('tracks the bytes downloaded', function() {
2455 // transmit some segment bytes 1987 // transmit some segment bytes
2456 requests[0].response = new ArrayBuffer(17); 1988 requests[0].response = new ArrayBuffer(17);
2457 requests.shift().respond(200, null, ''); 1989 requests.shift().respond(200, null, '');
1990 player.tech_.hls.sourceBuffer.trigger('updateend');
2458 1991
2459 strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received'); 1992 strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received');
2460 1993
...@@ -2509,12 +2042,15 @@ test('can be disposed before finishing initialization', function() { ...@@ -2509,12 +2042,15 @@ test('can be disposed before finishing initialization', function() {
2509 }); 2042 });
2510 2043
2511 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() {
2512 var endOfStreams = 0; 2045 var endOfStreams = 0, buffered = [[]];
2513 player.src({ 2046 player.src({
2514 src: 'http://example.com/media.m3u8', 2047 src: 'http://example.com/media.m3u8',
2515 type: 'application/vnd.apple.mpegurl' 2048 type: 'application/vnd.apple.mpegurl'
2516 }); 2049 });
2517 openMediaSource(player); 2050 openMediaSource(player);
2051 player.tech_.buffered = function() {
2052 return videojs.createTimeRanges(buffered);
2053 };
2518 player.tech_.hls.mediaSource.endOfStream = function() { 2054 player.tech_.hls.mediaSource.endOfStream = function() {
2519 endOfStreams++; 2055 endOfStreams++;
2520 }; 2056 };
...@@ -2529,70 +2065,61 @@ test('calls ended() on the media source at the end of a playlist', function() { ...@@ -2529,70 +2065,61 @@ test('calls ended() on the media source at the end of a playlist', function() {
2529 requests.shift().respond(200, null, ''); 2065 requests.shift().respond(200, null, '');
2530 strictEqual(endOfStreams, 0, 'waits for the buffer update to finish'); 2066 strictEqual(endOfStreams, 0, 'waits for the buffer update to finish');
2531 2067
2068 buffered =[[0, 10]];
2532 player.tech_.hls.sourceBuffer.trigger('updateend'); 2069 player.tech_.hls.sourceBuffer.trigger('updateend');
2533 strictEqual(endOfStreams, 1, 'ended media source'); 2070 strictEqual(endOfStreams, 1, 'ended media source');
2534 }); 2071 });
2535 2072
2536 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;
2537 player.src({ 2075 player.src({
2538 src: 'http://example.com/media.m3u8', 2076 src: 'http://example.com/media.m3u8',
2539 type: 'application/vnd.apple.mpegurl' 2077 type: 'application/vnd.apple.mpegurl'
2540 }); 2078 });
2541 openMediaSource(player); 2079 openMediaSource(player);
2080 player.tech_.setCurrentTime = function(time) {
2081 if (time !== undefined) {
2082 seekTime = time;
2083 }
2084 return 0;
2085 };
2542 requests.shift().respond(200, null, 2086 requests.shift().respond(200, null,
2543 '#EXTM3U\n' + 2087 '#EXTM3U\n' +
2544 '#EXTINF:10,\n' + 2088 '#EXTINF:10,\n' +
2545 '0.ts\n' + 2089 '0.ts\n' +
2546 '#EXT-X-ENDLIST\n'); 2090 '#EXT-X-ENDLIST\n');
2547 standardXHRResponse(requests.shift()); 2091 standardXHRResponse(requests.shift());
2548
2549 strictEqual(player.tech_.hls.mediaIndex, 1, 'index is 1 after the first segment');
2550 player.tech_.ended = function() { 2092 player.tech_.ended = function() {
2551 return true; 2093 return true;
2552 }; 2094 };
2553 2095
2554 player.tech_.trigger('play'); 2096 player.tech_.trigger('play');
2555 strictEqual(player.tech_.hls.mediaIndex, 0, 'index is 0 after the first segment'); 2097 equal(seekTime, 0, 'seeked to the beginning');
2556 }); 2098 });
2557 2099
2558 test('drainBuffer will not proceed with empty source buffer', function() { 2100 test('segments remain pending without a source buffer', function() {
2559 var oldMedia, newMedia, compareBuffer;
2560 player.src({ 2101 player.src({
2561 src: 'https://example.com/encrypted-media.m3u8', 2102 src: 'https://example.com/encrypted-media.m3u8',
2562 type: 'application/vnd.apple.mpegurl' 2103 type: 'application/vnd.apple.mpegurl'
2563 }); 2104 });
2564 openMediaSource(player); 2105 openMediaSource(player);
2565 2106
2566 oldMedia = player.tech_.hls.playlists.media; 2107 requests.shift().respond(200, null,
2567 newMedia = {segments: [{ 2108 '#EXTM3U\n' +
2568 key: { 2109 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=52"\n' +
2569 'retries': 5 2110 '#EXTINF:10,\n' +
2570 }, 2111 'http://media.example.com/fileSequence52-A.ts' +
2571 uri: 'http://media.example.com/fileSequence52-A.ts' 2112 '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=53"\n' +
2572 }, { 2113 '#EXTINF:10,\n' +
2573 key: { 2114 'http://media.example.com/fileSequence53-B.ts\n' +
2574 'method': 'AES-128', 2115 '#EXT-X-ENDLIST\n');
2575 'uri': 'https://priv.example.com/key.php?r=53'
2576 },
2577 uri: 'http://media.example.com/fileSequence53-B.ts'
2578 }]};
2579 player.tech_.hls.playlists.media = function() {
2580 return newMedia;
2581 };
2582 2116
2583 player.tech_.hls.sourceBuffer = undefined; 2117 player.tech_.hls.sourceBuffer = undefined;
2584 compareBuffer = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}];
2585 player.tech_.hls.segmentBuffer_ = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}];
2586 2118
2587 player.tech_.hls.drainBuffer(); 2119 standardXHRResponse(requests.shift()); // key
2588 2120 standardXHRResponse(requests.shift()); // segment
2589 /* Normally, drainBuffer() calls segmentBuffer.shift(), removing a segment from the stack. 2121 player.tech_.hls.checkBuffer_();
2590 * Comparing two buffers to ensure no segment was popped verifies that we returned early 2122 ok(player.tech_.hls.pendingSegment_, 'waiting for the source buffer');
2591 * from drainBuffer() because sourceBuffer was empty.
2592 */
2593 deepEqual(player.tech_.hls.segmentBuffer_, compareBuffer, 'playlist remains unchanged');
2594
2595 player.tech_.hls.playlists.media = oldMedia;
2596 }); 2123 });
2597 2124
2598 test('keys are requested when an encrypted segment is loaded', function() { 2125 test('keys are requested when an encrypted segment is loaded', function() {
...@@ -2603,12 +2130,14 @@ test('keys are requested when an encrypted segment is loaded', function() { ...@@ -2603,12 +2130,14 @@ test('keys are requested when an encrypted segment is loaded', function() {
2603 openMediaSource(player); 2130 openMediaSource(player);
2604 player.tech_.trigger('play'); 2131 player.tech_.trigger('play');
2605 standardXHRResponse(requests.shift()); // playlist 2132 standardXHRResponse(requests.shift()); // playlist
2606 standardXHRResponse(requests.shift()); // first segment
2607 2133
2608 strictEqual(requests.length, 1, 'a key XHR is created'); 2134 strictEqual(requests.length, 2, 'a key XHR is created');
2609 strictEqual(requests[0].url, 2135 strictEqual(requests[0].url,
2610 player.tech_.hls.playlists.media().segments[0].key.uri, 2136 player.tech_.hls.playlists.media().segments[0].key.uri,
2611 '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');
2612 }); 2141 });
2613 2142
2614 test('keys are resolved relative to the master playlist', function() { 2143 test('keys are resolved relative to the master playlist', function() {
...@@ -2629,10 +2158,9 @@ test('keys are resolved relative to the master playlist', function() { ...@@ -2629,10 +2158,9 @@ test('keys are resolved relative to the master playlist', function() {
2629 '#EXTINF:2.833,\n' + 2158 '#EXTINF:2.833,\n' +
2630 'http://media.example.com/fileSequence1.ts\n' + 2159 'http://media.example.com/fileSequence1.ts\n' +
2631 '#EXT-X-ENDLIST\n'); 2160 '#EXT-X-ENDLIST\n');
2632 2161 equal(requests.length, 2, 'requested the key');
2633 standardXHRResponse(requests.shift()); 2162 equal(requests[0].url,
2634 equal(requests.length, 1, 'requested the key'); 2163 absoluteUrl('video/playlist/keys/key.php'),
2635 ok((/video\/playlist\/keys\/key\.php$/).test(requests[0].url),
2636 'resolves multiple relative paths'); 2164 'resolves multiple relative paths');
2637 }); 2165 });
2638 2166
...@@ -2649,13 +2177,13 @@ test('keys are resolved relative to their containing playlist', function() { ...@@ -2649,13 +2177,13 @@ test('keys are resolved relative to their containing playlist', function() {
2649 '#EXTINF:2.833,\n' + 2177 '#EXTINF:2.833,\n' +
2650 'http://media.example.com/fileSequence1.ts\n' + 2178 'http://media.example.com/fileSequence1.ts\n' +
2651 '#EXT-X-ENDLIST\n'); 2179 '#EXT-X-ENDLIST\n');
2652 standardXHRResponse(requests.shift()); 2180 equal(requests.length, 2, 'requested a key');
2653 equal(requests.length, 1, 'requested a key'); 2181 equal(requests[0].url,
2654 ok((/video\/keys\/key\.php$/).test(requests[0].url), 2182 absoluteUrl('video/keys/key.php'),
2655 'resolves multiple relative paths'); 2183 'resolves multiple relative paths');
2656 }); 2184 });
2657 2185
2658 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() {
2659 player.src({ 2187 player.src({
2660 src: 'https://example.com/encrypted-media.m3u8', 2188 src: 'https://example.com/encrypted-media.m3u8',
2661 type: 'application/vnd.apple.mpegurl' 2189 type: 'application/vnd.apple.mpegurl'
...@@ -2672,15 +2200,17 @@ test('a new key XHR is created when a the segment is received', function() { ...@@ -2672,15 +2200,17 @@ test('a new key XHR is created when a the segment is received', function() {
2672 '#EXTINF:2.833,\n' + 2200 '#EXTINF:2.833,\n' +
2673 'http://media.example.com/fileSequence2.ts\n' + 2201 'http://media.example.com/fileSequence2.ts\n' +
2674 '#EXT-X-ENDLIST\n'); 2202 '#EXT-X-ENDLIST\n');
2675 standardXHRResponse(requests.shift()); // segment 1
2676 standardXHRResponse(requests.shift()); // key 1 2203 standardXHRResponse(requests.shift()); // key 1
2204 standardXHRResponse(requests.shift()); // segment 1
2677 // "finish" decrypting segment 1 2205 // "finish" decrypting segment 1
2678 player.tech_.hls.segmentBuffer_[0].bytes = new Uint8Array(16); 2206 player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16);
2679 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');
2680 2212
2681 standardXHRResponse(requests.shift()); // segment 2 2213 strictEqual(requests.length, 2, 'a key XHR is created');
2682
2683 strictEqual(requests.length, 1, 'a key XHR is created');
2684 strictEqual(requests[0].url, 2214 strictEqual(requests[0].url,
2685 'https://example.com/' + 2215 'https://example.com/' +
2686 player.tech_.hls.playlists.media().segments[1].key.uri, 2216 player.tech_.hls.playlists.media().segments[1].key.uri,
...@@ -2704,16 +2234,14 @@ test('seeking should abort an outstanding key request and create a new one', fun ...@@ -2704,16 +2234,14 @@ test('seeking should abort an outstanding key request and create a new one', fun
2704 '#EXTINF:9,\n' + 2234 '#EXTINF:9,\n' +
2705 'http://media.example.com/fileSequence2.ts\n' + 2235 'http://media.example.com/fileSequence2.ts\n' +
2706 '#EXT-X-ENDLIST\n'); 2236 '#EXT-X-ENDLIST\n');
2707 standardXHRResponse(requests.shift()); // segment 1 2237 standardXHRResponse(requests.pop()); // segment 1
2708 2238
2709 player.currentTime(11); 2239 player.currentTime(11);
2710 clock.tick(1); 2240 clock.tick(1);
2711 ok(requests[0].aborted, 'the key XHR should be aborted'); 2241 ok(requests[0].aborted, 'the key XHR should be aborted');
2712 requests.shift(); // aborted key 1 2242 requests.shift(); // aborted key 1
2713 2243
2714 equal(requests.length, 1, 'requested the new segment'); 2244 equal(requests.length, 2, 'requested the new key');
2715 standardXHRResponse(requests.shift()); // segment 2
2716 equal(requests.length, 1, 'requested the new key');
2717 equal(requests[0].url, 2245 equal(requests[0].url,
2718 'https://example.com/' + 2246 'https://example.com/' +
2719 player.tech_.hls.playlists.media().segments[1].key.uri, 2247 player.tech_.hls.playlists.media().segments[1].key.uri,
...@@ -2736,7 +2264,7 @@ test('retries key requests once upon failure', function() { ...@@ -2736,7 +2264,7 @@ test('retries key requests once upon failure', function() {
2736 '#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' +
2737 '#EXTINF:15.0,\n' + 2265 '#EXTINF:15.0,\n' +
2738 'http://media.example.com/fileSequence53-A.ts\n'); 2266 'http://media.example.com/fileSequence53-A.ts\n');
2739 standardXHRResponse(requests.shift()); // segment 2267 standardXHRResponse(requests.pop()); // segment
2740 requests[0].respond(404); 2268 requests[0].respond(404);
2741 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');
2742 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');
...@@ -2745,7 +2273,7 @@ test('retries key requests once upon failure', function() { ...@@ -2745,7 +2273,7 @@ test('retries key requests once upon failure', function() {
2745 equal(requests.length, 2, 'gives up after one retry'); 2273 equal(requests.length, 2, 'gives up after one retry');
2746 }); 2274 });
2747 2275
2748 test('skip segments if key requests fail more than once', function() { 2276 test('errors if key requests fail more than once', function() {
2749 var bytes = []; 2277 var bytes = [];
2750 2278
2751 player.src({ 2279 player.src({
...@@ -2766,23 +2294,14 @@ test('skip segments if key requests fail more than once', function() { ...@@ -2766,23 +2294,14 @@ test('skip segments if key requests fail more than once', function() {
2766 player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) { 2294 player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) {
2767 bytes.push(chunk); 2295 bytes.push(chunk);
2768 }; 2296 };
2769 standardXHRResponse(requests.shift()); // segment 1 2297 standardXHRResponse(requests.pop()); // segment 1
2770 requests.shift().respond(404); // fail key 2298 requests.shift().respond(404); // fail key
2771 requests.shift().respond(404); // fail key, again 2299 requests.shift().respond(404); // fail key, again
2772
2773 player.tech_.hls.checkBuffer_(); 2300 player.tech_.hls.checkBuffer_();
2774 standardXHRResponse(requests.shift()); // segment 2
2775 equal(bytes.length, 0, 'did not append encrypted bytes');
2776 2301
2777 // key for second segment 2302 equal(player.tech_.hls.mediaSource.error_,
2778 requests[0].response = new Uint32Array([0,0,0,0]).buffer; 2303 'network',
2779 requests.shift().respond(200, null, ''); 2304 'triggered a network error');
2780 // "finish" decryption
2781 player.tech_.hls.segmentBuffer_[0].bytes = new Uint8Array(16);
2782 player.tech_.hls.checkBuffer_();
2783
2784 equal(bytes.length, 1, 'appended cleartext bytes from the second segment');
2785 deepEqual(bytes[0], new Uint8Array(16), 'appended bytes from the second segment, not the first');
2786 }); 2305 });
2787 2306
2788 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() {
...@@ -2804,12 +2323,11 @@ test('the key is supplied to the decrypter in the correct format', function() { ...@@ -2804,12 +2323,11 @@ test('the key is supplied to the decrypter in the correct format', function() {
2804 '#EXTINF:15.0,\n' + 2323 '#EXTINF:15.0,\n' +
2805 'http://media.example.com/fileSequence52-B.ts\n'); 2324 'http://media.example.com/fileSequence52-B.ts\n');
2806 2325
2807
2808 videojs.Hls.Decrypter = function(encrypted, key) { 2326 videojs.Hls.Decrypter = function(encrypted, key) {
2809 keys.push(key); 2327 keys.push(key);
2810 }; 2328 };
2811 2329
2812 standardXHRResponse(requests.shift()); // segment 2330 standardXHRResponse(requests.pop()); // segment
2813 requests[0].response = new Uint32Array([0,1,2,3]).buffer; 2331 requests[0].response = new Uint32Array([0,1,2,3]).buffer;
2814 requests[0].respond(200, null, ''); 2332 requests[0].respond(200, null, '');
2815 requests.shift(); // key 2333 requests.shift(); // key
...@@ -2856,6 +2374,7 @@ test('supplies the media sequence of current segment as the IV by default, if no ...@@ -2856,6 +2374,7 @@ test('supplies the media sequence of current segment as the IV by default, if no
2856 }); 2374 });
2857 2375
2858 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 = [];
2859 var media = '#EXTM3U\n' + 2378 var media = '#EXTM3U\n' +
2860 '#EXT-X-MEDIA-SEQUENCE:5\n' + 2379 '#EXT-X-MEDIA-SEQUENCE:5\n' +
2861 '#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' +
...@@ -2870,31 +2389,37 @@ test('switching playlists with an outstanding key request does not stall playbac ...@@ -2870,31 +2389,37 @@ test('switching playlists with an outstanding key request does not stall playbac
2870 openMediaSource(player); 2389 openMediaSource(player);
2871 player.tech_.trigger('play'); 2390 player.tech_.trigger('play');
2872 2391
2392 player.tech_.hls.bandwidth = 1;
2393 player.tech_.buffered = function() {
2394 return videojs.createTimeRange(buffered);
2395 };
2873 // master playlist 2396 // master playlist
2874 standardXHRResponse(requests.shift()); 2397 standardXHRResponse(requests.shift());
2875 // media playlist 2398 // media playlist
2876 requests.shift().respond(200, null, media); 2399 requests.shift().respond(200, null, media);
2877 // mock out media switching from this point on 2400 // mock out media switching from this point on
2878 player.tech_.hls.playlists.media = function() { 2401 player.tech_.hls.playlists.media = function() {
2879 return player.tech_.hls.playlists.master.playlists[0]; 2402 return player.tech_.hls.playlists.master.playlists[1];
2880 }; 2403 };
2881 // first segment of the original media playlist 2404 // first segment of the original media playlist
2882 standardXHRResponse(requests.shift()); 2405 standardXHRResponse(requests.pop());
2883 // don't respond to the initial key request
2884 requests.shift();
2885 2406
2886 // "switch" media 2407 // "switch" media
2887 player.tech_.hls.playlists.trigger('mediachange'); 2408 player.tech_.hls.playlists.trigger('mediachange');
2409 ok(!requests[0].aborted, 'did not abort the key request');
2888 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');
2889 player.tech_.hls.checkBuffer_(); 2417 player.tech_.hls.checkBuffer_();
2890 2418
2891 ok(requests.length, 'made a request'); 2419 equal(requests.length, 1, 'made a request');
2892 equal(requests[0].url, 2420 equal(requests[0].url,
2893 'http://media.example.com/fileSequence52-B.ts', 2421 'http://media.example.com/fileSequence52-B.ts',
2894 'requested the segment'); 2422 'requested the segment');
2895 equal(requests[1].url,
2896 'https://priv.example.com/key.php?r=52',
2897 'requested the key');
2898 }); 2423 });
2899 2424
2900 test('resolves relative key URLs against the playlist', function() { 2425 test('resolves relative key URLs against the playlist', function() {
...@@ -2911,8 +2436,6 @@ test('resolves relative key URLs against the playlist', function() { ...@@ -2911,8 +2436,6 @@ test('resolves relative key URLs against the playlist', function() {
2911 '#EXTINF:2.833,\n' + 2436 '#EXTINF:2.833,\n' +
2912 'http://media.example.com/fileSequence52-A.ts\n' + 2437 'http://media.example.com/fileSequence52-A.ts\n' +
2913 '#EXT-X-ENDLIST\n'); 2438 '#EXT-X-ENDLIST\n');
2914 standardXHRResponse(requests.shift()); // segment
2915
2916 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');
2917 }); 2440 });
2918 2441
...@@ -2937,7 +2460,7 @@ test('treats invalid keys as a key request failure', function() { ...@@ -2937,7 +2460,7 @@ test('treats invalid keys as a key request failure', function() {
2937 bytes.push(chunk); 2460 bytes.push(chunk);
2938 }; 2461 };
2939 // segment request 2462 // segment request
2940 standardXHRResponse(requests.shift()); 2463 standardXHRResponse(requests.pop());
2941 // keys should be 16 bytes long 2464 // keys should be 16 bytes long
2942 requests[0].response = new Uint8Array(1).buffer; 2465 requests[0].response = new Uint8Array(1).buffer;
2943 requests.shift().respond(200, null, ''); 2466 requests.shift().respond(200, null, '');
...@@ -2947,17 +2470,12 @@ test('treats invalid keys as a key request failure', function() { ...@@ -2947,17 +2470,12 @@ test('treats invalid keys as a key request failure', function() {
2947 // the retried response is invalid, too 2470 // the retried response is invalid, too
2948 requests[0].response = new Uint8Array(1); 2471 requests[0].response = new Uint8Array(1);
2949 requests.shift().respond(200, null, ''); 2472 requests.shift().respond(200, null, '');
2950
2951 // the first segment should be dropped and playback moves on
2952 player.tech_.hls.checkBuffer_(); 2473 player.tech_.hls.checkBuffer_();
2953 equal(bytes.length, 0, 'did not append bytes');
2954
2955 // second segment request
2956 requests[0].response = new Uint8Array([1, 2]);
2957 requests.shift().respond(200, null, '');
2958 2474
2959 equal(bytes.length, 1, 'appended bytes'); 2475 // two failed attempts is a network error
2960 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');
2961 }); 2479 });
2962 2480
2963 test('live stream should not call endOfStream', function(){ 2481 test('live stream should not call endOfStream', function(){
......