d8cf74c3 by David LaPalomento

Integrate playlist loader

Remove old playlist download code and use the playlist loader. Update test cases.
1 parent 2c6ddb6e
......@@ -47,7 +47,7 @@
return changed ? result : null;
},
PlaylistLoader = function(srcUrl) {
PlaylistLoader = function(srcUrl, withCredentials) {
var
loader = this,
media,
......@@ -92,6 +92,8 @@
loader.trigger('mediaupdatetimeout');
}, refreshDelay);
}
loader.trigger('loadedplaylist');
};
PlaylistLoader.prototype.init.call(this);
......@@ -122,13 +124,6 @@
if (loader.state === 'HAVE_NOTHING' || loader.state === 'HAVE_MASTER') {
throw new Error('Cannot switch media playlist from ' + loader.state);
}
loader.state = 'SWITCHING_MEDIA';
// abort any outstanding playlist refreshes
if (request) {
request.abort();
request = null;
}
// find the playlist object if the target playlist has been
// specified by URI
......@@ -139,8 +134,24 @@
playlist = loader.master.playlists[playlist];
}
if (playlist.uri === media.uri) {
// switching to the currently active playlist is a no-op
return;
}
loader.state = 'SWITCHING_MEDIA';
// abort any outstanding playlist refreshes
if (request) {
request.abort();
request = null;
}
// request the new playlist
request = xhr(resolveUrl(loader.master.uri, playlist.uri), function(error) {
request = xhr({
url: resolveUrl(loader.master.uri, playlist.uri),
withCredentials: withCredentials
}, function(error) {
haveMetadata(error, this, playlist.uri);
});
};
......@@ -153,21 +164,26 @@
}
loader.state = 'HAVE_CURRENT_METADATA';
request = xhr(resolveUrl(loader.master.uri, loader.media().uri),
function(error) {
haveMetadata(error, this, loader.media().uri);
});
request = xhr({
url: resolveUrl(loader.master.uri, loader.media().uri),
withCredentials: withCredentials
}, function(error) {
haveMetadata(error, this, loader.media().uri);
});
});
// request the specified URL
xhr(srcUrl, function(error) {
xhr({
url: srcUrl,
withCredentials: withCredentials
}, function(error) {
var parser, i;
if (error) {
loader.error = {
status: this.status,
message: 'HLS playlist request error at URL: ' + srcUrl,
code: (this.status >= 500) ? 4 : 2
code: 2 // MEDIA_ERR_NETWORK
};
return loader.trigger('error');
}
......@@ -189,13 +205,16 @@
loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i];
}
request = xhr(resolveUrl(srcUrl, parser.manifest.playlists[0].uri),
function(error) {
// pass along the URL specified in the master playlist
haveMetadata(error,
this,
parser.manifest.playlists[0].uri);
});
request = xhr({
url: resolveUrl(srcUrl, parser.manifest.playlists[0].uri),
withCredentials: withCredentials
}, function(error) {
// pass along the URL specified in the master playlist
haveMetadata(error,
this,
parser.manifest.playlists[0].uri);
loader.trigger('loadedmetadata');
});
return loader.trigger('loadedplaylist');
}
......@@ -208,7 +227,8 @@
}]
};
loader.master.playlists[srcUrl] = loader.master.playlists[0];
return haveMetadata(null, this, srcUrl);
haveMetadata(null, this, srcUrl);
return loader.trigger('loadedmetadata');
});
};
PlaylistLoader.prototype = new videojs.hls.Stream();
......
......@@ -194,15 +194,20 @@ var
*/
translateMediaIndex = function(mediaIndex, original, update) {
var
i = update.segments.length,
i,
originalSegment;
// no segments have been loaded from the original playlist
if (mediaIndex === 0) {
return 0;
}
if (!(update && update.segments)) {
// let the media index be zero when there are no segments defined
return 0;
}
// try to sync based on URI
i = update.segments.length;
originalSegment = original.segments[mediaIndex - 1];
while (i--) {
if (originalSegment.uri === update.segments[i].uri) {
......@@ -292,12 +297,9 @@ var
player = this,
srcUrl,
playlistXhr,
segmentXhr,
settings,
loadedPlaylist,
fillBuffer,
updateCurrentPlaylist,
updateDuration;
// if the video element supports HLS natively, do nothing
......@@ -376,7 +378,8 @@ var
player.on('seeking', function() {
var currentTime = player.currentTime();
player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime);
player.hls.mediaIndex = getMediaIndexByTime(player.hls.playlists.media(),
currentTime);
// abort any segments still being decoded
player.hls.sourceBuffer.abort();
......@@ -407,39 +410,6 @@ var
};
/**
* Determine whether the current media playlist should be changed
* and trigger a switch if necessary. If a sufficiently fresh
* version of the target playlist is available, the switch will take
* effect immediately. Otherwise, the target playlist will be
* refreshed.
*/
updateCurrentPlaylist = function() {
var playlist, mediaSequence;
playlist = player.hls.selectPlaylist();
mediaSequence = player.hls.mediaIndex + (player.hls.media.mediaSequence || 0);
if (!playlist.segments ||
mediaSequence < (playlist.mediaSequence || 0) ||
mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) {
if (playlistXhr) {
playlistXhr.abort();
}
playlistXhr = xhr({
url: resolveUrl(srcUrl, playlist.uri),
withCredentials: settings.withCredentials
}, loadedPlaylist);
} else {
player.hls.mediaIndex =
translateMediaIndex(player.hls.mediaIndex,
player.hls.media,
playlist);
player.hls.media = playlist;
updateDuration(player.hls.media);
}
};
/**
* Chooses the appropriate media playlist based on the current
* bandwidth estimate and the player size.
* @return the highest bitrate playlist less than the currently detected
......@@ -448,7 +418,7 @@ var
player.hls.selectPlaylist = function () {
var
effectiveBitrate,
sortedPlaylists = player.hls.master.playlists.slice(),
sortedPlaylists = player.hls.playlists.master.playlists.slice(),
bandwidthPlaylists = [],
i = sortedPlaylists.length,
variant,
......@@ -513,97 +483,6 @@ var
};
/**
* 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
*/
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
};
return player.trigger('error');
}
parser = new videojs.m3u8.Parser();
parser.push(this.responseText);
// 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);
if (playlistUri === url) {
// if the playlist is unchanged since the last reload,
// try again after half the target duration
// http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
if (playlist.segments &&
playlist.segments.length === parser.manifest.segments.length) {
refreshDelay /= 2;
}
player.hls.master.playlists[i] =
videojs.util.mergeOptions(playlist, parser.manifest);
if (playlist !== player.hls.media) {
continue;
}
// determine the new mediaIndex if we're updating the
// current media playlist
player.hls.mediaIndex =
translateMediaIndex(player.hls.mediaIndex,
playlist,
parser.manifest);
player.hls.media = parser.manifest;
}
}
// check the playlist for updates if EXT-X-ENDLIST isn't present
if (!parser.manifest.endList) {
window.setTimeout(function() {
if (!playlistXhr &&
resolveUrl(srcUrl, player.hls.media.uri) === url) {
playlistXhr = xhr(url, loadedPlaylist);
}
}, refreshDelay);
}
// always start playback with the default rendition
if (!player.hls.media) {
player.hls.media = player.hls.master.playlists[0];
// update the duration
updateDuration(parser.manifest);
// periodicaly check if the buffer needs to be refilled
player.on('timeupdate', fillBuffer);
player.trigger('loadedmanifest');
player.trigger('loadedmetadata');
fillBuffer();
return;
}
// select a playlist and download its metadata if necessary
updateCurrentPlaylist();
player.trigger('loadedmanifest');
};
/**
* Determines whether there is enough video data currently in the buffer
* and downloads a new segment if the buffered time is less than the goal.
* @param offset (optional) {number} the offset into the downloaded segment
......@@ -623,12 +502,12 @@ var
}
// if no segments are available, do nothing
if (!player.hls.media.segments) {
if (!player.hls.playlists.media().segments) {
return;
}
// if the video has finished downloading, stop trying to buffer
segment = player.hls.media.segments[player.hls.mediaIndex];
segment = player.hls.playlists.media().segments[player.hls.mediaIndex];
if (!segment) {
return;
}
......@@ -644,10 +523,10 @@ var
}
// resolve the segment URL relative to the playlist
if (player.hls.media.uri === srcUrl) {
if (player.hls.playlists.media().uri === srcUrl) {
segmentUri = resolveUrl(srcUrl, segment.uri);
} else {
segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''),
segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.playlists.media().uri || ''),
segment.uri);
}
......@@ -708,49 +587,55 @@ var
player.hls.mediaIndex++;
if (player.hls.mediaIndex === player.hls.media.segments.length) {
if (player.hls.mediaIndex === player.hls.playlists.media().segments.length) {
mediaSource.endOfStream();
}
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
updateCurrentPlaylist();
player.hls.playlists.media(player.hls.selectPlaylist());
});
};
// load the MediaSource into the player
mediaSource.addEventListener('sourceopen', function() {
// construct the video data buffer and set the appropriate MIME type
var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
var
sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'),
oldMediaPlaylist;
player.hls.sourceBuffer = sourceBuffer;
sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
player.hls.mediaIndex = 0;
xhr({
url: srcUrl,
withCredentials: settings.withCredentials
}, 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({
url: resolveUrl(url, parser.manifest.playlists[0].uri),
withCredentials: settings.withCredentials
}, 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.hls.playlists =
new videojs.hls.PlaylistLoader(srcUrl, settings.withCredentials);
player.hls.playlists.on('loadedmetadata', function() {
oldMediaPlaylist = player.hls.playlists.media();
// periodicaly check if the buffer needs to be refilled
fillBuffer();
player.on('timeupdate', fillBuffer);
player.trigger('loadedmetadata');
});
player.hls.playlists.on('error', function() {
player.hls.error = player.hls.playlists.error;
player.trigger('error');
});
player.hls.playlists.on('loadedplaylist', function() {
var updatedPlaylist = player.hls.playlists.media();
if (!updatedPlaylist) {
// do nothing before an initial media playlist has been activated
return;
}
updateDuration(player.hls.playlists.media());
player.hls.mediaIndex = translateMediaIndex(player.hls.mediaIndex,
oldMediaPlaylist,
updatedPlaylist);
oldMediaPlaylist = updatedPlaylist;
});
});
player.src([{
......
......@@ -65,7 +65,12 @@
});
test('jumps to HAVE_METADATA when initialized with a media playlist', function() {
var loader = new videojs.hls.PlaylistLoader('media.m3u8');
var
loadedmetadatas = 0,
loader = new videojs.hls.PlaylistLoader('media.m3u8');
loader.on('loadedmetadata', function() {
loadedmetadatas++;
});
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
......@@ -75,7 +80,8 @@
ok(loader.media(), 'sets the media playlist');
ok(loader.media().uri, 'sets the media playlist URI');
strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
strictEqual(0, requests.length, 'no more requests are made');
strictEqual(requests.length, 0, 'no more requests are made');
strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata');
});
test('jumps to HAVE_METADATA when initialized with a live media playlist', function() {
......@@ -91,17 +97,22 @@
test('moves to HAVE_METADATA after loading a media playlist', function() {
var
loadedPlaylist = false,
loadedPlaylist = 0,
loadedMetadata = 0,
loader = new videojs.hls.PlaylistLoader('master.m3u8');
loader.on('loadedplaylist', function() {
loadedPlaylist = true;
loadedPlaylist++;
});
loader.on('loadedmetadata', function() {
loadedMetadata++;
});
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:\n' +
'media.m3u8\n' +
'alt.m3u8\n');
ok(loadedPlaylist, 'loadedplaylist fired');
strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once');
strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata');
strictEqual(requests.length, 1, 'requests the media playlist');
strictEqual(requests[0].method, 'GET', 'GETs the media playlist');
strictEqual(requests[0].url,
......@@ -114,6 +125,8 @@
'0.ts\n');
ok(loader.master, 'sets the master playlist');
ok(loader.media(), 'sets the media playlist');
strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice');
strictEqual(loadedMetadata, 1, 'fired loadedmetadata once');
strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
});
......@@ -317,6 +330,25 @@
strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
});
test('switching to the active playlist is a no-op', function() {
var loader = new videojs.hls.PlaylistLoader('master.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'low.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
'high.m3u8\n');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'low-0.ts\n' +
'#EXT-X-ENDLIST\n');
loader.media('low.m3u8');
strictEqual(requests.length, 0, 'no requests is sent');
});
test('throws an error if a media switch is initiated too early', function() {
var loader = new videojs.hls.PlaylistLoader('master.m3u8');
......