d5f938a0 by David LaPalomento

Pull out code to manage different playlist download callbacks.

`downloadPlaylist` was quite complicated so try to tease apart some of the cases that were being managed there. Stopped playlists from refreshing when they're not currently active.
1 parent 6e4ebe73
...@@ -94,6 +94,15 @@ var ...@@ -94,6 +94,15 @@ var
94 } 94 }
95 }, 95 },
96 96
97 /**
98 * Creates and sends an XMLHttpRequest.
99 * @param options {string | object} if this argument is a string, it
100 * is intrepreted as a URL and a simple GET request is
101 * inititated. If it is an object, it should contain a `url`
102 * property that indicates the URL to request and optionally a
103 * `method` which is the type of HTTP request to send.
104 * @return {object} the XMLHttpRequest that was initiated.
105 */
97 xhr = function(url, callback) { 106 xhr = function(url, callback) {
98 var 107 var
99 options = { 108 options = {
...@@ -165,7 +174,7 @@ var ...@@ -165,7 +174,7 @@ var
165 * @param {number} the corresponding media index in the updated 174 * @param {number} the corresponding media index in the updated
166 * playlist 175 * playlist
167 */ 176 */
168 findCorrespondingMediaIndex = function(mediaIndex, original, update) { 177 translateMediaIndex = function(mediaIndex, original, update) {
169 var 178 var
170 i = update.segments.length, 179 i = update.segments.length,
171 originalSegment; 180 originalSegment;
...@@ -195,9 +204,14 @@ var ...@@ -195,9 +204,14 @@ var
195 totalDuration = function(playlist) { 204 totalDuration = function(playlist) {
196 var 205 var
197 duration = 0, 206 duration = 0,
198 i = playlist.segments.length, 207 i,
199 segment; 208 segment;
200 209
210 if (!playlist.segments) {
211 return 0;
212 }
213 i = playlist.segments.length;
214
201 // if present, use the duration specified in the playlist 215 // if present, use the duration specified in the playlist
202 if (playlist.totalDuration) { 216 if (playlist.totalDuration) {
203 return playlist.totalDuration; 217 return playlist.totalDuration;
...@@ -274,8 +288,9 @@ var ...@@ -274,8 +288,9 @@ var
274 }), 288 }),
275 srcUrl, 289 srcUrl,
276 290
291 playlistXhr,
277 segmentXhr, 292 segmentXhr,
278 downloadPlaylist, 293 loadedPlaylist,
279 fillBuffer, 294 fillBuffer,
280 updateCurrentPlaylist; 295 updateCurrentPlaylist;
281 296
...@@ -379,12 +394,16 @@ var ...@@ -379,12 +394,16 @@ var
379 if (!playlist.segments || 394 if (!playlist.segments ||
380 mediaSequence < (playlist.mediaSequence || 0) || 395 mediaSequence < (playlist.mediaSequence || 0) ||
381 mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) { 396 mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) {
382 xhr(resolveUrl(srcUrl, playlist.uri), downloadPlaylist); 397
398 if (playlistXhr) {
399 playlistXhr.abort();
400 }
401 playlistXhr = xhr(resolveUrl(srcUrl, playlist.uri), loadedPlaylist);
383 } else { 402 } else {
384 player.hls.mediaIndex = 403 player.hls.mediaIndex =
385 findCorrespondingMediaIndex(player.hls.mediaIndex, 404 translateMediaIndex(player.hls.mediaIndex,
386 player.hls.media, 405 player.hls.media,
387 playlist); 406 playlist);
388 player.hls.media = playlist; 407 player.hls.media = playlist;
389 408
390 // update the duration 409 // update the duration
...@@ -466,89 +485,71 @@ var ...@@ -466,89 +485,71 @@ var
466 }; 485 };
467 486
468 /** 487 /**
469 * Callback that is invoked when a playlist finishes 488 * Callback that is invoked when a media playlist finishes
470 * downloading. If the response is a master playlist, the default 489 * downloading. Triggers `loadedmanifest` once for each playlist
471 * variant will be downloaded and parsed as well. Triggers 490 * that is downloaded and `loadedmetadata` after at least one
472 * `loadedmanifest` once for each playlist that is downloaded and 491 * media playlist has been parsed.
473 * `loadedmetadata` after at least one media playlist has been
474 * parsed. Whether multiple playlists were downloaded or not, when
475 * `loadedmetadata` fires a parsed or inferred master playlist
476 * object will be available as `player.hls.master`.
477 * 492 *
478 * @param error {*} truthy if the request was not successful 493 * @param error {*} truthy if the request was not successful
479 * @param url {string} a URL to the M3U8 file to process 494 * @param url {string} a URL to the M3U8 file to process
480 */ 495 */
481 downloadPlaylist = function(error, url) { 496 loadedPlaylist = function(error, url) {
482 var i, parser, playlist, playlistUri, refreshDelay; 497 var i, parser, playlist, playlistUri, refreshDelay;
483 498
499 // clear the current playlist XHR
500 playlistXhr = null;
501
484 if (error) { 502 if (error) {
485 player.hls.error = { 503 player.hls.error = {
486 status: this.status, 504 status: this.status,
487 message: 'HLS playlist request error at URL: ' + url, 505 message: 'HLS playlist request error at URL: ' + url,
488 code: (this.status >= 500) ? 4 : 2 506 code: (this.status >= 500) ? 4 : 2
489 }; 507 };
490 player.trigger('error'); 508 return player.trigger('error');
491 return;
492 } 509 }
493 510
494 // readystate DONE
495 parser = new videojs.m3u8.Parser(); 511 parser = new videojs.m3u8.Parser();
496 parser.push(this.responseText); 512 parser.push(this.responseText);
497 513
498 // master playlists 514 // merge this playlist into the master
499 if (parser.manifest.playlists) { 515 i = player.hls.master.playlists.length;
500 player.hls.master = parser.manifest;
501 xhr(resolveUrl(url, parser.manifest.playlists[0].uri), downloadPlaylist);
502 player.trigger('loadedmanifest');
503 return;
504 }
505
506 // media playlists
507 refreshDelay = (parser.manifest.targetDuration || 10) * 1000; 516 refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
508 if (player.hls.master) { 517 while (i--) {
509 // merge this playlist into the master 518 playlist = player.hls.master.playlists[i];
510 i = player.hls.master.playlists.length; 519 playlistUri = resolveUrl(srcUrl, playlist.uri);
511 520 if (playlistUri === url) {
512 while (i--) { 521 // if the playlist is unchanged since the last reload,
513 playlist = player.hls.master.playlists[i]; 522 // try again after half the target duration
514 playlistUri = resolveUrl(srcUrl, playlist.uri); 523 // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
515 if (playlistUri === url) { 524 if (playlist.segments &&
516 // if the playlist is unchanged since the last reload, 525 playlist.segments.length === parser.manifest.segments.length) {
517 // try again after half the target duration 526 refreshDelay /= 2;
518 // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 527 }
519 if (playlist.segments && 528
520 playlist.segments.length === parser.manifest.segments.length) { 529 player.hls.master.playlists[i] =
521 refreshDelay /= 2; 530 videojs.util.mergeOptions(playlist, parser.manifest);
522 } 531
523 532 if (playlist !== player.hls.media) {
524 player.hls.master.playlists[i] = 533 continue;
525 videojs.util.mergeOptions(playlist, parser.manifest);
526
527 if (playlist !== player.hls.media) {
528 continue;
529 }
530
531 // determine the new mediaIndex if we're updating the
532 // current media playlist
533 player.hls.mediaIndex =
534 findCorrespondingMediaIndex(player.hls.mediaIndex,
535 playlist,
536 parser.manifest);
537 player.hls.media = parser.manifest;
538 } 534 }
535
536 // determine the new mediaIndex if we're updating the
537 // current media playlist
538 player.hls.mediaIndex =
539 translateMediaIndex(player.hls.mediaIndex,
540 playlist,
541 parser.manifest);
542 player.hls.media = parser.manifest;
539 } 543 }
540 } else {
541 // infer a master playlist if none was previously requested
542 player.hls.master = {
543 playlists: [parser.manifest]
544 };
545 parser.manifest.uri = url;
546 } 544 }
547 545
548 // check the playlist for updates if EXT-X-ENDLIST isn't present 546 // check the playlist for updates if EXT-X-ENDLIST isn't present
549 if (!parser.manifest.endList) { 547 if (!parser.manifest.endList) {
550 window.setTimeout(function() { 548 window.setTimeout(function() {
551 xhr(url, downloadPlaylist); 549 if (!playlistXhr &&
550 resolveUrl(srcUrl, player.hls.media.uri) === url) {
551 playlistXhr = xhr(url, loadedPlaylist);
552 }
552 }, refreshDelay); 553 }, refreshDelay);
553 } 554 }
554 555
...@@ -689,7 +690,26 @@ var ...@@ -689,7 +690,26 @@ var
689 sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); 690 sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
690 691
691 player.hls.mediaIndex = 0; 692 player.hls.mediaIndex = 0;
692 xhr(srcUrl, downloadPlaylist); 693 xhr(srcUrl, function(error, url) {
694 var uri, parser = new videojs.m3u8.Parser();
695 parser.push(this.responseText);
696
697 // master playlists
698 if (parser.manifest.playlists) {
699 player.hls.master = parser.manifest;
700 playlistXhr = xhr(resolveUrl(url, parser.manifest.playlists[0].uri), loadedPlaylist);
701 return player.trigger('loadedmanifest');
702 } else {
703 // infer a master playlist if a media playlist is loaded directly
704 uri = resolveUrl(window.location.href, url);
705 player.hls.master = {
706 playlists: [{
707 uri: uri
708 }]
709 };
710 loadedPlaylist.call(this, error, uri);
711 }
712 });
693 }); 713 });
694 player.src([{ 714 player.src([{
695 src: videojs.URL.createObjectURL(mediaSource), 715 src: videojs.URL.createObjectURL(mediaSource),
......
...@@ -105,6 +105,7 @@ module('HLS', { ...@@ -105,6 +105,7 @@ module('HLS', {
105 this.readyState = 4; 105 this.readyState = 4;
106 this.onreadystatechange(); 106 this.onreadystatechange();
107 }; 107 };
108 this.abort = function() {};
108 }; 109 };
109 xhrUrls = []; 110 xhrUrls = [];
110 }, 111 },
...@@ -920,21 +921,16 @@ test('reloads a live playlist after half a target duration if it has not ' + ...@@ -920,21 +921,16 @@ test('reloads a live playlist after half a target duration if it has not ' +
920 callbacks.push({ callback: callback, timeout: timeout }); 921 callbacks.push({ callback: callback, timeout: timeout });
921 }; 922 };
922 player.hls('http://example.com/manifest/missingEndlist.m3u8'); 923 player.hls('http://example.com/manifest/missingEndlist.m3u8');
923
924 // an identical manifest has already been parsed
925 player.hls.media = videojs.util.mergeOptions({}, window.expected['missingEndlist']);
926 player.hls.media.uri = 'http://example.com/manifest/missingEndlist.m3u8';
927 player.hls.master = {
928 playlists: [player.hls.media]
929 };
930
931 videojs.mediaSources[player.currentSrc()].trigger({ 924 videojs.mediaSources[player.currentSrc()].trigger({
932 type: 'sourceopen' 925 type: 'sourceopen'
933 }); 926 });
934 927
935 strictEqual(1, callbacks.length, 'refresh was scheduled'); 928 strictEqual(callbacks.length, 1, 'full-length refresh scheduled');
936 strictEqual(player.hls.media.targetDuration / 2 * 1000, 929 callbacks.pop().callback();
937 callbacks[0].timeout, 930
931 strictEqual(1, callbacks.length, 'half-length refresh was scheduled');
932 strictEqual(callbacks[0].timeout,
933 player.hls.media.targetDuration / 2 * 1000,
938 'waited half a target duration'); 934 'waited half a target duration');
939 }); 935 });
940 936
...@@ -1047,4 +1043,70 @@ test('reloads out-of-date live playlists when switching variants', function() { ...@@ -1047,4 +1043,70 @@ test('reloads out-of-date live playlists when switching variants', function() {
1047 strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment'); 1043 strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment');
1048 }); 1044 });
1049 1045
1046 test('does not reload master playlists', function() {
1047 var callbacks = [];
1048 window.setTimeout = function(callback) {
1049 callbacks.push(callback);
1050 };
1051
1052 player.hls('http://example.com/master.m3u8');
1053 videojs.mediaSources[player.currentSrc()].trigger({
1054 type: 'sourceopen'
1055 });
1056
1057 strictEqual(callbacks.length, 0, 'no reload scheduled');
1058 });
1059
1060 test('only reloads the active media playlist', function() {
1061 var callbacks = [], urls = [], responses = [];
1062 window.setTimeout = function(callback) {
1063 callbacks.push(callback);
1064 };
1065
1066 player.hls('http://example.com/missingEndlist.m3u8');
1067 videojs.mediaSources[player.currentSrc()].trigger({
1068 type: 'sourceopen'
1069 });
1070
1071 window.XMLHttpRequest = function() {
1072 this.open = function(method, url) {
1073 urls.push(url);
1074 };
1075 this.send = function() {
1076 var xhr = this;
1077 responses.push(function() {
1078 xhr.readyState = 4;
1079 xhr.responseText = '#EXTM3U\n' +
1080 '#EXT-X-MEDIA-SEQUENCE:1\n' +
1081 '#EXTINF:10,\n' +
1082 '1.ts\n';
1083 xhr.response = new Uint8Array([1]).buffer;
1084 xhr.onreadystatechange();
1085 });
1086 };
1087 };
1088 player.hls.selectPlaylist = function() {
1089 return player.hls.master.playlists[1];
1090 };
1091 player.hls.master.playlists.push({
1092 uri: 'http://example.com/switched.m3u8'
1093 });
1094
1095 player.trigger('timeupdate');
1096 strictEqual(callbacks.length, 1, 'a refresh is scheduled');
1097 strictEqual(responses.length, 1, 'segment requested');
1098
1099 responses.shift()(); // segment response
1100 responses.shift()(); // loaded switched.m3u8
1101
1102 urls = [];
1103 callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8
1104 callbacks.shift()(); // refresh switched.m3u8
1105
1106 strictEqual(urls.length, 1, 'one refresh was made');
1107 strictEqual(urls[0],
1108 'http://example.com/switched.m3u8',
1109 'refreshed the active playlist');
1110 });
1111
1050 })(window, window.videojs); 1112 })(window, window.videojs);
......