09fa9fb5 by Jon-Carlos Rivera

Merge pull request #547 from videojs/duration-fixes

Duration and endOfStream fixes
2 parents 264b9516 aac49993
...@@ -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() {
......