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.
Showing
2 changed files
with
132 additions
and
50 deletions
... | @@ -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,10 +394,14 @@ var | ... | @@ -379,10 +394,14 @@ 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; |
... | @@ -466,49 +485,35 @@ var | ... | @@ -466,49 +485,35 @@ 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 | ||
499 | if (parser.manifest.playlists) { | ||
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; | ||
508 | if (player.hls.master) { | ||
509 | // merge this playlist into the master | 514 | // merge this playlist into the master |
510 | i = player.hls.master.playlists.length; | 515 | i = player.hls.master.playlists.length; |
511 | 516 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | |
512 | while (i--) { | 517 | while (i--) { |
513 | playlist = player.hls.master.playlists[i]; | 518 | playlist = player.hls.master.playlists[i]; |
514 | playlistUri = resolveUrl(srcUrl, playlist.uri); | 519 | playlistUri = resolveUrl(srcUrl, playlist.uri); |
... | @@ -531,24 +536,20 @@ var | ... | @@ -531,24 +536,20 @@ var |
531 | // determine the new mediaIndex if we're updating the | 536 | // determine the new mediaIndex if we're updating the |
532 | // current media playlist | 537 | // current media playlist |
533 | player.hls.mediaIndex = | 538 | player.hls.mediaIndex = |
534 | findCorrespondingMediaIndex(player.hls.mediaIndex, | 539 | translateMediaIndex(player.hls.mediaIndex, |
535 | playlist, | 540 | playlist, |
536 | parser.manifest); | 541 | parser.manifest); |
537 | player.hls.media = parser.manifest; | 542 | player.hls.media = parser.manifest; |
538 | } | 543 | } |
539 | } | 544 | } |
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 | } | ||
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); | ... | ... |
-
Please register or sign in to post a comment