2633a46d by David LaPalomento

Ensure overlapping buffered edges are not interpreted as updates

If the old and new buffered ranges have a shared start or end point, that edge should not be interpreted as a new buffered boundary. Fix up a number of the tests. Some tests are still failing.
1 parent 5ee4363a
...@@ -187,20 +187,20 @@ ...@@ -187,20 +187,20 @@
187 * active media playlist. When called with a single argument, 187 * active media playlist. When called with a single argument,
188 * triggers the playlist loader to asynchronously switch to the 188 * triggers the playlist loader to asynchronously switch to the
189 * specified media playlist. Calling this method while the 189 * specified media playlist. Calling this method while the
190 * loader is in the HAVE_NOTHING or HAVE_MASTER states causes an 190 * loader is in the HAVE_NOTHING causes an error to be emitted
191 * error to be emitted but otherwise has no effect. 191 * but otherwise has no effect.
192 * @param playlist (optional) {object} the parsed media playlist 192 * @param playlist (optional) {object} the parsed media playlist
193 * object to switch to 193 * object to switch to
194 */ 194 */
195 loader.media = function(playlist) { 195 loader.media = function(playlist) {
196 var mediaChange = false; 196 var startingState = loader.state, mediaChange;
197 // getter 197 // getter
198 if (!playlist) { 198 if (!playlist) {
199 return loader.media_; 199 return loader.media_;
200 } 200 }
201 201
202 // setter 202 // setter
203 if (loader.state === 'HAVE_NOTHING' || loader.state === 'HAVE_MASTER') { 203 if (loader.state === 'HAVE_NOTHING') {
204 throw new Error('Cannot switch media playlist from ' + loader.state); 204 throw new Error('Cannot switch media playlist from ' + loader.state);
205 } 205 }
206 206
...@@ -213,7 +213,7 @@ ...@@ -213,7 +213,7 @@
213 playlist = loader.master.playlists[playlist]; 213 playlist = loader.master.playlists[playlist];
214 } 214 }
215 215
216 mediaChange = playlist.uri !== loader.media_.uri; 216 mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri;
217 217
218 // switch to fully loaded playlists immediately 218 // switch to fully loaded playlists immediately
219 if (loader.master.playlists[playlist.uri].endList) { 219 if (loader.master.playlists[playlist.uri].endList) {
...@@ -258,7 +258,17 @@ ...@@ -258,7 +258,17 @@
258 withCredentials: withCredentials 258 withCredentials: withCredentials
259 }, function(error, request) { 259 }, function(error, request) {
260 haveMetadata(error, request, playlist.uri); 260 haveMetadata(error, request, playlist.uri);
261 loader.trigger('mediachange'); 261
262 if (error) {
263 return;
264 }
265
266 // fire loadedmetadata the first time a media playlist is loaded
267 if (startingState === 'HAVE_MASTER') {
268 loader.trigger('loadedmetadata');
269 } else {
270 loader.trigger('mediachange');
271 }
262 }); 272 });
263 }; 273 };
264 274
...@@ -320,19 +330,13 @@ ...@@ -320,19 +330,13 @@
320 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; 330 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i];
321 } 331 }
322 332
323 request = xhr({ 333 loader.trigger('loadedplaylist');
324 uri: resolveUrl(srcUrl, parser.manifest.playlists[0].uri), 334 if (!request) {
325 withCredentials: withCredentials 335 // no media playlist was specifically selected so start
326 }, function(error, request) { 336 // from the first listed one
327 // pass along the URL specified in the master playlist 337 loader.media(parser.manifest.playlists[0]);
328 haveMetadata(error, 338 }
329 request, 339 return;
330 parser.manifest.playlists[0].uri);
331 if (!error) {
332 loader.trigger('loadedmetadata');
333 }
334 });
335 return loader.trigger('loadedplaylist');
336 } 340 }
337 341
338 // loaded a media playlist 342 // loaded a media playlist
...@@ -468,8 +472,8 @@ ...@@ -468,8 +472,8 @@
468 } 472 }
469 473
470 // the playback position is outside the range of available 474 // the playback position is outside the range of available
471 // segments so return the last one 475 // segments so return the length
472 return this.media_.segments.length - 1; 476 return this.media_.segments.length;
473 }; 477 };
474 478
475 videojs.Hls.PlaylistLoader = PlaylistLoader; 479 videojs.Hls.PlaylistLoader = PlaylistLoader;
......
...@@ -190,7 +190,8 @@ videojs.Hls.prototype.src = function(src) { ...@@ -190,7 +190,8 @@ videojs.Hls.prototype.src = function(src) {
190 var updatedPlaylist = this.playlists.media(); 190 var updatedPlaylist = this.playlists.media();
191 191
192 if (!updatedPlaylist) { 192 if (!updatedPlaylist) {
193 // do nothing before an initial media playlist has been activated 193 // select the initial variant
194 this.playlists.media(this.selectPlaylist());
194 return; 195 return;
195 } 196 }
196 197
...@@ -254,7 +255,7 @@ videojs.Hls.prototype.handleSourceOpen = function() { ...@@ -254,7 +255,7 @@ videojs.Hls.prototype.handleSourceOpen = function() {
254 255
255 // Returns the array of time range edge objects that were additively 256 // Returns the array of time range edge objects that were additively
256 // modified between two TimeRanges. 257 // modified between two TimeRanges.
257 var bufferedAdditions = function(original, update) { 258 videojs.Hls.bufferedAdditions_ = function(original, update) {
258 var result = [], edges = [], 259 var result = [], edges = [],
259 i, inOriginalRanges; 260 i, inOriginalRanges;
260 261
...@@ -271,6 +272,15 @@ var bufferedAdditions = function(original, update) { ...@@ -271,6 +272,15 @@ var bufferedAdditions = function(original, update) {
271 var leftTime, rightTime; 272 var leftTime, rightTime;
272 leftTime = left.start !== undefined ? left.start : left.end; 273 leftTime = left.start !== undefined ? left.start : left.end;
273 rightTime = right.start !== undefined ? right.start : right.end; 274 rightTime = right.start !== undefined ? right.start : right.end;
275
276 // when two times are equal, ensure the original edge covers the
277 // update
278 if (leftTime === rightTime) {
279 if (left.original) {
280 return left.start !== undefined ? -1 : 1;
281 }
282 return right.start !== undefined ? -1 : 1;
283 }
274 return leftTime - rightTime; 284 return leftTime - rightTime;
275 }); 285 });
276 286
...@@ -349,8 +359,8 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { ...@@ -349,8 +359,8 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() {
349 // annotate the segment with any start and end time information 359 // annotate the segment with any start and end time information
350 // added by the media processing 360 // added by the media processing
351 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; 361 segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
352 timelineUpdates = bufferedAdditions(segmentInfo.buffered, 362 timelineUpdates = videojs.Hls.bufferedAdditions_(segmentInfo.buffered,
353 this.tech_.buffered()); 363 this.tech_.buffered());
354 timelineUpdates.forEach(function(update) { 364 timelineUpdates.forEach(function(update) {
355 if (update.start !== undefined) { 365 if (update.start !== undefined) {
356 segment.start = update.start; 366 segment.start = update.start;
...@@ -1112,15 +1122,16 @@ videojs.Hls.prototype.drainBuffer = function(event) { ...@@ -1112,15 +1122,16 @@ videojs.Hls.prototype.drainBuffer = function(event) {
1112 this.sourceBuffer.timestampOffset = currentBuffered.end(0); 1122 this.sourceBuffer.timestampOffset = currentBuffered.end(0);
1113 } 1123 }
1114 1124
1115 // the segment is asynchronously added to the current buffered data
1116 if (currentBuffered.length) { 1125 if (currentBuffered.length) {
1117 this.sourceBuffer.videoBuffer_.appendWindowStart = Math.min(this.tech_.currentTime(), currentBuffered.end(0)); 1126 // Chrome 45 stalls if appends overlap the playhead
1118 } else if (this.sourceBuffer.videoBuffer_) { 1127 this.sourceBuffer.appendWindowStart = Math.min(this.tech_.currentTime(), currentBuffered.end(0));
1119 this.sourceBuffer.videoBuffer_.appendWindowStart = 0; 1128 } else {
1129 this.sourceBuffer.appendWindowStart = 0;
1120 } 1130 }
1121 this.pendingSegment_ = segmentBuffer.shift(); 1131 this.pendingSegment_ = segmentBuffer.shift();
1122 this.pendingSegment_.buffered = this.tech_.buffered(); 1132 this.pendingSegment_.buffered = this.tech_.buffered();
1123 1133
1134 // the segment is asynchronously added to the current buffered data
1124 this.sourceBuffer.appendBuffer(bytes); 1135 this.sourceBuffer.appendBuffer(bytes);
1125 }; 1136 };
1126 1137
......
...@@ -69,13 +69,16 @@ ...@@ -69,13 +69,16 @@
69 }); 69 });
70 70
71 test('moves to HAVE_MASTER after loading a master playlist', function() { 71 test('moves to HAVE_MASTER after loading a master playlist', function() {
72 var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); 72 var loader = new videojs.Hls.PlaylistLoader('master.m3u8'), state;
73 loader.on('loadedplaylist', function() {
74 state = loader.state;
75 });
73 requests.pop().respond(200, null, 76 requests.pop().respond(200, null,
74 '#EXTM3U\n' + 77 '#EXTM3U\n' +
75 '#EXT-X-STREAM-INF:\n' + 78 '#EXT-X-STREAM-INF:\n' +
76 'media.m3u8\n'); 79 'media.m3u8\n');
77 ok(loader.master, 'the master playlist is available'); 80 ok(loader.master, 'the master playlist is available');
78 strictEqual(loader.state, 'HAVE_MASTER', 'the state is correct'); 81 strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct');
79 }); 82 });
80 83
81 test('jumps to HAVE_METADATA when initialized with a media playlist', function() { 84 test('jumps to HAVE_METADATA when initialized with a media playlist', function() {
...@@ -453,6 +456,20 @@ ...@@ -453,6 +456,20 @@
453 'updated the active media'); 456 'updated the active media');
454 }); 457 });
455 458
459 test('can switch playlists immediately after the master is downloaded', function() {
460 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
461 loader.on('loadedplaylist', function() {
462 loader.media('high.m3u8');
463 });
464 requests.pop().respond(200, null,
465 '#EXTM3U\n' +
466 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
467 'low.m3u8\n' +
468 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
469 'high.m3u8\n');
470 equal(requests[0].url, urlTo('high.m3u8'), 'switched variants immediately');
471 });
472
456 test('can switch media playlists based on URI', function() { 473 test('can switch media playlists based on URI', function() {
457 var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); 474 var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
458 requests.pop().respond(200, null, 475 requests.pop().respond(200, null,
...@@ -624,9 +641,6 @@ ...@@ -624,9 +641,6 @@
624 'low.m3u8\n' + 641 'low.m3u8\n' +
625 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + 642 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
626 'high.m3u8\n'); 643 'high.m3u8\n');
627 throws(function() {
628 loader.media('high.m3u8');
629 }, 'throws an error from HAVE_MASTER');
630 }); 644 });
631 645
632 test('throws an error if a switch to an unrecognized playlist is requested', function() { 646 test('throws an error if a switch to an unrecognized playlist is requested', function() {
...@@ -757,10 +771,8 @@ ...@@ -757,10 +771,8 @@
757 '#EXTINF:5,\n' + 771 '#EXTINF:5,\n' +
758 '1.ts\n' + 772 '1.ts\n' +
759 '#EXT-X-ENDLIST\n'); 773 '#EXT-X-ENDLIST\n');
760 equal(loader.getMediaIndexForTime_(4), 0, 'rounds down exact matches'); 774 equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches');
761 equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); 775 equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down');
762 // FIXME: the test below should pass for HLSv3
763 //equal(loader.getMediaIndexForTime_(4.2), 0, 'rounds down');
764 equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); 776 equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5');
765 }); 777 });
766 778
......