736b7c35 by David LaPalomento

Compare the old playlist to the update to determine the new media index

When refreshing a playlist, determine the new media index by comparing segment URIs. Consolidate playlist update logic. "Sliding window" live streams are now working.
1 parent 5448f63f
...@@ -35,8 +35,7 @@ ...@@ -35,8 +35,7 @@
35 Stream = videojs.hls.Stream, 35 Stream = videojs.hls.Stream,
36 LineStream, 36 LineStream,
37 ParseStream, 37 ParseStream,
38 Parser, 38 Parser;
39 merge;
40 39
41 /** 40 /**
42 * A stream that buffers string input and generates a `data` event for each 41 * A stream that buffers string input and generates a `data` event for each
...@@ -456,52 +455,9 @@ ...@@ -456,52 +455,9 @@
456 this.lineStream.push('\n'); 455 this.lineStream.push('\n');
457 }; 456 };
458 457
459 /**
460 * Merges two versions of a media playlist.
461 * @param base {object} the earlier version of the media playlist.
462 * @param update {object} the updates to apply to the base playlist.
463 * @return {object} a new media playlist object that combines the
464 * information in the two arguments.
465 */
466 merge = function(base, update) {
467 var
468 result = mergeOptions({}, base),
469 uri = update.segments[0].uri,
470 i = base.segments ? base.segments.length : 0,
471 byterange,
472 segment;
473
474 result = mergeOptions(result, update);
475
476 // align and apply the updated segments
477 while (i--) {
478 segment = base.segments[i];
479 if (uri === segment.uri) {
480 // if there is no byterange information, match by URI
481 if (!segment.byterange) {
482 result.segments = base.segments.slice(0, i).concat(update.segments);
483 break;
484 }
485 // if a byterange is specified, make sure the segments match exactly
486 byterange = update.segments[0].byterange || {};
487 if (segment.byterange.offset === byterange.offset &&
488 segment.byterange.length === byterange.length) {
489 result.segments = base.segments.slice(0, i).concat(update.segments);
490 break;
491 }
492 }
493 }
494 // concatenate the two arrays if there was no overlap
495 if (i < 0) {
496 result.segments = (base.segments || []).concat(update.segments);
497 }
498 return result;
499 };
500
501 window.videojs.m3u8 = { 458 window.videojs.m3u8 = {
502 LineStream: LineStream, 459 LineStream: LineStream,
503 ParseStream: ParseStream, 460 ParseStream: ParseStream,
504 Parser: Parser, 461 Parser: Parser
505 merge: merge
506 }; 462 };
507 })(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions); 463 })(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions);
......
...@@ -132,6 +132,12 @@ var ...@@ -132,6 +132,12 @@ var
132 duration = 0, 132 duration = 0,
133 i = playlist.segments.length, 133 i = playlist.segments.length,
134 segment; 134 segment;
135
136 // if present, use the duration specified in the playlist
137 if (playlist.totalDuration) {
138 return playlist.totalDuration;
139 }
140
135 // duration should be Infinity for live playlists 141 // duration should be Infinity for live playlists
136 if (!playlist.endList) { 142 if (!playlist.endList) {
137 return window.Infinity; 143 return window.Infinity;
...@@ -205,7 +211,8 @@ var ...@@ -205,7 +211,8 @@ var
205 211
206 segmentXhr, 212 segmentXhr,
207 downloadPlaylist, 213 downloadPlaylist,
208 fillBuffer; 214 fillBuffer,
215 updateCurrentPlaylist;
209 216
210 // if the video element supports HLS natively, do nothing 217 // if the video element supports HLS natively, do nothing
211 if (videojs.hls.supportsNativeHls) { 218 if (videojs.hls.supportsNativeHls) {
...@@ -293,6 +300,28 @@ var ...@@ -293,6 +300,28 @@ var
293 fillBuffer(currentTime * 1000); 300 fillBuffer(currentTime * 1000);
294 }); 301 });
295 302
303 /**
304 * Determine whether the current media playlist should be changed
305 * and trigger a switch if necessary. If a sufficiently fresh
306 * version of the target playlist is available, the switch will take
307 * effect immediately. Otherwise, the target playlist will be
308 * refreshed.
309 */
310 updateCurrentPlaylist = function() {
311 var playlist, mediaSequence;
312 playlist = player.hls.selectPlaylist();
313 mediaSequence = player.hls.mediaIndex + (player.hls.media.mediaSequence || 0);
314 if (!playlist.segments ||
315 mediaSequence < (playlist.mediaSequence || 0) ||
316 mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) {
317 downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
318 } else {
319 player.hls.media = playlist;
320
321 // update the duration
322 player.duration(totalDuration(player.hls.media));
323 }
324 };
296 325
297 /** 326 /**
298 * Chooses the appropriate media playlist based on the current 327 * Chooses the appropriate media playlist based on the current
...@@ -382,7 +411,27 @@ var ...@@ -382,7 +411,27 @@ var
382 var xhr = new window.XMLHttpRequest(); 411 var xhr = new window.XMLHttpRequest();
383 xhr.open('GET', url); 412 xhr.open('GET', url);
384 xhr.onreadystatechange = function() { 413 xhr.onreadystatechange = function() {
385 var i, parser, playlist, playlistUri, refreshDelay; 414 var i, parser, playlist, playlistUri, refreshDelay,
415 updateMediaIndex = function(original, update) {
416 var
417 i = update.segments.length,
418 updatedIndex = 0,
419 originalSegment;
420
421 // no segments have been loaded from the original playlist
422 if (player.hls.mediaIndex === 0) {
423 return;
424 }
425
426 originalSegment = original.segments[player.hls.mediaIndex - 1];
427 while (i--) {
428 if (originalSegment.uri === update.segments[i].uri) {
429 updatedIndex = i + 1;
430 break;
431 }
432 }
433 player.hls.mediaIndex = updatedIndex;
434 };
386 435
387 // wait until the request completes 436 // wait until the request completes
388 if (xhr.readyState !== 4) { 437 if (xhr.readyState !== 4) {
...@@ -430,7 +479,15 @@ var ...@@ -430,7 +479,15 @@ var
430 } 479 }
431 480
432 player.hls.master.playlists[i] = 481 player.hls.master.playlists[i] =
433 videojs.m3u8.merge(playlist, parser.manifest); 482 videojs.util.mergeOptions(playlist, parser.manifest);
483
484 if (playlist !== player.hls.media) {
485 continue;
486 }
487
488 // determine the new mediaIndex if we're updating the
489 // current media playlist
490 updateMediaIndex(playlist, parser.manifest);
434 } 491 }
435 } 492 }
436 } else { 493 } else {
...@@ -453,11 +510,7 @@ var ...@@ -453,11 +510,7 @@ var
453 player.hls.media = player.hls.master.playlists[0]; 510 player.hls.media = player.hls.master.playlists[0];
454 511
455 // update the duration 512 // update the duration
456 if (parser.manifest.totalDuration) {
457 player.duration(parser.manifest.totalDuration);
458 } else {
459 player.duration(totalDuration(parser.manifest)); 513 player.duration(totalDuration(parser.manifest));
460 }
461 514
462 // periodicaly check if the buffer needs to be refilled 515 // periodicaly check if the buffer needs to be refilled
463 player.on('timeupdate', fillBuffer); 516 player.on('timeupdate', fillBuffer);
...@@ -469,19 +522,7 @@ var ...@@ -469,19 +522,7 @@ var
469 } 522 }
470 523
471 // select a playlist and download its metadata if necessary 524 // select a playlist and download its metadata if necessary
472 playlist = player.hls.selectPlaylist(); 525 updateCurrentPlaylist();
473 if (!playlist.segments) {
474 downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
475 } else {
476 player.hls.media = playlist;
477
478 // update the duration
479 if (player.hls.media.totalDuration) {
480 player.duration(player.hls.media.totalDuration);
481 } else {
482 player.duration(totalDuration(player.hls.media));
483 }
484 }
485 526
486 player.trigger('loadedmanifest'); 527 player.trigger('loadedmanifest');
487 }; 528 };
...@@ -535,8 +576,6 @@ var ...@@ -535,8 +576,6 @@ var
535 segmentXhr.open('GET', segmentUri); 576 segmentXhr.open('GET', segmentUri);
536 segmentXhr.responseType = 'arraybuffer'; 577 segmentXhr.responseType = 'arraybuffer';
537 segmentXhr.onreadystatechange = function() { 578 segmentXhr.onreadystatechange = function() {
538 var playlist;
539
540 // wait until the request completes 579 // wait until the request completes
541 if (this.readyState !== 4) { 580 if (this.readyState !== 4) {
542 return; 581 return;
...@@ -591,12 +630,7 @@ var ...@@ -591,12 +630,7 @@ var
591 630
592 // figure out what stream the next segment should be downloaded from 631 // figure out what stream the next segment should be downloaded from
593 // with the updated bandwidth information 632 // with the updated bandwidth information
594 playlist = player.hls.selectPlaylist(); 633 updateCurrentPlaylist();
595 if (!playlist.segments) {
596 downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
597 } else {
598 player.hls.media = playlist;
599 }
600 }; 634 };
601 startTime = +new Date(); 635 startTime = +new Date();
602 segmentXhr.send(null); 636 segmentXhr.send(null);
......
...@@ -513,131 +513,6 @@ ...@@ -513,131 +513,6 @@
513 notStrictEqual(new Parser(), undefined, 'parser is defined'); 513 notStrictEqual(new Parser(), undefined, 'parser is defined');
514 }); 514 });
515 515
516 test('merges a manifest that strictly adds to an earlier one', function() {
517 var key, base, manifest, mid;
518 for (key in window.manifests) {
519 if (window.expected[key]) {
520 manifest = window.manifests[key];
521 // parse the first half of the manifest
522 mid = manifest.length / 2;
523 parser = new Parser();
524 parser.push(manifest.substring(0, mid));
525 base = parser.manifest;
526 if (!base.segments) {
527 // only test merges for media playlists
528 continue;
529 }
530
531 // attach the partial manifest to a new parser
532 parser = new Parser();
533 parser.push(manifest);
534
535 // merge the manifests together
536 deepEqual(m3u8.merge(base, parser.manifest),
537 window.expected[key],
538 key + '.m3u8 was parsed correctly');
539 }
540 }
541 });
542
543 test('merges overlapping segments without media sequences', function() {
544 var base;
545 parser = new Parser();
546 parser.push('#EXTM3U\n');
547 parser.push('#EXTINF:10,\n');
548 parser.push('0.ts\n');
549 parser.push('#EXTINF:10,\n');
550 parser.push('1.ts\n');
551 base = parser.manifest;
552
553 parser = new Parser();
554 parser.push('#EXTM3U\n');
555 parser.push('#EXTINF:10,\n');
556 parser.push('1.ts\n');
557 parser.push('#EXTINF:10,\n');
558 parser.push('2.ts\n');
559
560 deepEqual({
561 allowCache: true,
562 mediaSequence: 0,
563 segments: [{ duration: 10, uri: '0.ts'},
564 { duration: 10, uri: '1.ts' },
565 { duration: 10, uri: '2.ts' }]
566 }, m3u8.merge(base, parser.manifest), 'merges segment additions');
567 });
568
569 test('appends non-overlapping segments without media sequences', function() {
570 var base;
571 parser = new Parser();
572 parser.push('#EXTM3U\n');
573 parser.push('#EXTINF:10,\n');
574 parser.push('0.ts\n');
575 base = parser.manifest;
576
577 parser = new Parser();
578 parser.push('#EXTM3U\n');
579 parser.push('#EXTINF:10,\n');
580 parser.push('1.ts\n');
581
582 deepEqual({
583 allowCache: true,
584 mediaSequence: 0,
585 segments: [{ duration: 10, uri: '0.ts'},
586 { duration: 10, uri: '1.ts' }]
587 }, m3u8.merge(base, parser.manifest), 'appends segment additions');
588 });
589
590 test('replaces segments when merging with a higher media sequence number', function() {
591 var base;
592 parser = new Parser();
593 parser.push('#EXTM3U\n');
594 parser.push('#EXT-X-MEDIA-SEQUENCE:3\n');
595 parser.push('#EXTINF:10,\n');
596 parser.push('3.ts\n');
597 base = parser.manifest;
598
599 parser = new Parser();
600 parser.push('#EXTM3U\n');
601 parser.push('#EXT-X-MEDIA-SEQUENCE:7\n');
602 parser.push('#EXTINF:10,\n');
603 parser.push('7.ts\n');
604 base = parser.manifest;
605
606 deepEqual({
607 allowCache: true,
608 mediaSequence: 7,
609 segments: [{ duration: 10, uri: '7.ts' }]
610 }, m3u8.merge(base, parser.manifest), 'replaces segments');
611 });
612
613 test('replaces overlapping segments when media sequence is present', function() {
614 var base;
615 parser = new Parser();
616 parser.push('#EXTM3U\n');
617 parser.push('#EXT-X-MEDIA-SEQUENCE:3\n');
618 parser.push('#EXTINF:10,\n');
619 parser.push('3.ts\n');
620 parser.push('#EXTINF:10,\n');
621 parser.push('4.ts\n');
622 base = parser.manifest;
623
624 parser = new Parser();
625 parser.push('#EXTM3U\n');
626 parser.push('#EXT-X-MEDIA-SEQUENCE:4\n');
627 parser.push('#EXTINF:10,\n');
628 parser.push('4.ts\n');
629 parser.push('#EXTINF:10,\n');
630 parser.push('5.ts\n');
631 base = parser.manifest;
632
633 deepEqual({
634 allowCache: true,
635 mediaSequence: 4,
636 segments: [{ duration: 10, uri: '4.ts' },
637 { duration: 10, uri: '5.ts' }]
638 }, m3u8.merge(base, parser.manifest), 'replaces segments');
639 });
640
641 module('m3u8s'); 516 module('m3u8s');
642 517
643 test('parses static manifests as expected', function() { 518 test('parses static manifests as expected', function() {
......
...@@ -170,7 +170,7 @@ test('sets the duration if one is available on the playlist', function() { ...@@ -170,7 +170,7 @@ test('sets the duration if one is available on the playlist', function() {
170 type: 'sourceopen' 170 type: 'sourceopen'
171 }); 171 });
172 172
173 strictEqual(1, calls, 'duration is set'); 173 strictEqual(calls, 2, 'duration is set');
174 }); 174 });
175 175
176 test('calculates the duration if needed', function() { 176 test('calculates the duration if needed', function() {
...@@ -186,7 +186,7 @@ test('calculates the duration if needed', function() { ...@@ -186,7 +186,7 @@ test('calculates the duration if needed', function() {
186 type: 'sourceopen' 186 type: 'sourceopen'
187 }); 187 });
188 188
189 strictEqual(durations.length, 1, 'duration is set'); 189 strictEqual(durations.length, 2, 'duration is set');
190 strictEqual(durations[0], 190 strictEqual(durations[0],
191 player.hls.media.segments.length * 10, 191 player.hls.media.segments.length * 10,
192 'duration is calculated'); 192 'duration is calculated');
...@@ -402,15 +402,12 @@ test('downloads additional playlists if required', function() { ...@@ -402,15 +402,12 @@ test('downloads additional playlists if required', function() {
402 called = true; 402 called = true;
403 return playlist; 403 return playlist;
404 } 404 }
405 playlist.segments = []; 405 playlist.segments = [1, 1, 1];
406 return playlist; 406 return playlist;
407 }; 407 };
408 xhrUrls = []; 408 xhrUrls = [];
409 409
410 // the playlist selection is revisited after a new segment is downloaded 410 // the playlist selection is revisited after a new segment is downloaded
411 player.currentTime = function() {
412 return 1;
413 };
414 player.trigger('timeupdate'); 411 player.trigger('timeupdate');
415 412
416 strictEqual(2, xhrUrls.length, 'requests were made'); 413 strictEqual(2, xhrUrls.length, 'requests were made');
...@@ -869,7 +866,7 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { ...@@ -869,7 +866,7 @@ test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
869 866
870 test('has no effect if native HLS is available', function() { 867 test('has no effect if native HLS is available', function() {
871 videojs.hls.supportsNativeHls = true; 868 videojs.hls.supportsNativeHls = true;
872 player.hls('manifest/master.m3u8'); 869 player.hls('http://example.com/manifest/master.m3u8');
873 870
874 ok(!(player.currentSrc() in videojs.mediaSources), 871 ok(!(player.currentSrc() in videojs.mediaSources),
875 'no media source was opened'); 872 'no media source was opened');
...@@ -881,7 +878,7 @@ test('reloads live playlists', function() { ...@@ -881,7 +878,7 @@ test('reloads live playlists', function() {
881 window.setTimeout = function(callback, timeout) { 878 window.setTimeout = function(callback, timeout) {
882 callbacks.push({ callback: callback, timeout: timeout }); 879 callbacks.push({ callback: callback, timeout: timeout });
883 }; 880 };
884 player.hls('manifest/missingEndlist.m3u8'); 881 player.hls('http://example.com/manifest/missingEndlist.m3u8');
885 videojs.mediaSources[player.currentSrc()].trigger({ 882 videojs.mediaSources[player.currentSrc()].trigger({
886 type: 'sourceopen' 883 type: 'sourceopen'
887 }); 884 });
...@@ -893,7 +890,7 @@ test('reloads live playlists', function() { ...@@ -893,7 +890,7 @@ test('reloads live playlists', function() {
893 }); 890 });
894 891
895 test('duration is Infinity for live playlists', function() { 892 test('duration is Infinity for live playlists', function() {
896 player.hls('manifest/missingEndlist.m3u8'); 893 player.hls('http://example.com/manifest/missingEndlist.m3u8');
897 videojs.mediaSources[player.currentSrc()].trigger({ 894 videojs.mediaSources[player.currentSrc()].trigger({
898 type: 'sourceopen' 895 type: 'sourceopen'
899 }); 896 });
...@@ -943,27 +940,114 @@ test('reloads a live playlist after half a target duration if it has not ' + ...@@ -943,27 +940,114 @@ test('reloads a live playlist after half a target duration if it has not ' +
943 940
944 test('merges playlist reloads', function() { 941 test('merges playlist reloads', function() {
945 var 942 var
946 realMerge = videojs.m3u8.merge, 943 oldPlaylist,
947 merges = 0,
948 callback; 944 callback;
949 // capture timeouts and playlist merges 945 // capture timeouts
950 window.setTimeout = function(cb) { 946 window.setTimeout = function(cb) {
951 callback = cb; 947 callback = cb;
952 }; 948 };
953 videojs.m3u8.merge = function(base, update) {
954 merges++;
955 return update;
956 };
957 949
958 player.hls('http://example.com/manifest/missingEndlist.m3u8'); 950 player.hls('http://example.com/manifest/missingEndlist.m3u8');
959 videojs.mediaSources[player.currentSrc()].trigger({ 951 videojs.mediaSources[player.currentSrc()].trigger({
960 type: 'sourceopen' 952 type: 'sourceopen'
961 }); 953 });
954 oldPlaylist = player.hls.media;
962 955
963 callback(); 956 callback();
964 strictEqual(1, merges, 'reloaded playlist was merged'); 957 ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated');
958 });
959
960 test('updates the media index when a playlist reloads', function() {
961 var callback;
962 window.setTimeout = function(cb) {
963 callback = cb;
964 };
965 // the initial playlist
966 window.manifests['live-updating'] =
967 '#EXTM3U\n' +
968 '#EXTINF:10,\n' +
969 '0.ts\n' +
970 '#EXTINF:10,\n' +
971 '1.ts\n' +
972 '#EXTINF:10,\n' +
973 '2.ts\n';
974
975 player.hls('http://example.com/live-updating.m3u8');
976 videojs.mediaSources[player.currentSrc()].trigger({
977 type: 'sourceopen'
978 });
979
980 // play the stream until 2.ts is playing
981 player.hls.mediaIndex = 3;
982
983 // reload the updated playlist
984 window.manifests['live-updating'] =
985 '#EXTM3U\n' +
986 '#EXTINF:10,\n' +
987 '1.ts\n' +
988 '#EXTINF:10,\n' +
989 '2.ts\n' +
990 '#EXTINF:10,\n' +
991 '3.ts\n';
992 callback();
993
994 strictEqual(2, player.hls.mediaIndex, 'mediaIndex is updated after the reload');
995 });
996
997 test('mediaIndex is zero before the first segment loads', function() {
998 window.manifests['first-seg-load'] =
999 '#EXTM3U\n' +
1000 '#EXTINF:10,\n' +
1001 '0.ts\n';
1002 window.XMLHttpRequest = function() {
1003 this.open = function() {};
1004 this.send = function() {};
1005 };
1006 player.hls('http://example.com/first-seg-load.m3u8');
1007 videojs.mediaSources[player.currentSrc()].trigger({
1008 type: 'sourceopen'
1009 });
1010
1011 strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero');
1012 });
1013
1014 test('reloads out-of-date live playlists when switching variants', function() {
1015 var callback;
1016 // capture timeouts
1017 window.setTimeout = function(cb) {
1018 callback = cb;
1019 };
1020
1021 player.hls('http://example.com/master.m3u8');
1022 videojs.mediaSources[player.currentSrc()].trigger({
1023 type: 'sourceopen'
1024 });
1025
1026 // playing segment 15 on playlist 0
1027 player.hls.master = {
1028 playlists: [{
1029 mediaSequence: 15,
1030 segments: [{}, {}]
1031 }, {
1032 uri: 'http://example.com/variant-update.m3u8',
1033 mediaSequence: 0,
1034 segments: [{}, {}]
1035 }]
1036 };
1037 player.hls.media = player.hls.master.playlists[0];
1038 player.mediaIndex = 0;
1039 window.manifests['variant-update'] = '#EXTM3U\n' +
1040 '#EXT-X-MEDIA-SEQUENCE:16\n' +
1041 '#EXTINF:10,\n' +
1042 '16.ts\n';
1043
1044 // switch playlists
1045 player.hls.selectPlaylist = function() {
1046 return player.hls.master.playlists[1];
1047 };
1048 player.trigger('timeupdate');
965 1049
966 videojs.m3u8.merge = realMerge; 1050 ok(callback, 'reload is scheduled');
967 }); 1051 });
968 1052
969 })(window, window.videojs); 1053 })(window, window.videojs);
......