Merge pull request #50 from videojs/feature/playlist-loader
Refactor M3U8 loading
Showing
12 changed files
with
758 additions
and
381 deletions
... | @@ -30,7 +30,8 @@ module.exports = function(grunt) { | ... | @@ -30,7 +30,8 @@ module.exports = function(grunt) { |
30 | 'src/aac-stream.js', | 30 | 'src/aac-stream.js', |
31 | 'src/segment-parser.js', | 31 | 'src/segment-parser.js', |
32 | 'src/stream.js', | 32 | 'src/stream.js', |
33 | 'src/m3u8/m3u8-parser.js' | 33 | 'src/m3u8/m3u8-parser.js', |
34 | 'src/playlist-loader.js' | ||
34 | ], | 35 | ], |
35 | dest: 'dist/videojs.hls.js' | 36 | dest: 'dist/videojs.hls.js' |
36 | } | 37 | } | ... | ... |
... | @@ -68,19 +68,25 @@ See html5rocks's [article](http://www.html5rocks.com/en/tutorials/cors/) | ... | @@ -68,19 +68,25 @@ See html5rocks's [article](http://www.html5rocks.com/en/tutorials/cors/) |
68 | for more info. | 68 | for more info. |
69 | 69 | ||
70 | ### Runtime Properties | 70 | ### Runtime Properties |
71 | #### player.hls.master | 71 | #### player.hls.playlists.master |
72 | Type: `object` | 72 | Type: `object` |
73 | 73 | ||
74 | An object representing the parsed master playlist. If a media playlist | 74 | An object representing the parsed master playlist. If a media playlist |
75 | is loaded directly, a master playlist with only one entry will be | 75 | is loaded directly, a master playlist with only one entry will be |
76 | created. | 76 | created. |
77 | 77 | ||
78 | #### player.hls.media | 78 | #### player.hls.playlists.media |
79 | Type: `object` | 79 | Type: `function` |
80 | 80 | ||
81 | An object representing the currently selected media playlist. This is | 81 | A function that can be used to retrieve or modify the currently active |
82 | the playlist that is being referred to when a additional video data | 82 | media playlist. The active media playlist is referred to when |
83 | needs to be downloaded. | 83 | additional video data needs to be downloaded. Calling this function |
84 | with no arguments returns the parsed playlist object for the active | ||
85 | media playlist. Calling this function with a playlist object from the | ||
86 | master playlist or a URI string as specified in the master playlist | ||
87 | will kick off an asynchronous load of the specified media | ||
88 | playlist. Once it has been retreived, it will become the active media | ||
89 | playlist. | ||
84 | 90 | ||
85 | #### player.hls.mediaIndex | 91 | #### player.hls.mediaIndex |
86 | Type: `number` | 92 | Type: `number` | ... | ... |
docs/playlist-loader-states.graffle
0 → 100644
No preview for this file type
docs/playlist-loader-states.png
0 → 100644
43.2 KB
... | @@ -25,6 +25,7 @@ | ... | @@ -25,6 +25,7 @@ |
25 | <!-- m3u8 handling --> | 25 | <!-- m3u8 handling --> |
26 | <script src="src/stream.js"></script> | 26 | <script src="src/stream.js"></script> |
27 | <script src="src/m3u8/m3u8-parser.js"></script> | 27 | <script src="src/m3u8/m3u8-parser.js"></script> |
28 | <script src="src/playlist-loader.js"></script> | ||
28 | 29 | ||
29 | <!-- example MPEG2-TS segments --> | 30 | <!-- example MPEG2-TS segments --> |
30 | <!-- bipbop --> | 31 | <!-- bipbop --> | ... | ... |
src/playlist-loader.js
0 → 100644
1 | /** | ||
2 | * A state machine that manages the loading, caching, and updating of | ||
3 | * M3U8 playlists. | ||
4 | */ | ||
5 | (function(window, videojs) { | ||
6 | 'use strict'; | ||
7 | var | ||
8 | resolveUrl = videojs.hls.resolveUrl, | ||
9 | xhr = videojs.hls.xhr, | ||
10 | |||
11 | /** | ||
12 | * Returns a new master playlist that is the result of merging an | ||
13 | * updated media playlist into the original version. If the | ||
14 | * updated media playlist does not match any of the playlist | ||
15 | * entries in the original master playlist, null is returned. | ||
16 | * @param master {object} a parsed master M3U8 object | ||
17 | * @param media {object} a parsed media M3U8 object | ||
18 | * @return {object} a new object that represents the original | ||
19 | * master playlist with the updated media playlist merged in, or | ||
20 | * null if the merge produced no change. | ||
21 | */ | ||
22 | updateMaster = function(master, media) { | ||
23 | var | ||
24 | changed = false, | ||
25 | result = videojs.util.mergeOptions(master, {}), | ||
26 | i, | ||
27 | playlist; | ||
28 | |||
29 | i = master.playlists.length; | ||
30 | while (i--) { | ||
31 | playlist = result.playlists[i]; | ||
32 | if (playlist.uri === media.uri) { | ||
33 | // consider the playlist unchanged if the number of segments | ||
34 | // are equal and the media sequence number is unchanged | ||
35 | if (playlist.segments && | ||
36 | media.segments && | ||
37 | playlist.segments.length === media.segments.length && | ||
38 | playlist.mediaSequence === media.mediaSequence) { | ||
39 | continue; | ||
40 | } | ||
41 | |||
42 | result.playlists[i] = videojs.util.mergeOptions(playlist, media); | ||
43 | result.playlists[media.uri] = result.playlists[i]; | ||
44 | changed = true; | ||
45 | } | ||
46 | } | ||
47 | return changed ? result : null; | ||
48 | }, | ||
49 | |||
50 | PlaylistLoader = function(srcUrl, withCredentials) { | ||
51 | var | ||
52 | loader = this, | ||
53 | media, | ||
54 | request, | ||
55 | |||
56 | haveMetadata = function(error, xhr, url) { | ||
57 | var parser, refreshDelay, update; | ||
58 | |||
59 | // any in-flight request is now finished | ||
60 | request = null; | ||
61 | |||
62 | if (error) { | ||
63 | loader.error = { | ||
64 | status: xhr.status, | ||
65 | message: 'HLS playlist request error at URL: ' + url, | ||
66 | code: (xhr.status >= 500) ? 4 : 2 | ||
67 | }; | ||
68 | return loader.trigger('error'); | ||
69 | } | ||
70 | |||
71 | loader.state = 'HAVE_METADATA'; | ||
72 | |||
73 | parser = new videojs.m3u8.Parser(); | ||
74 | parser.push(xhr.responseText); | ||
75 | parser.manifest.uri = url; | ||
76 | |||
77 | // merge this playlist into the master | ||
78 | update = updateMaster(loader.master, parser.manifest); | ||
79 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | ||
80 | if (update) { | ||
81 | loader.master = update; | ||
82 | media = loader.master.playlists[url]; | ||
83 | } else { | ||
84 | // if the playlist is unchanged since the last reload, | ||
85 | // try again after half the target duration | ||
86 | refreshDelay /= 2; | ||
87 | } | ||
88 | |||
89 | // refresh live playlists after a target duration passes | ||
90 | if (!loader.media().endList) { | ||
91 | window.setTimeout(function() { | ||
92 | loader.trigger('mediaupdatetimeout'); | ||
93 | }, refreshDelay); | ||
94 | } | ||
95 | |||
96 | loader.trigger('loadedplaylist'); | ||
97 | }; | ||
98 | |||
99 | PlaylistLoader.prototype.init.call(this); | ||
100 | |||
101 | if (!srcUrl) { | ||
102 | throw new Error('A non-empty playlist URL is required'); | ||
103 | } | ||
104 | |||
105 | loader.state = 'HAVE_NOTHING'; | ||
106 | |||
107 | /** | ||
108 | * When called without any arguments, returns the currently | ||
109 | * active media playlist. When called with a single argument, | ||
110 | * triggers the playlist loader to asynchronously switch to the | ||
111 | * specified media playlist. Calling this method while the | ||
112 | * loader is in the HAVE_NOTHING or HAVE_MASTER states causes an | ||
113 | * error to be emitted but otherwise has no effect. | ||
114 | * @param playlist (optional) {object} the parsed media playlist | ||
115 | * object to switch to | ||
116 | */ | ||
117 | loader.media = function(playlist) { | ||
118 | // getter | ||
119 | if (!playlist) { | ||
120 | return media; | ||
121 | } | ||
122 | |||
123 | // setter | ||
124 | if (loader.state === 'HAVE_NOTHING' || loader.state === 'HAVE_MASTER') { | ||
125 | throw new Error('Cannot switch media playlist from ' + loader.state); | ||
126 | } | ||
127 | |||
128 | // find the playlist object if the target playlist has been | ||
129 | // specified by URI | ||
130 | if (typeof playlist === 'string') { | ||
131 | if (!loader.master.playlists[playlist]) { | ||
132 | throw new Error('Unknown playlist URI: ' + playlist); | ||
133 | } | ||
134 | playlist = loader.master.playlists[playlist]; | ||
135 | } | ||
136 | |||
137 | if (playlist.uri === media.uri) { | ||
138 | // switching to the currently active playlist is a no-op | ||
139 | return; | ||
140 | } | ||
141 | |||
142 | loader.state = 'SWITCHING_MEDIA'; | ||
143 | |||
144 | // abort any outstanding playlist refreshes | ||
145 | if (request) { | ||
146 | request.abort(); | ||
147 | request = null; | ||
148 | } | ||
149 | |||
150 | // request the new playlist | ||
151 | request = xhr({ | ||
152 | url: resolveUrl(loader.master.uri, playlist.uri), | ||
153 | withCredentials: withCredentials | ||
154 | }, function(error) { | ||
155 | haveMetadata(error, this, playlist.uri); | ||
156 | }); | ||
157 | }; | ||
158 | |||
159 | // live playlist staleness timeout | ||
160 | loader.on('mediaupdatetimeout', function() { | ||
161 | if (loader.state !== 'HAVE_METADATA') { | ||
162 | // only refresh the media playlist if no other activity is going on | ||
163 | return; | ||
164 | } | ||
165 | |||
166 | loader.state = 'HAVE_CURRENT_METADATA'; | ||
167 | request = xhr({ | ||
168 | url: resolveUrl(loader.master.uri, loader.media().uri), | ||
169 | withCredentials: withCredentials | ||
170 | }, function(error) { | ||
171 | haveMetadata(error, this, loader.media().uri); | ||
172 | }); | ||
173 | }); | ||
174 | |||
175 | // request the specified URL | ||
176 | xhr({ | ||
177 | url: srcUrl, | ||
178 | withCredentials: withCredentials | ||
179 | }, function(error) { | ||
180 | var parser, i; | ||
181 | |||
182 | if (error) { | ||
183 | loader.error = { | ||
184 | status: this.status, | ||
185 | message: 'HLS playlist request error at URL: ' + srcUrl, | ||
186 | code: 2 // MEDIA_ERR_NETWORK | ||
187 | }; | ||
188 | return loader.trigger('error'); | ||
189 | } | ||
190 | |||
191 | parser = new videojs.m3u8.Parser(); | ||
192 | parser.push(this.responseText); | ||
193 | |||
194 | loader.state = 'HAVE_MASTER'; | ||
195 | |||
196 | parser.manifest.uri = srcUrl; | ||
197 | |||
198 | // loaded a master playlist | ||
199 | if (parser.manifest.playlists) { | ||
200 | loader.master = parser.manifest; | ||
201 | |||
202 | // setup by-URI lookups | ||
203 | i = loader.master.playlists.length; | ||
204 | while (i--) { | ||
205 | loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; | ||
206 | } | ||
207 | |||
208 | request = xhr({ | ||
209 | url: resolveUrl(srcUrl, parser.manifest.playlists[0].uri), | ||
210 | withCredentials: withCredentials | ||
211 | }, function(error) { | ||
212 | // pass along the URL specified in the master playlist | ||
213 | haveMetadata(error, | ||
214 | this, | ||
215 | parser.manifest.playlists[0].uri); | ||
216 | loader.trigger('loadedmetadata'); | ||
217 | }); | ||
218 | return loader.trigger('loadedplaylist'); | ||
219 | } | ||
220 | |||
221 | // loaded a media playlist | ||
222 | // infer a master playlist if none was previously requested | ||
223 | loader.master = { | ||
224 | uri: window.location.href, | ||
225 | playlists: [{ | ||
226 | uri: srcUrl | ||
227 | }] | ||
228 | }; | ||
229 | loader.master.playlists[srcUrl] = loader.master.playlists[0]; | ||
230 | haveMetadata(null, this, srcUrl); | ||
231 | return loader.trigger('loadedmetadata'); | ||
232 | }); | ||
233 | }; | ||
234 | PlaylistLoader.prototype = new videojs.hls.Stream(); | ||
235 | |||
236 | videojs.hls.PlaylistLoader = PlaylistLoader; | ||
237 | })(window, window.videojs); |
... | @@ -32,8 +32,6 @@ videojs.hls = { | ... | @@ -32,8 +32,6 @@ videojs.hls = { |
32 | 32 | ||
33 | var | 33 | var |
34 | 34 | ||
35 | settings, | ||
36 | |||
37 | // the desired length of video to maintain in the buffer, in seconds | 35 | // the desired length of video to maintain in the buffer, in seconds |
38 | goalBufferLength = 5, | 36 | goalBufferLength = 5, |
39 | 37 | ||
... | @@ -104,9 +102,12 @@ var | ... | @@ -104,9 +102,12 @@ var |
104 | * inititated. If it is an object, it should contain a `url` | 102 | * inititated. If it is an object, it should contain a `url` |
105 | * property that indicates the URL to request and optionally a | 103 | * property that indicates the URL to request and optionally a |
106 | * `method` which is the type of HTTP request to send. | 104 | * `method` which is the type of HTTP request to send. |
105 | * @param callback (optional) {function} a function to call when the | ||
106 | * request completes. If the request was not successful, the first | ||
107 | * argument will be falsey. | ||
107 | * @return {object} the XMLHttpRequest that was initiated. | 108 | * @return {object} the XMLHttpRequest that was initiated. |
108 | */ | 109 | */ |
109 | xhr = function(url, callback) { | 110 | xhr = videojs.hls.xhr = function(url, callback) { |
110 | var | 111 | var |
111 | options = { | 112 | options = { |
112 | method: 'GET' | 113 | method: 'GET' |
... | @@ -128,7 +129,7 @@ var | ... | @@ -128,7 +129,7 @@ var |
128 | if (options.responseType) { | 129 | if (options.responseType) { |
129 | request.responseType = options.responseType; | 130 | request.responseType = options.responseType; |
130 | } | 131 | } |
131 | if (settings.withCredentials) { | 132 | if (options.withCredentials) { |
132 | request.withCredentials = true; | 133 | request.withCredentials = true; |
133 | } | 134 | } |
134 | 135 | ||
... | @@ -193,15 +194,20 @@ var | ... | @@ -193,15 +194,20 @@ var |
193 | */ | 194 | */ |
194 | translateMediaIndex = function(mediaIndex, original, update) { | 195 | translateMediaIndex = function(mediaIndex, original, update) { |
195 | var | 196 | var |
196 | i = update.segments.length, | 197 | i, |
197 | originalSegment; | 198 | originalSegment; |
198 | 199 | ||
199 | // no segments have been loaded from the original playlist | 200 | // no segments have been loaded from the original playlist |
200 | if (mediaIndex === 0) { | 201 | if (mediaIndex === 0) { |
201 | return 0; | 202 | return 0; |
202 | } | 203 | } |
204 | if (!(update && update.segments)) { | ||
205 | // let the media index be zero when there are no segments defined | ||
206 | return 0; | ||
207 | } | ||
203 | 208 | ||
204 | // try to sync based on URI | 209 | // try to sync based on URI |
210 | i = update.segments.length; | ||
205 | originalSegment = original.segments[mediaIndex - 1]; | 211 | originalSegment = original.segments[mediaIndex - 1]; |
206 | while (i--) { | 212 | while (i--) { |
207 | if (originalSegment.uri === update.segments[i].uri) { | 213 | if (originalSegment.uri === update.segments[i].uri) { |
... | @@ -250,7 +256,7 @@ var | ... | @@ -250,7 +256,7 @@ var |
250 | * with `path` | 256 | * with `path` |
251 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue | 257 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue |
252 | */ | 258 | */ |
253 | resolveUrl = function(basePath, path) { | 259 | resolveUrl = videojs.hls.resolveUrl = function(basePath, path) { |
254 | // use the base element to get the browser to handle URI resolution | 260 | // use the base element to get the browser to handle URI resolution |
255 | var | 261 | var |
256 | oldBase = document.querySelector('base'), | 262 | oldBase = document.querySelector('base'), |
... | @@ -291,11 +297,9 @@ var | ... | @@ -291,11 +297,9 @@ var |
291 | player = this, | 297 | player = this, |
292 | srcUrl, | 298 | srcUrl, |
293 | 299 | ||
294 | playlistXhr, | ||
295 | segmentXhr, | 300 | segmentXhr, |
296 | loadedPlaylist, | 301 | settings, |
297 | fillBuffer, | 302 | fillBuffer, |
298 | updateCurrentPlaylist, | ||
299 | updateDuration; | 303 | updateDuration; |
300 | 304 | ||
301 | // if the video element supports HLS natively, do nothing | 305 | // if the video element supports HLS natively, do nothing |
... | @@ -374,7 +378,8 @@ var | ... | @@ -374,7 +378,8 @@ var |
374 | 378 | ||
375 | player.on('seeking', function() { | 379 | player.on('seeking', function() { |
376 | var currentTime = player.currentTime(); | 380 | var currentTime = player.currentTime(); |
377 | player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime); | 381 | player.hls.mediaIndex = getMediaIndexByTime(player.hls.playlists.media(), |
382 | currentTime); | ||
378 | 383 | ||
379 | // abort any segments still being decoded | 384 | // abort any segments still being decoded |
380 | player.hls.sourceBuffer.abort(); | 385 | player.hls.sourceBuffer.abort(); |
... | @@ -405,36 +410,6 @@ var | ... | @@ -405,36 +410,6 @@ var |
405 | }; | 410 | }; |
406 | 411 | ||
407 | /** | 412 | /** |
408 | * Determine whether the current media playlist should be changed | ||
409 | * and trigger a switch if necessary. If a sufficiently fresh | ||
410 | * version of the target playlist is available, the switch will take | ||
411 | * effect immediately. Otherwise, the target playlist will be | ||
412 | * refreshed. | ||
413 | */ | ||
414 | updateCurrentPlaylist = function() { | ||
415 | var playlist, mediaSequence; | ||
416 | playlist = player.hls.selectPlaylist(); | ||
417 | mediaSequence = player.hls.mediaIndex + (player.hls.media.mediaSequence || 0); | ||
418 | if (!playlist.segments || | ||
419 | mediaSequence < (playlist.mediaSequence || 0) || | ||
420 | mediaSequence > (playlist.mediaSequence || 0) + playlist.segments.length) { | ||
421 | |||
422 | if (playlistXhr) { | ||
423 | playlistXhr.abort(); | ||
424 | } | ||
425 | playlistXhr = xhr(resolveUrl(srcUrl, playlist.uri), loadedPlaylist); | ||
426 | } else { | ||
427 | player.hls.mediaIndex = | ||
428 | translateMediaIndex(player.hls.mediaIndex, | ||
429 | player.hls.media, | ||
430 | playlist); | ||
431 | player.hls.media = playlist; | ||
432 | |||
433 | updateDuration(player.hls.media); | ||
434 | } | ||
435 | }; | ||
436 | |||
437 | /** | ||
438 | * Chooses the appropriate media playlist based on the current | 413 | * Chooses the appropriate media playlist based on the current |
439 | * bandwidth estimate and the player size. | 414 | * bandwidth estimate and the player size. |
440 | * @return the highest bitrate playlist less than the currently detected | 415 | * @return the highest bitrate playlist less than the currently detected |
... | @@ -443,7 +418,7 @@ var | ... | @@ -443,7 +418,7 @@ var |
443 | player.hls.selectPlaylist = function () { | 418 | player.hls.selectPlaylist = function () { |
444 | var | 419 | var |
445 | effectiveBitrate, | 420 | effectiveBitrate, |
446 | sortedPlaylists = player.hls.master.playlists.slice(), | 421 | sortedPlaylists = player.hls.playlists.master.playlists.slice(), |
447 | bandwidthPlaylists = [], | 422 | bandwidthPlaylists = [], |
448 | i = sortedPlaylists.length, | 423 | i = sortedPlaylists.length, |
449 | variant, | 424 | variant, |
... | @@ -508,97 +483,6 @@ var | ... | @@ -508,97 +483,6 @@ var |
508 | }; | 483 | }; |
509 | 484 | ||
510 | /** | 485 | /** |
511 | * Callback that is invoked when a media playlist finishes | ||
512 | * downloading. Triggers `loadedmanifest` once for each playlist | ||
513 | * that is downloaded and `loadedmetadata` after at least one | ||
514 | * media playlist has been parsed. | ||
515 | * | ||
516 | * @param error {*} truthy if the request was not successful | ||
517 | * @param url {string} a URL to the M3U8 file to process | ||
518 | */ | ||
519 | loadedPlaylist = function(error, url) { | ||
520 | var i, parser, playlist, playlistUri, refreshDelay; | ||
521 | |||
522 | // clear the current playlist XHR | ||
523 | playlistXhr = null; | ||
524 | |||
525 | if (error) { | ||
526 | player.hls.error = { | ||
527 | status: this.status, | ||
528 | message: 'HLS playlist request error at URL: ' + url, | ||
529 | code: (this.status >= 500) ? 4 : 2 | ||
530 | }; | ||
531 | return player.trigger('error'); | ||
532 | } | ||
533 | |||
534 | parser = new videojs.m3u8.Parser(); | ||
535 | parser.push(this.responseText); | ||
536 | |||
537 | // merge this playlist into the master | ||
538 | i = player.hls.master.playlists.length; | ||
539 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | ||
540 | while (i--) { | ||
541 | playlist = player.hls.master.playlists[i]; | ||
542 | playlistUri = resolveUrl(srcUrl, playlist.uri); | ||
543 | if (playlistUri === url) { | ||
544 | // if the playlist is unchanged since the last reload, | ||
545 | // try again after half the target duration | ||
546 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 | ||
547 | if (playlist.segments && | ||
548 | playlist.segments.length === parser.manifest.segments.length) { | ||
549 | refreshDelay /= 2; | ||
550 | } | ||
551 | |||
552 | player.hls.master.playlists[i] = | ||
553 | videojs.util.mergeOptions(playlist, parser.manifest); | ||
554 | |||
555 | if (playlist !== player.hls.media) { | ||
556 | continue; | ||
557 | } | ||
558 | |||
559 | // determine the new mediaIndex if we're updating the | ||
560 | // current media playlist | ||
561 | player.hls.mediaIndex = | ||
562 | translateMediaIndex(player.hls.mediaIndex, | ||
563 | playlist, | ||
564 | parser.manifest); | ||
565 | player.hls.media = parser.manifest; | ||
566 | } | ||
567 | } | ||
568 | |||
569 | // check the playlist for updates if EXT-X-ENDLIST isn't present | ||
570 | if (!parser.manifest.endList) { | ||
571 | window.setTimeout(function() { | ||
572 | if (!playlistXhr && | ||
573 | resolveUrl(srcUrl, player.hls.media.uri) === url) { | ||
574 | playlistXhr = xhr(url, loadedPlaylist); | ||
575 | } | ||
576 | }, refreshDelay); | ||
577 | } | ||
578 | |||
579 | // always start playback with the default rendition | ||
580 | if (!player.hls.media) { | ||
581 | player.hls.media = player.hls.master.playlists[0]; | ||
582 | |||
583 | // update the duration | ||
584 | updateDuration(parser.manifest); | ||
585 | |||
586 | // periodicaly check if the buffer needs to be refilled | ||
587 | player.on('timeupdate', fillBuffer); | ||
588 | |||
589 | player.trigger('loadedmanifest'); | ||
590 | player.trigger('loadedmetadata'); | ||
591 | fillBuffer(); | ||
592 | return; | ||
593 | } | ||
594 | |||
595 | // select a playlist and download its metadata if necessary | ||
596 | updateCurrentPlaylist(); | ||
597 | |||
598 | player.trigger('loadedmanifest'); | ||
599 | }; | ||
600 | |||
601 | /** | ||
602 | * Determines whether there is enough video data currently in the buffer | 486 | * Determines whether there is enough video data currently in the buffer |
603 | * and downloads a new segment if the buffered time is less than the goal. | 487 | * and downloads a new segment if the buffered time is less than the goal. |
604 | * @param offset (optional) {number} the offset into the downloaded segment | 488 | * @param offset (optional) {number} the offset into the downloaded segment |
... | @@ -618,12 +502,12 @@ var | ... | @@ -618,12 +502,12 @@ var |
618 | } | 502 | } |
619 | 503 | ||
620 | // if no segments are available, do nothing | 504 | // if no segments are available, do nothing |
621 | if (!player.hls.media.segments) { | 505 | if (!player.hls.playlists.media().segments) { |
622 | return; | 506 | return; |
623 | } | 507 | } |
624 | 508 | ||
625 | // if the video has finished downloading, stop trying to buffer | 509 | // if the video has finished downloading, stop trying to buffer |
626 | segment = player.hls.media.segments[player.hls.mediaIndex]; | 510 | segment = player.hls.playlists.media().segments[player.hls.mediaIndex]; |
627 | if (!segment) { | 511 | if (!segment) { |
628 | return; | 512 | return; |
629 | } | 513 | } |
... | @@ -639,10 +523,10 @@ var | ... | @@ -639,10 +523,10 @@ var |
639 | } | 523 | } |
640 | 524 | ||
641 | // resolve the segment URL relative to the playlist | 525 | // resolve the segment URL relative to the playlist |
642 | if (player.hls.media.uri === srcUrl) { | 526 | if (player.hls.playlists.media().uri === srcUrl) { |
643 | segmentUri = resolveUrl(srcUrl, segment.uri); | 527 | segmentUri = resolveUrl(srcUrl, segment.uri); |
644 | } else { | 528 | } else { |
645 | segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''), | 529 | segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.playlists.media().uri || ''), |
646 | segment.uri); | 530 | segment.uri); |
647 | } | 531 | } |
648 | 532 | ||
... | @@ -651,7 +535,8 @@ var | ... | @@ -651,7 +535,8 @@ var |
651 | // request the next segment | 535 | // request the next segment |
652 | segmentXhr = xhr({ | 536 | segmentXhr = xhr({ |
653 | url: segmentUri, | 537 | url: segmentUri, |
654 | responseType: 'arraybuffer' | 538 | responseType: 'arraybuffer', |
539 | withCredentials: settings.withCredentials | ||
655 | }, function(error, url) { | 540 | }, function(error, url) { |
656 | // the segment request is no longer outstanding | 541 | // the segment request is no longer outstanding |
657 | segmentXhr = null; | 542 | segmentXhr = null; |
... | @@ -702,43 +587,55 @@ var | ... | @@ -702,43 +587,55 @@ var |
702 | 587 | ||
703 | player.hls.mediaIndex++; | 588 | player.hls.mediaIndex++; |
704 | 589 | ||
705 | if (player.hls.mediaIndex === player.hls.media.segments.length) { | 590 | if (player.hls.mediaIndex === player.hls.playlists.media().segments.length) { |
706 | mediaSource.endOfStream(); | 591 | mediaSource.endOfStream(); |
707 | } | 592 | } |
708 | 593 | ||
709 | // figure out what stream the next segment should be downloaded from | 594 | // figure out what stream the next segment should be downloaded from |
710 | // with the updated bandwidth information | 595 | // with the updated bandwidth information |
711 | updateCurrentPlaylist(); | 596 | player.hls.playlists.media(player.hls.selectPlaylist()); |
712 | }); | 597 | }); |
713 | }; | 598 | }; |
714 | 599 | ||
715 | // load the MediaSource into the player | 600 | // load the MediaSource into the player |
716 | mediaSource.addEventListener('sourceopen', function() { | 601 | mediaSource.addEventListener('sourceopen', function() { |
717 | // construct the video data buffer and set the appropriate MIME type | 602 | // construct the video data buffer and set the appropriate MIME type |
718 | var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'); | 603 | var |
604 | sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"'), | ||
605 | oldMediaPlaylist; | ||
606 | |||
719 | player.hls.sourceBuffer = sourceBuffer; | 607 | player.hls.sourceBuffer = sourceBuffer; |
720 | sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); | 608 | sourceBuffer.appendBuffer(segmentParser.getFlvHeader()); |
721 | 609 | ||
722 | player.hls.mediaIndex = 0; | 610 | player.hls.mediaIndex = 0; |
723 | xhr(srcUrl, function(error, url) { | 611 | player.hls.playlists = |
724 | var uri, parser = new videojs.m3u8.Parser(); | 612 | new videojs.hls.PlaylistLoader(srcUrl, settings.withCredentials); |
725 | parser.push(this.responseText); | 613 | player.hls.playlists.on('loadedmetadata', function() { |
726 | 614 | oldMediaPlaylist = player.hls.playlists.media(); | |
727 | // master playlists | 615 | |
728 | if (parser.manifest.playlists) { | 616 | // periodicaly check if the buffer needs to be refilled |
729 | player.hls.master = parser.manifest; | 617 | fillBuffer(); |
730 | playlistXhr = xhr(resolveUrl(url, parser.manifest.playlists[0].uri), loadedPlaylist); | 618 | player.on('timeupdate', fillBuffer); |
731 | return player.trigger('loadedmanifest'); | 619 | |
732 | } else { | 620 | player.trigger('loadedmetadata'); |
733 | // infer a master playlist if a media playlist is loaded directly | 621 | }); |
734 | uri = resolveUrl(window.location.href, url); | 622 | player.hls.playlists.on('error', function() { |
735 | player.hls.master = { | 623 | player.hls.error = player.hls.playlists.error; |
736 | playlists: [{ | 624 | player.trigger('error'); |
737 | uri: uri | 625 | }); |
738 | }] | 626 | player.hls.playlists.on('loadedplaylist', function() { |
739 | }; | 627 | var updatedPlaylist = player.hls.playlists.media(); |
740 | loadedPlaylist.call(this, error, uri); | 628 | |
629 | if (!updatedPlaylist) { | ||
630 | // do nothing before an initial media playlist has been activated | ||
631 | return; | ||
741 | } | 632 | } |
633 | |||
634 | updateDuration(player.hls.playlists.media()); | ||
635 | player.hls.mediaIndex = translateMediaIndex(player.hls.mediaIndex, | ||
636 | oldMediaPlaylist, | ||
637 | updatedPlaylist); | ||
638 | oldMediaPlaylist = updatedPlaylist; | ||
742 | }); | 639 | }); |
743 | }); | 640 | }); |
744 | player.src([{ | 641 | player.src([{ | ... | ... |
... | @@ -74,6 +74,7 @@ module.exports = function(config) { | ... | @@ -74,6 +74,7 @@ module.exports = function(config) { |
74 | '../node_modules/sinon/lib/sinon/util/event.js', | 74 | '../node_modules/sinon/lib/sinon/util/event.js', |
75 | '../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js', | 75 | '../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js', |
76 | '../node_modules/sinon/lib/sinon/util/xhr_ie.js', | 76 | '../node_modules/sinon/lib/sinon/util/xhr_ie.js', |
77 | '../node_modules/sinon/lib/sinon/util/fake_timers.js', | ||
77 | '../node_modules/video.js/dist/video-js/video.js', | 78 | '../node_modules/video.js/dist/video-js/video.js', |
78 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', | 79 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', |
79 | '../test/karma-qunit-shim.js', | 80 | '../test/karma-qunit-shim.js', |
... | @@ -85,6 +86,7 @@ module.exports = function(config) { | ... | @@ -85,6 +86,7 @@ module.exports = function(config) { |
85 | '../src/segment-parser.js', | 86 | '../src/segment-parser.js', |
86 | '../src/stream.js', | 87 | '../src/stream.js', |
87 | '../src/m3u8/m3u8-parser.js', | 88 | '../src/m3u8/m3u8-parser.js', |
89 | '../src/playlist-loader.js', | ||
88 | '../tmp/manifests.js', | 90 | '../tmp/manifests.js', |
89 | '../tmp/expected.js', | 91 | '../tmp/expected.js', |
90 | 'tsSegment-bc.js', | 92 | 'tsSegment-bc.js', | ... | ... |
... | @@ -38,6 +38,7 @@ module.exports = function(config) { | ... | @@ -38,6 +38,7 @@ module.exports = function(config) { |
38 | '../node_modules/sinon/lib/sinon/util/event.js', | 38 | '../node_modules/sinon/lib/sinon/util/event.js', |
39 | '../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js', | 39 | '../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js', |
40 | '../node_modules/sinon/lib/sinon/util/xhr_ie.js', | 40 | '../node_modules/sinon/lib/sinon/util/xhr_ie.js', |
41 | '../node_modules/sinon/lib/sinon/util/fake_timers.js', | ||
41 | '../node_modules/video.js/dist/video-js/video.js', | 42 | '../node_modules/video.js/dist/video-js/video.js', |
42 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', | 43 | '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js', |
43 | '../test/karma-qunit-shim.js', | 44 | '../test/karma-qunit-shim.js', |
... | @@ -49,6 +50,7 @@ module.exports = function(config) { | ... | @@ -49,6 +50,7 @@ module.exports = function(config) { |
49 | '../src/segment-parser.js', | 50 | '../src/segment-parser.js', |
50 | '../src/stream.js', | 51 | '../src/stream.js', |
51 | '../src/m3u8/m3u8-parser.js', | 52 | '../src/m3u8/m3u8-parser.js', |
53 | '../src/playlist-loader.js', | ||
52 | '../tmp/manifests.js', | 54 | '../tmp/manifests.js', |
53 | '../tmp/expected.js', | 55 | '../tmp/expected.js', |
54 | 'tsSegment-bc.js', | 56 | 'tsSegment-bc.js', | ... | ... |
test/playlist-loader_test.js
0 → 100644
1 | (function(window) { | ||
2 | 'use strict'; | ||
3 | var | ||
4 | sinonXhr, | ||
5 | clock, | ||
6 | requests, | ||
7 | videojs = window.videojs, | ||
8 | |||
9 | // Attempts to produce an absolute URL to a given relative path | ||
10 | // based on window.location.href | ||
11 | urlTo = function(path) { | ||
12 | return window.location.href | ||
13 | .split('/') | ||
14 | .slice(0, -1) | ||
15 | .concat([path]) | ||
16 | .join('/'); | ||
17 | }; | ||
18 | |||
19 | module('Playlist Loader', { | ||
20 | setup: function() { | ||
21 | // fake XHRs | ||
22 | sinonXhr = sinon.useFakeXMLHttpRequest(); | ||
23 | requests = []; | ||
24 | sinonXhr.onCreate = function(xhr) { | ||
25 | requests.push(xhr); | ||
26 | }; | ||
27 | |||
28 | // fake timers | ||
29 | clock = sinon.useFakeTimers(); | ||
30 | }, | ||
31 | teardown: function() { | ||
32 | sinonXhr.restore(); | ||
33 | clock.restore(); | ||
34 | } | ||
35 | }); | ||
36 | |||
37 | test('throws if the playlist url is empty or undefined', function() { | ||
38 | throws(function() { | ||
39 | videojs.hls.PlaylistLoader(); | ||
40 | }, 'requires an argument'); | ||
41 | throws(function() { | ||
42 | videojs.hls.PlaylistLoader(''); | ||
43 | }, 'does not accept the empty string'); | ||
44 | }); | ||
45 | |||
46 | test('starts without any metadata', function() { | ||
47 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
48 | strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); | ||
49 | }); | ||
50 | |||
51 | test('requests the initial playlist immediately', function() { | ||
52 | new videojs.hls.PlaylistLoader('master.m3u8'); | ||
53 | strictEqual(requests.length, 1, 'made a request'); | ||
54 | strictEqual(requests[0].url, 'master.m3u8', 'requested the initial playlist'); | ||
55 | }); | ||
56 | |||
57 | test('moves to HAVE_MASTER after loading a master playlist', function() { | ||
58 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
59 | requests.pop().respond(200, null, | ||
60 | '#EXTM3U\n' + | ||
61 | '#EXT-X-STREAM-INF:\n' + | ||
62 | 'media.m3u8\n'); | ||
63 | ok(loader.master, 'the master playlist is available'); | ||
64 | strictEqual(loader.state, 'HAVE_MASTER', 'the state is correct'); | ||
65 | }); | ||
66 | |||
67 | test('jumps to HAVE_METADATA when initialized with a media playlist', function() { | ||
68 | var | ||
69 | loadedmetadatas = 0, | ||
70 | loader = new videojs.hls.PlaylistLoader('media.m3u8'); | ||
71 | loader.on('loadedmetadata', function() { | ||
72 | loadedmetadatas++; | ||
73 | }); | ||
74 | requests.pop().respond(200, null, | ||
75 | '#EXTM3U\n' + | ||
76 | '#EXTINF:10,\n' + | ||
77 | '0.ts\n' + | ||
78 | '#EXT-X-ENDLIST\n'); | ||
79 | ok(loader.master, 'infers a master playlist'); | ||
80 | ok(loader.media(), 'sets the media playlist'); | ||
81 | ok(loader.media().uri, 'sets the media playlist URI'); | ||
82 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
83 | strictEqual(requests.length, 0, 'no more requests are made'); | ||
84 | strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata'); | ||
85 | }); | ||
86 | |||
87 | test('jumps to HAVE_METADATA when initialized with a live media playlist', function() { | ||
88 | var loader = new videojs.hls.PlaylistLoader('media.m3u8'); | ||
89 | requests.pop().respond(200, null, | ||
90 | '#EXTM3U\n' + | ||
91 | '#EXTINF:10,\n' + | ||
92 | '0.ts\n'); | ||
93 | ok(loader.master, 'infers a master playlist'); | ||
94 | ok(loader.media(), 'sets the media playlist'); | ||
95 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
96 | }); | ||
97 | |||
98 | test('moves to HAVE_METADATA after loading a media playlist', function() { | ||
99 | var | ||
100 | loadedPlaylist = 0, | ||
101 | loadedMetadata = 0, | ||
102 | loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
103 | loader.on('loadedplaylist', function() { | ||
104 | loadedPlaylist++; | ||
105 | }); | ||
106 | loader.on('loadedmetadata', function() { | ||
107 | loadedMetadata++; | ||
108 | }); | ||
109 | requests.pop().respond(200, null, | ||
110 | '#EXTM3U\n' + | ||
111 | '#EXT-X-STREAM-INF:\n' + | ||
112 | 'media.m3u8\n' + | ||
113 | 'alt.m3u8\n'); | ||
114 | strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once'); | ||
115 | strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata'); | ||
116 | strictEqual(requests.length, 1, 'requests the media playlist'); | ||
117 | strictEqual(requests[0].method, 'GET', 'GETs the media playlist'); | ||
118 | strictEqual(requests[0].url, | ||
119 | urlTo('media.m3u8'), | ||
120 | 'requests the first playlist'); | ||
121 | |||
122 | requests.pop().respond(200, null, | ||
123 | '#EXTM3U\n' + | ||
124 | '#EXTINF:10,\n' + | ||
125 | '0.ts\n'); | ||
126 | ok(loader.master, 'sets the master playlist'); | ||
127 | ok(loader.media(), 'sets the media playlist'); | ||
128 | strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice'); | ||
129 | strictEqual(loadedMetadata, 1, 'fired loadedmetadata once'); | ||
130 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
131 | }); | ||
132 | |||
133 | test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { | ||
134 | var loader = new videojs.hls.PlaylistLoader('live.m3u8'); | ||
135 | requests.pop().respond(200, null, | ||
136 | '#EXTM3U\n' + | ||
137 | '#EXTINF:10,\n' + | ||
138 | '0.ts\n'); | ||
139 | clock.tick(10 * 1000); // 10s, one target duration | ||
140 | strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); | ||
141 | strictEqual(requests.length, 1, 'requested playlist'); | ||
142 | strictEqual(requests[0].url, | ||
143 | urlTo('live.m3u8'), | ||
144 | 'refreshes the media playlist'); | ||
145 | }); | ||
146 | |||
147 | test('returns to HAVE_METADATA after refreshing the playlist', function() { | ||
148 | var loader = new videojs.hls.PlaylistLoader('live.m3u8'); | ||
149 | requests.pop().respond(200, null, | ||
150 | '#EXTM3U\n' + | ||
151 | '#EXTINF:10,\n' + | ||
152 | '0.ts\n'); | ||
153 | clock.tick(10 * 1000); // 10s, one target duration | ||
154 | requests.pop().respond(200, null, | ||
155 | '#EXTM3U\n' + | ||
156 | '#EXTINF:10,\n' + | ||
157 | '1.ts\n'); | ||
158 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
159 | }); | ||
160 | |||
161 | test('emits an error when an initial playlist request fails', function() { | ||
162 | var | ||
163 | errors = [], | ||
164 | loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
165 | |||
166 | loader.on('error', function() { | ||
167 | errors.push(loader.error); | ||
168 | }); | ||
169 | requests.pop().respond(500); | ||
170 | |||
171 | strictEqual(errors.length, 1, 'emitted one error'); | ||
172 | strictEqual(errors[0].status, 500, 'http status is captured'); | ||
173 | }); | ||
174 | |||
175 | test('errors when an initial media playlist request fails', function() { | ||
176 | var | ||
177 | errors = [], | ||
178 | loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
179 | |||
180 | loader.on('error', function() { | ||
181 | errors.push(loader.error); | ||
182 | }); | ||
183 | requests.pop().respond(200, null, | ||
184 | '#EXTM3U\n' + | ||
185 | '#EXT-X-STREAM-INF:\n' + | ||
186 | 'media.m3u8\n'); | ||
187 | |||
188 | strictEqual(errors.length, 0, 'emitted no errors'); | ||
189 | |||
190 | requests.pop().respond(500); | ||
191 | |||
192 | strictEqual(errors.length, 1, 'emitted one error'); | ||
193 | strictEqual(errors[0].status, 500, 'http status is captured'); | ||
194 | }); | ||
195 | |||
196 | |||
197 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 | ||
198 | test('halves the refresh timeout if a playlist is unchanged' + | ||
199 | 'since the last reload', function() { | ||
200 | new videojs.hls.PlaylistLoader('live.m3u8'); | ||
201 | requests.pop().respond(200, null, | ||
202 | '#EXTM3U\n' + | ||
203 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
204 | '#EXTINF:10,\n' + | ||
205 | '0.ts\n'); | ||
206 | clock.tick(10 * 1000); // trigger a refresh | ||
207 | requests.pop().respond(200, null, | ||
208 | '#EXTM3U\n' + | ||
209 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
210 | '#EXTINF:10,\n' + | ||
211 | '0.ts\n'); | ||
212 | clock.tick(5 * 1000); // half the default target-duration | ||
213 | |||
214 | strictEqual(requests.length, 1, 'sent a request'); | ||
215 | strictEqual(requests[0].url, | ||
216 | urlTo('live.m3u8'), | ||
217 | 'requested the media playlist'); | ||
218 | }); | ||
219 | |||
220 | test('media-sequence updates are considered a playlist change', function() { | ||
221 | new videojs.hls.PlaylistLoader('live.m3u8'); | ||
222 | requests.pop().respond(200, null, | ||
223 | '#EXTM3U\n' + | ||
224 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
225 | '#EXTINF:10,\n' + | ||
226 | '0.ts\n'); | ||
227 | clock.tick(10 * 1000); // trigger a refresh | ||
228 | requests.pop().respond(200, null, | ||
229 | '#EXTM3U\n' + | ||
230 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
231 | '#EXTINF:10,\n' + | ||
232 | '0.ts\n'); | ||
233 | clock.tick(5 * 1000); // half the default target-duration | ||
234 | |||
235 | strictEqual(requests.length, 0, 'no request is sent'); | ||
236 | }); | ||
237 | |||
238 | test('emits an error if a media refresh fails', function() { | ||
239 | var | ||
240 | errors = 0, | ||
241 | loader = new videojs.hls.PlaylistLoader('live.m3u8'); | ||
242 | |||
243 | loader.on('error', function() { | ||
244 | errors++; | ||
245 | }); | ||
246 | requests.pop().respond(200, null, | ||
247 | '#EXTM3U\n' + | ||
248 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
249 | '#EXTINF:10,\n' + | ||
250 | '0.ts\n'); | ||
251 | clock.tick(10 * 1000); // trigger a refresh | ||
252 | requests.pop().respond(500); | ||
253 | |||
254 | strictEqual(errors, 1, 'emitted an error'); | ||
255 | strictEqual(loader.error.status, 500, 'captured the status code'); | ||
256 | }); | ||
257 | |||
258 | test('switches media playlists when requested', function() { | ||
259 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
260 | requests.pop().respond(200, null, | ||
261 | '#EXTM3U\n' + | ||
262 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
263 | 'low.m3u8\n' + | ||
264 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
265 | 'high.m3u8\n'); | ||
266 | requests.pop().respond(200, null, | ||
267 | '#EXTM3U\n' + | ||
268 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
269 | '#EXTINF:10,\n' + | ||
270 | 'low-0.ts\n'); | ||
271 | |||
272 | loader.media(loader.master.playlists[1]); | ||
273 | strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | ||
274 | |||
275 | requests.pop().respond(200, null, | ||
276 | '#EXTM3U\n' + | ||
277 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
278 | '#EXTINF:10,\n' + | ||
279 | 'high-0.ts\n'); | ||
280 | strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | ||
281 | strictEqual(loader.media(), | ||
282 | loader.master.playlists[1], | ||
283 | 'updated the active media'); | ||
284 | }); | ||
285 | |||
286 | test('can switch media playlists based on URI', function() { | ||
287 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
288 | requests.pop().respond(200, null, | ||
289 | '#EXTM3U\n' + | ||
290 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
291 | 'low.m3u8\n' + | ||
292 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
293 | 'high.m3u8\n'); | ||
294 | requests.pop().respond(200, null, | ||
295 | '#EXTM3U\n' + | ||
296 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
297 | '#EXTINF:10,\n' + | ||
298 | 'low-0.ts\n'); | ||
299 | |||
300 | loader.media('high.m3u8'); | ||
301 | strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | ||
302 | |||
303 | requests.pop().respond(200, null, | ||
304 | '#EXTM3U\n' + | ||
305 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
306 | '#EXTINF:10,\n' + | ||
307 | 'high-0.ts\n'); | ||
308 | strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | ||
309 | strictEqual(loader.media(), | ||
310 | loader.master.playlists[1], | ||
311 | 'updated the active media'); | ||
312 | }); | ||
313 | |||
314 | test('aborts in-flight playlist refreshes when switching', function() { | ||
315 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
316 | requests.pop().respond(200, null, | ||
317 | '#EXTM3U\n' + | ||
318 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
319 | 'low.m3u8\n' + | ||
320 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
321 | 'high.m3u8\n'); | ||
322 | requests.pop().respond(200, null, | ||
323 | '#EXTM3U\n' + | ||
324 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
325 | '#EXTINF:10,\n' + | ||
326 | 'low-0.ts\n'); | ||
327 | clock.tick(10 * 1000); | ||
328 | loader.media('high.m3u8'); | ||
329 | strictEqual(requests[0].aborted, true, 'aborted refresh request'); | ||
330 | strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | ||
331 | }); | ||
332 | |||
333 | test('switching to the active playlist is a no-op', function() { | ||
334 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
335 | requests.pop().respond(200, null, | ||
336 | '#EXTM3U\n' + | ||
337 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
338 | 'low.m3u8\n' + | ||
339 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
340 | 'high.m3u8\n'); | ||
341 | requests.pop().respond(200, null, | ||
342 | '#EXTM3U\n' + | ||
343 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
344 | '#EXTINF:10,\n' + | ||
345 | 'low-0.ts\n' + | ||
346 | '#EXT-X-ENDLIST\n'); | ||
347 | loader.media('low.m3u8'); | ||
348 | |||
349 | strictEqual(requests.length, 0, 'no requests is sent'); | ||
350 | }); | ||
351 | |||
352 | test('throws an error if a media switch is initiated too early', function() { | ||
353 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
354 | |||
355 | throws(function() { | ||
356 | loader.media('high.m3u8'); | ||
357 | }, 'threw an error from HAVE_NOTHING'); | ||
358 | |||
359 | requests.pop().respond(200, null, | ||
360 | '#EXTM3U\n' + | ||
361 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
362 | 'low.m3u8\n' + | ||
363 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
364 | 'high.m3u8\n'); | ||
365 | throws(function() { | ||
366 | loader.media('high.m3u8'); | ||
367 | }, 'throws an error from HAVE_MASTER'); | ||
368 | }); | ||
369 | |||
370 | test('throws an error if a switch to an unrecognized playlist is requested', function() { | ||
371 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
372 | requests.pop().respond(200, null, | ||
373 | '#EXTM3U\n' + | ||
374 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
375 | 'media.m3u8\n'); | ||
376 | |||
377 | throws(function() { | ||
378 | loader.media('unrecognized.m3u8'); | ||
379 | }, 'throws an error'); | ||
380 | }); | ||
381 | })(window); |
... | @@ -8,6 +8,7 @@ | ... | @@ -8,6 +8,7 @@ |
8 | <script src="../node_modules/sinon/lib/sinon/util/event.js"></script> | 8 | <script src="../node_modules/sinon/lib/sinon/util/event.js"></script> |
9 | <script src="../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js"></script> | 9 | <script src="../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js"></script> |
10 | <script src="../node_modules/sinon/lib/sinon/util/xhr_ie.js"></script> | 10 | <script src="../node_modules/sinon/lib/sinon/util/xhr_ie.js"></script> |
11 | <script src="../node_modules/sinon/lib/sinon/util/fake_timers.js"></script> | ||
11 | 12 | ||
12 | <!-- Load local QUnit. --> | 13 | <!-- Load local QUnit. --> |
13 | <link rel="stylesheet" href="../libs/qunit/qunit.css" media="screen"> | 14 | <link rel="stylesheet" href="../libs/qunit/qunit.css" media="screen"> |
... | @@ -28,6 +29,7 @@ | ... | @@ -28,6 +29,7 @@ |
28 | <!-- M3U8 --> | 29 | <!-- M3U8 --> |
29 | <script src="../src/stream.js"></script> | 30 | <script src="../src/stream.js"></script> |
30 | <script src="../src/m3u8/m3u8-parser.js"></script> | 31 | <script src="../src/m3u8/m3u8-parser.js"></script> |
32 | <script src="../src/playlist-loader.js"></script> | ||
31 | <!-- M3U8 TEST DATA --> | 33 | <!-- M3U8 TEST DATA --> |
32 | <script src="../tmp/manifests.js"></script> | 34 | <script src="../tmp/manifests.js"></script> |
33 | <script src="../tmp/expected.js"></script> | 35 | <script src="../tmp/expected.js"></script> |
... | @@ -51,6 +53,7 @@ | ... | @@ -51,6 +53,7 @@ |
51 | <script src="exp-golomb_test.js"></script> | 53 | <script src="exp-golomb_test.js"></script> |
52 | <script src="flv-tag_test.js"></script> | 54 | <script src="flv-tag_test.js"></script> |
53 | <script src="m3u8_test.js"></script> | 55 | <script src="m3u8_test.js"></script> |
56 | <script src="playlist-loader_test.js"></script> | ||
54 | </head> | 57 | </head> |
55 | <body> | 58 | <body> |
56 | <div id="qunit"></div> | 59 | <div id="qunit"></div> | ... | ... |
... | @@ -115,7 +115,7 @@ module('HLS', { | ... | @@ -115,7 +115,7 @@ module('HLS', { |
115 | oldSegmentParser = videojs.hls.SegmentParser; | 115 | oldSegmentParser = videojs.hls.SegmentParser; |
116 | oldSetTimeout = window.setTimeout; | 116 | oldSetTimeout = window.setTimeout; |
117 | 117 | ||
118 | // make XHRs synchronous | 118 | // fake XHRs |
119 | xhr = sinon.useFakeXMLHttpRequest(); | 119 | xhr = sinon.useFakeXMLHttpRequest(); |
120 | requests = []; | 120 | requests = []; |
121 | xhr.onCreate = function(xhr) { | 121 | xhr.onCreate = function(xhr) { |
... | @@ -149,30 +149,25 @@ test('starts playing if autoplay is specified', function() { | ... | @@ -149,30 +149,25 @@ test('starts playing if autoplay is specified', function() { |
149 | strictEqual(1, plays, 'play was called'); | 149 | strictEqual(1, plays, 'play was called'); |
150 | }); | 150 | }); |
151 | 151 | ||
152 | test('loads the specified manifest URL on init', function() { | 152 | test('creates a PlaylistLoader on init', function() { |
153 | var loadedmanifest = false, loadedmetadata = false; | 153 | var loadedmetadata = false; |
154 | player.on('loadedmanifest', function() { | ||
155 | loadedmanifest = true; | ||
156 | }); | ||
157 | player.on('loadedmetadata', function() { | 154 | player.on('loadedmetadata', function() { |
158 | loadedmetadata = true; | 155 | loadedmetadata = true; |
159 | }); | 156 | }); |
160 | 157 | ||
161 | player.hls('manifest/playlist.m3u8'); | 158 | player.hls('manifest/playlist.m3u8'); |
162 | strictEqual(player.hls.readyState(), 0, 'the readyState is HAVE_NOTHING'); | 159 | ok(!player.hls.playlists, 'waits for sourceopen to create the loader'); |
163 | videojs.mediaSources[player.currentSrc()].trigger({ | 160 | videojs.mediaSources[player.currentSrc()].trigger({ |
164 | type: 'sourceopen' | 161 | type: 'sourceopen' |
165 | }); | 162 | }); |
166 | standardXHRResponse(requests[0]); | 163 | standardXHRResponse(requests[0]); |
167 | ok(loadedmanifest, 'loadedmanifest fires'); | ||
168 | ok(loadedmetadata, 'loadedmetadata fires'); | 164 | ok(loadedmetadata, 'loadedmetadata fires'); |
169 | ok(player.hls.master, 'a master is inferred'); | 165 | ok(player.hls.playlists.master, 'set the master playlist'); |
170 | ok(player.hls.media, 'the manifest is available'); | 166 | ok(player.hls.playlists.media(), 'set the media playlist'); |
171 | ok(player.hls.media.segments, 'the segment entries are parsed'); | 167 | ok(player.hls.playlists.media().segments, 'the segment entries are parsed'); |
172 | strictEqual(player.hls.master.playlists[0], | 168 | strictEqual(player.hls.playlists.master.playlists[0], |
173 | player.hls.media, | 169 | player.hls.playlists.media(), |
174 | 'the playlist is selected'); | 170 | 'the playlist is selected'); |
175 | strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA'); | ||
176 | }); | 171 | }); |
177 | 172 | ||
178 | test('sets the duration if one is available on the playlist', function() { | 173 | 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() { | ... | @@ -189,8 +184,9 @@ test('sets the duration if one is available on the playlist', function() { |
189 | }); | 184 | }); |
190 | 185 | ||
191 | standardXHRResponse(requests[0]); | 186 | standardXHRResponse(requests[0]); |
187 | strictEqual(calls, 1, 'duration is set'); | ||
192 | standardXHRResponse(requests[1]); | 188 | standardXHRResponse(requests[1]); |
193 | strictEqual(calls, 2, 'duration is set'); | 189 | strictEqual(calls, 1, 'duration is set'); |
194 | }); | 190 | }); |
195 | 191 | ||
196 | test('calculates the duration if needed', function() { | 192 | test('calculates the duration if needed', function() { |
... | @@ -207,10 +203,9 @@ test('calculates the duration if needed', function() { | ... | @@ -207,10 +203,9 @@ test('calculates the duration if needed', function() { |
207 | }); | 203 | }); |
208 | 204 | ||
209 | standardXHRResponse(requests[0]); | 205 | standardXHRResponse(requests[0]); |
210 | standardXHRResponse(requests[1]); | 206 | strictEqual(durations.length, 1, 'duration is set'); |
211 | strictEqual(durations.length, 2, 'duration is set'); | ||
212 | strictEqual(durations[0], | 207 | strictEqual(durations[0], |
213 | player.hls.media.segments.length * 10, | 208 | player.hls.playlists.media().segments.length * 10, |
214 | 'duration is calculated'); | 209 | 'duration is calculated'); |
215 | }); | 210 | }); |
216 | 211 | ||
... | @@ -269,18 +264,7 @@ test('re-initializes the plugin for each source', function() { | ... | @@ -269,18 +264,7 @@ test('re-initializes the plugin for each source', function() { |
269 | }); | 264 | }); |
270 | 265 | ||
271 | test('triggers an error when a master playlist request errors', function() { | 266 | test('triggers an error when a master playlist request errors', function() { |
272 | var | 267 | var error; |
273 | status = 0, | ||
274 | error; | ||
275 | window.XMLHttpRequest = function() { | ||
276 | this.open = function() {}; | ||
277 | this.send = function() { | ||
278 | this.readyState = 4; | ||
279 | this.status = status; | ||
280 | this.onreadystatechange(); | ||
281 | }; | ||
282 | }; | ||
283 | |||
284 | player.on('error', function() { | 268 | player.on('error', function() { |
285 | error = player.hls.error; | 269 | error = player.hls.error; |
286 | }); | 270 | }); |
... | @@ -288,6 +272,7 @@ test('triggers an error when a master playlist request errors', function() { | ... | @@ -288,6 +272,7 @@ test('triggers an error when a master playlist request errors', function() { |
288 | videojs.mediaSources[player.currentSrc()].trigger({ | 272 | videojs.mediaSources[player.currentSrc()].trigger({ |
289 | type: 'sourceopen' | 273 | type: 'sourceopen' |
290 | }); | 274 | }); |
275 | requests.pop().respond(500); | ||
291 | 276 | ||
292 | ok(error, 'an error is triggered'); | 277 | ok(error, 'an error is triggered'); |
293 | strictEqual(2, error.code, 'a network error is triggered'); | 278 | strictEqual(2, error.code, 'a network error is triggered'); |
... | @@ -355,7 +340,7 @@ test('selects a playlist after segment downloads', function() { | ... | @@ -355,7 +340,7 @@ test('selects a playlist after segment downloads', function() { |
355 | player.hls('manifest/master.m3u8'); | 340 | player.hls('manifest/master.m3u8'); |
356 | player.hls.selectPlaylist = function() { | 341 | player.hls.selectPlaylist = function() { |
357 | calls++; | 342 | calls++; |
358 | return player.hls.master.playlists[0]; | 343 | return player.hls.playlists.master.playlists[0]; |
359 | }; | 344 | }; |
360 | videojs.mediaSources[player.currentSrc()].trigger({ | 345 | videojs.mediaSources[player.currentSrc()].trigger({ |
361 | type: 'sourceopen' | 346 | type: 'sourceopen' |
... | @@ -375,6 +360,7 @@ test('selects a playlist after segment downloads', function() { | ... | @@ -375,6 +360,7 @@ test('selects a playlist after segment downloads', function() { |
375 | player.trigger('timeupdate'); | 360 | player.trigger('timeupdate'); |
376 | 361 | ||
377 | standardXHRResponse(requests[3]); | 362 | standardXHRResponse(requests[3]); |
363 | console.log(requests.map(function(i) { return i.url; })); | ||
378 | strictEqual(calls, 2, 'selects after additional segments'); | 364 | strictEqual(calls, 2, 'selects after additional segments'); |
379 | }); | 365 | }); |
380 | 366 | ||
... | @@ -403,14 +389,14 @@ test('updates the duration after switching playlists', function() { | ... | @@ -403,14 +389,14 @@ test('updates the duration after switching playlists', function() { |
403 | player.hls('manifest/master.m3u8'); | 389 | player.hls('manifest/master.m3u8'); |
404 | player.hls.selectPlaylist = function() { | 390 | player.hls.selectPlaylist = function() { |
405 | selectedPlaylist = true; | 391 | selectedPlaylist = true; |
406 | return player.hls.master.playlists[1]; | 392 | return player.hls.playlists.master.playlists[1]; |
407 | }; | 393 | }; |
408 | player.duration = function(duration) { | 394 | player.duration = function(duration) { |
409 | if (duration === undefined) { | 395 | if (duration === undefined) { |
410 | return 0; | 396 | return 0; |
411 | } | 397 | } |
412 | // only track calls that occur after the playlist has been switched | 398 | // only track calls that occur after the playlist has been switched |
413 | if (player.hls.media === player.hls.master.playlists[1]) { | 399 | if (player.hls.playlists.media() === player.hls.playlists.master.playlists[1]) { |
414 | calls++; | 400 | calls++; |
415 | } | 401 | } |
416 | }; | 402 | }; |
... | @@ -462,8 +448,10 @@ test('downloads additional playlists if required', function() { | ... | @@ -462,8 +448,10 @@ test('downloads additional playlists if required', function() { |
462 | '/manifest/' + | 448 | '/manifest/' + |
463 | playlist.uri, | 449 | playlist.uri, |
464 | 'made playlist request'); | 450 | 'made playlist request'); |
465 | strictEqual(playlist, player.hls.media, 'a new playlists was selected'); | 451 | strictEqual(playlist.uri, |
466 | ok(player.hls.media.segments, 'segments are now available'); | 452 | player.hls.playlists.media().uri, |
453 | 'a new playlists was selected'); | ||
454 | ok(player.hls.playlists.media().segments, 'segments are now available'); | ||
467 | }); | 455 | }); |
468 | 456 | ||
469 | test('selects a playlist below the current bandwidth', function() { | 457 | test('selects a playlist below the current bandwidth', function() { |
... | @@ -476,15 +464,15 @@ test('selects a playlist below the current bandwidth', function() { | ... | @@ -476,15 +464,15 @@ test('selects a playlist below the current bandwidth', function() { |
476 | standardXHRResponse(requests[0]); | 464 | standardXHRResponse(requests[0]); |
477 | 465 | ||
478 | // the default playlist has a really high bitrate | 466 | // the default playlist has a really high bitrate |
479 | player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10; | 467 | player.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10; |
480 | // playlist 1 has a very low bitrate | 468 | // playlist 1 has a very low bitrate |
481 | player.hls.master.playlists[1].attributes.BANDWIDTH = 1; | 469 | player.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 1; |
482 | // but the detected client bandwidth is really low | 470 | // but the detected client bandwidth is really low |
483 | player.hls.bandwidth = 10; | 471 | player.hls.bandwidth = 10; |
484 | 472 | ||
485 | playlist = player.hls.selectPlaylist(); | 473 | playlist = player.hls.selectPlaylist(); |
486 | strictEqual(playlist, | 474 | strictEqual(playlist, |
487 | player.hls.master.playlists[1], | 475 | player.hls.playlists.master.playlists[1], |
488 | 'the low bitrate stream is selected'); | 476 | 'the low bitrate stream is selected'); |
489 | }); | 477 | }); |
490 | 478 | ||
... | @@ -498,15 +486,15 @@ test('raises the minimum bitrate for a stream proportionially', function() { | ... | @@ -498,15 +486,15 @@ test('raises the minimum bitrate for a stream proportionially', function() { |
498 | standardXHRResponse(requests[0]); | 486 | standardXHRResponse(requests[0]); |
499 | 487 | ||
500 | // the default playlist's bandwidth + 10% is equal to the current bandwidth | 488 | // the default playlist's bandwidth + 10% is equal to the current bandwidth |
501 | player.hls.master.playlists[0].attributes.BANDWIDTH = 10; | 489 | player.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10; |
502 | player.hls.bandwidth = 11; | 490 | player.hls.bandwidth = 11; |
503 | 491 | ||
504 | // 9.9 * 1.1 < 11 | 492 | // 9.9 * 1.1 < 11 |
505 | player.hls.master.playlists[1].attributes.BANDWIDTH = 9.9; | 493 | player.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 9.9; |
506 | playlist = player.hls.selectPlaylist(); | 494 | playlist = player.hls.selectPlaylist(); |
507 | 495 | ||
508 | strictEqual(playlist, | 496 | strictEqual(playlist, |
509 | player.hls.master.playlists[1], | 497 | player.hls.playlists.master.playlists[1], |
510 | 'a lower bitrate stream is selected'); | 498 | 'a lower bitrate stream is selected'); |
511 | }); | 499 | }); |
512 | 500 | ||
... | @@ -525,7 +513,7 @@ test('uses the lowest bitrate if no other is suitable', function() { | ... | @@ -525,7 +513,7 @@ test('uses the lowest bitrate if no other is suitable', function() { |
525 | 513 | ||
526 | // playlist 1 has the lowest advertised bitrate | 514 | // playlist 1 has the lowest advertised bitrate |
527 | strictEqual(playlist, | 515 | strictEqual(playlist, |
528 | player.hls.master.playlists[1], | 516 | player.hls.playlists.master.playlists[1], |
529 | 'the lowest bitrate stream is selected'); | 517 | 'the lowest bitrate stream is selected'); |
530 | }); | 518 | }); |
531 | 519 | ||
... | @@ -855,30 +843,21 @@ test('clears pending buffer updates when seeking', function() { | ... | @@ -855,30 +843,21 @@ test('clears pending buffer updates when seeking', function() { |
855 | 843 | ||
856 | test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { | 844 | test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { |
857 | var errorTriggered = false; | 845 | var errorTriggered = false; |
858 | |||
859 | window.XMLHttpRequest = function() { | ||
860 | this.open = function(method, url) { | ||
861 | xhrUrls.push(url); | ||
862 | }; | ||
863 | this.send = function() { | ||
864 | this.readyState = 4; | ||
865 | this.status = 404; | ||
866 | this.onreadystatechange(); | ||
867 | }; | ||
868 | }; | ||
869 | |||
870 | player.hls('manifest/media.m3u8'); | ||
871 | |||
872 | player.on('error', function() { | 846 | player.on('error', function() { |
873 | errorTriggered = true; | 847 | errorTriggered = true; |
874 | }); | 848 | }); |
875 | 849 | player.hls('manifest/media.m3u8'); | |
876 | videojs.mediaSources[player.currentSrc()].trigger({ | 850 | videojs.mediaSources[player.currentSrc()].trigger({ |
877 | type: 'sourceopen' | 851 | type: 'sourceopen' |
878 | }); | 852 | }); |
879 | 853 | requests.pop().respond(404); | |
880 | equal(true, errorTriggered, 'Missing Playlist error event should trigger'); | 854 | |
881 | equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK'); | 855 | equal(errorTriggered, |
856 | true, | ||
857 | 'Missing Playlist error event should trigger'); | ||
858 | equal(player.hls.error.code, | ||
859 | 2, | ||
860 | 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK'); | ||
882 | ok(player.hls.error.message, 'Player error type should inform user correctly'); | 861 | ok(player.hls.error.message, 'Player error type should inform user correctly'); |
883 | }); | 862 | }); |
884 | 863 | ||
... | @@ -916,24 +895,6 @@ test('has no effect if native HLS is available', function() { | ... | @@ -916,24 +895,6 @@ test('has no effect if native HLS is available', function() { |
916 | 'no media source was opened'); | 895 | 'no media source was opened'); |
917 | }); | 896 | }); |
918 | 897 | ||
919 | test('reloads live playlists', function() { | ||
920 | var callbacks = []; | ||
921 | // capture timeouts | ||
922 | window.setTimeout = function(callback, timeout) { | ||
923 | callbacks.push({ callback: callback, timeout: timeout }); | ||
924 | }; | ||
925 | player.hls('http://example.com/manifest/missingEndlist.m3u8'); | ||
926 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
927 | type: 'sourceopen' | ||
928 | }); | ||
929 | standardXHRResponse(requests[0]); | ||
930 | |||
931 | strictEqual(1, callbacks.length, 'refresh was scheduled'); | ||
932 | strictEqual(player.hls.media.targetDuration * 1000, | ||
933 | callbacks[0].timeout, | ||
934 | 'waited one target duration'); | ||
935 | }); | ||
936 | |||
937 | test('duration is Infinity for live playlists', function() { | 898 | test('duration is Infinity for live playlists', function() { |
938 | player.hls('http://example.com/manifest/missingEndlist.m3u8'); | 899 | player.hls('http://example.com/manifest/missingEndlist.m3u8'); |
939 | videojs.mediaSources[player.currentSrc()].trigger({ | 900 | videojs.mediaSources[player.currentSrc()].trigger({ |
... | @@ -959,88 +920,37 @@ test('does not reload playlists with an endlist tag', function() { | ... | @@ -959,88 +920,37 @@ test('does not reload playlists with an endlist tag', function() { |
959 | strictEqual(0, callbacks.length, 'no refresh was scheduled'); | 920 | strictEqual(0, callbacks.length, 'no refresh was scheduled'); |
960 | }); | 921 | }); |
961 | 922 | ||
962 | test('reloads a live playlist after half a target duration if it has not ' + | ||
963 | 'changed since the last request', function() { | ||
964 | var callbacks = []; | ||
965 | // capture timeouts | ||
966 | window.setTimeout = function(callback, timeout) { | ||
967 | callbacks.push({ callback: callback, timeout: timeout }); | ||
968 | }; | ||
969 | player.hls('http://example.com/manifest/missingEndlist.m3u8'); | ||
970 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
971 | type: 'sourceopen' | ||
972 | }); | ||
973 | |||
974 | standardXHRResponse(requests[0]); | ||
975 | standardXHRResponse(requests[1]); | ||
976 | strictEqual(callbacks.length, 1, 'full-length refresh scheduled'); | ||
977 | callbacks.pop().callback(); | ||
978 | standardXHRResponse(requests[2]); | ||
979 | |||
980 | strictEqual(callbacks.length, 1, 'half-length refresh was scheduled'); | ||
981 | strictEqual(callbacks[0].timeout, | ||
982 | player.hls.media.targetDuration / 2 * 1000, | ||
983 | 'waited half a target duration'); | ||
984 | }); | ||
985 | |||
986 | test('merges playlist reloads', function() { | ||
987 | var oldPlaylist, | ||
988 | callback; | ||
989 | |||
990 | // capture timeouts | ||
991 | window.setTimeout = function(cb) { | ||
992 | callback = cb; | ||
993 | }; | ||
994 | |||
995 | player.hls('http://example.com/manifest/missingEndlist.m3u8'); | ||
996 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
997 | type: 'sourceopen' | ||
998 | }); | ||
999 | standardXHRResponse(requests[0]); | ||
1000 | standardXHRResponse(requests[1]); | ||
1001 | oldPlaylist = player.hls.media; | ||
1002 | |||
1003 | callback(); | ||
1004 | standardXHRResponse(requests[2]); | ||
1005 | ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated'); | ||
1006 | }); | ||
1007 | |||
1008 | test('updates the media index when a playlist reloads', function() { | 923 | test('updates the media index when a playlist reloads', function() { |
1009 | var callback; | ||
1010 | window.setTimeout = function(cb) { | ||
1011 | callback = cb; | ||
1012 | }; | ||
1013 | // the initial playlist | ||
1014 | window.manifests['live-updating'] = | ||
1015 | '#EXTM3U\n' + | ||
1016 | '#EXTINF:10,\n' + | ||
1017 | '0.ts\n' + | ||
1018 | '#EXTINF:10,\n' + | ||
1019 | '1.ts\n' + | ||
1020 | '#EXTINF:10,\n' + | ||
1021 | '2.ts\n'; | ||
1022 | |||
1023 | player.hls('http://example.com/live-updating.m3u8'); | 924 | player.hls('http://example.com/live-updating.m3u8'); |
1024 | videojs.mediaSources[player.currentSrc()].trigger({ | 925 | videojs.mediaSources[player.currentSrc()].trigger({ |
1025 | type: 'sourceopen' | 926 | type: 'sourceopen' |
1026 | }); | 927 | }); |
1027 | 928 | ||
1028 | standardXHRResponse(requests[0]); | 929 | requests[0].respond(200, null, |
930 | '#EXTM3U\n' + | ||
931 | '#EXTINF:10,\n' + | ||
932 | '0.ts\n' + | ||
933 | '#EXTINF:10,\n' + | ||
934 | '1.ts\n' + | ||
935 | '#EXTINF:10,\n' + | ||
936 | '2.ts\n'); | ||
1029 | standardXHRResponse(requests[1]); | 937 | standardXHRResponse(requests[1]); |
1030 | // play the stream until 2.ts is playing | 938 | // play the stream until 2.ts is playing |
1031 | player.hls.mediaIndex = 3; | 939 | player.hls.mediaIndex = 3; |
1032 | 940 | ||
1033 | // reload the updated playlist | 941 | // reload the updated playlist |
1034 | window.manifests['live-updating'] = | 942 | player.hls.playlists.media = function() { |
1035 | '#EXTM3U\n' + | 943 | return { |
1036 | '#EXTINF:10,\n' + | 944 | segments: [{ |
1037 | '1.ts\n' + | 945 | uri: '1.ts' |
1038 | '#EXTINF:10,\n' + | 946 | }, { |
1039 | '2.ts\n' + | 947 | uri: '2.ts' |
1040 | '#EXTINF:10,\n' + | 948 | }, { |
1041 | '3.ts\n'; | 949 | uri: '3.ts' |
1042 | callback(); | 950 | }] |
1043 | standardXHRResponse(requests[2]); | 951 | }; |
952 | }; | ||
953 | player.hls.playlists.trigger('loadedplaylist'); | ||
1044 | 954 | ||
1045 | strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload'); | 955 | strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload'); |
1046 | }); | 956 | }); |
... | @@ -1112,64 +1022,6 @@ test('does not reload master playlists', function() { | ... | @@ -1112,64 +1022,6 @@ test('does not reload master playlists', function() { |
1112 | strictEqual(callbacks.length, 0, 'no reload scheduled'); | 1022 | strictEqual(callbacks.length, 0, 'no reload scheduled'); |
1113 | }); | 1023 | }); |
1114 | 1024 | ||
1115 | test('only reloads the active media playlist', function() { | ||
1116 | var callbacks = [], | ||
1117 | i = 0, | ||
1118 | filteredRequests = [], | ||
1119 | customResponse; | ||
1120 | |||
1121 | customResponse = function(request) { | ||
1122 | request.response = new Uint8Array([1]).buffer; | ||
1123 | request.respond(200, | ||
1124 | {'Content-Type': 'application/vnd.apple.mpegurl'}, | ||
1125 | '#EXTM3U\n' + | ||
1126 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
1127 | '#EXTINF:10,\n' + | ||
1128 | '1.ts\n'); | ||
1129 | }; | ||
1130 | |||
1131 | window.setTimeout = function(callback) { | ||
1132 | callbacks.push(callback); | ||
1133 | }; | ||
1134 | |||
1135 | player.hls('http://example.com/missingEndlist.m3u8'); | ||
1136 | videojs.mediaSources[player.currentSrc()].trigger({ | ||
1137 | type: 'sourceopen' | ||
1138 | }); | ||
1139 | |||
1140 | standardXHRResponse(requests[0]); | ||
1141 | standardXHRResponse(requests[1]); | ||
1142 | |||
1143 | videojs.mediaSources[player.currentSrc()].endOfStream = function() {}; | ||
1144 | |||
1145 | player.hls.selectPlaylist = function() { | ||
1146 | return player.hls.master.playlists[1]; | ||
1147 | }; | ||
1148 | player.hls.master.playlists.push({ | ||
1149 | uri: 'http://example.com/switched.m3u8' | ||
1150 | }); | ||
1151 | |||
1152 | player.trigger('timeupdate'); | ||
1153 | strictEqual(callbacks.length, 1, 'a refresh is scheduled'); | ||
1154 | |||
1155 | standardXHRResponse(requests[2]); // segment response | ||
1156 | customResponse(requests[3]); // loaded witched.m3u8 | ||
1157 | |||
1158 | callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8 | ||
1159 | callbacks.shift()(); // refresh switched.m3u8 | ||
1160 | |||
1161 | for (; i < requests.length; i++) { | ||
1162 | if (/switched/.test(requests[i].url)) { | ||
1163 | filteredRequests.push(requests[i]); | ||
1164 | } | ||
1165 | } | ||
1166 | strictEqual(filteredRequests.length, 2, 'one refresh was made'); | ||
1167 | strictEqual(filteredRequests[1].url, | ||
1168 | 'http://example.com/switched.m3u8', | ||
1169 | 'refreshed the active playlist'); | ||
1170 | |||
1171 | }); | ||
1172 | |||
1173 | test('if withCredentials option is used, withCredentials is set on the XHR object', function() { | 1025 | test('if withCredentials option is used, withCredentials is set on the XHR object', function() { |
1174 | player.hls({ | 1026 | player.hls({ |
1175 | url: 'http://example.com/media.m3u8', | 1027 | url: 'http://example.com/media.m3u8', |
... | @@ -1182,20 +1034,15 @@ test('if withCredentials option is used, withCredentials is set on the XHR objec | ... | @@ -1182,20 +1034,15 @@ test('if withCredentials option is used, withCredentials is set on the XHR objec |
1182 | }); | 1034 | }); |
1183 | 1035 | ||
1184 | test('does not break if the playlist has no segments', function() { | 1036 | test('does not break if the playlist has no segments', function() { |
1185 | var customResponse = function(request) { | ||
1186 | request.response = new Uint8Array([1]).buffer; | ||
1187 | request.respond(200, | ||
1188 | {'Content-Type': 'application/vnd.apple.mpegurl'}, | ||
1189 | '#EXTM3U\n' + | ||
1190 | '#EXT-X-PLAYLIST-TYPE:VOD\n' + | ||
1191 | '#EXT-X-TARGETDURATION:10\n'); | ||
1192 | }; | ||
1193 | player.hls('manifest/master.m3u8'); | 1037 | player.hls('manifest/master.m3u8'); |
1194 | try { | 1038 | try { |
1195 | videojs.mediaSources[player.currentSrc()].trigger({ | 1039 | videojs.mediaSources[player.currentSrc()].trigger({ |
1196 | type: 'sourceopen' | 1040 | type: 'sourceopen' |
1197 | }); | 1041 | }); |
1198 | customResponse(requests[0]); | 1042 | requests[0].respond(200, null, |
1043 | '#EXTM3U\n' + | ||
1044 | '#EXT-X-PLAYLIST-TYPE:VOD\n' + | ||
1045 | '#EXT-X-TARGETDURATION:10\n'); | ||
1199 | } catch(e) { | 1046 | } catch(e) { |
1200 | ok(false, 'an error was thrown'); | 1047 | ok(false, 'an error was thrown'); |
1201 | throw e; | 1048 | throw e; | ... | ... |
-
Please register or sign in to post a comment