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.
Showing
3 changed files
with
171 additions
and
9 deletions
No preview for this file type
... | @@ -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); | ... | ... |
-
Please register or sign in to post a comment