Merge pull request #547 from videojs/duration-fixes
Duration and endOfStream fixes
Showing
2 changed files
with
217 additions
and
19 deletions
... | @@ -297,6 +297,72 @@ videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) { | ... | @@ -297,6 +297,72 @@ videojs.Hls.findSoleUncommonTimeRangesEnd_ = function(original, update) { |
297 | return result[0]; | 297 | return result[0]; |
298 | }; | 298 | }; |
299 | 299 | ||
300 | /** | ||
301 | * Updates segment with information about its end-point in time and, optionally, | ||
302 | * the segment duration if we have enough information to determine a segment duration | ||
303 | * accurately. | ||
304 | * @param playlist {object} a media playlist object | ||
305 | * @param segmentIndex {number} the index of segment we last appended | ||
306 | * @param segmentEnd {number} the known of the segment referenced by segmentIndex | ||
307 | */ | ||
308 | videojs.HlsHandler.prototype.updateSegmentMetadata_ = function(playlist, segmentIndex, segmentEnd) { | ||
309 | var | ||
310 | segment, | ||
311 | previousSegment; | ||
312 | |||
313 | if (!playlist) { | ||
314 | return; | ||
315 | } | ||
316 | |||
317 | segment = playlist.segments[segmentIndex]; | ||
318 | previousSegment = playlist.segments[segmentIndex - 1]; | ||
319 | |||
320 | if (segmentEnd && segment) { | ||
321 | segment.end = segmentEnd; | ||
322 | |||
323 | // fix up segment durations based on segment end data | ||
324 | if (!previousSegment) { | ||
325 | // first segment is always has a start time of 0 making its duration | ||
326 | // equal to the segment end | ||
327 | segment.duration = segment.end; | ||
328 | } else if (previousSegment.end) { | ||
329 | segment.duration = segment.end - previousSegment.end; | ||
330 | } | ||
331 | } | ||
332 | }; | ||
333 | |||
334 | /** | ||
335 | * Determines if we should call endOfStream on the media source based on the state | ||
336 | * of the buffer or if appened segment was the final segment in the playlist. | ||
337 | * @param playlist {object} a media playlist object | ||
338 | * @param segmentIndex {number} the index of segment we last appended | ||
339 | * @param currentBuffered {object} the buffered region that currentTime resides in | ||
340 | * @return {boolean} whether the calling function should call endOfStream on the MediaSource | ||
341 | */ | ||
342 | videojs.HlsHandler.prototype.isEndOfStream_ = function(playlist, segmentIndex, currentBuffered) { | ||
343 | var | ||
344 | segments = playlist.segments, | ||
345 | appendedLastSegment, | ||
346 | bufferedToEnd; | ||
347 | |||
348 | if (!playlist) { | ||
349 | return false; | ||
350 | } | ||
351 | |||
352 | // determine a few boolean values to help make the branch below easier | ||
353 | // to read | ||
354 | appendedLastSegment = (segmentIndex === segments.length - 1); | ||
355 | bufferedToEnd = (currentBuffered.length && | ||
356 | segments[segments.length - 1].end <= currentBuffered.end(0)); | ||
357 | |||
358 | // if we've buffered to the end of the video, we need to call endOfStream | ||
359 | // so that MediaSources can trigger the `ended` event when it runs out of | ||
360 | // buffered data instead of waiting for me | ||
361 | return playlist.endList && | ||
362 | this.mediaSource.readyState === 'open' && | ||
363 | (appendedLastSegment || bufferedToEnd); | ||
364 | }; | ||
365 | |||
300 | var parseCodecs = function(codecs) { | 366 | var parseCodecs = function(codecs) { |
301 | var result = { | 367 | var result = { |
302 | codecCount: 0, | 368 | codecCount: 0, |
... | @@ -506,11 +572,18 @@ videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) { | ... | @@ -506,11 +572,18 @@ videojs.HlsHandler.prototype.setCurrentTime = function(currentTime) { |
506 | }; | 572 | }; |
507 | 573 | ||
508 | videojs.HlsHandler.prototype.duration = function() { | 574 | videojs.HlsHandler.prototype.duration = function() { |
509 | var playlists = this.playlists; | 575 | var |
510 | if (playlists) { | 576 | playlists = this.playlists; |
511 | return videojs.Hls.Playlist.duration(playlists.media()); | 577 | |
578 | if (!playlists) { | ||
579 | return 0; | ||
580 | } | ||
581 | |||
582 | if (this.mediaSource) { | ||
583 | return this.mediaSource.duration; | ||
512 | } | 584 | } |
513 | return 0; | 585 | |
586 | return videojs.Hls.Playlist.duration(playlists.media()); | ||
514 | }; | 587 | }; |
515 | 588 | ||
516 | videojs.HlsHandler.prototype.seekable = function() { | 589 | videojs.HlsHandler.prototype.seekable = function() { |
... | @@ -551,6 +624,7 @@ videojs.HlsHandler.prototype.seekable = function() { | ... | @@ -551,6 +624,7 @@ videojs.HlsHandler.prototype.seekable = function() { |
551 | videojs.HlsHandler.prototype.updateDuration = function(playlist) { | 624 | videojs.HlsHandler.prototype.updateDuration = function(playlist) { |
552 | var oldDuration = this.mediaSource.duration, | 625 | var oldDuration = this.mediaSource.duration, |
553 | newDuration = videojs.Hls.Playlist.duration(playlist), | 626 | newDuration = videojs.Hls.Playlist.duration(playlist), |
627 | buffered = this.tech_.buffered(), | ||
554 | setDuration = function() { | 628 | setDuration = function() { |
555 | this.mediaSource.duration = newDuration; | 629 | this.mediaSource.duration = newDuration; |
556 | this.tech_.trigger('durationchange'); | 630 | this.tech_.trigger('durationchange'); |
... | @@ -558,6 +632,10 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) { | ... | @@ -558,6 +632,10 @@ videojs.HlsHandler.prototype.updateDuration = function(playlist) { |
558 | this.mediaSource.removeEventListener('sourceopen', setDuration); | 632 | this.mediaSource.removeEventListener('sourceopen', setDuration); |
559 | }.bind(this); | 633 | }.bind(this); |
560 | 634 | ||
635 | if (buffered.length > 0) { | ||
636 | newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); | ||
637 | } | ||
638 | |||
561 | // if the duration has changed, invalidate the cached value | 639 | // if the duration has changed, invalidate the cached value |
562 | if (oldDuration !== newDuration) { | 640 | if (oldDuration !== newDuration) { |
563 | // update the duration | 641 | // update the duration |
... | @@ -1227,7 +1305,8 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { | ... | @@ -1227,7 +1305,8 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { |
1227 | currentMediaIndex, | 1305 | currentMediaIndex, |
1228 | currentBuffered, | 1306 | currentBuffered, |
1229 | seekable, | 1307 | seekable, |
1230 | timelineUpdate; | 1308 | timelineUpdate, |
1309 | isEndOfStream; | ||
1231 | 1310 | ||
1232 | // stop here if the update errored or was aborted | 1311 | // stop here if the update errored or was aborted |
1233 | if (!segmentInfo) { | 1312 | if (!segmentInfo) { |
... | @@ -1243,14 +1322,18 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { | ... | @@ -1243,14 +1322,18 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { |
1243 | 1322 | ||
1244 | this.pendingSegment_ = null; | 1323 | this.pendingSegment_ = null; |
1245 | 1324 | ||
1246 | playlist = this.playlists.media(); | 1325 | playlist = segmentInfo.playlist; |
1247 | segments = playlist.segments; | 1326 | segments = playlist.segments; |
1248 | currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence); | 1327 | currentMediaIndex = segmentInfo.mediaIndex + (segmentInfo.mediaSequence - playlist.mediaSequence); |
1249 | currentBuffered = this.findBufferedRange_(); | 1328 | currentBuffered = this.findBufferedRange_(); |
1329 | isEndOfStream = this.isEndOfStream_(playlist, currentMediaIndex, currentBuffered); | ||
1250 | 1330 | ||
1251 | // if we switched renditions don't try to add segment timeline | 1331 | // if we switched renditions don't try to add segment timeline |
1252 | // information to the playlist | 1332 | // information to the playlist |
1253 | if (segmentInfo.playlist.uri !== this.playlists.media().uri) { | 1333 | if (segmentInfo.playlist.uri !== this.playlists.media().uri) { |
1334 | if (isEndOfStream) { | ||
1335 | return this.mediaSource.endOfStream(); | ||
1336 | } | ||
1254 | return this.fillBuffer(); | 1337 | return this.fillBuffer(); |
1255 | } | 1338 | } |
1256 | 1339 | ||
... | @@ -1275,21 +1358,16 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { | ... | @@ -1275,21 +1358,16 @@ videojs.HlsHandler.prototype.updateEndHandler_ = function () { |
1275 | } | 1358 | } |
1276 | } | 1359 | } |
1277 | 1360 | ||
1278 | |||
1279 | timelineUpdate = videojs.Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, | 1361 | timelineUpdate = videojs.Hls.findSoleUncommonTimeRangesEnd_(segmentInfo.buffered, |
1280 | this.tech_.buffered()); | 1362 | this.tech_.buffered()); |
1281 | 1363 | ||
1282 | if (timelineUpdate && segment) { | 1364 | // Update segment meta-data (duration and end-point) based on timeline |
1283 | segment.end = timelineUpdate; | 1365 | this.updateSegmentMetadata_(playlist, currentMediaIndex, timelineUpdate); |
1284 | } | ||
1285 | 1366 | ||
1286 | // if we've buffered to the end of the video, let the MediaSource know | 1367 | // If we decide to signal the end of stream, then we can return instead |
1287 | if (this.playlists.media().endList && | 1368 | // of trying to fetch more segments |
1288 | currentBuffered.length && | 1369 | if (isEndOfStream) { |
1289 | segments[segments.length - 1].end <= currentBuffered.end(0) && | 1370 | return this.mediaSource.endOfStream(); |
1290 | this.mediaSource.readyState === 'open') { | ||
1291 | this.mediaSource.endOfStream(); | ||
1292 | return; | ||
1293 | } | 1371 | } |
1294 | 1372 | ||
1295 | if (timelineUpdate !== null || | 1373 | if (timelineUpdate !== null || | ... | ... |
... | @@ -2144,6 +2144,79 @@ test('tracks segment end times as they are buffered', function() { | ... | @@ -2144,6 +2144,79 @@ test('tracks segment end times as they are buffered', function() { |
2144 | equal(player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration'); | 2144 | equal(player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration'); |
2145 | }); | 2145 | }); |
2146 | 2146 | ||
2147 | test('updates first segment duration as it is buffered', function() { | ||
2148 | var bufferEnd = 0; | ||
2149 | player.src({ | ||
2150 | src: 'media.m3u8', | ||
2151 | type: 'application/x-mpegURL' | ||
2152 | }); | ||
2153 | openMediaSource(player); | ||
2154 | |||
2155 | // as new segments are downloaded, the buffer end is updated | ||
2156 | player.tech_.buffered = function() { | ||
2157 | return videojs.createTimeRange(0, bufferEnd); | ||
2158 | }; | ||
2159 | requests.shift().respond(200, null, | ||
2160 | '#EXTM3U\n' + | ||
2161 | '#EXTINF:10,\n' + | ||
2162 | '0.ts\n' + | ||
2163 | '#EXTINF:10,\n' + | ||
2164 | '1.ts\n' + | ||
2165 | '#EXT-X-ENDLIST\n'); | ||
2166 | |||
2167 | // 0.ts is shorter than advertised | ||
2168 | standardXHRResponse(requests.shift()); | ||
2169 | equal(player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8'); | ||
2170 | equal(player.tech_.hls.playlists.media().segments[0].duration, 10, | ||
2171 | 'segment duration initially based on playlist'); | ||
2172 | |||
2173 | bufferEnd = 9.5; | ||
2174 | player.tech_.hls.sourceBuffer.trigger('update'); | ||
2175 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2176 | equal(player.tech_.hls.playlists.media().segments[0].duration, 9.5, | ||
2177 | 'updated segment duration'); | ||
2178 | }); | ||
2179 | |||
2180 | test('updates segment durations as they are buffered', function() { | ||
2181 | var bufferEnd = 0; | ||
2182 | player.src({ | ||
2183 | src: 'media.m3u8', | ||
2184 | type: 'application/x-mpegURL' | ||
2185 | }); | ||
2186 | openMediaSource(player); | ||
2187 | |||
2188 | // as new segments are downloaded, the buffer end is updated | ||
2189 | player.tech_.buffered = function() { | ||
2190 | return videojs.createTimeRange(0, bufferEnd); | ||
2191 | }; | ||
2192 | requests.shift().respond(200, null, | ||
2193 | '#EXTM3U\n' + | ||
2194 | '#EXTINF:10,\n' + | ||
2195 | '0.ts\n' + | ||
2196 | '#EXTINF:10,\n' + | ||
2197 | '1.ts\n' + | ||
2198 | '#EXT-X-ENDLIST\n'); | ||
2199 | |||
2200 | // 0.ts is shorter than advertised | ||
2201 | standardXHRResponse(requests.shift()); | ||
2202 | equal(player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8'); | ||
2203 | |||
2204 | equal(player.tech_.hls.playlists.media().segments[1].duration, 10, | ||
2205 | 'segment duration initially based on playlist'); | ||
2206 | |||
2207 | bufferEnd = 9.5; | ||
2208 | player.tech_.hls.sourceBuffer.trigger('update'); | ||
2209 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2210 | clock.tick(1); | ||
2211 | standardXHRResponse(requests.shift()); | ||
2212 | bufferEnd = 19; | ||
2213 | player.tech_.hls.sourceBuffer.trigger('update'); | ||
2214 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2215 | |||
2216 | equal(player.tech_.hls.playlists.media().segments[1].duration, 9.5, | ||
2217 | 'updated segment duration'); | ||
2218 | }); | ||
2219 | |||
2147 | QUnit.skip('seeking does not fail when targeted between segments', function() { | 2220 | QUnit.skip('seeking does not fail when targeted between segments', function() { |
2148 | var currentTime, segmentUrl; | 2221 | var currentTime, segmentUrl; |
2149 | player.src({ | 2222 | player.src({ |
... | @@ -2428,7 +2501,7 @@ test('can be disposed before finishing initialization', function() { | ... | @@ -2428,7 +2501,7 @@ test('can be disposed before finishing initialization', function() { |
2428 | } | 2501 | } |
2429 | }); | 2502 | }); |
2430 | 2503 | ||
2431 | test('calls ended() on the media source at the end of a playlist', function() { | 2504 | test('calls endOfStream on the media source after appending the last segment', function() { |
2432 | var endOfStreams = 0, buffered = [[]]; | 2505 | var endOfStreams = 0, buffered = [[]]; |
2433 | player.src({ | 2506 | player.src({ |
2434 | src: 'http://example.com/media.m3u8', | 2507 | src: 'http://example.com/media.m3u8', |
... | @@ -2441,11 +2514,15 @@ test('calls ended() on the media source at the end of a playlist', function() { | ... | @@ -2441,11 +2514,15 @@ test('calls ended() on the media source at the end of a playlist', function() { |
2441 | player.tech_.hls.mediaSource.endOfStream = function() { | 2514 | player.tech_.hls.mediaSource.endOfStream = function() { |
2442 | endOfStreams++; | 2515 | endOfStreams++; |
2443 | }; | 2516 | }; |
2517 | player.currentTime(20); | ||
2518 | clock.tick(1); | ||
2444 | // playlist response | 2519 | // playlist response |
2445 | requests.shift().respond(200, null, | 2520 | requests.shift().respond(200, null, |
2446 | '#EXTM3U\n' + | 2521 | '#EXTM3U\n' + |
2447 | '#EXTINF:10,\n' + | 2522 | '#EXTINF:10,\n' + |
2448 | '0.ts\n' + | 2523 | '0.ts\n' + |
2524 | '#EXTINF:10,\n' + | ||
2525 | '1.ts\n' + | ||
2449 | '#EXT-X-ENDLIST\n'); | 2526 | '#EXT-X-ENDLIST\n'); |
2450 | // segment response | 2527 | // segment response |
2451 | requests[0].response = new ArrayBuffer(17); | 2528 | requests[0].response = new ArrayBuffer(17); |
... | @@ -2454,7 +2531,50 @@ test('calls ended() on the media source at the end of a playlist', function() { | ... | @@ -2454,7 +2531,50 @@ test('calls ended() on the media source at the end of a playlist', function() { |
2454 | 2531 | ||
2455 | buffered =[[0, 10]]; | 2532 | buffered =[[0, 10]]; |
2456 | player.tech_.hls.sourceBuffer.trigger('updateend'); | 2533 | player.tech_.hls.sourceBuffer.trigger('updateend'); |
2457 | strictEqual(endOfStreams, 1, 'ended media source'); | 2534 | strictEqual(endOfStreams, 1, 'called endOfStream on the media source'); |
2535 | }); | ||
2536 | |||
2537 | test('calls endOfStream on the media source when the current buffer ends at duration', function() { | ||
2538 | var endOfStreams = 0, buffered = [[]]; | ||
2539 | player.src({ | ||
2540 | src: 'http://example.com/media.m3u8', | ||
2541 | type: 'application/vnd.apple.mpegurl' | ||
2542 | }); | ||
2543 | openMediaSource(player); | ||
2544 | player.tech_.buffered = function() { | ||
2545 | return videojs.createTimeRanges(buffered); | ||
2546 | }; | ||
2547 | player.tech_.hls.mediaSource.endOfStream = function() { | ||
2548 | endOfStreams++; | ||
2549 | }; | ||
2550 | player.currentTime(19); | ||
2551 | clock.tick(1); | ||
2552 | // playlist response | ||
2553 | requests.shift().respond(200, null, | ||
2554 | '#EXTM3U\n' + | ||
2555 | '#EXTINF:10,\n' + | ||
2556 | '0.ts\n' + | ||
2557 | '#EXTINF:10,\n' + | ||
2558 | '1.ts\n' + | ||
2559 | '#EXT-X-ENDLIST\n'); | ||
2560 | // segment response | ||
2561 | requests[0].response = new ArrayBuffer(17); | ||
2562 | requests.shift().respond(200, null, ''); | ||
2563 | strictEqual(endOfStreams, 0, 'waits for the buffer update to finish'); | ||
2564 | |||
2565 | buffered =[[10, 20]]; | ||
2566 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2567 | |||
2568 | player.currentTime(5); | ||
2569 | clock.tick(1); | ||
2570 | // segment response | ||
2571 | requests[0].response = new ArrayBuffer(17); | ||
2572 | requests.shift().respond(200, null, ''); | ||
2573 | |||
2574 | buffered =[[0, 20]]; | ||
2575 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2576 | |||
2577 | strictEqual(endOfStreams, 2, 'called endOfStream on the media source twice'); | ||
2458 | }); | 2578 | }); |
2459 | 2579 | ||
2460 | test('calling play() at the end of a video replays', function() { | 2580 | test('calling play() at the end of a video replays', function() { | ... | ... |
-
Please register or sign in to post a comment