1cf4604e by David LaPalomento

Allow the active playlist to be changed

Add a new state, SWITCHING_MEDIA, that manages requests to change the active media playlist. Track media playlists by-URI on the master playlist so they can be looked up more easily.
1 parent 259d464f
......@@ -134,6 +134,7 @@
}
result.playlists[i] = videojs.util.mergeOptions(playlist, media);
result.playlists[media.uri] = result.playlists[i];
changed = true;
}
}
......@@ -143,10 +144,15 @@
PlaylistLoader = function(srcUrl) {
var
loader = this,
media,
request,
haveMetadata = function(error, url) {
var parser, refreshDelay, update;
// any in-flight request is now finished
request = null;
if (error) {
loader.error = {
status: this.status,
......@@ -167,7 +173,7 @@
refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
if (update) {
loader.master = update;
loader.media = parser.manifest;
media = loader.master.playlists[url];
} else {
// if the playlist is unchanged since the last reload,
// try again after half the target duration
......@@ -175,7 +181,7 @@
}
// refresh live playlists after a target duration passes
if (!loader.media.endList) {
if (!loader.media().endList) {
window.setTimeout(function() {
loader.trigger('mediaupdatetimeout');
}, refreshDelay);
......@@ -190,6 +196,49 @@
loader.state = 'HAVE_NOTHING';
/**
* When called without any arguments, returns the currently
* active media playlist. When called with a single argument,
* triggers the playlist loader to asynchronously switch to the
* specified media playlist. Calling this method while the
* loader is in the HAVE_NOTHING or HAVE_MASTER states causes an
* error to be emitted but otherwise has no effect.
* @param playlist (optional) {object} the parsed media playlist
* object to switch to
*/
loader.media = function(playlist) {
// getter
if (!playlist) {
return media;
}
// setter
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
if (typeof playlist === 'string') {
if (!loader.master.playlists[playlist]) {
throw new Error('Unknown playlist URI: ' + playlist);
}
playlist = loader.master.playlists[playlist];
}
// request the new playlist
request = xhr(resolveUrl(loader.master.uri, playlist.uri), function(error) {
haveMetadata.call(this, error, playlist.uri);
});
};
// live playlist staleness timeout
loader.on('mediaupdatetimeout', function() {
if (loader.state !== 'HAVE_METADATA') {
......@@ -198,15 +247,15 @@
}
loader.state = 'HAVE_CURRENT_METADATA';
request = xhr(resolveUrl(loader.master.uri, loader.media.uri),
request = xhr(resolveUrl(loader.master.uri, loader.media().uri),
function(error) {
haveMetadata.call(this, error, loader.media.uri);
haveMetadata.call(this, error, loader.media().uri);
});
});
// request the specified URL
xhr(srcUrl, function(error) {
var parser;
var parser, i;
if (error) {
loader.error = {
......@@ -227,6 +276,13 @@
// loaded a master playlist
if (parser.manifest.playlists) {
loader.master = parser.manifest;
// setup by-URI lookups
i = loader.master.playlists.length;
while (i--) {
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
......@@ -245,6 +301,7 @@
uri: srcUrl
}]
};
loader.master.playlists[srcUrl] = loader.master.playlists[0];
return haveMetadata.call(this, null, srcUrl);
});
};
......
......@@ -72,8 +72,8 @@
'0.ts\n' +
'#EXT-X-ENDLIST\n');
ok(loader.master, 'infers a master playlist');
ok(loader.media, 'sets the media playlist');
ok(loader.media.uri, 'sets the media playlist URI');
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');
});
......@@ -85,7 +85,7 @@
'#EXTINF:10,\n' +
'0.ts\n');
ok(loader.master, 'infers a master playlist');
ok(loader.media, 'sets the media playlist');
ok(loader.media(), 'sets the media playlist');
strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
});
......@@ -113,7 +113,7 @@
'#EXTINF:10,\n' +
'0.ts\n');
ok(loader.master, 'sets the master playlist');
ok(loader.media, 'sets the media playlist');
ok(loader.media(), 'sets the media playlist');
strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
});
......@@ -241,4 +241,109 @@
strictEqual(errors, 1, 'emitted an error');
strictEqual(loader.error.status, 500, 'captured the status code');
});
test('switches media playlists when requested', 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');
loader.media(loader.master.playlists[1]);
strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n');
strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
strictEqual(loader.media(),
loader.master.playlists[1],
'updated the active media');
});
test('can switch media playlists based on URI', 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');
loader.media('high.m3u8');
strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:10,\n' +
'high-0.ts\n');
strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
strictEqual(loader.media(),
loader.master.playlists[1],
'updated the active media');
});
test('aborts in-flight playlist refreshes when switching', 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');
clock.tick(10 * 1000);
loader.media('high.m3u8');
strictEqual(requests[0].aborted, true, 'aborted refresh request');
strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
});
test('throws an error if a media switch is initiated too early', function() {
var loader = new videojs.hls.PlaylistLoader('master.m3u8');
throws(function() {
loader.media('high.m3u8');
}, 'threw an error from HAVE_NOTHING');
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');
throws(function() {
loader.media('high.m3u8');
}, 'throws an error from HAVE_MASTER');
});
test('throws an error if a switch to an unrecognized playlist is requested', function() {
var loader = new videojs.hls.PlaylistLoader('master.m3u8');
requests.pop().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media.m3u8\n');
throws(function() {
loader.media('unrecognized.m3u8');
}, 'throws an error');
});
})(window);
......