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 @@ ...@@ -134,6 +134,7 @@
134 } 134 }
135 135
136 result.playlists[i] = videojs.util.mergeOptions(playlist, media); 136 result.playlists[i] = videojs.util.mergeOptions(playlist, media);
137 result.playlists[media.uri] = result.playlists[i];
137 changed = true; 138 changed = true;
138 } 139 }
139 } 140 }
...@@ -143,10 +144,15 @@ ...@@ -143,10 +144,15 @@
143 PlaylistLoader = function(srcUrl) { 144 PlaylistLoader = function(srcUrl) {
144 var 145 var
145 loader = this, 146 loader = this,
147 media,
146 request, 148 request,
147 149
148 haveMetadata = function(error, url) { 150 haveMetadata = function(error, url) {
149 var parser, refreshDelay, update; 151 var parser, refreshDelay, update;
152
153 // any in-flight request is now finished
154 request = null;
155
150 if (error) { 156 if (error) {
151 loader.error = { 157 loader.error = {
152 status: this.status, 158 status: this.status,
...@@ -167,7 +173,7 @@ ...@@ -167,7 +173,7 @@
167 refreshDelay = (parser.manifest.targetDuration || 10) * 1000; 173 refreshDelay = (parser.manifest.targetDuration || 10) * 1000;
168 if (update) { 174 if (update) {
169 loader.master = update; 175 loader.master = update;
170 loader.media = parser.manifest; 176 media = loader.master.playlists[url];
171 } else { 177 } else {
172 // if the playlist is unchanged since the last reload, 178 // if the playlist is unchanged since the last reload,
173 // try again after half the target duration 179 // try again after half the target duration
...@@ -175,7 +181,7 @@ ...@@ -175,7 +181,7 @@
175 } 181 }
176 182
177 // refresh live playlists after a target duration passes 183 // refresh live playlists after a target duration passes
178 if (!loader.media.endList) { 184 if (!loader.media().endList) {
179 window.setTimeout(function() { 185 window.setTimeout(function() {
180 loader.trigger('mediaupdatetimeout'); 186 loader.trigger('mediaupdatetimeout');
181 }, refreshDelay); 187 }, refreshDelay);
...@@ -190,6 +196,49 @@ ...@@ -190,6 +196,49 @@
190 196
191 loader.state = 'HAVE_NOTHING'; 197 loader.state = 'HAVE_NOTHING';
192 198
199 /**
200 * When called without any arguments, returns the currently
201 * active media playlist. When called with a single argument,
202 * triggers the playlist loader to asynchronously switch to the
203 * specified media playlist. Calling this method while the
204 * loader is in the HAVE_NOTHING or HAVE_MASTER states causes an
205 * error to be emitted but otherwise has no effect.
206 * @param playlist (optional) {object} the parsed media playlist
207 * object to switch to
208 */
209 loader.media = function(playlist) {
210 // getter
211 if (!playlist) {
212 return media;
213 }
214
215 // setter
216 if (loader.state === 'HAVE_NOTHING' || loader.state === 'HAVE_MASTER') {
217 throw new Error('Cannot switch media playlist from ' + loader.state);
218 }
219 loader.state = 'SWITCHING_MEDIA';
220
221 // abort any outstanding playlist refreshes
222 if (request) {
223 request.abort();
224 request = null;
225 }
226
227 // find the playlist object if the target playlist has been
228 // specified by URI
229 if (typeof playlist === 'string') {
230 if (!loader.master.playlists[playlist]) {
231 throw new Error('Unknown playlist URI: ' + playlist);
232 }
233 playlist = loader.master.playlists[playlist];
234 }
235
236 // request the new playlist
237 request = xhr(resolveUrl(loader.master.uri, playlist.uri), function(error) {
238 haveMetadata.call(this, error, playlist.uri);
239 });
240 };
241
193 // live playlist staleness timeout 242 // live playlist staleness timeout
194 loader.on('mediaupdatetimeout', function() { 243 loader.on('mediaupdatetimeout', function() {
195 if (loader.state !== 'HAVE_METADATA') { 244 if (loader.state !== 'HAVE_METADATA') {
...@@ -198,15 +247,15 @@ ...@@ -198,15 +247,15 @@
198 } 247 }
199 248
200 loader.state = 'HAVE_CURRENT_METADATA'; 249 loader.state = 'HAVE_CURRENT_METADATA';
201 request = xhr(resolveUrl(loader.master.uri, loader.media.uri), 250 request = xhr(resolveUrl(loader.master.uri, loader.media().uri),
202 function(error) { 251 function(error) {
203 haveMetadata.call(this, error, loader.media.uri); 252 haveMetadata.call(this, error, loader.media().uri);
204 }); 253 });
205 }); 254 });
206 255
207 // request the specified URL 256 // request the specified URL
208 xhr(srcUrl, function(error) { 257 xhr(srcUrl, function(error) {
209 var parser; 258 var parser, i;
210 259
211 if (error) { 260 if (error) {
212 loader.error = { 261 loader.error = {
...@@ -227,6 +276,13 @@ ...@@ -227,6 +276,13 @@
227 // loaded a master playlist 276 // loaded a master playlist
228 if (parser.manifest.playlists) { 277 if (parser.manifest.playlists) {
229 loader.master = parser.manifest; 278 loader.master = parser.manifest;
279
280 // setup by-URI lookups
281 i = loader.master.playlists.length;
282 while (i--) {
283 loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i];
284 }
285
230 request = xhr(resolveUrl(srcUrl, parser.manifest.playlists[0].uri), 286 request = xhr(resolveUrl(srcUrl, parser.manifest.playlists[0].uri),
231 function(error) { 287 function(error) {
232 // pass along the URL specified in the master playlist 288 // pass along the URL specified in the master playlist
...@@ -245,6 +301,7 @@ ...@@ -245,6 +301,7 @@
245 uri: srcUrl 301 uri: srcUrl
246 }] 302 }]
247 }; 303 };
304 loader.master.playlists[srcUrl] = loader.master.playlists[0];
248 return haveMetadata.call(this, null, srcUrl); 305 return haveMetadata.call(this, null, srcUrl);
249 }); 306 });
250 }; 307 };
......
...@@ -72,8 +72,8 @@ ...@@ -72,8 +72,8 @@
72 '0.ts\n' + 72 '0.ts\n' +
73 '#EXT-X-ENDLIST\n'); 73 '#EXT-X-ENDLIST\n');
74 ok(loader.master, 'infers a master playlist'); 74 ok(loader.master, 'infers a master playlist');
75 ok(loader.media, 'sets the media playlist'); 75 ok(loader.media(), 'sets the media playlist');
76 ok(loader.media.uri, 'sets the media playlist URI'); 76 ok(loader.media().uri, 'sets the media playlist URI');
77 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); 77 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
78 strictEqual(0, requests.length, 'no more requests are made'); 78 strictEqual(0, requests.length, 'no more requests are made');
79 }); 79 });
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
85 '#EXTINF:10,\n' + 85 '#EXTINF:10,\n' +
86 '0.ts\n'); 86 '0.ts\n');
87 ok(loader.master, 'infers a master playlist'); 87 ok(loader.master, 'infers a master playlist');
88 ok(loader.media, 'sets the media playlist'); 88 ok(loader.media(), 'sets the media playlist');
89 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); 89 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
90 }); 90 });
91 91
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
113 '#EXTINF:10,\n' + 113 '#EXTINF:10,\n' +
114 '0.ts\n'); 114 '0.ts\n');
115 ok(loader.master, 'sets the master playlist'); 115 ok(loader.master, 'sets the master playlist');
116 ok(loader.media, 'sets the media playlist'); 116 ok(loader.media(), 'sets the media playlist');
117 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); 117 strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
118 }); 118 });
119 119
...@@ -241,4 +241,109 @@ ...@@ -241,4 +241,109 @@
241 strictEqual(errors, 1, 'emitted an error'); 241 strictEqual(errors, 1, 'emitted an error');
242 strictEqual(loader.error.status, 500, 'captured the status code'); 242 strictEqual(loader.error.status, 500, 'captured the status code');
243 }); 243 });
244
245 test('switches media playlists when requested', function() {
246 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
247 requests.pop().respond(200, null,
248 '#EXTM3U\n' +
249 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
250 'low.m3u8\n' +
251 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
252 'high.m3u8\n');
253 requests.pop().respond(200, null,
254 '#EXTM3U\n' +
255 '#EXT-X-MEDIA-SEQUENCE:0\n' +
256 '#EXTINF:10,\n' +
257 'low-0.ts\n');
258
259 loader.media(loader.master.playlists[1]);
260 strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
261
262 requests.pop().respond(200, null,
263 '#EXTM3U\n' +
264 '#EXT-X-MEDIA-SEQUENCE:0\n' +
265 '#EXTINF:10,\n' +
266 'high-0.ts\n');
267 strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
268 strictEqual(loader.media(),
269 loader.master.playlists[1],
270 'updated the active media');
271 });
272
273 test('can switch media playlists based on URI', function() {
274 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
275 requests.pop().respond(200, null,
276 '#EXTM3U\n' +
277 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
278 'low.m3u8\n' +
279 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
280 'high.m3u8\n');
281 requests.pop().respond(200, null,
282 '#EXTM3U\n' +
283 '#EXT-X-MEDIA-SEQUENCE:0\n' +
284 '#EXTINF:10,\n' +
285 'low-0.ts\n');
286
287 loader.media('high.m3u8');
288 strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
289
290 requests.pop().respond(200, null,
291 '#EXTM3U\n' +
292 '#EXT-X-MEDIA-SEQUENCE:0\n' +
293 '#EXTINF:10,\n' +
294 'high-0.ts\n');
295 strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
296 strictEqual(loader.media(),
297 loader.master.playlists[1],
298 'updated the active media');
299 });
300
301 test('aborts in-flight playlist refreshes when switching', function() {
302 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
303 requests.pop().respond(200, null,
304 '#EXTM3U\n' +
305 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
306 'low.m3u8\n' +
307 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
308 'high.m3u8\n');
309 requests.pop().respond(200, null,
310 '#EXTM3U\n' +
311 '#EXT-X-MEDIA-SEQUENCE:0\n' +
312 '#EXTINF:10,\n' +
313 'low-0.ts\n');
314 clock.tick(10 * 1000);
315 loader.media('high.m3u8');
316 strictEqual(requests[0].aborted, true, 'aborted refresh request');
317 strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
318 });
319
320 test('throws an error if a media switch is initiated too early', function() {
321 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
322
323 throws(function() {
324 loader.media('high.m3u8');
325 }, 'threw an error from HAVE_NOTHING');
326
327 requests.pop().respond(200, null,
328 '#EXTM3U\n' +
329 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
330 'low.m3u8\n' +
331 '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
332 'high.m3u8\n');
333 throws(function() {
334 loader.media('high.m3u8');
335 }, 'throws an error from HAVE_MASTER');
336 });
337
338 test('throws an error if a switch to an unrecognized playlist is requested', function() {
339 var loader = new videojs.hls.PlaylistLoader('master.m3u8');
340 requests.pop().respond(200, null,
341 '#EXTM3U\n' +
342 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
343 'media.m3u8\n');
344
345 throws(function() {
346 loader.media('unrecognized.m3u8');
347 }, 'throws an error');
348 });
244 })(window); 349 })(window);
......