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.
Showing
4 changed files
with
64 additions
and
37 deletions
... | @@ -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 | ... | ... |
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment