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
}
},
/**
* Creates and sends an XMLHttpRequest.
* @param options {string | object} if this argument is a string, it
* is intrepreted as a URL and a simple GET request is
* inititated. If it is an object, it should contain a `url`
* property that indicates the URL to request and optionally a
* `method` which is the type of HTTP request to send.
* @return {object} the XMLHttpRequest that was initiated.
*/
xhr = function(url, callback) {
var
options = {
......@@ -165,7 +174,7 @@ var
* @param {number} the corresponding media index in the updated
* playlist
*/
findCorrespondingMediaIndex = function(mediaIndex, original, update) {
translateMediaIndex = function(mediaIndex, original, update) {
var
i = update.segments.length,
originalSegment;
......@@ -195,9 +204,14 @@ var
totalDuration = function(playlist) {
var
duration = 0,
i = playlist.segments.length,
i,
segment;
if (!playlist.segments) {
return 0;
}
i = playlist.segments.length;
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
......@@ -274,8 +288,9 @@ var
}),
srcUrl,
playlistXhr,
segmentXhr,
downloadPlaylist,
loadedPlaylist,
fillBuffer,
updateCurrentPlaylist;
......@@ -379,10 +394,14 @@ var
if (!playlist.segments ||
mediaSequence < (playlist.mediaSequence || 0) ||
mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) {
xhr(resolveUrl(srcUrl, playlist.uri), downloadPlaylist);
if (playlistXhr) {
playlistXhr.abort();
}
playlistXhr = xhr(resolveUrl(srcUrl, playlist.uri), loadedPlaylist);
} else {
player.hls.mediaIndex =
findCorrespondingMediaIndex(player.hls.mediaIndex,
translateMediaIndex(player.hls.mediaIndex,
player.hls.media,
playlist);
player.hls.media = playlist;
......@@ -466,49 +485,35 @@ var
};
/**
* Callback that is invoked when a playlist finishes
* downloading. If the response is a master playlist, the default
* variant will be downloaded and parsed as well. Triggers
* `loadedmanifest` once for each playlist that is downloaded and
* `loadedmetadata` after at least one media playlist has been
* parsed. Whether multiple playlists were downloaded or not, when
* `loadedmetadata` fires a parsed or inferred master playlist
* object will be available as `player.hls.master`.
* Callback that is invoked when a media playlist finishes
* downloading. Triggers `loadedmanifest` once for each playlist
* that is downloaded and `loadedmetadata` after at least one
* media playlist has been parsed.
*
* @param error {*} truthy if the request was not successful
* @param url {string} a URL to the M3U8 file to process
*/
downloadPlaylist = function(error, url) {
loadedPlaylist = function(error, url) {
var i, parser, playlist, playlistUri, refreshDelay;
// clear the current playlist XHR
playlistXhr = null;
if (error) {
player.hls.error = {
status: this.status,
message: 'HLS playlist request error at URL: ' + url,
code: (this.status >= 500) ? 4 : 2
};
player.trigger('error');
return;
return player.trigger('error');
}
// readystate DONE
parser = new videojs.m3u8.Parser();
parser.push(this.responseText);
// master playlists
if (parser.manifest.playlists) {
player.hls.master = parser.manifest;
xhr(resolveUrl(url, parser.manifest.playlists[0].uri), downloadPlaylist);
player.trigger('loadedmanifest');
return;
}
// media playlists
refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
if (player.hls.master) {
// merge this playlist into the master
i = player.hls.master.playlists.length;
refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
while (i--) {
playlist = player.hls.master.playlists[i];
playlistUri = resolveUrl(srcUrl, playlist.uri);
......@@ -531,24 +536,20 @@ var
// determine the new mediaIndex if we're updating the
// current media playlist
player.hls.mediaIndex =
findCorrespondingMediaIndex(player.hls.mediaIndex,
translateMediaIndex(player.hls.mediaIndex,
playlist,
parser.manifest);
player.hls.media = parser.manifest;
}
}
} else {
// infer a master playlist if none was previously requested
player.hls.master = {
playlists: [parser.manifest]
};
parser.manifest.uri = url;
}
// check the playlist for updates if EXT-X-ENDLIST isn't present
if (!parser.manifest.endList) {
window.setTimeout(function() {
xhr(url, downloadPlaylist);
if (!playlistXhr &&
resolveUrl(srcUrl, player.hls.media.uri) === url) {
playlistXhr = xhr(url, loadedPlaylist);
}
}, refreshDelay);
}
......@@ -689,7 +690,26 @@ var
sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
player.hls.mediaIndex = 0;
xhr(srcUrl, downloadPlaylist);
xhr(srcUrl, function(error, url) {
var uri, parser = new videojs.m3u8.Parser();
parser.push(this.responseText);
// master playlists
if (parser.manifest.playlists) {
player.hls.master = parser.manifest;
playlistXhr = xhr(resolveUrl(url, parser.manifest.playlists[0].uri), loadedPlaylist);
return player.trigger('loadedmanifest');
} else {
// infer a master playlist if a media playlist is loaded directly
uri = resolveUrl(window.location.href, url);
player.hls.master = {
playlists: [{
uri: uri
}]
};
loadedPlaylist.call(this, error, uri);
}
});
});
player.src([{
src: videojs.URL.createObjectURL(mediaSource),
......
......@@ -105,6 +105,7 @@ module('HLS', {
this.readyState = 4;
this.onreadystatechange();
};
this.abort = function() {};
};
xhrUrls = [];
},
......@@ -920,21 +921,16 @@ test('reloads a live playlist after half a target duration if it has not ' +
callbacks.push({ callback: callback, timeout: timeout });
};
player.hls('http://example.com/manifest/missingEndlist.m3u8');
// an identical manifest has already been parsed
player.hls.media = videojs.util.mergeOptions({}, window.expected['missingEndlist']);
player.hls.media.uri = 'http://example.com/manifest/missingEndlist.m3u8';
player.hls.master = {
playlists: [player.hls.media]
};
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(1, callbacks.length, 'refresh was scheduled');
strictEqual(player.hls.media.targetDuration / 2 * 1000,
callbacks[0].timeout,
strictEqual(callbacks.length, 1, 'full-length refresh scheduled');
callbacks.pop().callback();
strictEqual(1, callbacks.length, 'half-length refresh was scheduled');
strictEqual(callbacks[0].timeout,
player.hls.media.targetDuration / 2 * 1000,
'waited half a target duration');
});
......@@ -1047,4 +1043,70 @@ test('reloads out-of-date live playlists when switching variants', function() {
strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment');
});
test('does not reload master playlists', function() {
var callbacks = [];
window.setTimeout = function(callback) {
callbacks.push(callback);
};
player.hls('http://example.com/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(callbacks.length, 0, 'no reload scheduled');
});
test('only reloads the active media playlist', function() {
var callbacks = [], urls = [], responses = [];
window.setTimeout = function(callback) {
callbacks.push(callback);
};
player.hls('http://example.com/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
window.XMLHttpRequest = function() {
this.open = function(method, url) {
urls.push(url);
};
this.send = function() {
var xhr = this;
responses.push(function() {
xhr.readyState = 4;
xhr.responseText = '#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n';
xhr.response = new Uint8Array([1]).buffer;
xhr.onreadystatechange();
});
};
};
player.hls.selectPlaylist = function() {
return player.hls.master.playlists[1];
};
player.hls.master.playlists.push({
uri: 'http://example.com/switched.m3u8'
});
player.trigger('timeupdate');
strictEqual(callbacks.length, 1, 'a refresh is scheduled');
strictEqual(responses.length, 1, 'segment requested');
responses.shift()(); // segment response
responses.shift()(); // loaded switched.m3u8
urls = [];
callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8
callbacks.shift()(); // refresh switched.m3u8
strictEqual(urls.length, 1, 'one refresh was made');
strictEqual(urls[0],
'http://example.com/switched.m3u8',
'refreshed the active playlist');
});
})(window, window.videojs);
......