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');
......
......@@ -149,30 +149,25 @@ test('starts playing if autoplay is specified', function() {
strictEqual(1, plays, 'play was called');
});
test('loads the specified manifest URL on init', function() {
var loadedmanifest = false, loadedmetadata = false;
player.on('loadedmanifest', function() {
loadedmanifest = true;
});
test('creates a PlaylistLoader on init', function() {
var loadedmetadata = false;
player.on('loadedmetadata', function() {
loadedmetadata = true;
});
player.hls('manifest/playlist.m3u8');
strictEqual(player.hls.readyState(), 0, 'the readyState is HAVE_NOTHING');
ok(!player.hls.playlists, 'waits for sourceopen to create the loader');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
ok(loadedmanifest, 'loadedmanifest fires');
ok(loadedmetadata, 'loadedmetadata fires');
ok(player.hls.master, 'a master is inferred');
ok(player.hls.media, 'the manifest is available');
ok(player.hls.media.segments, 'the segment entries are parsed');
strictEqual(player.hls.master.playlists[0],
player.hls.media,
ok(player.hls.playlists.master, 'set the master playlist');
ok(player.hls.playlists.media(), 'set the media playlist');
ok(player.hls.playlists.media().segments, 'the segment entries are parsed');
strictEqual(player.hls.playlists.master.playlists[0],
player.hls.playlists.media(),
'the playlist is selected');
strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA');
});
test('sets the duration if one is available on the playlist', function() {
......@@ -189,8 +184,9 @@ test('sets the duration if one is available on the playlist', function() {
});
standardXHRResponse(requests[0]);
strictEqual(calls, 1, 'duration is set');
standardXHRResponse(requests[1]);
strictEqual(calls, 2, 'duration is set');
strictEqual(calls, 1, 'duration is set');
});
test('calculates the duration if needed', function() {
......@@ -207,10 +203,9 @@ test('calculates the duration if needed', function() {
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(durations.length, 2, 'duration is set');
strictEqual(durations.length, 1, 'duration is set');
strictEqual(durations[0],
player.hls.media.segments.length * 10,
player.hls.playlists.media().segments.length * 10,
'duration is calculated');
});
......@@ -269,18 +264,7 @@ test('re-initializes the plugin for each source', function() {
});
test('triggers an error when a master playlist request errors', function() {
var
status = 0,
error;
window.XMLHttpRequest = function() {
this.open = function() {};
this.send = function() {
this.readyState = 4;
this.status = status;
this.onreadystatechange();
};
};
var error;
player.on('error', function() {
error = player.hls.error;
});
......@@ -288,6 +272,7 @@ test('triggers an error when a master playlist request errors', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
requests.pop().respond(500);
ok(error, 'an error is triggered');
strictEqual(2, error.code, 'a network error is triggered');
......@@ -355,7 +340,7 @@ test('selects a playlist after segment downloads', function() {
player.hls('manifest/master.m3u8');
player.hls.selectPlaylist = function() {
calls++;
return player.hls.master.playlists[0];
return player.hls.playlists.master.playlists[0];
};
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
......@@ -375,6 +360,7 @@ test('selects a playlist after segment downloads', function() {
player.trigger('timeupdate');
standardXHRResponse(requests[3]);
console.log(requests.map(function(i) { return i.url; }));
strictEqual(calls, 2, 'selects after additional segments');
});
......@@ -403,14 +389,14 @@ test('updates the duration after switching playlists', function() {
player.hls('manifest/master.m3u8');
player.hls.selectPlaylist = function() {
selectedPlaylist = true;
return player.hls.master.playlists[1];
return player.hls.playlists.master.playlists[1];
};
player.duration = function(duration) {
if (duration === undefined) {
return 0;
}
// only track calls that occur after the playlist has been switched
if (player.hls.media === player.hls.master.playlists[1]) {
if (player.hls.playlists.media() === player.hls.playlists.master.playlists[1]) {
calls++;
}
};
......@@ -462,8 +448,10 @@ test('downloads additional playlists if required', function() {
'/manifest/' +
playlist.uri,
'made playlist request');
strictEqual(playlist, player.hls.media, 'a new playlists was selected');
ok(player.hls.media.segments, 'segments are now available');
strictEqual(playlist.uri,
player.hls.playlists.media().uri,
'a new playlists was selected');
ok(player.hls.playlists.media().segments, 'segments are now available');
});
test('selects a playlist below the current bandwidth', function() {
......@@ -476,15 +464,15 @@ test('selects a playlist below the current bandwidth', function() {
standardXHRResponse(requests[0]);
// the default playlist has a really high bitrate
player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10;
player.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10;
// playlist 1 has a very low bitrate
player.hls.master.playlists[1].attributes.BANDWIDTH = 1;
player.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 1;
// but the detected client bandwidth is really low
player.hls.bandwidth = 10;
playlist = player.hls.selectPlaylist();
strictEqual(playlist,
player.hls.master.playlists[1],
player.hls.playlists.master.playlists[1],
'the low bitrate stream is selected');
});
......@@ -498,15 +486,15 @@ test('raises the minimum bitrate for a stream proportionially', function() {
standardXHRResponse(requests[0]);
// the default playlist's bandwidth + 10% is equal to the current bandwidth
player.hls.master.playlists[0].attributes.BANDWIDTH = 10;
player.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10;
player.hls.bandwidth = 11;
// 9.9 * 1.1 < 11
player.hls.master.playlists[1].attributes.BANDWIDTH = 9.9;
player.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 9.9;
playlist = player.hls.selectPlaylist();
strictEqual(playlist,
player.hls.master.playlists[1],
player.hls.playlists.master.playlists[1],
'a lower bitrate stream is selected');
});
......@@ -525,7 +513,7 @@ test('uses the lowest bitrate if no other is suitable', function() {
// playlist 1 has the lowest advertised bitrate
strictEqual(playlist,
player.hls.master.playlists[1],
player.hls.playlists.master.playlists[1],
'the lowest bitrate stream is selected');
});
......@@ -855,30 +843,21 @@ test('clears pending buffer updates when seeking', function() {
test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
var errorTriggered = false;
window.XMLHttpRequest = function() {
this.open = function(method, url) {
xhrUrls.push(url);
};
this.send = function() {
this.readyState = 4;
this.status = 404;
this.onreadystatechange();
};
};
player.hls('manifest/media.m3u8');
player.on('error', function() {
errorTriggered = true;
});
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
equal(true, errorTriggered, 'Missing Playlist error event should trigger');
equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
requests.pop().respond(404);
equal(errorTriggered,
true,
'Missing Playlist error event should trigger');
equal(player.hls.error.code,
2,
'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
ok(player.hls.error.message, 'Player error type should inform user correctly');
});
......@@ -916,24 +895,6 @@ test('has no effect if native HLS is available', function() {
'no media source was opened');
});
test('reloads live playlists', function() {
var callbacks = [];
// capture timeouts
window.setTimeout = function(callback, timeout) {
callbacks.push({ callback: callback, timeout: timeout });
};
player.hls('http://example.com/manifest/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
strictEqual(1, callbacks.length, 'refresh was scheduled');
strictEqual(player.hls.media.targetDuration * 1000,
callbacks[0].timeout,
'waited one target duration');
});
test('duration is Infinity for live playlists', function() {
player.hls('http://example.com/manifest/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
......@@ -959,88 +920,37 @@ test('does not reload playlists with an endlist tag', function() {
strictEqual(0, callbacks.length, 'no refresh was scheduled');
});
test('reloads a live playlist after half a target duration if it has not ' +
'changed since the last request', function() {
var callbacks = [];
// capture timeouts
window.setTimeout = function(callback, timeout) {
callbacks.push({ callback: callback, timeout: timeout });
};
player.hls('http://example.com/manifest/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(callbacks.length, 1, 'full-length refresh scheduled');
callbacks.pop().callback();
standardXHRResponse(requests[2]);
strictEqual(callbacks.length, 1, 'half-length refresh was scheduled');
strictEqual(callbacks[0].timeout,
player.hls.media.targetDuration / 2 * 1000,
'waited half a target duration');
});
test('merges playlist reloads', function() {
var oldPlaylist,
callback;
// capture timeouts
window.setTimeout = function(cb) {
callback = cb;
};
player.hls('http://example.com/manifest/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
oldPlaylist = player.hls.media;
callback();
standardXHRResponse(requests[2]);
ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated');
});
test('updates the media index when a playlist reloads', function() {
var callback;
window.setTimeout = function(cb) {
callback = cb;
};
// the initial playlist
window.manifests['live-updating'] =
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n';
player.hls('http://example.com/live-updating.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
requests[0].respond(200, null,
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'0.ts\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n');
standardXHRResponse(requests[1]);
// play the stream until 2.ts is playing
player.hls.mediaIndex = 3;
// reload the updated playlist
window.manifests['live-updating'] =
'#EXTM3U\n' +
'#EXTINF:10,\n' +
'1.ts\n' +
'#EXTINF:10,\n' +
'2.ts\n' +
'#EXTINF:10,\n' +
'3.ts\n';
callback();
standardXHRResponse(requests[2]);
player.hls.playlists.media = function() {
return {
segments: [{
uri: '1.ts'
}, {
uri: '2.ts'
}, {
uri: '3.ts'
}]
};
};
player.hls.playlists.trigger('loadedplaylist');
strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload');
});
......@@ -1112,64 +1022,6 @@ test('does not reload master playlists', function() {
strictEqual(callbacks.length, 0, 'no reload scheduled');
});
test('only reloads the active media playlist', function() {
var callbacks = [],
i = 0,
filteredRequests = [],
customResponse;
customResponse = function(request) {
request.response = new Uint8Array([1]).buffer;
request.respond(200,
{'Content-Type': 'application/vnd.apple.mpegurl'},
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n');
};
window.setTimeout = function(callback) {
callbacks.push(callback);
};
player.hls('http://example.com/missingEndlist.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
videojs.mediaSources[player.currentSrc()].endOfStream = function() {};
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');
standardXHRResponse(requests[2]); // segment response
customResponse(requests[3]); // loaded witched.m3u8
callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8
callbacks.shift()(); // refresh switched.m3u8
for (; i < requests.length; i++) {
if (/switched/.test(requests[i].url)) {
filteredRequests.push(requests[i]);
}
}
strictEqual(filteredRequests.length, 2, 'one refresh was made');
strictEqual(filteredRequests[1].url,
'http://example.com/switched.m3u8',
'refreshed the active playlist');
});
test('if withCredentials option is used, withCredentials is set on the XHR object', function() {
player.hls({
url: 'http://example.com/media.m3u8',
......@@ -1182,20 +1034,15 @@ test('if withCredentials option is used, withCredentials is set on the XHR objec
});
test('does not break if the playlist has no segments', function() {
var customResponse = function(request) {
request.response = new Uint8Array([1]).buffer;
request.respond(200,
{'Content-Type': 'application/vnd.apple.mpegurl'},
'#EXTM3U\n' +
'#EXT-X-PLAYLIST-TYPE:VOD\n' +
'#EXT-X-TARGETDURATION:10\n');
};
player.hls('manifest/master.m3u8');
try {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
customResponse(requests[0]);
requests[0].respond(200, null,
'#EXTM3U\n' +
'#EXT-X-PLAYLIST-TYPE:VOD\n' +
'#EXT-X-TARGETDURATION:10\n');
} catch(e) {
ok(false, 'an error was thrown');
throw e;
......