Implement live playlist support
If the active media playlist does not have an ENDLIST tag, refresh it periodically.
Showing
5 changed files
with
373 additions
and
75 deletions
... | @@ -2,10 +2,10 @@ | ... | @@ -2,10 +2,10 @@ |
2 | * A state machine that manages the loading, caching, and updating of | 2 | * A state machine that manages the loading, caching, and updating of |
3 | * M3U8 playlists. | 3 | * M3U8 playlists. |
4 | */ | 4 | */ |
5 | (function(window) { | 5 | (function(window, videojs) { |
6 | 'use strict'; | 6 | 'use strict'; |
7 | var | 7 | var |
8 | 8 | ||
9 | /* XXX COPIED REMOVE ME */ | 9 | /* XXX COPIED REMOVE ME */ |
10 | /** | 10 | /** |
11 | * Constructs a new URI by interpreting a path relative to another | 11 | * Constructs a new URI by interpreting a path relative to another |
... | @@ -15,7 +15,7 @@ | ... | @@ -15,7 +15,7 @@ |
15 | * @return {string} a URI that is equivalent to composing `base` | 15 | * @return {string} a URI that is equivalent to composing `base` |
16 | * with `path` | 16 | * with `path` |
17 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue | 17 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue |
18 | */ | 18 | */ |
19 | resolveUrl = function(basePath, path) { | 19 | resolveUrl = function(basePath, path) { |
20 | // use the base element to get the browser to handle URI resolution | 20 | // use the base element to get the browser to handle URI resolution |
21 | var | 21 | var |
... | @@ -46,33 +46,209 @@ | ... | @@ -46,33 +46,209 @@ |
46 | return result; | 46 | return result; |
47 | }, | 47 | }, |
48 | 48 | ||
49 | PlaylistLoader = function(url) { | 49 | /* XXX COPIED REMOVE ME */ |
50 | var | 50 | /** |
51 | loader = this, | 51 | * Creates and sends an XMLHttpRequest. |
52 | request; | 52 | * @param options {string | object} if this argument is a string, it |
53 | if (!url) { | 53 | * is intrepreted as a URL and a simple GET request is |
54 | throw new Error('A non-empty playlist URL is required'); | 54 | * inititated. If it is an object, it should contain a `url` |
55 | } | 55 | * property that indicates the URL to request and optionally a |
56 | loader.state = 'HAVE_NOTHING'; | 56 | * `method` which is the type of HTTP request to send. |
57 | request = new window.XMLHttpRequest(); | 57 | * @param callback (optional) {function} a function to call when the |
58 | request.open('GET', url); | 58 | * request completes. If the request was not successful, the first |
59 | request.onreadystatechange = function() { | 59 | * argument will be falsey. |
60 | var parser = new videojs.m3u8.Parser(); | 60 | * @return {object} the XMLHttpRequest that was initiated. |
61 | parser.push(this.responseText); | 61 | */ |
62 | 62 | xhr = function(url, callback) { | |
63 | if (parser.manifest.playlists) { | 63 | var |
64 | loader.master = parser.manifest; | 64 | options = { |
65 | } else { | 65 | method: 'GET' |
66 | }, | ||
67 | request; | ||
68 | |||
69 | if (typeof callback !== 'function') { | ||
70 | callback = function() {}; | ||
71 | } | ||
72 | |||
73 | if (typeof url === 'object') { | ||
74 | options = videojs.util.mergeOptions(options, url); | ||
75 | url = options.url; | ||
76 | } | ||
77 | |||
78 | request = new window.XMLHttpRequest(); | ||
79 | request.open(options.method, url); | ||
80 | |||
81 | if (options.responseType) { | ||
82 | request.responseType = options.responseType; | ||
83 | } | ||
84 | if (options.withCredentials) { | ||
85 | request.withCredentials = true; | ||
86 | } | ||
87 | |||
88 | request.onreadystatechange = function() { | ||
89 | // wait until the request completes | ||
90 | if (this.readyState !== 4) { | ||
91 | return; | ||
92 | } | ||
93 | |||
94 | // request error | ||
95 | if (this.status >= 400 || this.status === 0) { | ||
96 | return callback.call(this, true, url); | ||
97 | } | ||
98 | |||
99 | return callback.call(this, false, url); | ||
100 | }; | ||
101 | request.send(null); | ||
102 | return request; | ||
103 | }, | ||
104 | |||
105 | /** | ||
106 | * Returns a new master playlist that is the result of merging an | ||
107 | * updated media playlist into the original version. If the | ||
108 | * updated media playlist does not match any of the playlist | ||
109 | * entries in the original master playlist, null is returned. | ||
110 | * @param master {object} a parsed master M3U8 object | ||
111 | * @param media {object} a parsed media M3U8 object | ||
112 | * @return {object} a new object that represents the original | ||
113 | * master playlist with the updated media playlist merged in, or | ||
114 | * null if the merge produced no change. | ||
115 | */ | ||
116 | updateMaster = function(master, media) { | ||
117 | var | ||
118 | changed = false, | ||
119 | result = videojs.util.mergeOptions(master, {}), | ||
120 | i, | ||
121 | playlist; | ||
122 | |||
123 | i = master.playlists.length; | ||
124 | while (i--) { | ||
125 | playlist = result.playlists[i]; | ||
126 | if (playlist.uri === media.uri) { | ||
127 | // consider the playlist unchanged if the number of segments | ||
128 | // are equal and the media sequence number is unchanged | ||
129 | if (playlist.segments && | ||
130 | media.segments && | ||
131 | playlist.segments.length === media.segments.length && | ||
132 | playlist.mediaSequence === media.mediaSequence) { | ||
133 | continue; | ||
134 | } | ||
135 | |||
136 | result.playlists[i] = videojs.util.mergeOptions(playlist, media); | ||
137 | changed = true; | ||
138 | } | ||
139 | } | ||
140 | return changed ? result : null; | ||
141 | }, | ||
142 | |||
143 | PlaylistLoader = function(srcUrl) { | ||
144 | var | ||
145 | loader = this, | ||
146 | request, | ||
147 | |||
148 | haveMetadata = function(error, url) { | ||
149 | var parser, refreshDelay, update; | ||
150 | if (error) { | ||
151 | loader.error = { | ||
152 | status: this.status, | ||
153 | message: 'HLS playlist request error at URL: ' + url, | ||
154 | code: (this.status >= 500) ? 4 : 2 | ||
155 | }; | ||
156 | return loader.trigger('error'); | ||
157 | } | ||
158 | |||
159 | loader.state = 'HAVE_METADATA'; | ||
160 | |||
161 | parser = new videojs.m3u8.Parser(); | ||
162 | parser.push(this.responseText); | ||
163 | parser.manifest.uri = url; | ||
164 | |||
165 | // merge this playlist into the master | ||
166 | update = updateMaster(loader.master, parser.manifest); | ||
167 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | ||
168 | if (update) { | ||
169 | loader.master = update; | ||
170 | loader.media = parser.manifest; | ||
171 | } else { | ||
172 | // if the playlist is unchanged since the last reload, | ||
173 | // try again after half the target duration | ||
174 | refreshDelay /= 2; | ||
175 | } | ||
176 | |||
177 | // refresh live playlists after a target duration passes | ||
178 | if (!loader.media.endList) { | ||
179 | window.setTimeout(function() { | ||
180 | loader.trigger('mediaupdatetimeout'); | ||
181 | }, refreshDelay); | ||
182 | } | ||
183 | }; | ||
184 | |||
185 | PlaylistLoader.prototype.init.call(this); | ||
186 | |||
187 | if (!srcUrl) { | ||
188 | throw new Error('A non-empty playlist URL is required'); | ||
189 | } | ||
190 | |||
191 | loader.state = 'HAVE_NOTHING'; | ||
192 | |||
193 | // live playlist staleness timeout | ||
194 | loader.on('mediaupdatetimeout', function() { | ||
195 | if (loader.state !== 'HAVE_METADATA') { | ||
196 | // only refresh the media playlist if no other activity is going on | ||
197 | return; | ||
198 | } | ||
199 | |||
200 | loader.state = 'HAVE_CURRENT_METADATA'; | ||
201 | request = xhr(resolveUrl(loader.master.uri, loader.media.uri), | ||
202 | function(error) { | ||
203 | haveMetadata.call(this, error, loader.media.uri); | ||
204 | }); | ||
205 | }); | ||
206 | |||
207 | // request the specified URL | ||
208 | xhr(srcUrl, function(error) { | ||
209 | var parser; | ||
210 | |||
211 | if (error) { | ||
212 | loader.error = { | ||
213 | status: this.status, | ||
214 | message: 'HLS playlist request error at URL: ' + srcUrl, | ||
215 | code: (this.status >= 500) ? 4 : 2 | ||
216 | }; | ||
217 | return loader.trigger('error'); | ||
218 | } | ||
219 | |||
220 | parser = new videojs.m3u8.Parser(); | ||
221 | parser.push(this.responseText); | ||
222 | |||
223 | loader.state = 'HAVE_MASTER'; | ||
224 | |||
225 | parser.manifest.uri = srcUrl; | ||
226 | |||
227 | // loaded a master playlist | ||
228 | if (parser.manifest.playlists) { | ||
229 | loader.master = parser.manifest; | ||
230 | request = xhr(resolveUrl(srcUrl, parser.manifest.playlists[0].uri), | ||
231 | function(error) { | ||
232 | // pass along the URL specified in the master playlist | ||
233 | haveMetadata.call(this, | ||
234 | error, | ||
235 | parser.manifest.playlists[0].uri); | ||
236 | }); | ||
237 | return loader.trigger('loadedplaylist'); | ||
238 | } | ||
239 | |||
240 | // loaded a media playlist | ||
66 | // infer a master playlist if none was previously requested | 241 | // infer a master playlist if none was previously requested |
67 | loader.master = { | 242 | loader.master = { |
68 | playlists: [parser.manifest] | 243 | uri: window.location.href, |
244 | playlists: [{ | ||
245 | uri: srcUrl | ||
246 | }] | ||
69 | }; | 247 | }; |
70 | } | 248 | return haveMetadata.call(this, null, srcUrl); |
71 | loader.state = 'HAVE_MASTER'; | 249 | }); |
72 | return; | ||
73 | }; | 250 | }; |
74 | request.send(null); | 251 | PlaylistLoader.prototype = new videojs.hls.Stream(); |
75 | }; | ||
76 | 252 | ||
77 | window.videojs.hls.PlaylistLoader = PlaylistLoader; | 253 | videojs.hls.PlaylistLoader = PlaylistLoader; |
78 | })(window); | 254 | })(window, window.videojs); | ... | ... |
... | @@ -104,6 +104,9 @@ var | ... | @@ -104,6 +104,9 @@ var |
104 | * inititated. If it is an object, it should contain a `url` | 104 | * inititated. If it is an object, it should contain a `url` |
105 | * property that indicates the URL to request and optionally a | 105 | * property that indicates the URL to request and optionally a |
106 | * `method` which is the type of HTTP request to send. | 106 | * `method` which is the type of HTTP request to send. |
107 | * @param callback (optional) {function} a function to call when the | ||
108 | * request completes. If the request was not successful, the first | ||
109 | * argument will be falsey. | ||
107 | * @return {object} the XMLHttpRequest that was initiated. | 110 | * @return {object} the XMLHttpRequest that was initiated. |
108 | */ | 111 | */ |
109 | xhr = function(url, callback) { | 112 | xhr = function(url, callback) { | ... | ... |
1 | (function(window) { | 1 | (function(window) { |
2 | 'use strict'; | 2 | 'use strict'; |
3 | var | 3 | var |
4 | oldXhr, | 4 | sinonXhr, |
5 | clock, | ||
5 | requests, | 6 | requests, |
6 | videojs = window.videojs; | 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 | }; | ||
7 | 18 | ||
8 | module('Playlist Loader', { | 19 | module('Playlist Loader', { |
9 | setup: function() { | 20 | setup: function() { |
10 | oldXhr = window.XMLHttpRequest; | 21 | // fake XHRs |
22 | sinonXhr = sinon.useFakeXMLHttpRequest(); | ||
11 | requests = []; | 23 | requests = []; |
12 | 24 | sinonXhr.onCreate = function(xhr) { | |
13 | window.XMLHttpRequest = function() { | 25 | requests.push(xhr); |
14 | this.open = function(method, url) { | ||
15 | this.method = method; | ||
16 | this.url = url; | ||
17 | }; | ||
18 | this.send = function() { | ||
19 | requests.push(this); | ||
20 | }; | ||
21 | this.respond = function(response) { | ||
22 | this.responseText = response; | ||
23 | this.readyState = 4; | ||
24 | this.onreadystatechange(); | ||
25 | }; | ||
26 | }; | ||
27 | this.send = function() { | ||
28 | }; | 26 | }; |
27 | |||
28 | // fake timers | ||
29 | clock = sinon.useFakeTimers(); | ||
29 | }, | 30 | }, |
30 | teardown: function() { | 31 | teardown: function() { |
31 | window.XMLHttpRequest = oldXhr; | 32 | sinonXhr.restore(); |
33 | clock.restore(); | ||
32 | } | 34 | } |
33 | }); | 35 | }); |
34 | 36 | ||
... | @@ -47,14 +49,15 @@ | ... | @@ -47,14 +49,15 @@ |
47 | }); | 49 | }); |
48 | 50 | ||
49 | test('requests the initial playlist immediately', function() { | 51 | test('requests the initial playlist immediately', function() { |
50 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | 52 | new videojs.hls.PlaylistLoader('master.m3u8'); |
51 | strictEqual(requests.length, 1, 'made a request'); | 53 | strictEqual(requests.length, 1, 'made a request'); |
52 | strictEqual(requests[0].url, 'master.m3u8', 'requested the initial playlist'); | 54 | strictEqual(requests[0].url, 'master.m3u8', 'requested the initial playlist'); |
53 | }); | 55 | }); |
54 | 56 | ||
55 | test('moves to HAVE_MASTER after loading a master playlist', function() { | 57 | test('moves to HAVE_MASTER after loading a master playlist', function() { |
56 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | 58 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); |
57 | requests.pop().respond('#EXTM3U\n' + | 59 | requests.pop().respond(200, null, |
60 | '#EXTM3U\n' + | ||
58 | '#EXT-X-STREAM-INF:\n' + | 61 | '#EXT-X-STREAM-INF:\n' + |
59 | 'media.m3u8\n'); | 62 | 'media.m3u8\n'); |
60 | ok(loader.master, 'the master playlist is available'); | 63 | ok(loader.master, 'the master playlist is available'); |
... | @@ -63,20 +66,23 @@ | ... | @@ -63,20 +66,23 @@ |
63 | 66 | ||
64 | test('jumps to HAVE_METADATA when initialized with a media playlist', function() { | 67 | test('jumps to HAVE_METADATA when initialized with a media playlist', function() { |
65 | var loader = new videojs.hls.PlaylistLoader('media.m3u8'); | 68 | var loader = new videojs.hls.PlaylistLoader('media.m3u8'); |
66 | requests.pop().respond('#EXTM3U\n' + | 69 | requests.pop().respond(200, null, |
67 | '#EXTINF:10,\n' + | 70 | '#EXTM3U\n' + |
71 | '#EXTINF:10,\n' + | ||
68 | '0.ts\n' + | 72 | '0.ts\n' + |
69 | '#EXT-X-ENDLIST\n'); | 73 | '#EXT-X-ENDLIST\n'); |
70 | ok(loader.master, 'infers a master playlist'); | 74 | ok(loader.master, 'infers a master playlist'); |
71 | 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'); | ||
72 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 77 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
73 | strictEqual(0, requests.length, 'no more requests are made'); | 78 | strictEqual(0, requests.length, 'no more requests are made'); |
74 | }); | 79 | }); |
75 | 80 | ||
76 | test('jumps to HAVE_METADATA when initialized with a live media playlist', function() { | 81 | test('jumps to HAVE_METADATA when initialized with a live media playlist', function() { |
77 | var loader = new videojs.hls.PlaylistLoader('media.m3u8'); | 82 | var loader = new videojs.hls.PlaylistLoader('media.m3u8'); |
78 | requests.pop().respond('#EXTM3U\n' + | 83 | requests.pop().respond(200, null, |
79 | '#EXTINF:10,\n' + | 84 | '#EXTM3U\n' + |
85 | '#EXTINF:10,\n' + | ||
80 | '0.ts\n'); | 86 | '0.ts\n'); |
81 | ok(loader.master, 'infers a master playlist'); | 87 | ok(loader.master, 'infers a master playlist'); |
82 | ok(loader.media, 'sets the media playlist'); | 88 | ok(loader.media, 'sets the media playlist'); |
... | @@ -84,18 +90,28 @@ | ... | @@ -84,18 +90,28 @@ |
84 | }); | 90 | }); |
85 | 91 | ||
86 | test('moves to HAVE_METADATA after loading a media playlist', function() { | 92 | test('moves to HAVE_METADATA after loading a media playlist', function() { |
87 | var loader = new videojs.hls.PlaylistLoader('master.m3u8'); | 93 | var |
88 | requests.pop().respond('#EXTM3U\n' + | 94 | loadedPlaylist = false, |
95 | loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
96 | loader.on('loadedplaylist', function() { | ||
97 | loadedPlaylist = true; | ||
98 | }); | ||
99 | requests.pop().respond(200, null, | ||
100 | '#EXTM3U\n' + | ||
89 | '#EXT-X-STREAM-INF:\n' + | 101 | '#EXT-X-STREAM-INF:\n' + |
90 | 'media.m3u8\n' + | 102 | 'media.m3u8\n' + |
91 | 'alt.m3u8\n'); | 103 | 'alt.m3u8\n'); |
104 | ok(loadedPlaylist, 'loadedplaylist fired'); | ||
92 | strictEqual(requests.length, 1, 'requests the media playlist'); | 105 | strictEqual(requests.length, 1, 'requests the media playlist'); |
93 | strictEqual(requests[0].method, 'GET', 'GETs the media playlist'); | 106 | strictEqual(requests[0].method, 'GET', 'GETs the media playlist'); |
94 | strictEqual(requests[0].url, 'media.m3u8', 'requests the first playlist'); | 107 | strictEqual(requests[0].url, |
108 | urlTo('media.m3u8'), | ||
109 | 'requests the first playlist'); | ||
95 | 110 | ||
96 | requests.pop().response('#EXTM3U\n' + | 111 | requests.pop().respond(200, null, |
97 | '#EXTINF:10,\n' + | 112 | '#EXTM3U\n' + |
98 | '0.ts\n'); | 113 | '#EXTINF:10,\n' + |
114 | '0.ts\n'); | ||
99 | ok(loader.master, 'sets the master playlist'); | 115 | ok(loader.master, 'sets the master playlist'); |
100 | ok(loader.media, 'sets the media playlist'); | 116 | ok(loader.media, 'sets the media playlist'); |
101 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 117 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
... | @@ -103,24 +119,126 @@ | ... | @@ -103,24 +119,126 @@ |
103 | 119 | ||
104 | test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { | 120 | test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { |
105 | var loader = new videojs.hls.PlaylistLoader('live.m3u8'); | 121 | var loader = new videojs.hls.PlaylistLoader('live.m3u8'); |
106 | requests.pop().response('#EXTM3U\n' + | 122 | requests.pop().respond(200, null, |
107 | '#EXTINF:10,\n' + | 123 | '#EXTM3U\n' + |
108 | '0.ts\n'); | 124 | '#EXTINF:10,\n' + |
109 | loader.refreshMedia(); | 125 | '0.ts\n'); |
126 | clock.tick(10 * 1000); // 10s, one target duration | ||
110 | strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); | 127 | strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); |
111 | strictEqual(requests.length, 1, 'requested playlist'); | 128 | strictEqual(requests.length, 1, 'requested playlist'); |
112 | strictEqual(requests[0].url, 'live.m3u8', 'refreshes the media playlist'); | 129 | strictEqual(requests[0].url, |
130 | urlTo('live.m3u8'), | ||
131 | 'refreshes the media playlist'); | ||
113 | }); | 132 | }); |
114 | 133 | ||
115 | test('returns to HAVE_METADATA after refreshing the playlist', function() { | 134 | test('returns to HAVE_METADATA after refreshing the playlist', function() { |
116 | var loader = new videojs.hls.PlaylistLoader('live.m3u8'); | 135 | var loader = new videojs.hls.PlaylistLoader('live.m3u8'); |
117 | requests.pop().response('#EXTM3U\n' + | 136 | requests.pop().respond(200, null, |
118 | '#EXTINF:10,\n' + | 137 | '#EXTM3U\n' + |
119 | '0.ts\n'); | 138 | '#EXTINF:10,\n' + |
120 | loader.refreshMedia(); | 139 | '0.ts\n'); |
121 | requests.pop().response('#EXTM3U\n' + | 140 | clock.tick(10 * 1000); // 10s, one target duration |
122 | '#EXTINF:10,\n' + | 141 | requests.pop().respond(200, null, |
123 | '1.ts\n'); | 142 | '#EXTM3U\n' + |
124 | strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); | 143 | '#EXTINF:10,\n' + |
144 | '1.ts\n'); | ||
145 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | ||
146 | }); | ||
147 | |||
148 | test('emits an error when an initial playlist request fails', function() { | ||
149 | var | ||
150 | errors = [], | ||
151 | loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
152 | |||
153 | loader.on('error', function() { | ||
154 | errors.push(loader.error); | ||
155 | }); | ||
156 | requests.pop().respond(500); | ||
157 | |||
158 | strictEqual(errors.length, 1, 'emitted one error'); | ||
159 | strictEqual(errors[0].status, 500, 'http status is captured'); | ||
160 | }); | ||
161 | |||
162 | test('errors when an initial media playlist request fails', function() { | ||
163 | var | ||
164 | errors = [], | ||
165 | loader = new videojs.hls.PlaylistLoader('master.m3u8'); | ||
166 | |||
167 | loader.on('error', function() { | ||
168 | errors.push(loader.error); | ||
169 | }); | ||
170 | requests.pop().respond(200, null, | ||
171 | '#EXTM3U\n' + | ||
172 | '#EXT-X-STREAM-INF:\n' + | ||
173 | 'media.m3u8\n'); | ||
174 | |||
175 | strictEqual(errors.length, 0, 'emitted no errors'); | ||
176 | |||
177 | requests.pop().respond(500); | ||
178 | |||
179 | strictEqual(errors.length, 1, 'emitted one error'); | ||
180 | strictEqual(errors[0].status, 500, 'http status is captured'); | ||
181 | }); | ||
182 | |||
183 | |||
184 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 | ||
185 | test('halves the refresh timeout if a playlist is unchanged' + | ||
186 | 'since the last reload', function() { | ||
187 | new videojs.hls.PlaylistLoader('live.m3u8'); | ||
188 | requests.pop().respond(200, null, | ||
189 | '#EXTM3U\n' + | ||
190 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
191 | '#EXTINF:10,\n' + | ||
192 | '0.ts\n'); | ||
193 | clock.tick(10 * 1000); // trigger a refresh | ||
194 | requests.pop().respond(200, null, | ||
195 | '#EXTM3U\n' + | ||
196 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
197 | '#EXTINF:10,\n' + | ||
198 | '0.ts\n'); | ||
199 | clock.tick(5 * 1000); // half the default target-duration | ||
200 | |||
201 | strictEqual(requests.length, 1, 'sent a request'); | ||
202 | strictEqual(requests[0].url, | ||
203 | urlTo('live.m3u8'), | ||
204 | 'requested the media playlist'); | ||
205 | }); | ||
206 | |||
207 | test('media-sequence updates are considered a playlist change', function() { | ||
208 | new videojs.hls.PlaylistLoader('live.m3u8'); | ||
209 | requests.pop().respond(200, null, | ||
210 | '#EXTM3U\n' + | ||
211 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
212 | '#EXTINF:10,\n' + | ||
213 | '0.ts\n'); | ||
214 | clock.tick(10 * 1000); // trigger a refresh | ||
215 | requests.pop().respond(200, null, | ||
216 | '#EXTM3U\n' + | ||
217 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
218 | '#EXTINF:10,\n' + | ||
219 | '0.ts\n'); | ||
220 | clock.tick(5 * 1000); // half the default target-duration | ||
221 | |||
222 | strictEqual(requests.length, 0, 'no request is sent'); | ||
223 | }); | ||
224 | |||
225 | test('emits an error if a media refresh fails', function() { | ||
226 | var | ||
227 | errors = 0, | ||
228 | loader = new videojs.hls.PlaylistLoader('live.m3u8'); | ||
229 | |||
230 | loader.on('error', function() { | ||
231 | errors++; | ||
232 | }); | ||
233 | requests.pop().respond(200, null, | ||
234 | '#EXTM3U\n' + | ||
235 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
236 | '#EXTINF:10,\n' + | ||
237 | '0.ts\n'); | ||
238 | clock.tick(10 * 1000); // trigger a refresh | ||
239 | requests.pop().respond(500); | ||
240 | |||
241 | strictEqual(errors, 1, 'emitted an error'); | ||
242 | strictEqual(loader.error.status, 500, 'captured the status code'); | ||
125 | }); | 243 | }); |
126 | })(window); | 244 | })(window); | ... | ... |
... | @@ -6,8 +6,9 @@ | ... | @@ -6,8 +6,9 @@ |
6 | <!-- Load sinon server for fakeXHR --> | 6 | <!-- Load sinon server for fakeXHR --> |
7 | <script src="../node_modules/sinon/lib/sinon.js"></script> | 7 | <script src="../node_modules/sinon/lib/sinon.js"></script> |
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/xhr_ie.js"></script> | ||
10 | <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> | ||
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"> | ... | ... |
... | @@ -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) { | ... | ... |
-
Please register or sign in to post a comment