browserify-p4: playlist*, xhr, and resolve-url
updated stub.js to keep unit tests working updated build/watch scripts ripped resolve-url out of videojs-contrib-hls for now
Showing
12 changed files
with
982 additions
and
798 deletions
... | @@ -48,10 +48,7 @@ | ... | @@ -48,10 +48,7 @@ |
48 | <script src="/node_modules/video.js/dist/video.js"></script> | 48 | <script src="/node_modules/video.js/dist/video.js"></script> |
49 | <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script> | 49 | <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script> |
50 | <script src="/src/videojs-contrib-hls.js"></script> | 50 | <script src="/src/videojs-contrib-hls.js"></script> |
51 | <script src="/src/xhr.js"></script> | ||
52 | <script src="/dist/videojs-contrib-hls.js"></script> | 51 | <script src="/dist/videojs-contrib-hls.js"></script> |
53 | <script src="/src/playlist.js"></script> | ||
54 | <script src="/src/playlist-loader.js"></script> | ||
55 | <script src="/src/bin-utils.js"></script> | 52 | <script src="/src/bin-utils.js"></script> |
56 | <script> | 53 | <script> |
57 | (function(window, videojs) { | 54 | (function(window, videojs) { | ... | ... |
... | @@ -2,7 +2,7 @@ var browserify = require('browserify'); | ... | @@ -2,7 +2,7 @@ var browserify = require('browserify'); |
2 | var fs = require('fs'); | 2 | var fs = require('fs'); |
3 | var glob = require('glob'); | 3 | var glob = require('glob'); |
4 | 4 | ||
5 | glob('test/{decryper,m3u8,stub}.test.js', function(err, files) { | 5 | glob('test/{playlist*,decryper,m3u8,stub}.test.js', function(err, files) { |
6 | browserify(files) | 6 | browserify(files) |
7 | .transform('babelify') | 7 | .transform('babelify') |
8 | .bundle() | 8 | .bundle() | ... | ... |
... | @@ -3,7 +3,7 @@ var fs = require('fs'); | ... | @@ -3,7 +3,7 @@ var fs = require('fs'); |
3 | var glob = require('glob'); | 3 | var glob = require('glob'); |
4 | var watchify = require('watchify'); | 4 | var watchify = require('watchify'); |
5 | 5 | ||
6 | glob('test/{decrypter,m3u8,stub}.test.js', function(err, files) { | 6 | glob('test/{playlist*,decrypter,m3u8,stub}.test.js', function(err, files) { |
7 | var b = browserify(files, { | 7 | var b = browserify(files, { |
8 | cache: {}, | 8 | cache: {}, |
9 | packageCache: {}, | 9 | packageCache: {}, | ... | ... |
... | @@ -5,14 +5,41 @@ | ... | @@ -5,14 +5,41 @@ |
5 | * M3U8 playlists. | 5 | * M3U8 playlists. |
6 | * | 6 | * |
7 | */ | 7 | */ |
8 | (function(window, videojs) { | 8 | import resolveUrl from './resolve-url'; |
9 | 'use strict'; | 9 | import XhrModule from './xhr'; |
10 | var | 10 | import {mergeOptions} from 'video.js'; |
11 | resolveUrl = videojs.Hls.resolveUrl, | 11 | import Stream from './stream'; |
12 | xhr = videojs.Hls.xhr, | 12 | import m3u8 from './m3u8'; |
13 | mergeOptions = videojs.mergeOptions, | ||
14 | 13 | ||
15 | /** | 14 | /** |
15 | * Returns a new array of segments that is the result of merging | ||
16 | * properties from an older list of segments onto an updated | ||
17 | * list. No properties on the updated playlist will be overridden. | ||
18 | * @param original {array} the outdated list of segments | ||
19 | * @param update {array} the updated list of segments | ||
20 | * @param offset {number} (optional) the index of the first update | ||
21 | * segment in the original segment list. For non-live playlists, | ||
22 | * this should always be zero and does not need to be | ||
23 | * specified. For live playlists, it should be the difference | ||
24 | * between the media sequence numbers in the original and updated | ||
25 | * playlists. | ||
26 | * @return a list of merged segment objects | ||
27 | */ | ||
28 | const updateSegments = function(original, update, offset) { | ||
29 | let result = update.slice(); | ||
30 | let length; | ||
31 | let i; | ||
32 | |||
33 | offset = offset || 0; | ||
34 | length = Math.min(original.length, update.length + offset); | ||
35 | |||
36 | for (i = offset; i < length; i++) { | ||
37 | result[i - offset] = mergeOptions(original[i], result[i - offset]); | ||
38 | } | ||
39 | return result; | ||
40 | }; | ||
41 | |||
42 | /** | ||
16 | * Returns a new master playlist that is the result of merging an | 43 | * Returns a new master playlist that is the result of merging an |
17 | * updated media playlist into the original version. If the | 44 | * updated media playlist into the original version. If the |
18 | * updated media playlist does not match any of the playlist | 45 | * updated media playlist does not match any of the playlist |
... | @@ -23,14 +50,12 @@ | ... | @@ -23,14 +50,12 @@ |
23 | * master playlist with the updated media playlist merged in, or | 50 | * master playlist with the updated media playlist merged in, or |
24 | * null if the merge produced no change. | 51 | * null if the merge produced no change. |
25 | */ | 52 | */ |
26 | updateMaster = function(master, media) { | 53 | const updateMaster = function(master, media) { |
27 | var | 54 | let changed = false; |
28 | changed = false, | 55 | let result = mergeOptions(master, {}); |
29 | result = mergeOptions(master, {}), | 56 | let i = master.playlists.length; |
30 | i, | 57 | let playlist; |
31 | playlist; | 58 | |
32 | |||
33 | i = master.playlists.length; | ||
34 | while (i--) { | 59 | while (i--) { |
35 | playlist = result.playlists[i]; | 60 | playlist = result.playlists[i]; |
36 | if (playlist.uri === media.uri) { | 61 | if (playlist.uri === media.uri) { |
... | @@ -49,103 +74,175 @@ | ... | @@ -49,103 +74,175 @@ |
49 | // if the update could overlap existing segment information, | 74 | // if the update could overlap existing segment information, |
50 | // merge the two lists | 75 | // merge the two lists |
51 | if (playlist.segments) { | 76 | if (playlist.segments) { |
52 | result.playlists[i].segments = updateSegments(playlist.segments, | 77 | result.playlists[i].segments = updateSegments( |
78 | playlist.segments, | ||
53 | media.segments, | 79 | media.segments, |
54 | media.mediaSequence - playlist.mediaSequence); | 80 | media.mediaSequence - playlist.mediaSequence |
81 | ); | ||
55 | } | 82 | } |
56 | changed = true; | 83 | changed = true; |
57 | } | 84 | } |
58 | } | 85 | } |
59 | return changed ? result : null; | 86 | return changed ? result : null; |
60 | }, | 87 | }; |
61 | 88 | ||
62 | /** | 89 | export default class PlaylistLoader extends Stream { |
63 | * Returns a new array of segments that is the result of merging | 90 | constructor(srcUrl, withCredentials) { |
64 | * properties from an older list of segments onto an updated | 91 | super(); |
65 | * list. No properties on the updated playlist will be overridden. | 92 | this.srcUrl = srcUrl; |
66 | * @param original {array} the outdated list of segments | 93 | this.withCredentials = withCredentials; |
67 | * @param update {array} the updated list of segments | ||
68 | * @param offset {number} (optional) the index of the first update | ||
69 | * segment in the original segment list. For non-live playlists, | ||
70 | * this should always be zero and does not need to be | ||
71 | * specified. For live playlists, it should be the difference | ||
72 | * between the media sequence numbers in the original and updated | ||
73 | * playlists. | ||
74 | * @return a list of merged segment objects | ||
75 | */ | ||
76 | updateSegments = function(original, update, offset) { | ||
77 | var result = update.slice(), length, i; | ||
78 | offset = offset || 0; | ||
79 | length = Math.min(original.length, update.length + offset); | ||
80 | 94 | ||
81 | for (i = offset; i < length; i++) { | 95 | this.mediaUpdateTimeout = null; |
82 | result[i - offset] = mergeOptions(original[i], result[i - offset]); | ||
83 | } | ||
84 | return result; | ||
85 | }, | ||
86 | 96 | ||
87 | PlaylistLoader = function(srcUrl, withCredentials) { | 97 | // initialize the loader state |
88 | var | 98 | this.state = 'HAVE_NOTHING'; |
89 | loader = this, | ||
90 | dispose, | ||
91 | mediaUpdateTimeout, | ||
92 | request, | ||
93 | playlistRequestError, | ||
94 | haveMetadata; | ||
95 | 99 | ||
96 | PlaylistLoader.prototype.init.call(this); | 100 | // track the time that has expired from the live window |
101 | // this allows the seekable start range to be calculated even if | ||
102 | // all segments with timing information have expired | ||
103 | this.expired_ = 0; | ||
97 | 104 | ||
98 | // a flag that disables "expired time"-tracking this setting has | 105 | // a flag that disables "expired time"-tracking this setting has |
99 | // no effect when not playing a live stream | 106 | // no effect when not playing a live stream |
100 | this.trackExpiredTime_ = false; | 107 | this.trackExpiredTime_ = false; |
101 | 108 | ||
102 | if (!srcUrl) { | 109 | if (!this.srcUrl) { |
103 | throw new Error('A non-empty playlist URL is required'); | 110 | throw new Error('A non-empty playlist URL is required'); |
104 | } | 111 | } |
105 | 112 | ||
106 | playlistRequestError = function(xhr, url, startingState) { | 113 | // In a live list, don't keep track of the expired time until |
107 | loader.setBandwidth(request || xhr); | 114 | // HLS tells us that "first play" has commenced |
115 | this.on('firstplay', function() { | ||
116 | this.trackExpiredTime_ = true; | ||
117 | }); | ||
118 | |||
119 | // live playlist staleness timeout | ||
120 | this.on('mediaupdatetimeout', () => { | ||
121 | if (this.state !== 'HAVE_METADATA') { | ||
122 | // only refresh the media playlist if no other activity is going on | ||
123 | return; | ||
124 | } | ||
125 | |||
126 | this.state = 'HAVE_CURRENT_METADATA'; | ||
127 | this.request = XhrModule({ | ||
128 | uri: resolveUrl(this.master.uri, this.media().uri), | ||
129 | withCredentials: this.withCredentials | ||
130 | }, (error, request) => { | ||
131 | if (error) { | ||
132 | return this.playlistRequestError(request, this.media().uri); | ||
133 | } | ||
134 | this.haveMetadata(request, this.media().uri); | ||
135 | }); | ||
136 | }); | ||
137 | |||
138 | // request the specified URL | ||
139 | this.request = XhrModule({ | ||
140 | uri: this.srcUrl, | ||
141 | withCredentials: this.withCredentials | ||
142 | }, (error, request) => { | ||
143 | let parser = new m3u8.Parser(); | ||
144 | let i; | ||
145 | |||
146 | // clear the loader's request reference | ||
147 | this.request = null; | ||
148 | |||
149 | if (error) { | ||
150 | this.error = { | ||
151 | status: request.status, | ||
152 | message: 'HLS playlist request error at URL: ' + this.srcUrl, | ||
153 | responseText: request.responseText, | ||
154 | // MEDIA_ERR_NETWORK | ||
155 | code: 2 | ||
156 | }; | ||
157 | return this.trigger('error'); | ||
158 | } | ||
159 | |||
160 | parser.push(request.responseText); | ||
161 | parser.end(); | ||
162 | |||
163 | this.state = 'HAVE_MASTER'; | ||
164 | |||
165 | parser.manifest.uri = this.srcUrl; | ||
166 | |||
167 | // loaded a master playlist | ||
168 | if (parser.manifest.playlists) { | ||
169 | this.master = parser.manifest; | ||
170 | |||
171 | // setup by-URI lookups | ||
172 | i = this.master.playlists.length; | ||
173 | while (i--) { | ||
174 | this.master.playlists[this.master.playlists[i].uri] = | ||
175 | this.master.playlists[i]; | ||
176 | } | ||
177 | |||
178 | this.trigger('loadedplaylist'); | ||
179 | if (!this.request) { | ||
180 | // no media playlist was specifically selected so start | ||
181 | // from the first listed one | ||
182 | this.media(parser.manifest.playlists[0]); | ||
183 | } | ||
184 | return; | ||
185 | } | ||
186 | |||
187 | // loaded a media playlist | ||
188 | // infer a master playlist if none was previously requested | ||
189 | this.master = { | ||
190 | uri: window.location.href, | ||
191 | playlists: [{ | ||
192 | uri: this.srcUrl | ||
193 | }] | ||
194 | }; | ||
195 | this.master.playlists[this.srcUrl] = this.master.playlists[0]; | ||
196 | this.haveMetadata(request, this.srcUrl); | ||
197 | return this.trigger('loadedmetadata'); | ||
198 | }); | ||
199 | } | ||
200 | |||
201 | playlistRequestError(xhr, url, startingState) { | ||
202 | this.setBandwidth(this.request || xhr); | ||
108 | 203 | ||
109 | // any in-flight request is now finished | 204 | // any in-flight request is now finished |
110 | request = null; | 205 | this.request = null; |
111 | 206 | ||
112 | if (startingState) { | 207 | if (startingState) { |
113 | loader.state = startingState; | 208 | this.state = startingState; |
114 | } | 209 | } |
115 | 210 | ||
116 | loader.error = { | 211 | this.error = { |
117 | playlist: loader.master.playlists[url], | 212 | playlist: this.master.playlists[url], |
118 | status: xhr.status, | 213 | status: xhr.status, |
119 | message: 'HLS playlist request error at URL: ' + url, | 214 | message: 'HLS playlist request error at URL: ' + url, |
120 | responseText: xhr.responseText, | 215 | responseText: xhr.responseText, |
121 | code: (xhr.status >= 500) ? 4 : 2 | 216 | code: (xhr.status >= 500) ? 4 : 2 |
122 | }; | 217 | }; |
123 | loader.trigger('error'); | 218 | this.trigger('error'); |
124 | }; | 219 | } |
125 | 220 | ||
126 | // update the playlist loader's state in response to a new or | 221 | // update the playlist loader's state in response to a new or |
127 | // updated playlist. | 222 | // updated playlist. |
223 | haveMetadata(xhr, url) { | ||
224 | let parser; | ||
225 | let refreshDelay; | ||
226 | let update; | ||
128 | 227 | ||
129 | haveMetadata = function(xhr, url) { | 228 | this.setBandwidth(this.request || xhr); |
130 | var parser, refreshDelay, update; | ||
131 | |||
132 | loader.setBandwidth(request || xhr); | ||
133 | 229 | ||
134 | // any in-flight request is now finished | 230 | // any in-flight request is now finished |
135 | request = null; | 231 | this.request = null; |
136 | loader.state = 'HAVE_METADATA'; | 232 | |
233 | this.state = 'HAVE_METADATA'; | ||
137 | 234 | ||
138 | parser = new videojs.m3u8.Parser(); | 235 | parser = new m3u8.Parser(); |
139 | parser.push(xhr.responseText); | 236 | parser.push(xhr.responseText); |
140 | parser.end(); | 237 | parser.end(); |
141 | parser.manifest.uri = url; | 238 | parser.manifest.uri = url; |
142 | 239 | ||
143 | // merge this playlist into the master | 240 | // merge this playlist into the master |
144 | update = updateMaster(loader.master, parser.manifest); | 241 | update = updateMaster(this.master, parser.manifest); |
145 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; | 242 | refreshDelay = (parser.manifest.targetDuration || 10) * 1000; |
146 | if (update) { | 243 | if (update) { |
147 | loader.master = update; | 244 | this.master = update; |
148 | loader.updateMediaPlaylist_(parser.manifest); | 245 | this.updateMediaPlaylist_(parser.manifest); |
149 | } else { | 246 | } else { |
150 | // if the playlist is unchanged since the last reload, | 247 | // if the playlist is unchanged since the last reload, |
151 | // try again after half the target duration | 248 | // try again after half the target duration |
... | @@ -153,39 +250,38 @@ | ... | @@ -153,39 +250,38 @@ |
153 | } | 250 | } |
154 | 251 | ||
155 | // refresh live playlists after a target duration passes | 252 | // refresh live playlists after a target duration passes |
156 | if (!loader.media().endList) { | 253 | if (!this.media().endList) { |
157 | window.clearTimeout(mediaUpdateTimeout); | 254 | this.clearMediaUpdateTimeout_(); |
158 | mediaUpdateTimeout = window.setTimeout(function() { | 255 | this.mediaUpdateTimeout = window.setTimeout(() => { |
159 | loader.trigger('mediaupdatetimeout'); | 256 | this.trigger('mediaupdatetimeout'); |
160 | }, refreshDelay); | 257 | }, refreshDelay); |
161 | } | 258 | } |
162 | 259 | ||
163 | loader.trigger('loadedplaylist'); | 260 | this.trigger('loadedplaylist'); |
164 | }; | 261 | } |
165 | |||
166 | // initialize the loader state | ||
167 | loader.state = 'HAVE_NOTHING'; | ||
168 | 262 | ||
169 | // track the time that has expired from the live window | 263 | clearMediaUpdateTimeout_() { |
170 | // this allows the seekable start range to be calculated even if | 264 | if (this.mediaUpdateTimeout) { |
171 | // all segments with timing information have expired | 265 | window.clearTimeout(this.mediaUpdateTimeout); |
172 | this.expired_ = 0; | 266 | } |
267 | } | ||
173 | 268 | ||
174 | // capture the prototype dispose function | 269 | requestDispose_() { |
175 | dispose = this.dispose; | 270 | if (this.request) { |
271 | this.request.onreadystatechange = null; | ||
272 | this.request.abort(); | ||
273 | this.request = null; | ||
274 | } | ||
275 | } | ||
176 | 276 | ||
177 | /** | 277 | /** |
178 | * Abort any outstanding work and clean up. | 278 | * Abort any outstanding work and clean up. |
179 | */ | 279 | */ |
180 | loader.dispose = function() { | 280 | dispose() { |
181 | if (request) { | 281 | this.requestDispose_(); |
182 | request.onreadystatechange = null; | 282 | this.clearMediaUpdateTimeout_(); |
183 | request.abort(); | 283 | super.dispose(); |
184 | request = null; | 284 | } |
185 | } | ||
186 | window.clearTimeout(mediaUpdateTimeout); | ||
187 | dispose.call(this); | ||
188 | }; | ||
189 | 285 | ||
190 | /** | 286 | /** |
191 | * When called without any arguments, returns the currently | 287 | * When called without any arguments, returns the currently |
... | @@ -197,43 +293,41 @@ | ... | @@ -197,43 +293,41 @@ |
197 | * @param playlist (optional) {object} the parsed media playlist | 293 | * @param playlist (optional) {object} the parsed media playlist |
198 | * object to switch to | 294 | * object to switch to |
199 | */ | 295 | */ |
200 | loader.media = function(playlist) { | 296 | media(playlist) { |
201 | var startingState = loader.state, mediaChange; | 297 | let startingState = this.state; |
298 | let mediaChange; | ||
299 | |||
202 | // getter | 300 | // getter |
203 | if (!playlist) { | 301 | if (!playlist) { |
204 | return loader.media_; | 302 | return this.media_; |
205 | } | 303 | } |
206 | 304 | ||
207 | // setter | 305 | // setter |
208 | if (loader.state === 'HAVE_NOTHING') { | 306 | if (this.state === 'HAVE_NOTHING') { |
209 | throw new Error('Cannot switch media playlist from ' + loader.state); | 307 | throw new Error('Cannot switch media playlist from ' + this.state); |
210 | } | 308 | } |
211 | 309 | ||
212 | // find the playlist object if the target playlist has been | 310 | // find the playlist object if the target playlist has been |
213 | // specified by URI | 311 | // specified by URI |
214 | if (typeof playlist === 'string') { | 312 | if (typeof playlist === 'string') { |
215 | if (!loader.master.playlists[playlist]) { | 313 | if (!this.master.playlists[playlist]) { |
216 | throw new Error('Unknown playlist URI: ' + playlist); | 314 | throw new Error('Unknown playlist URI: ' + playlist); |
217 | } | 315 | } |
218 | playlist = loader.master.playlists[playlist]; | 316 | playlist = this.master.playlists[playlist]; |
219 | } | 317 | } |
220 | 318 | ||
221 | mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri; | 319 | mediaChange = !this.media_ || playlist.uri !== this.media_.uri; |
222 | 320 | ||
223 | // switch to fully loaded playlists immediately | 321 | // switch to fully loaded playlists immediately |
224 | if (loader.master.playlists[playlist.uri].endList) { | 322 | if (this.master.playlists[playlist.uri].endList) { |
225 | // abort outstanding playlist requests | 323 | // abort outstanding playlist requests |
226 | if (request) { | 324 | this.requestDispose_(); |
227 | request.onreadystatechange = null; | 325 | this.state = 'HAVE_METADATA'; |
228 | request.abort(); | 326 | this.media_ = playlist; |
229 | request = null; | ||
230 | } | ||
231 | loader.state = 'HAVE_METADATA'; | ||
232 | loader.media_ = playlist; | ||
233 | 327 | ||
234 | // trigger media change if the active media has been updated | 328 | // trigger media change if the active media has been updated |
235 | if (mediaChange) { | 329 | if (mediaChange) { |
236 | loader.trigger('mediachange'); | 330 | this.trigger('mediachange'); |
237 | } | 331 | } |
238 | return; | 332 | return; |
239 | } | 333 | } |
... | @@ -243,138 +337,53 @@ | ... | @@ -243,138 +337,53 @@ |
243 | return; | 337 | return; |
244 | } | 338 | } |
245 | 339 | ||
246 | loader.state = 'SWITCHING_MEDIA'; | 340 | this.state = 'SWITCHING_MEDIA'; |
247 | 341 | ||
248 | // there is already an outstanding playlist request | 342 | // there is already an outstanding playlist request |
249 | if (request) { | 343 | if (this.request) { |
250 | if (resolveUrl(loader.master.uri, playlist.uri) === request.url) { | 344 | if (resolveUrl(this.master.uri, playlist.uri) === this.request.url) { |
251 | // requesting to switch to the same playlist multiple times | 345 | // requesting to switch to the same playlist multiple times |
252 | // has no effect after the first | 346 | // has no effect after the first |
253 | return; | 347 | return; |
254 | } | 348 | } |
255 | request.onreadystatechange = null; | 349 | this.requestDispose_(); |
256 | request.abort(); | ||
257 | request = null; | ||
258 | } | 350 | } |
259 | 351 | ||
260 | // request the new playlist | 352 | // request the new playlist |
261 | request = xhr({ | 353 | this.request = XhrModule({ |
262 | uri: resolveUrl(loader.master.uri, playlist.uri), | 354 | uri: resolveUrl(this.master.uri, playlist.uri), |
263 | withCredentials: withCredentials | 355 | withCredentials: this.withCredentials |
264 | }, function(error, request) { | 356 | }, (error, request) => { |
265 | if (error) { | 357 | if (error) { |
266 | return playlistRequestError(request, playlist.uri, startingState); | 358 | return this.playlistRequestError(request, playlist.uri, startingState); |
267 | } | 359 | } |
360 | this.haveMetadata(request, playlist.uri); | ||
268 | 361 | ||
269 | haveMetadata(request, playlist.uri); | 362 | if (error) { |
363 | return; | ||
364 | } | ||
270 | 365 | ||
271 | // fire loadedmetadata the first time a media playlist is loaded | 366 | // fire loadedmetadata the first time a media playlist is loaded |
272 | if (startingState === 'HAVE_MASTER') { | 367 | if (startingState === 'HAVE_MASTER') { |
273 | loader.trigger('loadedmetadata'); | 368 | this.trigger('loadedmetadata'); |
274 | } else { | 369 | } else { |
275 | loader.trigger('mediachange'); | 370 | this.trigger('mediachange'); |
276 | } | ||
277 | }); | ||
278 | }; | ||
279 | |||
280 | loader.setBandwidth = function(xhr) { | ||
281 | loader.bandwidth = xhr.bandwidth; | ||
282 | }; | ||
283 | |||
284 | // In a live list, don't keep track of the expired time until | ||
285 | // HLS tells us that "first play" has commenced | ||
286 | loader.on('firstplay', function() { | ||
287 | this.trackExpiredTime_ = true; | ||
288 | }); | ||
289 | |||
290 | // live playlist staleness timeout | ||
291 | loader.on('mediaupdatetimeout', function() { | ||
292 | if (loader.state !== 'HAVE_METADATA') { | ||
293 | // only refresh the media playlist if no other activity is going on | ||
294 | return; | ||
295 | } | ||
296 | |||
297 | loader.state = 'HAVE_CURRENT_METADATA'; | ||
298 | request = xhr({ | ||
299 | uri: resolveUrl(loader.master.uri, loader.media().uri), | ||
300 | withCredentials: withCredentials | ||
301 | }, function(error, request) { | ||
302 | if (error) { | ||
303 | return playlistRequestError(request, loader.media().uri); | ||
304 | } | 371 | } |
305 | haveMetadata(request, loader.media().uri); | ||
306 | }); | ||
307 | }); | 372 | }); |
308 | |||
309 | // request the specified URL | ||
310 | request = xhr({ | ||
311 | uri: srcUrl, | ||
312 | withCredentials: withCredentials | ||
313 | }, function(error, req) { | ||
314 | var parser, i; | ||
315 | |||
316 | // clear the loader's request reference | ||
317 | request = null; | ||
318 | |||
319 | if (error) { | ||
320 | loader.error = { | ||
321 | status: req.status, | ||
322 | message: 'HLS playlist request error at URL: ' + srcUrl, | ||
323 | responseText: req.responseText, | ||
324 | code: 2 // MEDIA_ERR_NETWORK | ||
325 | }; | ||
326 | return loader.trigger('error'); | ||
327 | } | 373 | } |
328 | 374 | ||
329 | parser = new videojs.m3u8.Parser(); | 375 | setBandwidth(xhr) { |
330 | parser.push(req.responseText); | 376 | this.bandwidth = xhr.bandwidth; |
331 | parser.end(); | ||
332 | |||
333 | loader.state = 'HAVE_MASTER'; | ||
334 | |||
335 | parser.manifest.uri = srcUrl; | ||
336 | |||
337 | // loaded a master playlist | ||
338 | if (parser.manifest.playlists) { | ||
339 | loader.master = parser.manifest; | ||
340 | |||
341 | // setup by-URI lookups | ||
342 | i = loader.master.playlists.length; | ||
343 | while (i--) { | ||
344 | loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; | ||
345 | } | 377 | } |
346 | |||
347 | loader.trigger('loadedplaylist'); | ||
348 | if (!request) { | ||
349 | // no media playlist was specifically selected so start | ||
350 | // from the first listed one | ||
351 | loader.media(parser.manifest.playlists[0]); | ||
352 | } | ||
353 | return; | ||
354 | } | ||
355 | |||
356 | // loaded a media playlist | ||
357 | // infer a master playlist if none was previously requested | ||
358 | loader.master = { | ||
359 | uri: window.location.href, | ||
360 | playlists: [{ | ||
361 | uri: srcUrl | ||
362 | }] | ||
363 | }; | ||
364 | loader.master.playlists[srcUrl] = loader.master.playlists[0]; | ||
365 | haveMetadata(req, srcUrl); | ||
366 | return loader.trigger('loadedmetadata'); | ||
367 | }); | ||
368 | }; | ||
369 | PlaylistLoader.prototype = new videojs.Hls.Stream(); | ||
370 | |||
371 | /** | 378 | /** |
372 | * Update the PlaylistLoader state to reflect the changes in an | 379 | * Update the PlaylistLoader state to reflect the changes in an |
373 | * update to the current media playlist. | 380 | * update to the current media playlist. |
374 | * @param update {object} the updated media playlist object | 381 | * @param update {object} the updated media playlist object |
375 | */ | 382 | */ |
376 | PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { | 383 | updateMediaPlaylist_(update) { |
377 | var outdated, i, segment; | 384 | let outdated; |
385 | let i; | ||
386 | let segment; | ||
378 | 387 | ||
379 | outdated = this.media_; | 388 | outdated = this.media_; |
380 | this.media_ = this.master.playlists[update.uri]; | 389 | this.media_ = this.master.playlists[update.uri]; |
... | @@ -398,10 +407,10 @@ | ... | @@ -398,10 +407,10 @@ |
398 | // try using precise timing from first segment of the updated | 407 | // try using precise timing from first segment of the updated |
399 | // playlist | 408 | // playlist |
400 | if (update.segments.length) { | 409 | if (update.segments.length) { |
401 | if (update.segments[0].start !== undefined) { | 410 | if (typeof update.segments[0].start !== 'undefined') { |
402 | this.expired_ = update.segments[0].start; | 411 | this.expired_ = update.segments[0].start; |
403 | return; | 412 | return; |
404 | } else if (update.segments[0].end !== undefined) { | 413 | } else if (typeof update.segments[0].end !== 'undefined') { |
405 | this.expired_ = update.segments[0].end - update.segments[0].duration; | 414 | this.expired_ = update.segments[0].end - update.segments[0].duration; |
406 | return; | 415 | return; |
407 | } | 416 | } |
... | @@ -422,17 +431,17 @@ | ... | @@ -422,17 +431,17 @@ |
422 | continue; | 431 | continue; |
423 | } | 432 | } |
424 | 433 | ||
425 | if (segment.end !== undefined) { | 434 | if (typeof segment.end !== 'undefined') { |
426 | this.expired_ = segment.end; | 435 | this.expired_ = segment.end; |
427 | return; | 436 | return; |
428 | } | 437 | } |
429 | if (segment.start !== undefined) { | 438 | if (typeof segment.start !== 'undefined') { |
430 | this.expired_ = segment.start + segment.duration; | 439 | this.expired_ = segment.start + segment.duration; |
431 | return; | 440 | return; |
432 | } | 441 | } |
433 | this.expired_ += segment.duration; | 442 | this.expired_ += segment.duration; |
434 | } | 443 | } |
435 | }; | 444 | } |
436 | 445 | ||
437 | /** | 446 | /** |
438 | * Determine the index of the segment that contains a specified | 447 | * Determine the index of the segment that contains a specified |
... | @@ -452,17 +461,16 @@ | ... | @@ -452,17 +461,16 @@ |
452 | * value will be clamped to the index of the segment containing the | 461 | * value will be clamped to the index of the segment containing the |
453 | * closest playback position that is currently available. | 462 | * closest playback position that is currently available. |
454 | */ | 463 | */ |
455 | PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { | 464 | getMediaIndexForTime_(time) { |
456 | var | 465 | let i; |
457 | i, | 466 | let segment; |
458 | segment, | 467 | let originalTime = time; |
459 | originalTime = time, | 468 | let numSegments = this.media_.segments.length; |
460 | numSegments = this.media_.segments.length, | 469 | let lastSegment = numSegments - 1; |
461 | lastSegment = numSegments - 1, | 470 | let startIndex; |
462 | startIndex, | 471 | let endIndex; |
463 | endIndex, | 472 | let knownStart; |
464 | knownStart, | 473 | let knownEnd; |
465 | knownEnd; | ||
466 | 474 | ||
467 | if (!this.media_) { | 475 | if (!this.media_) { |
468 | return 0; | 476 | return 0; |
... | @@ -492,7 +500,7 @@ | ... | @@ -492,7 +500,7 @@ |
492 | 500 | ||
493 | // use the bounds we just found and playlist information to | 501 | // use the bounds we just found and playlist information to |
494 | // estimate the segment that contains the time we are looking for | 502 | // estimate the segment that contains the time we are looking for |
495 | if (startIndex !== undefined) { | 503 | if (typeof startIndex !== 'undefined') { |
496 | // We have a known-start point that is before our desired time so | 504 | // We have a known-start point that is before our desired time so |
497 | // walk from that point forwards | 505 | // walk from that point forwards |
498 | time = time - knownStart; | 506 | time = time - knownStart; |
... | @@ -517,7 +525,7 @@ | ... | @@ -517,7 +525,7 @@ |
517 | 525 | ||
518 | // We _still_ haven't found a segment so load the last one | 526 | // We _still_ haven't found a segment so load the last one |
519 | return lastSegment; | 527 | return lastSegment; |
520 | } else if (endIndex !== undefined) { | 528 | } else if (typeof endIndex !== 'undefined') { |
521 | // We _only_ have a known-end point that is after our desired time so | 529 | // We _only_ have a known-end point that is after our desired time so |
522 | // walk from that point backwards | 530 | // walk from that point backwards |
523 | time = knownEnd - time; | 531 | time = knownEnd - time; |
... | @@ -533,10 +541,9 @@ | ... | @@ -533,10 +541,9 @@ |
533 | // We haven't found a segment so load the first one if time is zero | 541 | // We haven't found a segment so load the first one if time is zero |
534 | if (time === 0) { | 542 | if (time === 0) { |
535 | return 0; | 543 | return 0; |
536 | } else { | 544 | } |
537 | return -1; | 545 | return -1; |
538 | } | 546 | } |
539 | } else { | ||
540 | // We known nothing so walk from the front of the playlist, | 547 | // We known nothing so walk from the front of the playlist, |
541 | // subtracting durations until we find a segment that contains | 548 | // subtracting durations until we find a segment that contains |
542 | // time and return it | 549 | // time and return it |
... | @@ -558,7 +565,4 @@ | ... | @@ -558,7 +565,4 @@ |
558 | // the one most likely to tell us something about the timeline | 565 | // the one most likely to tell us something about the timeline |
559 | return lastSegment; | 566 | return lastSegment; |
560 | } | 567 | } |
561 | }; | 568 | } |
562 | |||
563 | videojs.Hls.PlaylistLoader = PlaylistLoader; | ||
564 | })(window, window.videojs); | ... | ... |
1 | /** | 1 | /** |
2 | * Playlist related utilities. | 2 | * Playlist related utilities. |
3 | */ | 3 | */ |
4 | (function(window, videojs) { | 4 | import {createTimeRange} from 'video.js'; |
5 | 'use strict'; | ||
6 | 5 | ||
7 | var duration, intervalDuration, backwardDuration, forwardDuration, seekable; | 6 | const backwardDuration = function(playlist, endSequence) { |
8 | 7 | let result = 0; | |
9 | backwardDuration = function(playlist, endSequence) { | 8 | let i = endSequence - playlist.mediaSequence; |
10 | var result = 0, segment, i; | ||
11 | |||
12 | i = endSequence - playlist.mediaSequence; | ||
13 | // if a start time is available for segment immediately following | 9 | // if a start time is available for segment immediately following |
14 | // the interval, use it | 10 | // the interval, use it |
15 | segment = playlist.segments[i]; | 11 | let segment = playlist.segments[i]; |
12 | |||
16 | // Walk backward until we find the latest segment with timeline | 13 | // Walk backward until we find the latest segment with timeline |
17 | // information that is earlier than endSequence | 14 | // information that is earlier than endSequence |
18 | if (segment) { | 15 | if (segment) { |
19 | if (segment.start !== undefined) { | 16 | if (typeof segment.start !== 'undefined') { |
20 | return { result: segment.start, precise: true }; | 17 | return { result: segment.start, precise: true }; |
21 | } | 18 | } |
22 | if (segment.end !== undefined) { | 19 | if (typeof segment.end !== 'undefined') { |
23 | return { | 20 | return { |
24 | result: segment.end - segment.duration, | 21 | result: segment.end - segment.duration, |
25 | precise: true | 22 | precise: true |
... | @@ -28,28 +25,29 @@ | ... | @@ -28,28 +25,29 @@ |
28 | } | 25 | } |
29 | while (i--) { | 26 | while (i--) { |
30 | segment = playlist.segments[i]; | 27 | segment = playlist.segments[i]; |
31 | if (segment.end !== undefined) { | 28 | if (typeof segment.end !== 'undefined') { |
32 | return { result: result + segment.end, precise: true }; | 29 | return { result: result + segment.end, precise: true }; |
33 | } | 30 | } |
34 | 31 | ||
35 | result += segment.duration; | 32 | result += segment.duration; |
36 | 33 | ||
37 | if (segment.start !== undefined) { | 34 | if (typeof segment.start !== 'undefined') { |
38 | return { result: result + segment.start, precise: true }; | 35 | return { result: result + segment.start, precise: true }; |
39 | } | 36 | } |
40 | } | 37 | } |
41 | return { result: result, precise: false }; | 38 | return { result, precise: false }; |
42 | }; | 39 | }; |
43 | |||
44 | forwardDuration = function(playlist, endSequence) { | ||
45 | var result = 0, segment, i; | ||
46 | 40 | ||
47 | i = endSequence - playlist.mediaSequence; | 41 | const forwardDuration = function(playlist, endSequence) { |
42 | let result = 0; | ||
43 | let segment; | ||
44 | let i = endSequence - playlist.mediaSequence; | ||
48 | // Walk forward until we find the earliest segment with timeline | 45 | // Walk forward until we find the earliest segment with timeline |
49 | // information | 46 | // information |
47 | |||
50 | for (; i < playlist.segments.length; i++) { | 48 | for (; i < playlist.segments.length; i++) { |
51 | segment = playlist.segments[i]; | 49 | segment = playlist.segments[i]; |
52 | if (segment.start !== undefined) { | 50 | if (typeof segment.start !== 'undefined') { |
53 | return { | 51 | return { |
54 | result: segment.start - result, | 52 | result: segment.start - result, |
55 | precise: true | 53 | precise: true |
... | @@ -58,7 +56,7 @@ | ... | @@ -58,7 +56,7 @@ |
58 | 56 | ||
59 | result += segment.duration; | 57 | result += segment.duration; |
60 | 58 | ||
61 | if (segment.end !== undefined) { | 59 | if (typeof segment.end !== 'undefined') { |
62 | return { | 60 | return { |
63 | result: segment.end - result, | 61 | result: segment.end - result, |
64 | precise: true | 62 | precise: true |
... | @@ -68,9 +66,9 @@ | ... | @@ -68,9 +66,9 @@ |
68 | } | 66 | } |
69 | // indicate we didn't find a useful duration estimate | 67 | // indicate we didn't find a useful duration estimate |
70 | return { result: -1, precise: false }; | 68 | return { result: -1, precise: false }; |
71 | }; | 69 | }; |
72 | 70 | ||
73 | /** | 71 | /** |
74 | * Calculate the media duration from the segments associated with a | 72 | * Calculate the media duration from the segments associated with a |
75 | * playlist. The duration of a subinterval of the available segments | 73 | * playlist. The duration of a subinterval of the available segments |
76 | * may be calculated by specifying an end index. | 74 | * may be calculated by specifying an end index. |
... | @@ -81,10 +79,11 @@ | ... | @@ -81,10 +79,11 @@ |
81 | * @return {number} the duration between the first available segment | 79 | * @return {number} the duration between the first available segment |
82 | * and end index. | 80 | * and end index. |
83 | */ | 81 | */ |
84 | intervalDuration = function(playlist, endSequence) { | 82 | const intervalDuration = function(playlist, endSequence) { |
85 | var backward, forward; | 83 | let backward; |
84 | let forward; | ||
86 | 85 | ||
87 | if (endSequence === undefined) { | 86 | if (typeof endSequence === 'undefined') { |
88 | endSequence = playlist.mediaSequence + playlist.segments.length; | 87 | endSequence = playlist.mediaSequence + playlist.segments.length; |
89 | } | 88 | } |
90 | 89 | ||
... | @@ -112,9 +111,9 @@ | ... | @@ -112,9 +111,9 @@ |
112 | 111 | ||
113 | // return the less-precise, playlist-based duration estimate | 112 | // return the less-precise, playlist-based duration estimate |
114 | return backward.result; | 113 | return backward.result; |
115 | }; | 114 | }; |
116 | 115 | ||
117 | /** | 116 | /** |
118 | * Calculates the duration of a playlist. If a start and end index | 117 | * Calculates the duration of a playlist. If a start and end index |
119 | * are specified, the duration will be for the subset of the media | 118 | * are specified, the duration will be for the subset of the media |
120 | * timeline between those two indices. The total duration for live | 119 | * timeline between those two indices. The total duration for live |
... | @@ -129,18 +128,18 @@ | ... | @@ -129,18 +128,18 @@ |
129 | * @return {number} the duration between the start index and end | 128 | * @return {number} the duration between the start index and end |
130 | * index. | 129 | * index. |
131 | */ | 130 | */ |
132 | duration = function(playlist, endSequence, includeTrailingTime) { | 131 | export const duration = function(playlist, endSequence, includeTrailingTime) { |
133 | if (!playlist) { | 132 | if (!playlist) { |
134 | return 0; | 133 | return 0; |
135 | } | 134 | } |
136 | 135 | ||
137 | if (includeTrailingTime === undefined) { | 136 | if (typeof includeTrailingTime === 'undefined') { |
138 | includeTrailingTime = true; | 137 | includeTrailingTime = true; |
139 | } | 138 | } |
140 | 139 | ||
141 | // if a slice of the total duration is not requested, use | 140 | // if a slice of the total duration is not requested, use |
142 | // playlist-level duration indicators when they're present | 141 | // playlist-level duration indicators when they're present |
143 | if (endSequence === undefined) { | 142 | if (typeof endSequence === 'undefined') { |
144 | // if present, use the duration specified in the playlist | 143 | // if present, use the duration specified in the playlist |
145 | if (playlist.totalDuration) { | 144 | if (playlist.totalDuration) { |
146 | return playlist.totalDuration; | 145 | return playlist.totalDuration; |
... | @@ -153,12 +152,14 @@ | ... | @@ -153,12 +152,14 @@ |
153 | } | 152 | } |
154 | 153 | ||
155 | // calculate the total duration based on the segment durations | 154 | // calculate the total duration based on the segment durations |
156 | return intervalDuration(playlist, | 155 | return intervalDuration( |
156 | playlist, | ||
157 | endSequence, | 157 | endSequence, |
158 | includeTrailingTime); | 158 | includeTrailingTime |
159 | }; | 159 | ); |
160 | }; | ||
160 | 161 | ||
161 | /** | 162 | /** |
162 | * Calculates the interval of time that is currently seekable in a | 163 | * Calculates the interval of time that is currently seekable in a |
163 | * playlist. The returned time ranges are relative to the earliest | 164 | * playlist. The returned time ranges are relative to the earliest |
164 | * moment in the specified playlist that is still available. A full | 165 | * moment in the specified playlist that is still available. A full |
... | @@ -169,30 +170,32 @@ | ... | @@ -169,30 +170,32 @@ |
169 | * @return {TimeRanges} the periods of time that are valid targets | 170 | * @return {TimeRanges} the periods of time that are valid targets |
170 | * for seeking | 171 | * for seeking |
171 | */ | 172 | */ |
172 | seekable = function(playlist) { | 173 | export const seekable = function(playlist) { |
173 | var start, end; | 174 | let start; |
175 | let end; | ||
174 | 176 | ||
175 | // without segments, there are no seekable ranges | 177 | // without segments, there are no seekable ranges |
176 | if (!playlist.segments) { | 178 | if (!playlist.segments) { |
177 | return videojs.createTimeRange(); | 179 | return createTimeRange(); |
178 | } | 180 | } |
179 | // when the playlist is complete, the entire duration is seekable | 181 | // when the playlist is complete, the entire duration is seekable |
180 | if (playlist.endList) { | 182 | if (playlist.endList) { |
181 | return videojs.createTimeRange(0, duration(playlist)); | 183 | return createTimeRange(0, duration(playlist)); |
182 | } | 184 | } |
183 | 185 | ||
184 | // live playlists should not expose three segment durations worth | 186 | // live playlists should not expose three segment durations worth |
185 | // of content from the end of the playlist | 187 | // of content from the end of the playlist |
186 | // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3 | 188 | // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3 |
187 | start = intervalDuration(playlist, playlist.mediaSequence); | 189 | start = intervalDuration(playlist, playlist.mediaSequence); |
188 | end = intervalDuration(playlist, | 190 | end = intervalDuration( |
189 | playlist.mediaSequence + Math.max(0, playlist.segments.length - 3)); | 191 | playlist, |
190 | return videojs.createTimeRange(start, end); | 192 | playlist.mediaSequence + Math.max(0, playlist.segments.length - 3) |
191 | }; | 193 | ); |
192 | 194 | return createTimeRange(start, end); | |
193 | // exports | 195 | }; |
194 | videojs.Hls.Playlist = { | 196 | |
195 | duration: duration, | 197 | // exports |
196 | seekable: seekable | 198 | export default { |
197 | }; | 199 | duration, |
198 | })(window, window.videojs); | 200 | seekable |
201 | }; | ... | ... |
src/resolve-url.js
0 → 100644
1 | import document from 'global/document'; | ||
2 | /* eslint-disable max-len */ | ||
3 | /** | ||
4 | * Constructs a new URI by interpreting a path relative to another | ||
5 | * URI. | ||
6 | * @param basePath {string} a relative or absolute URI | ||
7 | * @param path {string} a path part to combine with the base | ||
8 | * @return {string} a URI that is equivalent to composing `base` | ||
9 | * with `path` | ||
10 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue | ||
11 | */ | ||
12 | /* eslint-enable max-len */ | ||
13 | const resolveUrl = function(basePath, path) { | ||
14 | // use the base element to get the browser to handle URI resolution | ||
15 | let oldBase = document.querySelector('base'); | ||
16 | let docHead = document.querySelector('head'); | ||
17 | let a = document.createElement('a'); | ||
18 | let base = oldBase; | ||
19 | let oldHref; | ||
20 | let result; | ||
21 | |||
22 | // prep the document | ||
23 | if (oldBase) { | ||
24 | oldHref = oldBase.href; | ||
25 | } else { | ||
26 | base = docHead.appendChild(document.createElement('base')); | ||
27 | } | ||
28 | |||
29 | base.href = basePath; | ||
30 | a.href = path; | ||
31 | result = a.href; | ||
32 | |||
33 | // clean up | ||
34 | if (oldBase) { | ||
35 | oldBase.href = oldHref; | ||
36 | } else { | ||
37 | docHead.removeChild(base); | ||
38 | } | ||
39 | return result; | ||
40 | }; | ||
41 | |||
42 | export default resolveUrl; |
... | @@ -2,6 +2,10 @@ import m3u8 from './m3u8'; | ... | @@ -2,6 +2,10 @@ import m3u8 from './m3u8'; |
2 | import Stream from './stream'; | 2 | import Stream from './stream'; |
3 | import videojs from 'video.js'; | 3 | import videojs from 'video.js'; |
4 | import {Decrypter, decrypt, AsyncStream} from './decrypter'; | 4 | import {Decrypter, decrypt, AsyncStream} from './decrypter'; |
5 | import Playlist from './playlist'; | ||
6 | import PlaylistLoader from './playlist-loader'; | ||
7 | import xhr from './xhr'; | ||
8 | |||
5 | 9 | ||
6 | if(typeof window.videojs.Hls === 'undefined') { | 10 | if(typeof window.videojs.Hls === 'undefined') { |
7 | videojs.Hls = {}; | 11 | videojs.Hls = {}; |
... | @@ -11,3 +15,6 @@ videojs.m3u8 = m3u8; | ... | @@ -11,3 +15,6 @@ videojs.m3u8 = m3u8; |
11 | videojs.Hls.decrypt = decrypt; | 15 | videojs.Hls.decrypt = decrypt; |
12 | videojs.Hls.Decrypter = Decrypter; | 16 | videojs.Hls.Decrypter = Decrypter; |
13 | videojs.Hls.AsyncStream = AsyncStream; | 17 | videojs.Hls.AsyncStream = AsyncStream; |
18 | videojs.Hls.xhr = xhr; | ||
19 | videojs.Hls.Playlist = Playlist; | ||
20 | videojs.Hls.PlaylistLoader = PlaylistLoader; | ... | ... |
1 | (function(videojs) { | 1 | /** |
2 | 'use strict'; | ||
3 | |||
4 | /** | ||
5 | * A wrapper for videojs.xhr that tracks bandwidth. | 2 | * A wrapper for videojs.xhr that tracks bandwidth. |
6 | */ | 3 | */ |
7 | videojs.Hls.xhr = function(options, callback) { | 4 | import {xhr as videojsXHR, mergeOptions} from 'video.js'; |
5 | const xhr = function(options, callback) { | ||
8 | // Add a default timeout for all hls requests | 6 | // Add a default timeout for all hls requests |
9 | options = videojs.mergeOptions({ | 7 | options = mergeOptions({ |
10 | timeout: 45e3 | 8 | timeout: 45e3 |
11 | }, options); | 9 | }, options); |
12 | 10 | ||
13 | var request = videojs.xhr(options, function(error, response) { | 11 | let request = videojsXHR(options, function(error, response) { |
14 | if (!error && request.response) { | 12 | if (!error && request.response) { |
15 | request.responseTime = (new Date()).getTime(); | 13 | request.responseTime = (new Date()).getTime(); |
16 | request.roundTripTime = request.responseTime - request.requestTime; | 14 | request.roundTripTime = request.responseTime - request.requestTime; |
17 | request.bytesReceived = request.response.byteLength || request.response.length; | 15 | request.bytesReceived = request.response.byteLength || request.response.length; |
18 | if (!request.bandwidth) { | 16 | if (!request.bandwidth) { |
19 | request.bandwidth = Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000); | 17 | request.bandwidth = |
18 | Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000); | ||
20 | } | 19 | } |
21 | } | 20 | } |
22 | 21 | ||
23 | // videojs.xhr now uses a specific code on the error object to signal that a request has | 22 | // videojs.xhr now uses a specific code |
23 | // on the error object to signal that a request has | ||
24 | // timed out errors of setting a boolean on the request object | 24 | // timed out errors of setting a boolean on the request object |
25 | if (error || request.timedout) { | 25 | if (error || request.timedout) { |
26 | request.timedout = request.timedout || (error.code === 'ETIMEDOUT'); | 26 | request.timedout = request.timedout || (error.code === 'ETIMEDOUT'); |
... | @@ -35,8 +35,10 @@ | ... | @@ -35,8 +35,10 @@ |
35 | response.statusCode !== 200 && | 35 | response.statusCode !== 200 && |
36 | response.statusCode !== 206 && | 36 | response.statusCode !== 206 && |
37 | response.statusCode !== 0) { | 37 | response.statusCode !== 0) { |
38 | error = new Error('XHR Failed with a response of: ' + | 38 | error = new Error( |
39 | (request && (request.response || request.responseText))); | 39 | 'XHR Failed with a response of: ' + |
40 | (request && (request.response || request.responseText)) | ||
41 | ); | ||
40 | } | 42 | } |
41 | 43 | ||
42 | callback(error, request); | 44 | callback(error, request); |
... | @@ -44,5 +46,6 @@ | ... | @@ -44,5 +46,6 @@ |
44 | 46 | ||
45 | request.requestTime = (new Date()).getTime(); | 47 | request.requestTime = (new Date()).getTime(); |
46 | return request; | 48 | return request; |
47 | }; | 49 | }; |
48 | })(window.videojs); | 50 | |
51 | export default xhr; | ... | ... |
... | @@ -16,16 +16,11 @@ | ... | @@ -16,16 +16,11 @@ |
16 | <script src="/node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> | 16 | <script src="/node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script> |
17 | 17 | ||
18 | <script src="/src/videojs-contrib-hls.js"></script> | 18 | <script src="/src/videojs-contrib-hls.js"></script> |
19 | <script src="/src/xhr.js"></script> | ||
20 | <script src="/dist/videojs-contrib-hls.js"></script> | 19 | <script src="/dist/videojs-contrib-hls.js"></script> |
21 | <script src="/src/playlist.js"></script> | ||
22 | <script src="/src/playlist-loader.js"></script> | ||
23 | <script src="/src/bin-utils.js"></script> | 20 | <script src="/src/bin-utils.js"></script> |
24 | 21 | ||
25 | <script src="/test/videojs-contrib-hls.test.js"></script> | 22 | <script src="/test/videojs-contrib-hls.test.js"></script> |
26 | <script src="/dist-test/videojs-contrib-hls.js"></script> | 23 | <script src="/dist-test/videojs-contrib-hls.js"></script> |
27 | <script src="/test/playlist.test.js"></script> | ||
28 | <script src="/test/playlist-loader.test.js"></script> | ||
29 | 24 | ||
30 | </body> | 25 | </body> |
31 | </html> | 26 | </html> | ... | ... |
... | @@ -16,11 +16,8 @@ var DEFAULTS = { | ... | @@ -16,11 +16,8 @@ var DEFAULTS = { |
16 | 16 | ||
17 | // these two stub old functionality | 17 | // these two stub old functionality |
18 | 'src/videojs-contrib-hls.js', | 18 | 'src/videojs-contrib-hls.js', |
19 | 'src/xhr.js', | ||
20 | 'dist/videojs-contrib-hls.js', | 19 | 'dist/videojs-contrib-hls.js', |
21 | 20 | ||
22 | 'src/playlist.js', | ||
23 | 'src/playlist-loader.js', | ||
24 | 'src/bin-utils.js', | 21 | 'src/bin-utils.js', |
25 | 22 | ||
26 | 'test/stub.test.js', | 23 | 'test/stub.test.js', |
... | @@ -45,7 +42,7 @@ var DEFAULTS = { | ... | @@ -45,7 +42,7 @@ var DEFAULTS = { |
45 | ], | 42 | ], |
46 | 43 | ||
47 | preprocessors: { | 44 | preprocessors: { |
48 | 'test/{decrypter,stub,m3u8}.test.js': ['browserify'] | 45 | 'test/{playlist*,decrypter,stub,m3u8}.test.js': ['browserify'] |
49 | }, | 46 | }, |
50 | 47 | ||
51 | reporters: ['dots'], | 48 | reporters: ['dots'], | ... | ... |
1 | (function(window) { | 1 | import sinon from 'sinon'; |
2 | 'use strict'; | 2 | import QUnit from 'qunit'; |
3 | var | 3 | import PlaylistLoader from '../src/playlist-loader'; |
4 | sinonXhr, | 4 | import videojs from 'video.js'; |
5 | clock, | 5 | // Attempts to produce an absolute URL to a given relative path |
6 | requests, | 6 | // based on window.location.href |
7 | videojs = window.videojs, | 7 | const urlTo = function(path) { |
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 | 8 | return window.location.href |
13 | .split('/') | 9 | .split('/') |
14 | .slice(0, -1) | 10 | .slice(0, -1) |
15 | .concat([path]) | 11 | .concat([path]) |
16 | .join('/'); | 12 | .join('/'); |
17 | }; | 13 | }; |
18 | 14 | ||
19 | QUnit.module('Playlist Loader', { | 15 | const respond = function(request, string) { |
20 | setup: function() { | 16 | return request.respond(200, null, string); |
21 | // fake XHRs | 17 | }; |
22 | sinonXhr = sinon.useFakeXMLHttpRequest(); | ||
23 | videojs.xhr.XMLHttpRequest = sinonXhr; | ||
24 | 18 | ||
25 | requests = []; | 19 | QUnit.module('Playlist Loader', { |
26 | sinonXhr.onCreate = function(xhr) { | 20 | beforeEach() { |
21 | // fake XHRs | ||
22 | this.oldXHR = videojs.xhr.XMLHttpRequest; | ||
23 | this.sinonXhr = sinon.useFakeXMLHttpRequest(); | ||
24 | this.requests = []; | ||
25 | this.sinonXhr.onCreate = (xhr) => { | ||
27 | // force the XHR2 timeout polyfill | 26 | // force the XHR2 timeout polyfill |
28 | xhr.timeout = undefined; | 27 | xhr.timeout = null; |
29 | requests.push(xhr); | 28 | this.requests.push(xhr); |
30 | }; | 29 | }; |
31 | 30 | ||
32 | // fake timers | 31 | // fake timers |
33 | clock = sinon.useFakeTimers(); | 32 | this.clock = sinon.useFakeTimers(); |
33 | videojs.xhr.XMLHttpRequest = this.sinonXhr; | ||
34 | }, | 34 | }, |
35 | teardown: function() { | 35 | afterEach() { |
36 | sinonXhr.restore(); | 36 | this.sinonXhr.restore(); |
37 | videojs.xhr.XMLHttpRequest = window.XMLHttpRequest; | 37 | this.clock.restore(); |
38 | clock.restore(); | 38 | videojs.xhr.XMLHttpRequest = this.oldXHR; |
39 | } | 39 | } |
40 | }); | 40 | }); |
41 | 41 | ||
42 | test('throws if the playlist url is empty or undefined', function() { | 42 | QUnit.test('throws if the playlist url is empty or undefined', function() { |
43 | throws(function() { | 43 | QUnit.throws(function() { |
44 | videojs.Hls.PlaylistLoader(); | 44 | PlaylistLoader(); |
45 | }, 'requires an argument'); | 45 | }, 'requires an argument'); |
46 | throws(function() { | 46 | QUnit.throws(function() { |
47 | videojs.Hls.PlaylistLoader(''); | 47 | PlaylistLoader(''); |
48 | }, 'does not accept the empty string'); | 48 | }, 'does not accept the empty string'); |
49 | }); | 49 | }); |
50 | 50 | ||
51 | test('starts without any metadata', function() { | 51 | QUnit.test('starts without any metadata', function() { |
52 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | 52 | let loader = new PlaylistLoader('master.m3u8'); |
53 | strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); | ||
54 | }); | ||
55 | 53 | ||
56 | test('starts with no expired time', function() { | 54 | QUnit.strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); |
57 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | 55 | }); |
58 | requests.pop().respond(200, null, | 56 | |
57 | QUnit.test('starts with no expired time', function() { | ||
58 | let loader = new PlaylistLoader('media.m3u8'); | ||
59 | |||
60 | respond(this.requests.pop(), | ||
59 | '#EXTM3U\n' + | 61 | '#EXTM3U\n' + |
60 | '#EXTINF:10,\n' + | 62 | '#EXTINF:10,\n' + |
61 | '0.ts\n'); | 63 | '0.ts\n'); |
62 | equal(loader.expired_, | 64 | QUnit.equal(loader.expired_, |
63 | 0, | 65 | 0, |
64 | 'zero seconds expired'); | 66 | 'zero seconds expired'); |
65 | }); | 67 | }); |
66 | 68 | ||
67 | test('requests the initial playlist immediately', function() { | 69 | QUnit.test('this.requests the initial playlist immediately', function() { |
68 | new videojs.Hls.PlaylistLoader('master.m3u8'); | 70 | /* eslint-disable no-unused-vars */ |
69 | strictEqual(requests.length, 1, 'made a request'); | 71 | let loader = new PlaylistLoader('master.m3u8'); |
70 | strictEqual(requests[0].url, 'master.m3u8', 'requested the initial playlist'); | 72 | /* eslint-enable no-unused-vars */ |
71 | }); | 73 | |
74 | QUnit.strictEqual(this.requests.length, 1, 'made a request'); | ||
75 | QUnit.strictEqual(this.requests[0].url, | ||
76 | 'master.m3u8', | ||
77 | 'requested the initial playlist'); | ||
78 | }); | ||
79 | |||
80 | QUnit.test('moves to HAVE_MASTER after loading a master playlist', function() { | ||
81 | let loader = new PlaylistLoader('master.m3u8'); | ||
82 | let state; | ||
72 | 83 | ||
73 | test('moves to HAVE_MASTER after loading a master playlist', function() { | ||
74 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'), state; | ||
75 | loader.on('loadedplaylist', function() { | 84 | loader.on('loadedplaylist', function() { |
76 | state = loader.state; | 85 | state = loader.state; |
77 | }); | 86 | }); |
78 | requests.pop().respond(200, null, | 87 | respond(this.requests.pop(), |
79 | '#EXTM3U\n' + | 88 | '#EXTM3U\n' + |
80 | '#EXT-X-STREAM-INF:\n' + | 89 | '#EXT-X-STREAM-INF:\n' + |
81 | 'media.m3u8\n'); | 90 | 'media.m3u8\n'); |
82 | ok(loader.master, 'the master playlist is available'); | 91 | QUnit.ok(loader.master, 'the master playlist is available'); |
83 | strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct'); | 92 | QUnit.strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct'); |
84 | }); | 93 | }); |
94 | |||
95 | QUnit.test('jumps to HAVE_METADATA when initialized with a media playlist', function() { | ||
96 | let loadedmetadatas = 0; | ||
97 | let loader = new PlaylistLoader('media.m3u8'); | ||
85 | 98 | ||
86 | test('jumps to HAVE_METADATA when initialized with a media playlist', function() { | ||
87 | var | ||
88 | loadedmetadatas = 0, | ||
89 | loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
90 | loader.on('loadedmetadata', function() { | 99 | loader.on('loadedmetadata', function() { |
91 | loadedmetadatas++; | 100 | loadedmetadatas++; |
92 | }); | 101 | }); |
93 | requests.pop().respond(200, null, | 102 | respond(this.requests.pop(), |
94 | '#EXTM3U\n' + | 103 | '#EXTM3U\n' + |
95 | '#EXTINF:10,\n' + | 104 | '#EXTINF:10,\n' + |
96 | '0.ts\n' + | 105 | '0.ts\n' + |
97 | '#EXT-X-ENDLIST\n'); | 106 | '#EXT-X-ENDLIST\n'); |
98 | ok(loader.master, 'infers a master playlist'); | 107 | QUnit.ok(loader.master, 'infers a master playlist'); |
99 | ok(loader.media(), 'sets the media playlist'); | 108 | QUnit.ok(loader.media(), 'sets the media playlist'); |
100 | ok(loader.media().uri, 'sets the media playlist URI'); | 109 | QUnit.ok(loader.media().uri, 'sets the media playlist URI'); |
101 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 110 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
102 | strictEqual(requests.length, 0, 'no more requests are made'); | 111 | QUnit.strictEqual(this.requests.length, 0, 'no more this.requests are made'); |
103 | strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata'); | 112 | QUnit.strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata'); |
104 | }); | 113 | }); |
105 | 114 | ||
106 | test('jumps to HAVE_METADATA when initialized with a live media playlist', function() { | 115 | QUnit.test( |
107 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | 116 | 'jumps to HAVE_METADATA when initialized with a live media playlist', |
108 | requests.pop().respond(200, null, | 117 | function() { |
118 | let loader = new PlaylistLoader('media.m3u8'); | ||
119 | |||
120 | respond(this.requests.pop(), | ||
109 | '#EXTM3U\n' + | 121 | '#EXTM3U\n' + |
110 | '#EXTINF:10,\n' + | 122 | '#EXTINF:10,\n' + |
111 | '0.ts\n'); | 123 | '0.ts\n'); |
112 | ok(loader.master, 'infers a master playlist'); | 124 | QUnit.ok(loader.master, 'infers a master playlist'); |
113 | ok(loader.media(), 'sets the media playlist'); | 125 | QUnit.ok(loader.media(), 'sets the media playlist'); |
114 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 126 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
115 | }); | 127 | }); |
128 | |||
129 | QUnit.test('moves to HAVE_METADATA after loading a media playlist', function() { | ||
130 | let loadedPlaylist = 0; | ||
131 | let loadedMetadata = 0; | ||
132 | let loader = new PlaylistLoader('master.m3u8'); | ||
116 | 133 | ||
117 | test('moves to HAVE_METADATA after loading a media playlist', function() { | ||
118 | var | ||
119 | loadedPlaylist = 0, | ||
120 | loadedMetadata = 0, | ||
121 | loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
122 | loader.on('loadedplaylist', function() { | 134 | loader.on('loadedplaylist', function() { |
123 | loadedPlaylist++; | 135 | loadedPlaylist++; |
124 | }); | 136 | }); |
125 | loader.on('loadedmetadata', function() { | 137 | loader.on('loadedmetadata', function() { |
126 | loadedMetadata++; | 138 | loadedMetadata++; |
127 | }); | 139 | }); |
128 | requests.pop().respond(200, null, | 140 | respond(this.requests.pop(), |
129 | '#EXTM3U\n' + | 141 | '#EXTM3U\n' + |
130 | '#EXT-X-STREAM-INF:\n' + | 142 | '#EXT-X-STREAM-INF:\n' + |
131 | 'media.m3u8\n' + | 143 | 'media.m3u8\n' + |
132 | 'alt.m3u8\n'); | 144 | 'alt.m3u8\n'); |
133 | strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once'); | 145 | QUnit.strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once'); |
134 | strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata'); | 146 | QUnit.strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata'); |
135 | strictEqual(requests.length, 1, 'requests the media playlist'); | 147 | QUnit.strictEqual(this.requests.length, 1, 'this.requests the media playlist'); |
136 | strictEqual(requests[0].method, 'GET', 'GETs the media playlist'); | 148 | QUnit.strictEqual(this.requests[0].method, 'GET', 'GETs the media playlist'); |
137 | strictEqual(requests[0].url, | 149 | QUnit.strictEqual(this.requests[0].url, |
138 | urlTo('media.m3u8'), | 150 | urlTo('media.m3u8'), |
139 | 'requests the first playlist'); | 151 | 'this.requests the first playlist'); |
140 | 152 | ||
141 | requests.pop().respond(200, null, | 153 | respond(this.requests.pop(), |
142 | '#EXTM3U\n' + | 154 | '#EXTM3U\n' + |
143 | '#EXTINF:10,\n' + | 155 | '#EXTINF:10,\n' + |
144 | '0.ts\n'); | 156 | '0.ts\n'); |
145 | ok(loader.master, 'sets the master playlist'); | 157 | QUnit.ok(loader.master, 'sets the master playlist'); |
146 | ok(loader.media(), 'sets the media playlist'); | 158 | QUnit.ok(loader.media(), 'sets the media playlist'); |
147 | strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice'); | 159 | QUnit.strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice'); |
148 | strictEqual(loadedMetadata, 1, 'fired loadedmetadata once'); | 160 | QUnit.strictEqual(loadedMetadata, 1, 'fired loadedmetadata once'); |
149 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 161 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
150 | }); | 162 | }); |
163 | |||
164 | QUnit.test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { | ||
165 | let loader = new PlaylistLoader('live.m3u8'); | ||
151 | 166 | ||
152 | test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() { | 167 | respond(this.requests.pop(), |
153 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
154 | requests.pop().respond(200, null, | ||
155 | '#EXTM3U\n' + | 168 | '#EXTM3U\n' + |
156 | '#EXTINF:10,\n' + | 169 | '#EXTINF:10,\n' + |
157 | '0.ts\n'); | 170 | '0.ts\n'); |
158 | clock.tick(10 * 1000); // 10s, one target duration | 171 | // 10s, one target duration |
159 | strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); | 172 | this.clock.tick(10 * 1000); |
160 | strictEqual(requests.length, 1, 'requested playlist'); | 173 | QUnit.strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct'); |
161 | strictEqual(requests[0].url, | 174 | QUnit.strictEqual(this.requests.length, 1, 'requested playlist'); |
175 | QUnit.strictEqual(this.requests[0].url, | ||
162 | urlTo('live.m3u8'), | 176 | urlTo('live.m3u8'), |
163 | 'refreshes the media playlist'); | 177 | 'refreshes the media playlist'); |
164 | }); | 178 | }); |
165 | 179 | ||
166 | test('returns to HAVE_METADATA after refreshing the playlist', function() { | 180 | QUnit.test('returns to HAVE_METADATA after refreshing the playlist', function() { |
167 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | 181 | let loader = new PlaylistLoader('live.m3u8'); |
168 | requests.pop().respond(200, null, | 182 | |
183 | respond(this.requests.pop(), | ||
169 | '#EXTM3U\n' + | 184 | '#EXTM3U\n' + |
170 | '#EXTINF:10,\n' + | 185 | '#EXTINF:10,\n' + |
171 | '0.ts\n'); | 186 | '0.ts\n'); |
172 | clock.tick(10 * 1000); // 10s, one target duration | 187 | // 10s, one target duration |
173 | requests.pop().respond(200, null, | 188 | this.clock.tick(10 * 1000); |
189 | respond(this.requests.pop(), | ||
174 | '#EXTM3U\n' + | 190 | '#EXTM3U\n' + |
175 | '#EXTINF:10,\n' + | 191 | '#EXTINF:10,\n' + |
176 | '1.ts\n'); | 192 | '1.ts\n'); |
177 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 193 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
178 | }); | 194 | }); |
195 | |||
196 | QUnit.test( | ||
197 | 'does not increment expired seconds before firstplay is triggered', | ||
198 | function() { | ||
199 | let loader = new PlaylistLoader('live.m3u8'); | ||
179 | 200 | ||
180 | test('does not increment expired seconds before firstplay is triggered', function() { | 201 | respond(this.requests.pop(), |
181 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
182 | requests.pop().respond(200, null, | ||
183 | '#EXTM3U\n' + | 202 | '#EXTM3U\n' + |
184 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 203 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
185 | '#EXTINF:10,\n' + | 204 | '#EXTINF:10,\n' + |
... | @@ -190,8 +209,9 @@ | ... | @@ -190,8 +209,9 @@ |
190 | '2.ts\n' + | 209 | '2.ts\n' + |
191 | '#EXTINF:10,\n' + | 210 | '#EXTINF:10,\n' + |
192 | '3.ts\n'); | 211 | '3.ts\n'); |
193 | clock.tick(10 * 1000); // 10s, one target duration | 212 | // 10s, one target duration |
194 | requests.pop().respond(200, null, | 213 | this.clock.tick(10 * 1000); |
214 | respond(this.requests.pop(), | ||
195 | '#EXTM3U\n' + | 215 | '#EXTM3U\n' + |
196 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 216 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
197 | '#EXTINF:10,\n' + | 217 | '#EXTINF:10,\n' + |
... | @@ -202,13 +222,14 @@ | ... | @@ -202,13 +222,14 @@ |
202 | '3.ts\n' + | 222 | '3.ts\n' + |
203 | '#EXTINF:10,\n' + | 223 | '#EXTINF:10,\n' + |
204 | '4.ts\n'); | 224 | '4.ts\n'); |
205 | equal(loader.expired_, 0, 'expired one segment'); | 225 | QUnit.equal(loader.expired_, 0, 'expired one segment'); |
206 | }); | 226 | }); |
227 | |||
228 | QUnit.test('increments expired seconds after a segment is removed', function() { | ||
229 | let loader = new PlaylistLoader('live.m3u8'); | ||
207 | 230 | ||
208 | test('increments expired seconds after a segment is removed', function() { | ||
209 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
210 | loader.trigger('firstplay'); | 231 | loader.trigger('firstplay'); |
211 | requests.pop().respond(200, null, | 232 | respond(this.requests.pop(), |
212 | '#EXTM3U\n' + | 233 | '#EXTM3U\n' + |
213 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 234 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
214 | '#EXTINF:10,\n' + | 235 | '#EXTINF:10,\n' + |
... | @@ -219,8 +240,9 @@ | ... | @@ -219,8 +240,9 @@ |
219 | '2.ts\n' + | 240 | '2.ts\n' + |
220 | '#EXTINF:10,\n' + | 241 | '#EXTINF:10,\n' + |
221 | '3.ts\n'); | 242 | '3.ts\n'); |
222 | clock.tick(10 * 1000); // 10s, one target duration | 243 | // 10s, one target duration |
223 | requests.pop().respond(200, null, | 244 | this.clock.tick(10 * 1000); |
245 | respond(this.requests.pop(), | ||
224 | '#EXTM3U\n' + | 246 | '#EXTM3U\n' + |
225 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 247 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
226 | '#EXTINF:10,\n' + | 248 | '#EXTINF:10,\n' + |
... | @@ -231,13 +253,14 @@ | ... | @@ -231,13 +253,14 @@ |
231 | '3.ts\n' + | 253 | '3.ts\n' + |
232 | '#EXTINF:10,\n' + | 254 | '#EXTINF:10,\n' + |
233 | '4.ts\n'); | 255 | '4.ts\n'); |
234 | equal(loader.expired_, 10, 'expired one segment'); | 256 | QUnit.equal(loader.expired_, 10, 'expired one segment'); |
235 | }); | 257 | }); |
258 | |||
259 | QUnit.test('increments expired seconds after a discontinuity', function() { | ||
260 | let loader = new PlaylistLoader('live.m3u8'); | ||
236 | 261 | ||
237 | test('increments expired seconds after a discontinuity', function() { | ||
238 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
239 | loader.trigger('firstplay'); | 262 | loader.trigger('firstplay'); |
240 | requests.pop().respond(200, null, | 263 | respond(this.requests.pop(), |
241 | '#EXTM3U\n' + | 264 | '#EXTM3U\n' + |
242 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 265 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
243 | '#EXTINF:10,\n' + | 266 | '#EXTINF:10,\n' + |
... | @@ -247,8 +270,9 @@ | ... | @@ -247,8 +270,9 @@ |
247 | '#EXT-X-DISCONTINUITY\n' + | 270 | '#EXT-X-DISCONTINUITY\n' + |
248 | '#EXTINF:4,\n' + | 271 | '#EXTINF:4,\n' + |
249 | '2.ts\n'); | 272 | '2.ts\n'); |
250 | clock.tick(10 * 1000); // 10s, one target duration | 273 | // 10s, one target duration |
251 | requests.pop().respond(200, null, | 274 | this.clock.tick(10 * 1000); |
275 | respond(this.requests.pop(), | ||
252 | '#EXTM3U\n' + | 276 | '#EXTM3U\n' + |
253 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 277 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
254 | '#EXTINF:3,\n' + | 278 | '#EXTINF:3,\n' + |
... | @@ -256,31 +280,36 @@ | ... | @@ -256,31 +280,36 @@ |
256 | '#EXT-X-DISCONTINUITY\n' + | 280 | '#EXT-X-DISCONTINUITY\n' + |
257 | '#EXTINF:4,\n' + | 281 | '#EXTINF:4,\n' + |
258 | '2.ts\n'); | 282 | '2.ts\n'); |
259 | equal(loader.expired_, 10, 'expired one segment'); | 283 | QUnit.equal(loader.expired_, 10, 'expired one segment'); |
260 | 284 | ||
261 | clock.tick(10 * 1000); // 10s, one target duration | 285 | // 10s, one target duration |
262 | requests.pop().respond(200, null, | 286 | this.clock.tick(10 * 1000); |
287 | respond(this.requests.pop(), | ||
263 | '#EXTM3U\n' + | 288 | '#EXTM3U\n' + |
264 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | 289 | '#EXT-X-MEDIA-SEQUENCE:2\n' + |
265 | '#EXT-X-DISCONTINUITY\n' + | 290 | '#EXT-X-DISCONTINUITY\n' + |
266 | '#EXTINF:4,\n' + | 291 | '#EXTINF:4,\n' + |
267 | '2.ts\n'); | 292 | '2.ts\n'); |
268 | equal(loader.expired_, 13, 'no expirations after the discontinuity yet'); | 293 | QUnit.equal(loader.expired_, 13, 'no expirations after the discontinuity yet'); |
269 | 294 | ||
270 | clock.tick(10 * 1000); // 10s, one target duration | 295 | // 10s, one target duration |
271 | requests.pop().respond(200, null, | 296 | this.clock.tick(10 * 1000); |
297 | respond(this.requests.pop(), | ||
272 | '#EXTM3U\n' + | 298 | '#EXTM3U\n' + |
273 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | 299 | '#EXT-X-MEDIA-SEQUENCE:3\n' + |
274 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + | 300 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + |
275 | '#EXTINF:10,\n' + | 301 | '#EXTINF:10,\n' + |
276 | '3.ts\n'); | 302 | '3.ts\n'); |
277 | equal(loader.expired_, 17, 'tracked expiration across the discontinuity'); | 303 | QUnit.equal(loader.expired_, 17, 'tracked expiration across the discontinuity'); |
278 | }); | 304 | }); |
305 | |||
306 | QUnit.test( | ||
307 | 'tracks expired seconds properly when two discontinuities expire at once', | ||
308 | function() { | ||
309 | let loader = new PlaylistLoader('live.m3u8'); | ||
279 | 310 | ||
280 | test('tracks expired seconds properly when two discontinuities expire at once', function() { | ||
281 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
282 | loader.trigger('firstplay'); | 311 | loader.trigger('firstplay'); |
283 | requests.pop().respond(200, null, | 312 | respond(this.requests.pop(), |
284 | '#EXTM3U\n' + | 313 | '#EXTM3U\n' + |
285 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 314 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
286 | '#EXTINF:4,\n' + | 315 | '#EXTINF:4,\n' + |
... | @@ -293,20 +322,23 @@ | ... | @@ -293,20 +322,23 @@ |
293 | '2.ts\n' + | 322 | '2.ts\n' + |
294 | '#EXTINF:7,\n' + | 323 | '#EXTINF:7,\n' + |
295 | '3.ts\n'); | 324 | '3.ts\n'); |
296 | clock.tick(10 * 1000); | 325 | this.clock.tick(10 * 1000); |
297 | requests.pop().respond(200, null, | 326 | respond(this.requests.pop(), |
298 | '#EXTM3U\n' + | 327 | '#EXTM3U\n' + |
299 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | 328 | '#EXT-X-MEDIA-SEQUENCE:3\n' + |
300 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + | 329 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + |
301 | '#EXTINF:7,\n' + | 330 | '#EXTINF:7,\n' + |
302 | '3.ts\n'); | 331 | '3.ts\n'); |
303 | equal(loader.expired_, 4 + 5 + 6, 'tracked multiple expiring discontinuities'); | 332 | QUnit.equal(loader.expired_, 4 + 5 + 6, 'tracked multiple expiring discontinuities'); |
304 | }); | 333 | }); |
334 | |||
335 | QUnit.test( | ||
336 | 'estimates expired if an entire window elapses between live playlist updates', | ||
337 | function() { | ||
338 | let loader = new PlaylistLoader('live.m3u8'); | ||
305 | 339 | ||
306 | test('estimates expired if an entire window elapses between live playlist updates', function() { | ||
307 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
308 | loader.trigger('firstplay'); | 340 | loader.trigger('firstplay'); |
309 | requests.pop().respond(200, null, | 341 | respond(this.requests.pop(), |
310 | '#EXTM3U\n' + | 342 | '#EXTM3U\n' + |
311 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 343 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
312 | '#EXTINF:4,\n' + | 344 | '#EXTINF:4,\n' + |
... | @@ -314,8 +346,8 @@ | ... | @@ -314,8 +346,8 @@ |
314 | '#EXTINF:5,\n' + | 346 | '#EXTINF:5,\n' + |
315 | '1.ts\n'); | 347 | '1.ts\n'); |
316 | 348 | ||
317 | clock.tick(10 * 1000); | 349 | this.clock.tick(10 * 1000); |
318 | requests.pop().respond(200, null, | 350 | respond(this.requests.pop(), |
319 | '#EXTM3U\n' + | 351 | '#EXTM3U\n' + |
320 | '#EXT-X-MEDIA-SEQUENCE:4\n' + | 352 | '#EXT-X-MEDIA-SEQUENCE:4\n' + |
321 | '#EXTINF:6,\n' + | 353 | '#EXTINF:6,\n' + |
... | @@ -323,72 +355,78 @@ | ... | @@ -323,72 +355,78 @@ |
323 | '#EXTINF:7,\n' + | 355 | '#EXTINF:7,\n' + |
324 | '5.ts\n'); | 356 | '5.ts\n'); |
325 | 357 | ||
326 | equal(loader.expired_, | 358 | QUnit.equal(loader.expired_, |
327 | 4 + 5 + (2 * 10), | 359 | 4 + 5 + (2 * 10), |
328 | 'made a very rough estimate of expired time'); | 360 | 'made a very rough estimate of expired time'); |
329 | }); | 361 | }); |
330 | 362 | ||
331 | test('emits an error when an initial playlist request fails', function() { | 363 | QUnit.test('emits an error when an initial playlist request fails', function() { |
332 | var | 364 | let errors = []; |
333 | errors = [], | 365 | let loader = new PlaylistLoader('master.m3u8'); |
334 | loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
335 | 366 | ||
336 | loader.on('error', function() { | 367 | loader.on('error', function() { |
337 | errors.push(loader.error); | 368 | errors.push(loader.error); |
338 | }); | 369 | }); |
339 | requests.pop().respond(500); | 370 | this.requests.pop().respond(500); |
340 | 371 | ||
341 | strictEqual(errors.length, 1, 'emitted one error'); | 372 | QUnit.strictEqual(errors.length, 1, 'emitted one error'); |
342 | strictEqual(errors[0].status, 500, 'http status is captured'); | 373 | QUnit.strictEqual(errors[0].status, 500, 'http status is captured'); |
343 | }); | 374 | }); |
344 | 375 | ||
345 | test('errors when an initial media playlist request fails', function() { | 376 | QUnit.test('errors when an initial media playlist request fails', function() { |
346 | var | 377 | let errors = []; |
347 | errors = [], | 378 | let loader = new PlaylistLoader('master.m3u8'); |
348 | loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
349 | 379 | ||
350 | loader.on('error', function() { | 380 | loader.on('error', function() { |
351 | errors.push(loader.error); | 381 | errors.push(loader.error); |
352 | }); | 382 | }); |
353 | requests.pop().respond(200, null, | 383 | respond(this.requests.pop(), |
354 | '#EXTM3U\n' + | 384 | '#EXTM3U\n' + |
355 | '#EXT-X-STREAM-INF:\n' + | 385 | '#EXT-X-STREAM-INF:\n' + |
356 | 'media.m3u8\n'); | 386 | 'media.m3u8\n'); |
357 | 387 | ||
358 | strictEqual(errors.length, 0, 'emitted no errors'); | 388 | QUnit.strictEqual(errors.length, 0, 'emitted no errors'); |
359 | 389 | ||
360 | requests.pop().respond(500); | 390 | this.requests.pop().respond(500); |
361 | 391 | ||
362 | strictEqual(errors.length, 1, 'emitted one error'); | 392 | QUnit.strictEqual(errors.length, 1, 'emitted one error'); |
363 | strictEqual(errors[0].status, 500, 'http status is captured'); | 393 | QUnit.strictEqual(errors[0].status, 500, 'http status is captured'); |
364 | }); | 394 | }); |
395 | |||
396 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 | ||
397 | QUnit.test( | ||
398 | 'halves the refresh timeout if a playlist is unchanged since the last reload', | ||
399 | function() { | ||
400 | /* eslint-disable no-unused-vars */ | ||
401 | let loader = new PlaylistLoader('live.m3u8'); | ||
402 | /* eslint-enable no-unused-vars */ | ||
365 | 403 | ||
366 | // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4 | 404 | respond(this.requests.pop(), |
367 | test('halves the refresh timeout if a playlist is unchanged' + | ||
368 | 'since the last reload', function() { | ||
369 | new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
370 | requests.pop().respond(200, null, | ||
371 | '#EXTM3U\n' + | 405 | '#EXTM3U\n' + |
372 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 406 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
373 | '#EXTINF:10,\n' + | 407 | '#EXTINF:10,\n' + |
374 | '0.ts\n'); | 408 | '0.ts\n'); |
375 | clock.tick(10 * 1000); // trigger a refresh | 409 | // trigger a refresh |
376 | requests.pop().respond(200, null, | 410 | this.clock.tick(10 * 1000); |
411 | respond(this.requests.pop(), | ||
377 | '#EXTM3U\n' + | 412 | '#EXTM3U\n' + |
378 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 413 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
379 | '#EXTINF:10,\n' + | 414 | '#EXTINF:10,\n' + |
380 | '0.ts\n'); | 415 | '0.ts\n'); |
381 | clock.tick(5 * 1000); // half the default target-duration | 416 | // half the default target-duration |
417 | this.clock.tick(5 * 1000); | ||
382 | 418 | ||
383 | strictEqual(requests.length, 1, 'sent a request'); | 419 | QUnit.strictEqual(this.requests.length, 1, 'sent a request'); |
384 | strictEqual(requests[0].url, | 420 | QUnit.strictEqual(this.requests[0].url, |
385 | urlTo('live.m3u8'), | 421 | urlTo('live.m3u8'), |
386 | 'requested the media playlist'); | 422 | 'requested the media playlist'); |
387 | }); | 423 | }); |
424 | |||
425 | QUnit.test('preserves segment metadata across playlist refreshes', function() { | ||
426 | let loader = new PlaylistLoader('live.m3u8'); | ||
427 | let segment; | ||
388 | 428 | ||
389 | test('preserves segment metadata across playlist refreshes', function() { | 429 | respond(this.requests.pop(), |
390 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'), segment; | ||
391 | requests.pop().respond(200, null, | ||
392 | '#EXTM3U\n' + | 430 | '#EXTM3U\n' + |
393 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 431 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
394 | '#EXTINF:10,\n' + | 432 | '#EXTINF:10,\n' + |
... | @@ -403,8 +441,9 @@ | ... | @@ -403,8 +441,9 @@ |
403 | segment.maxAudioPts = 27; | 441 | segment.maxAudioPts = 27; |
404 | segment.preciseDuration = 10.045; | 442 | segment.preciseDuration = 10.045; |
405 | 443 | ||
406 | clock.tick(10 * 1000); // trigger a refresh | 444 | // trigger a refresh |
407 | requests.pop().respond(200, null, | 445 | this.clock.tick(10 * 1000); |
446 | respond(this.requests.pop(), | ||
408 | '#EXTM3U\n' + | 447 | '#EXTM3U\n' + |
409 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 448 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
410 | '#EXTINF:10,\n' + | 449 | '#EXTINF:10,\n' + |
... | @@ -412,179 +451,195 @@ | ... | @@ -412,179 +451,195 @@ |
412 | '#EXTINF:10,\n' + | 451 | '#EXTINF:10,\n' + |
413 | '2.ts\n'); | 452 | '2.ts\n'); |
414 | 453 | ||
415 | deepEqual(loader.media().segments[0], segment, 'preserved segment attributes'); | 454 | QUnit.deepEqual(loader.media().segments[0], segment, 'preserved segment attributes'); |
416 | }); | 455 | }); |
456 | |||
457 | QUnit.test('clears the update timeout when switching quality', function() { | ||
458 | let loader = new PlaylistLoader('live-master.m3u8'); | ||
459 | let refreshes = 0; | ||
417 | 460 | ||
418 | test('clears the update timeout when switching quality', function() { | ||
419 | var loader = new videojs.Hls.PlaylistLoader('live-master.m3u8'), refreshes = 0; | ||
420 | // track the number of playlist refreshes triggered | 461 | // track the number of playlist refreshes triggered |
421 | loader.on('mediaupdatetimeout', function() { | 462 | loader.on('mediaupdatetimeout', function() { |
422 | refreshes++; | 463 | refreshes++; |
423 | }); | 464 | }); |
424 | // deliver the master | 465 | // deliver the master |
425 | requests.pop().respond(200, null, | 466 | respond(this.requests.pop(), |
426 | '#EXTM3U\n' + | 467 | '#EXTM3U\n' + |
427 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 468 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
428 | 'live-low.m3u8\n' + | 469 | 'live-low.m3u8\n' + |
429 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 470 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
430 | 'live-high.m3u8\n'); | 471 | 'live-high.m3u8\n'); |
431 | // deliver the low quality playlist | 472 | // deliver the low quality playlist |
432 | requests.pop().respond(200, null, | 473 | respond(this.requests.pop(), |
433 | '#EXTM3U\n' + | 474 | '#EXTM3U\n' + |
434 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 475 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
435 | '#EXTINF:10,\n' + | 476 | '#EXTINF:10,\n' + |
436 | 'low-0.ts\n'); | 477 | 'low-0.ts\n'); |
437 | // change to a higher quality playlist | 478 | // change to a higher quality playlist |
438 | loader.media('live-high.m3u8'); | 479 | loader.media('live-high.m3u8'); |
439 | requests.pop().respond(200, null, | 480 | respond(this.requests.pop(), |
440 | '#EXTM3U\n' + | 481 | '#EXTM3U\n' + |
441 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 482 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
442 | '#EXTINF:10,\n' + | 483 | '#EXTINF:10,\n' + |
443 | 'high-0.ts\n'); | 484 | 'high-0.ts\n'); |
444 | clock.tick(10 * 1000); // trigger a refresh | 485 | // trigger a refresh |
486 | this.clock.tick(10 * 1000); | ||
445 | 487 | ||
446 | equal(1, refreshes, 'only one refresh was triggered'); | 488 | QUnit.equal(1, refreshes, 'only one refresh was triggered'); |
447 | }); | 489 | }); |
490 | |||
491 | QUnit.test('media-sequence updates are considered a playlist change', function() { | ||
492 | /* eslint-disable no-unused-vars */ | ||
493 | let loader = new PlaylistLoader('live.m3u8'); | ||
494 | /* eslint-enable no-unused-vars */ | ||
448 | 495 | ||
449 | test('media-sequence updates are considered a playlist change', function() { | 496 | respond(this.requests.pop(), |
450 | new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
451 | requests.pop().respond(200, null, | ||
452 | '#EXTM3U\n' + | 497 | '#EXTM3U\n' + |
453 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 498 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
454 | '#EXTINF:10,\n' + | 499 | '#EXTINF:10,\n' + |
455 | '0.ts\n'); | 500 | '0.ts\n'); |
456 | clock.tick(10 * 1000); // trigger a refresh | 501 | // trigger a refresh |
457 | requests.pop().respond(200, null, | 502 | this.clock.tick(10 * 1000); |
503 | respond(this.requests.pop(), | ||
458 | '#EXTM3U\n' + | 504 | '#EXTM3U\n' + |
459 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | 505 | '#EXT-X-MEDIA-SEQUENCE:1\n' + |
460 | '#EXTINF:10,\n' + | 506 | '#EXTINF:10,\n' + |
461 | '0.ts\n'); | 507 | '0.ts\n'); |
462 | clock.tick(5 * 1000); // half the default target-duration | 508 | // half the default target-duration |
509 | this.clock.tick(5 * 1000); | ||
463 | 510 | ||
464 | strictEqual(requests.length, 0, 'no request is sent'); | 511 | QUnit.strictEqual(this.requests.length, 0, 'no request is sent'); |
465 | }); | 512 | }); |
466 | 513 | ||
467 | test('emits an error if a media refresh fails', function() { | 514 | QUnit.test('emits an error if a media refresh fails', function() { |
468 | var | 515 | let errors = 0; |
469 | errors = 0, | 516 | let errorResponseText = 'custom error message'; |
470 | errorResponseText = 'custom error message', | 517 | let loader = new PlaylistLoader('live.m3u8'); |
471 | loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
472 | 518 | ||
473 | loader.on('error', function() { | 519 | loader.on('error', function() { |
474 | errors++; | 520 | errors++; |
475 | }); | 521 | }); |
476 | requests.pop().respond(200, null, | 522 | respond(this.requests.pop(), |
477 | '#EXTM3U\n' + | 523 | '#EXTM3U\n' + |
478 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 524 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
479 | '#EXTINF:10,\n' + | 525 | '#EXTINF:10,\n' + |
480 | '0.ts\n'); | 526 | '0.ts\n'); |
481 | clock.tick(10 * 1000); // trigger a refresh | 527 | // trigger a refresh |
482 | requests.pop().respond(500, null, errorResponseText); | 528 | this.clock.tick(10 * 1000); |
529 | this.requests.pop().respond(500, null, errorResponseText); | ||
483 | 530 | ||
484 | strictEqual(errors, 1, 'emitted an error'); | 531 | QUnit.strictEqual(errors, 1, 'emitted an error'); |
485 | strictEqual(loader.error.status, 500, 'captured the status code'); | 532 | QUnit.strictEqual(loader.error.status, 500, 'captured the status code'); |
486 | strictEqual(loader.error.responseText, errorResponseText, 'captured the responseText'); | 533 | QUnit.strictEqual(loader.error.responseText, |
487 | }); | 534 | errorResponseText, |
535 | 'captured the responseText'); | ||
536 | }); | ||
537 | |||
538 | QUnit.test('switches media playlists when requested', function() { | ||
539 | let loader = new PlaylistLoader('master.m3u8'); | ||
488 | 540 | ||
489 | test('switches media playlists when requested', function() { | 541 | respond(this.requests.pop(), |
490 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
491 | requests.pop().respond(200, null, | ||
492 | '#EXTM3U\n' + | 542 | '#EXTM3U\n' + |
493 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 543 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
494 | 'low.m3u8\n' + | 544 | 'low.m3u8\n' + |
495 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 545 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
496 | 'high.m3u8\n'); | 546 | 'high.m3u8\n'); |
497 | requests.pop().respond(200, null, | 547 | respond(this.requests.pop(), |
498 | '#EXTM3U\n' + | 548 | '#EXTM3U\n' + |
499 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 549 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
500 | '#EXTINF:10,\n' + | 550 | '#EXTINF:10,\n' + |
501 | 'low-0.ts\n'); | 551 | 'low-0.ts\n'); |
502 | 552 | ||
503 | loader.media(loader.master.playlists[1]); | 553 | loader.media(loader.master.playlists[1]); |
504 | strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | 554 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); |
505 | 555 | ||
506 | requests.pop().respond(200, null, | 556 | respond(this.requests.pop(), |
507 | '#EXTM3U\n' + | 557 | '#EXTM3U\n' + |
508 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 558 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
509 | '#EXTINF:10,\n' + | 559 | '#EXTINF:10,\n' + |
510 | 'high-0.ts\n'); | 560 | 'high-0.ts\n'); |
511 | strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | 561 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); |
512 | strictEqual(loader.media(), | 562 | QUnit.strictEqual(loader.media(), |
513 | loader.master.playlists[1], | 563 | loader.master.playlists[1], |
514 | 'updated the active media'); | 564 | 'updated the active media'); |
515 | }); | 565 | }); |
566 | |||
567 | QUnit.test('can switch playlists immediately after the master is downloaded', function() { | ||
568 | let loader = new PlaylistLoader('master.m3u8'); | ||
516 | 569 | ||
517 | test('can switch playlists immediately after the master is downloaded', function() { | ||
518 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
519 | loader.on('loadedplaylist', function() { | 570 | loader.on('loadedplaylist', function() { |
520 | loader.media('high.m3u8'); | 571 | loader.media('high.m3u8'); |
521 | }); | 572 | }); |
522 | requests.pop().respond(200, null, | 573 | respond(this.requests.pop(), |
523 | '#EXTM3U\n' + | 574 | '#EXTM3U\n' + |
524 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 575 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
525 | 'low.m3u8\n' + | 576 | 'low.m3u8\n' + |
526 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 577 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
527 | 'high.m3u8\n'); | 578 | 'high.m3u8\n'); |
528 | equal(requests[0].url, urlTo('high.m3u8'), 'switched variants immediately'); | 579 | QUnit.equal(this.requests[0].url, urlTo('high.m3u8'), 'switched variants immediately'); |
529 | }); | 580 | }); |
581 | |||
582 | QUnit.test('can switch media playlists based on URI', function() { | ||
583 | let loader = new PlaylistLoader('master.m3u8'); | ||
530 | 584 | ||
531 | test('can switch media playlists based on URI', function() { | 585 | respond(this.requests.pop(), |
532 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
533 | requests.pop().respond(200, null, | ||
534 | '#EXTM3U\n' + | 586 | '#EXTM3U\n' + |
535 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 587 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
536 | 'low.m3u8\n' + | 588 | 'low.m3u8\n' + |
537 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 589 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
538 | 'high.m3u8\n'); | 590 | 'high.m3u8\n'); |
539 | requests.pop().respond(200, null, | 591 | respond(this.requests.pop(), |
540 | '#EXTM3U\n' + | 592 | '#EXTM3U\n' + |
541 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 593 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
542 | '#EXTINF:10,\n' + | 594 | '#EXTINF:10,\n' + |
543 | 'low-0.ts\n'); | 595 | 'low-0.ts\n'); |
544 | 596 | ||
545 | loader.media('high.m3u8'); | 597 | loader.media('high.m3u8'); |
546 | strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | 598 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); |
547 | 599 | ||
548 | requests.pop().respond(200, null, | 600 | respond(this.requests.pop(), |
549 | '#EXTM3U\n' + | 601 | '#EXTM3U\n' + |
550 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 602 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
551 | '#EXTINF:10,\n' + | 603 | '#EXTINF:10,\n' + |
552 | 'high-0.ts\n'); | 604 | 'high-0.ts\n'); |
553 | strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); | 605 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'switched active media'); |
554 | strictEqual(loader.media(), | 606 | QUnit.strictEqual(loader.media(), |
555 | loader.master.playlists[1], | 607 | loader.master.playlists[1], |
556 | 'updated the active media'); | 608 | 'updated the active media'); |
557 | }); | 609 | }); |
558 | 610 | ||
559 | test('aborts in-flight playlist refreshes when switching', function() { | 611 | QUnit.test('aborts in-flight playlist refreshes when switching', function() { |
560 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | 612 | let loader = new PlaylistLoader('master.m3u8'); |
561 | requests.pop().respond(200, null, | 613 | |
614 | respond(this.requests.pop(), | ||
562 | '#EXTM3U\n' + | 615 | '#EXTM3U\n' + |
563 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 616 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
564 | 'low.m3u8\n' + | 617 | 'low.m3u8\n' + |
565 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 618 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
566 | 'high.m3u8\n'); | 619 | 'high.m3u8\n'); |
567 | requests.pop().respond(200, null, | 620 | respond(this.requests.pop(), |
568 | '#EXTM3U\n' + | 621 | '#EXTM3U\n' + |
569 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 622 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
570 | '#EXTINF:10,\n' + | 623 | '#EXTINF:10,\n' + |
571 | 'low-0.ts\n'); | 624 | 'low-0.ts\n'); |
572 | clock.tick(10 * 1000); | 625 | this.clock.tick(10 * 1000); |
573 | loader.media('high.m3u8'); | 626 | loader.media('high.m3u8'); |
574 | strictEqual(requests[0].aborted, true, 'aborted refresh request'); | 627 | QUnit.strictEqual(this.requests[0].aborted, true, 'aborted refresh request'); |
575 | ok(!requests[0].onreadystatechange, 'onreadystatechange handlers should be removed on abort'); | 628 | QUnit.ok(!this.requests[0].onreadystatechange, |
576 | strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); | 629 | 'onreadystatechange handlers should be removed on abort'); |
577 | }); | 630 | QUnit.strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state'); |
631 | }); | ||
632 | |||
633 | QUnit.test('switching to the active playlist is a no-op', function() { | ||
634 | let loader = new PlaylistLoader('master.m3u8'); | ||
578 | 635 | ||
579 | test('switching to the active playlist is a no-op', function() { | 636 | respond(this.requests.pop(), |
580 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
581 | requests.pop().respond(200, null, | ||
582 | '#EXTM3U\n' + | 637 | '#EXTM3U\n' + |
583 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 638 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
584 | 'low.m3u8\n' + | 639 | 'low.m3u8\n' + |
585 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 640 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
586 | 'high.m3u8\n'); | 641 | 'high.m3u8\n'); |
587 | requests.pop().respond(200, null, | 642 | respond(this.requests.pop(), |
588 | '#EXTM3U\n' + | 643 | '#EXTM3U\n' + |
589 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 644 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
590 | '#EXTINF:10,\n' + | 645 | '#EXTINF:10,\n' + |
... | @@ -592,43 +647,45 @@ | ... | @@ -592,43 +647,45 @@ |
592 | '#EXT-X-ENDLIST\n'); | 647 | '#EXT-X-ENDLIST\n'); |
593 | loader.media('low.m3u8'); | 648 | loader.media('low.m3u8'); |
594 | 649 | ||
595 | strictEqual(requests.length, 0, 'no requests are sent'); | 650 | QUnit.strictEqual(this.requests.length, 0, 'no this.requests are sent'); |
596 | }); | 651 | }); |
652 | |||
653 | QUnit.test('switching to the active live playlist is a no-op', function() { | ||
654 | let loader = new PlaylistLoader('master.m3u8'); | ||
597 | 655 | ||
598 | test('switching to the active live playlist is a no-op', function() { | 656 | respond(this.requests.pop(), |
599 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
600 | requests.pop().respond(200, null, | ||
601 | '#EXTM3U\n' + | 657 | '#EXTM3U\n' + |
602 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 658 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
603 | 'low.m3u8\n' + | 659 | 'low.m3u8\n' + |
604 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 660 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
605 | 'high.m3u8\n'); | 661 | 'high.m3u8\n'); |
606 | requests.pop().respond(200, null, | 662 | respond(this.requests.pop(), |
607 | '#EXTM3U\n' + | 663 | '#EXTM3U\n' + |
608 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 664 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
609 | '#EXTINF:10,\n' + | 665 | '#EXTINF:10,\n' + |
610 | 'low-0.ts\n'); | 666 | 'low-0.ts\n'); |
611 | loader.media('low.m3u8'); | 667 | loader.media('low.m3u8'); |
612 | 668 | ||
613 | strictEqual(requests.length, 0, 'no requests are sent'); | 669 | QUnit.strictEqual(this.requests.length, 0, 'no this.requests are sent'); |
614 | }); | 670 | }); |
615 | 671 | ||
616 | test('switches back to loaded playlists without re-requesting them', function() { | 672 | QUnit.test('switches back to loaded playlists without re-requesting them', function() { |
617 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | 673 | let loader = new PlaylistLoader('master.m3u8'); |
618 | requests.pop().respond(200, null, | 674 | |
675 | respond(this.requests.pop(), | ||
619 | '#EXTM3U\n' + | 676 | '#EXTM3U\n' + |
620 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 677 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
621 | 'low.m3u8\n' + | 678 | 'low.m3u8\n' + |
622 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 679 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
623 | 'high.m3u8\n'); | 680 | 'high.m3u8\n'); |
624 | requests.pop().respond(200, null, | 681 | respond(this.requests.pop(), |
625 | '#EXTM3U\n' + | 682 | '#EXTM3U\n' + |
626 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 683 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
627 | '#EXTINF:10,\n' + | 684 | '#EXTINF:10,\n' + |
628 | 'low-0.ts\n' + | 685 | 'low-0.ts\n' + |
629 | '#EXT-X-ENDLIST\n'); | 686 | '#EXT-X-ENDLIST\n'); |
630 | loader.media('high.m3u8'); | 687 | loader.media('high.m3u8'); |
631 | requests.pop().respond(200, null, | 688 | respond(this.requests.pop(), |
632 | '#EXTM3U\n' + | 689 | '#EXTM3U\n' + |
633 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 690 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
634 | '#EXTINF:10,\n' + | 691 | '#EXTINF:10,\n' + |
... | @@ -636,19 +693,22 @@ | ... | @@ -636,19 +693,22 @@ |
636 | '#EXT-X-ENDLIST\n'); | 693 | '#EXT-X-ENDLIST\n'); |
637 | loader.media('low.m3u8'); | 694 | loader.media('low.m3u8'); |
638 | 695 | ||
639 | strictEqual(requests.length, 0, 'no outstanding requests'); | 696 | QUnit.strictEqual(this.requests.length, 0, 'no outstanding this.requests'); |
640 | strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist'); | 697 | QUnit.strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist'); |
641 | }); | 698 | }); |
699 | |||
700 | QUnit.test( | ||
701 | 'aborts outstanding this.requests if switching back to an already loaded playlist', | ||
702 | function() { | ||
703 | let loader = new PlaylistLoader('master.m3u8'); | ||
642 | 704 | ||
643 | test('aborts outstanding requests if switching back to an already loaded playlist', function() { | 705 | respond(this.requests.pop(), |
644 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
645 | requests.pop().respond(200, null, | ||
646 | '#EXTM3U\n' + | 706 | '#EXTM3U\n' + |
647 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 707 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
648 | 'low.m3u8\n' + | 708 | 'low.m3u8\n' + |
649 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 709 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
650 | 'high.m3u8\n'); | 710 | 'high.m3u8\n'); |
651 | requests.pop().respond(200, null, | 711 | respond(this.requests.pop(), |
652 | '#EXTM3U\n' + | 712 | '#EXTM3U\n' + |
653 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 713 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
654 | '#EXTINF:10,\n' + | 714 | '#EXTINF:10,\n' + |
... | @@ -657,23 +717,33 @@ | ... | @@ -657,23 +717,33 @@ |
657 | loader.media('high.m3u8'); | 717 | loader.media('high.m3u8'); |
658 | loader.media('low.m3u8'); | 718 | loader.media('low.m3u8'); |
659 | 719 | ||
660 | strictEqual(requests.length, 1, 'requested high playlist'); | 720 | QUnit.strictEqual(this.requests.length, |
661 | ok(requests[0].aborted, 'aborted playlist request'); | 721 | 1, |
662 | ok(!requests[0].onreadystatechange, 'onreadystatechange handlers should be removed on abort'); | 722 | 'requested high playlist'); |
663 | strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist'); | 723 | QUnit.ok(this.requests[0].aborted, |
664 | strictEqual(loader.media(), loader.master.playlists[0], 'switched to loaded playlist'); | 724 | 'aborted playlist request'); |
665 | }); | 725 | QUnit.ok(!this.requests[0].onreadystatechange, |
666 | 726 | 'onreadystatechange handlers should be removed on abort'); | |
667 | 727 | QUnit.strictEqual(loader.state, | |
668 | test('does not abort requests when the same playlist is re-requested', function() { | 728 | 'HAVE_METADATA', |
669 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | 729 | 'returned to loaded playlist'); |
670 | requests.pop().respond(200, null, | 730 | QUnit.strictEqual(loader.media(), |
731 | loader.master.playlists[0], | ||
732 | 'switched to loaded playlist'); | ||
733 | }); | ||
734 | |||
735 | QUnit.test( | ||
736 | 'does not abort this.requests when the same playlist is re-requested', | ||
737 | function() { | ||
738 | let loader = new PlaylistLoader('master.m3u8'); | ||
739 | |||
740 | respond(this.requests.pop(), | ||
671 | '#EXTM3U\n' + | 741 | '#EXTM3U\n' + |
672 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 742 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
673 | 'low.m3u8\n' + | 743 | 'low.m3u8\n' + |
674 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 744 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
675 | 'high.m3u8\n'); | 745 | 'high.m3u8\n'); |
676 | requests.pop().respond(200, null, | 746 | respond(this.requests.pop(), |
677 | '#EXTM3U\n' + | 747 | '#EXTM3U\n' + |
678 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 748 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
679 | '#EXTINF:10,\n' + | 749 | '#EXTINF:10,\n' + |
... | @@ -682,122 +752,131 @@ | ... | @@ -682,122 +752,131 @@ |
682 | loader.media('high.m3u8'); | 752 | loader.media('high.m3u8'); |
683 | loader.media('high.m3u8'); | 753 | loader.media('high.m3u8'); |
684 | 754 | ||
685 | strictEqual(requests.length, 1, 'made only one request'); | 755 | QUnit.strictEqual(this.requests.length, 1, 'made only one request'); |
686 | ok(!requests[0].aborted, 'request not aborted'); | 756 | QUnit.ok(!this.requests[0].aborted, 'request not aborted'); |
687 | }); | 757 | }); |
688 | 758 | ||
689 | test('throws an error if a media switch is initiated too early', function() { | 759 | QUnit.test('throws an error if a media switch is initiated too early', function() { |
690 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | 760 | let loader = new PlaylistLoader('master.m3u8'); |
691 | 761 | ||
692 | throws(function() { | 762 | QUnit.throws(function() { |
693 | loader.media('high.m3u8'); | 763 | loader.media('high.m3u8'); |
694 | }, 'threw an error from HAVE_NOTHING'); | 764 | }, 'threw an error from HAVE_NOTHING'); |
695 | 765 | ||
696 | requests.pop().respond(200, null, | 766 | respond(this.requests.pop(), |
697 | '#EXTM3U\n' + | 767 | '#EXTM3U\n' + |
698 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 768 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
699 | 'low.m3u8\n' + | 769 | 'low.m3u8\n' + |
700 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 770 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
701 | 'high.m3u8\n'); | 771 | 'high.m3u8\n'); |
702 | }); | 772 | }); |
773 | |||
774 | QUnit.test( | ||
775 | 'throws an error if a switch to an unrecognized playlist is requested', | ||
776 | function() { | ||
777 | let loader = new PlaylistLoader('master.m3u8'); | ||
703 | 778 | ||
704 | test('throws an error if a switch to an unrecognized playlist is requested', function() { | 779 | respond(this.requests.pop(), |
705 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
706 | requests.pop().respond(200, null, | ||
707 | '#EXTM3U\n' + | 780 | '#EXTM3U\n' + |
708 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 781 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
709 | 'media.m3u8\n'); | 782 | 'media.m3u8\n'); |
710 | 783 | ||
711 | throws(function() { | 784 | QUnit.throws(function() { |
712 | loader.media('unrecognized.m3u8'); | 785 | loader.media('unrecognized.m3u8'); |
713 | }, 'throws an error'); | 786 | }, 'throws an error'); |
714 | }); | 787 | }); |
788 | |||
789 | QUnit.test('dispose cancels the refresh timeout', function() { | ||
790 | let loader = new PlaylistLoader('live.m3u8'); | ||
715 | 791 | ||
716 | test('dispose cancels the refresh timeout', function() { | 792 | respond(this.requests.pop(), |
717 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
718 | requests.pop().respond(200, null, | ||
719 | '#EXTM3U\n' + | 793 | '#EXTM3U\n' + |
720 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 794 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
721 | '#EXTINF:10,\n' + | 795 | '#EXTINF:10,\n' + |
722 | '0.ts\n'); | 796 | '0.ts\n'); |
723 | loader.dispose(); | 797 | loader.dispose(); |
724 | // a lot of time passes... | 798 | // a lot of time passes... |
725 | clock.tick(15 * 1000); | 799 | this.clock.tick(15 * 1000); |
726 | 800 | ||
727 | strictEqual(requests.length, 0, 'no refresh request was made'); | 801 | QUnit.strictEqual(this.requests.length, 0, 'no refresh request was made'); |
728 | }); | 802 | }); |
729 | 803 | ||
730 | test('dispose aborts pending refresh requests', function() { | 804 | QUnit.test('dispose aborts pending refresh this.requests', function() { |
731 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | 805 | let loader = new PlaylistLoader('live.m3u8'); |
732 | requests.pop().respond(200, null, | 806 | |
807 | respond(this.requests.pop(), | ||
733 | '#EXTM3U\n' + | 808 | '#EXTM3U\n' + |
734 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 809 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
735 | '#EXTINF:10,\n' + | 810 | '#EXTINF:10,\n' + |
736 | '0.ts\n'); | 811 | '0.ts\n'); |
737 | clock.tick(10 * 1000); | 812 | this.clock.tick(10 * 1000); |
738 | 813 | ||
739 | loader.dispose(); | 814 | loader.dispose(); |
740 | ok(requests[0].aborted, 'refresh request aborted'); | 815 | QUnit.ok(this.requests[0].aborted, 'refresh request aborted'); |
741 | ok(!requests[0].onreadystatechange, 'onreadystatechange handler should not exist after dispose called'); | 816 | QUnit.ok( |
742 | }); | 817 | !this.requests[0].onreadystatechange, |
818 | 'onreadystatechange handler should not exist after dispose called' | ||
819 | ); | ||
820 | }); | ||
821 | |||
822 | QUnit.test('errors if this.requests take longer than 45s', function() { | ||
823 | let loader = new PlaylistLoader('media.m3u8'); | ||
824 | let errors = 0; | ||
743 | 825 | ||
744 | test('errors if requests take longer than 45s', function() { | ||
745 | var | ||
746 | loader = new videojs.Hls.PlaylistLoader('media.m3u8'), | ||
747 | errors = 0; | ||
748 | loader.on('error', function() { | 826 | loader.on('error', function() { |
749 | errors++; | 827 | errors++; |
750 | }); | 828 | }); |
751 | clock.tick(45 * 1000); | 829 | this.clock.tick(45 * 1000); |
752 | 830 | ||
753 | strictEqual(errors, 1, 'fired one error'); | 831 | QUnit.strictEqual(errors, 1, 'fired one error'); |
754 | strictEqual(loader.error.code, 2, 'fired a network error'); | 832 | QUnit.strictEqual(loader.error.code, 2, 'fired a network error'); |
755 | }); | 833 | }); |
834 | |||
835 | QUnit.test('triggers an event when the active media changes', function() { | ||
836 | let loader = new PlaylistLoader('master.m3u8'); | ||
837 | let mediaChanges = 0; | ||
756 | 838 | ||
757 | test('triggers an event when the active media changes', function() { | ||
758 | var | ||
759 | loader = new videojs.Hls.PlaylistLoader('master.m3u8'), | ||
760 | mediaChanges = 0; | ||
761 | loader.on('mediachange', function() { | 839 | loader.on('mediachange', function() { |
762 | mediaChanges++; | 840 | mediaChanges++; |
763 | }); | 841 | }); |
764 | requests.pop().respond(200, null, | 842 | respond(this.requests.pop(), |
765 | '#EXTM3U\n' + | 843 | '#EXTM3U\n' + |
766 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | 844 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + |
767 | 'low.m3u8\n' + | 845 | 'low.m3u8\n' + |
768 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 846 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
769 | 'high.m3u8\n'); | 847 | 'high.m3u8\n'); |
770 | requests.shift().respond(200, null, | 848 | respond(this.requests.shift(), |
771 | '#EXTM3U\n' + | 849 | '#EXTM3U\n' + |
772 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 850 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
773 | '#EXTINF:10,\n' + | 851 | '#EXTINF:10,\n' + |
774 | 'low-0.ts\n' + | 852 | 'low-0.ts\n' + |
775 | '#EXT-X-ENDLIST\n'); | 853 | '#EXT-X-ENDLIST\n'); |
776 | strictEqual(mediaChanges, 0, 'initial selection is not a media change'); | 854 | QUnit.strictEqual(mediaChanges, 0, 'initial selection is not a media change'); |
777 | 855 | ||
778 | loader.media('high.m3u8'); | 856 | loader.media('high.m3u8'); |
779 | strictEqual(mediaChanges, 0, 'mediachange does not fire immediately'); | 857 | QUnit.strictEqual(mediaChanges, 0, 'mediachange does not fire immediately'); |
780 | 858 | ||
781 | requests.shift().respond(200, null, | 859 | respond(this.requests.shift(), |
782 | '#EXTM3U\n' + | 860 | '#EXTM3U\n' + |
783 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 861 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
784 | '#EXTINF:10,\n' + | 862 | '#EXTINF:10,\n' + |
785 | 'high-0.ts\n' + | 863 | 'high-0.ts\n' + |
786 | '#EXT-X-ENDLIST\n'); | 864 | '#EXT-X-ENDLIST\n'); |
787 | strictEqual(mediaChanges, 1, 'fired a mediachange'); | 865 | QUnit.strictEqual(mediaChanges, 1, 'fired a mediachange'); |
788 | 866 | ||
789 | // switch back to an already loaded playlist | 867 | // switch back to an already loaded playlist |
790 | loader.media('low.m3u8'); | 868 | loader.media('low.m3u8'); |
791 | strictEqual(mediaChanges, 2, 'fired a mediachange'); | 869 | QUnit.strictEqual(mediaChanges, 2, 'fired a mediachange'); |
792 | 870 | ||
793 | // trigger a no-op switch | 871 | // trigger a no-op switch |
794 | loader.media('low.m3u8'); | 872 | loader.media('low.m3u8'); |
795 | strictEqual(mediaChanges, 2, 'ignored a no-op media change'); | 873 | QUnit.strictEqual(mediaChanges, 2, 'ignored a no-op media change'); |
796 | }); | 874 | }); |
797 | 875 | ||
798 | test('can get media index by playback position for non-live videos', function() { | 876 | QUnit.test('can get media index by playback position for non-live videos', function() { |
799 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | 877 | let loader = new PlaylistLoader('media.m3u8'); |
800 | requests.shift().respond(200, null, | 878 | |
879 | respond(this.requests.shift(), | ||
801 | '#EXTM3U\n' + | 880 | '#EXTM3U\n' + |
802 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 881 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
803 | '#EXTINF:4,\n' + | 882 | '#EXTINF:4,\n' + |
... | @@ -808,20 +887,21 @@ | ... | @@ -808,20 +887,21 @@ |
808 | '2.ts\n' + | 887 | '2.ts\n' + |
809 | '#EXT-X-ENDLIST\n'); | 888 | '#EXT-X-ENDLIST\n'); |
810 | 889 | ||
811 | equal(loader.getMediaIndexForTime_(-1), | 890 | QUnit.equal(loader.getMediaIndexForTime_(-1), |
812 | 0, | 891 | 0, |
813 | 'the index is never less than zero'); | 892 | 'the index is never less than zero'); |
814 | equal(loader.getMediaIndexForTime_(0), 0, 'time zero is index zero'); | 893 | QUnit.equal(loader.getMediaIndexForTime_(0), 0, 'time zero is index zero'); |
815 | equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); | 894 | QUnit.equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); |
816 | equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); | 895 | QUnit.equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); |
817 | equal(loader.getMediaIndexForTime_(22), | 896 | QUnit.equal(loader.getMediaIndexForTime_(22), |
818 | 2, | 897 | 2, |
819 | 'time greater than the length is index 2'); | 898 | 'time greater than the length is index 2'); |
820 | }); | 899 | }); |
900 | |||
901 | QUnit.test('returns the lower index when calculating for a segment boundary', function() { | ||
902 | let loader = new PlaylistLoader('media.m3u8'); | ||
821 | 903 | ||
822 | test('returns the lower index when calculating for a segment boundary', function() { | 904 | respond(this.requests.shift(), |
823 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
824 | requests.shift().respond(200, null, | ||
825 | '#EXTM3U\n' + | 905 | '#EXTM3U\n' + |
826 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 906 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
827 | '#EXTINF:4,\n' + | 907 | '#EXTINF:4,\n' + |
... | @@ -829,14 +909,17 @@ | ... | @@ -829,14 +909,17 @@ |
829 | '#EXTINF:5,\n' + | 909 | '#EXTINF:5,\n' + |
830 | '1.ts\n' + | 910 | '1.ts\n' + |
831 | '#EXT-X-ENDLIST\n'); | 911 | '#EXT-X-ENDLIST\n'); |
832 | equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches'); | 912 | QUnit.equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches'); |
833 | equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); | 913 | QUnit.equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); |
834 | equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); | 914 | QUnit.equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); |
835 | }); | 915 | }); |
916 | |||
917 | QUnit.test( | ||
918 | 'accounts for non-zero starting segment time when calculating media index', | ||
919 | function() { | ||
920 | let loader = new PlaylistLoader('media.m3u8'); | ||
836 | 921 | ||
837 | test('accounts for non-zero starting segment time when calculating media index', function() { | 922 | respond(this.requests.shift(), |
838 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
839 | requests.shift().respond(200, null, | ||
840 | '#EXTM3U\n' + | 923 | '#EXTM3U\n' + |
841 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | 924 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + |
842 | '#EXTINF:4,\n' + | 925 | '#EXTINF:4,\n' + |
... | @@ -845,21 +928,40 @@ | ... | @@ -845,21 +928,40 @@ |
845 | '1002.ts\n'); | 928 | '1002.ts\n'); |
846 | loader.media().segments[0].end = 154; | 929 | loader.media().segments[0].end = 154; |
847 | 930 | ||
848 | equal(loader.getMediaIndexForTime_(0), -1, 'the lowest returned value is negative one'); | 931 | QUnit.equal(loader.getMediaIndexForTime_(0), |
849 | equal(loader.getMediaIndexForTime_(45), -1, 'expired content returns negative one'); | 932 | -1, |
850 | equal(loader.getMediaIndexForTime_(75), -1, 'expired content returns negative one'); | 933 | 'the lowest returned value is negative one'); |
851 | equal(loader.getMediaIndexForTime_(50 + 100), 0, 'calculates the earliest available position'); | 934 | QUnit.equal(loader.getMediaIndexForTime_(45), |
852 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | 935 | -1, |
853 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | 936 | 'expired content returns negative one'); |
854 | equal(loader.getMediaIndexForTime_(50 + 100 + 4), 1, 'calculates within the second segment'); | 937 | QUnit.equal(loader.getMediaIndexForTime_(75), |
855 | equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), 1, 'calculates within the second segment'); | 938 | -1, |
856 | equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); | 939 | 'expired content returns negative one'); |
857 | }); | 940 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100), |
941 | 0, | ||
942 | 'calculates the earliest available position'); | ||
943 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | ||
944 | 0, | ||
945 | 'calculates within the first segment'); | ||
946 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | ||
947 | 0, | ||
948 | 'calculates within the first segment'); | ||
949 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4), | ||
950 | 1, | ||
951 | 'calculates within the second segment'); | ||
952 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), | ||
953 | 1, | ||
954 | 'calculates within the second segment'); | ||
955 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 6), | ||
956 | 1, | ||
957 | 'calculates within the second segment'); | ||
958 | }); | ||
959 | |||
960 | QUnit.test('prefers precise segment timing when tracking expired time', function() { | ||
961 | let loader = new PlaylistLoader('media.m3u8'); | ||
858 | 962 | ||
859 | test('prefers precise segment timing when tracking expired time', function() { | ||
860 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
861 | loader.trigger('firstplay'); | 963 | loader.trigger('firstplay'); |
862 | requests.shift().respond(200, null, | 964 | respond(this.requests.shift(), |
863 | '#EXTM3U\n' + | 965 | '#EXTM3U\n' + |
864 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | 966 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + |
865 | '#EXTINF:4,\n' + | 967 | '#EXTINF:4,\n' + |
... | @@ -873,20 +975,26 @@ | ... | @@ -873,20 +975,26 @@ |
873 | // this number would be coming from the Source Buffer in practice | 975 | // this number would be coming from the Source Buffer in practice |
874 | loader.media().segments[0].end = 150; | 976 | loader.media().segments[0].end = 150; |
875 | 977 | ||
876 | equal(loader.getMediaIndexForTime_(149), 0, 'prefers the value on the first segment'); | 978 | QUnit.equal(loader.getMediaIndexForTime_(149), |
979 | 0, | ||
980 | 'prefers the value on the first segment'); | ||
877 | 981 | ||
878 | clock.tick(10 * 1000); // trigger a playlist refresh | 982 | // trigger a playlist refresh |
879 | requests.shift().respond(200, null, | 983 | this.clock.tick(10 * 1000); |
984 | respond(this.requests.shift(), | ||
880 | '#EXTM3U\n' + | 985 | '#EXTM3U\n' + |
881 | '#EXT-X-MEDIA-SEQUENCE:1002\n' + | 986 | '#EXT-X-MEDIA-SEQUENCE:1002\n' + |
882 | '#EXTINF:5,\n' + | 987 | '#EXTINF:5,\n' + |
883 | '1002.ts\n'); | 988 | '1002.ts\n'); |
884 | equal(loader.getMediaIndexForTime_(150 + 4 + 1), 0, 'tracks precise expired times'); | 989 | QUnit.equal(loader.getMediaIndexForTime_(150 + 4 + 1), |
885 | }); | 990 | 0, |
991 | 'tracks precise expired times'); | ||
992 | }); | ||
886 | 993 | ||
887 | test('accounts for expired time when calculating media index', function() { | 994 | QUnit.test('accounts for expired time when calculating media index', function() { |
888 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | 995 | let loader = new PlaylistLoader('media.m3u8'); |
889 | requests.shift().respond(200, null, | 996 | |
997 | respond(this.requests.shift(), | ||
890 | '#EXTM3U\n' + | 998 | '#EXTM3U\n' + |
891 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + | 999 | '#EXT-X-MEDIA-SEQUENCE:1001\n' + |
892 | '#EXTINF:4,\n' + | 1000 | '#EXTINF:4,\n' + |
... | @@ -895,24 +1003,38 @@ | ... | @@ -895,24 +1003,38 @@ |
895 | '1002.ts\n'); | 1003 | '1002.ts\n'); |
896 | loader.expired_ = 150; | 1004 | loader.expired_ = 150; |
897 | 1005 | ||
898 | equal(loader.getMediaIndexForTime_(0), -1, 'expired content returns a negative index'); | 1006 | QUnit.equal(loader.getMediaIndexForTime_(0), |
899 | equal(loader.getMediaIndexForTime_(75), -1, 'expired content returns a negative index'); | 1007 | -1, |
900 | equal(loader.getMediaIndexForTime_(50 + 100), 0, 'calculates the earliest available position'); | 1008 | 'expired content returns a negative index'); |
901 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | 1009 | QUnit.equal(loader.getMediaIndexForTime_(75), |
902 | equal(loader.getMediaIndexForTime_(50 + 100 + 2), 0, 'calculates within the first segment'); | 1010 | -1, |
903 | equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), 1, 'calculates within the second segment'); | 1011 | 'expired content returns a negative index'); |
904 | equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); | 1012 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100), |
905 | }); | 1013 | 0, |
1014 | 'calculates the earliest available position'); | ||
1015 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | ||
1016 | 0, | ||
1017 | 'calculates within the first segment'); | ||
1018 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 2), | ||
1019 | 0, | ||
1020 | 'calculates within the first segment'); | ||
1021 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 4.5), | ||
1022 | 1, | ||
1023 | 'calculates within the second segment'); | ||
1024 | QUnit.equal(loader.getMediaIndexForTime_(50 + 100 + 6), | ||
1025 | 1, | ||
1026 | 'calculates within the second segment'); | ||
1027 | }); | ||
1028 | |||
1029 | QUnit.test('does not misintrepret playlists missing newlines at the end', function() { | ||
1030 | let loader = new PlaylistLoader('media.m3u8'); | ||
906 | 1031 | ||
907 | test('does not misintrepret playlists missing newlines at the end', function() { | 1032 | // no newline |
908 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | 1033 | respond(this.requests.shift(), |
909 | requests.shift().respond(200, null, | ||
910 | '#EXTM3U\n' + | 1034 | '#EXTM3U\n' + |
911 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | 1035 | '#EXT-X-MEDIA-SEQUENCE:0\n' + |
912 | '#EXTINF:10,\n' + | 1036 | '#EXTINF:10,\n' + |
913 | 'low-0.ts\n' + | 1037 | 'low-0.ts\n' + |
914 | '#EXT-X-ENDLIST'); // no newline | 1038 | '#EXT-X-ENDLIST'); |
915 | ok(loader.media().endList, 'flushed the final line of input'); | 1039 | QUnit.ok(loader.media().endList, 'flushed the final line of input'); |
916 | }); | 1040 | }); |
917 | |||
918 | })(window); | ... | ... |
1 | /* Tests for the playlist utilities */ | 1 | import Playlist from '../src/playlist'; |
2 | (function(window, videojs) { | 2 | import QUnit from 'qunit'; |
3 | 'use strict'; | 3 | QUnit.module('Playlist Duration'); |
4 | var Playlist = videojs.Hls.Playlist; | ||
5 | 4 | ||
6 | QUnit.module('Playlist Duration'); | 5 | QUnit.test('total duration for live playlists is Infinity', function() { |
7 | 6 | let duration = Playlist.duration({ | |
8 | test('total duration for live playlists is Infinity', function() { | ||
9 | var duration = Playlist.duration({ | ||
10 | segments: [{ | 7 | segments: [{ |
11 | duration: 4, | 8 | duration: 4, |
12 | uri: '0.ts' | 9 | uri: '0.ts' |
13 | }] | 10 | }] |
14 | }); | 11 | }); |
15 | 12 | ||
16 | equal(duration, Infinity, 'duration is infinity'); | 13 | QUnit.equal(duration, Infinity, 'duration is infinity'); |
17 | }); | 14 | }); |
18 | 15 | ||
19 | QUnit.module('Playlist Interval Duration'); | 16 | QUnit.module('Playlist Interval Duration'); |
20 | 17 | ||
21 | test('accounts for non-zero starting VOD media sequences', function() { | 18 | QUnit.test('accounts for non-zero starting VOD media sequences', function() { |
22 | var duration = Playlist.duration({ | 19 | let duration = Playlist.duration({ |
23 | mediaSequence: 10, | 20 | mediaSequence: 10, |
24 | endList: true, | 21 | endList: true, |
25 | segments: [{ | 22 | segments: [{ |
... | @@ -37,11 +34,11 @@ | ... | @@ -37,11 +34,11 @@ |
37 | }] | 34 | }] |
38 | }); | 35 | }); |
39 | 36 | ||
40 | equal(duration, 4 * 10, 'includes only listed segments'); | 37 | QUnit.equal(duration, 4 * 10, 'includes only listed segments'); |
41 | }); | 38 | }); |
42 | 39 | ||
43 | test('uses timeline values when available', function() { | 40 | QUnit.test('uses timeline values when available', function() { |
44 | var duration = Playlist.duration({ | 41 | let duration = Playlist.duration({ |
45 | mediaSequence: 0, | 42 | mediaSequence: 0, |
46 | endList: true, | 43 | endList: true, |
47 | segments: [{ | 44 | segments: [{ |
... | @@ -62,11 +59,11 @@ | ... | @@ -62,11 +59,11 @@ |
62 | }] | 59 | }] |
63 | }, 4); | 60 | }, 4); |
64 | 61 | ||
65 | equal(duration, 4 * 10 + 2, 'used timeline values'); | 62 | QUnit.equal(duration, 4 * 10 + 2, 'used timeline values'); |
66 | }); | 63 | }); |
67 | 64 | ||
68 | test('works when partial timeline information is available', function() { | 65 | QUnit.test('works when partial timeline information is available', function() { |
69 | var duration = Playlist.duration({ | 66 | let duration = Playlist.duration({ |
70 | mediaSequence: 0, | 67 | mediaSequence: 0, |
71 | endList: true, | 68 | endList: true, |
72 | segments: [{ | 69 | segments: [{ |
... | @@ -90,11 +87,11 @@ | ... | @@ -90,11 +87,11 @@ |
90 | }] | 87 | }] |
91 | }, 5); | 88 | }, 5); |
92 | 89 | ||
93 | equal(duration, 50.0002, 'calculated with mixed intervals'); | 90 | QUnit.equal(duration, 50.0002, 'calculated with mixed intervals'); |
94 | }); | 91 | }); |
95 | 92 | ||
96 | test('uses timeline values for the expired duration of live playlists', function() { | 93 | QUnit.test('uses timeline values for the expired duration of live playlists', function() { |
97 | var playlist = { | 94 | let playlist = { |
98 | mediaSequence: 12, | 95 | mediaSequence: 12, |
99 | segments: [{ | 96 | segments: [{ |
100 | duration: 10, | 97 | duration: 10, |
... | @@ -104,18 +101,21 @@ | ... | @@ -104,18 +101,21 @@ |
104 | duration: 9, | 101 | duration: 9, |
105 | uri: '1.ts' | 102 | uri: '1.ts' |
106 | }] | 103 | }] |
107 | }, duration; | 104 | }; |
105 | let duration; | ||
108 | 106 | ||
109 | duration = Playlist.duration(playlist, playlist.mediaSequence); | 107 | duration = Playlist.duration(playlist, playlist.mediaSequence); |
110 | equal(duration, 110.5, 'used segment end time'); | 108 | QUnit.equal(duration, 110.5, 'used segment end time'); |
111 | duration = Playlist.duration(playlist, playlist.mediaSequence + 1); | 109 | duration = Playlist.duration(playlist, playlist.mediaSequence + 1); |
112 | equal(duration, 120.5, 'used segment end time'); | 110 | QUnit.equal(duration, 120.5, 'used segment end time'); |
113 | duration = Playlist.duration(playlist, playlist.mediaSequence + 2); | 111 | duration = Playlist.duration(playlist, playlist.mediaSequence + 2); |
114 | equal(duration, 120.5 + 9, 'used segment end time'); | 112 | QUnit.equal(duration, 120.5 + 9, 'used segment end time'); |
115 | }); | 113 | }); |
116 | 114 | ||
117 | test('looks outside the queried interval for live playlist timeline values', function() { | 115 | QUnit.test( |
118 | var playlist = { | 116 | 'looks outside the queried interval for live playlist timeline values', |
117 | function() { | ||
118 | let playlist = { | ||
119 | mediaSequence: 12, | 119 | mediaSequence: 12, |
120 | segments: [{ | 120 | segments: [{ |
121 | duration: 10, | 121 | duration: 10, |
... | @@ -125,14 +125,15 @@ | ... | @@ -125,14 +125,15 @@ |
125 | end: 120.5, | 125 | end: 120.5, |
126 | uri: '1.ts' | 126 | uri: '1.ts' |
127 | }] | 127 | }] |
128 | }, duration; | 128 | }; |
129 | let duration; | ||
129 | 130 | ||
130 | duration = Playlist.duration(playlist, playlist.mediaSequence); | 131 | duration = Playlist.duration(playlist, playlist.mediaSequence); |
131 | equal(duration, 120.5 - 9 - 10, 'used segment end time'); | 132 | QUnit.equal(duration, 120.5 - 9 - 10, 'used segment end time'); |
132 | }); | 133 | }); |
133 | 134 | ||
134 | test('ignores discontinuity sequences later than the end', function() { | 135 | QUnit.test('ignores discontinuity sequences later than the end', function() { |
135 | var duration = Playlist.duration({ | 136 | let duration = Playlist.duration({ |
136 | mediaSequence: 0, | 137 | mediaSequence: 0, |
137 | discontinuityStarts: [1, 3], | 138 | discontinuityStarts: [1, 3], |
138 | segments: [{ | 139 | segments: [{ |
... | @@ -152,12 +153,12 @@ | ... | @@ -152,12 +153,12 @@ |
152 | }] | 153 | }] |
153 | }, 2); | 154 | }, 2); |
154 | 155 | ||
155 | equal(duration, 19, 'excluded the later segments'); | 156 | QUnit.equal(duration, 19, 'excluded the later segments'); |
156 | }); | 157 | }); |
157 | 158 | ||
158 | test('handles trailing segments without timeline information', function() { | 159 | QUnit.test('handles trailing segments without timeline information', function() { |
159 | var playlist, duration; | 160 | let duration; |
160 | playlist = { | 161 | let playlist = { |
161 | mediaSequence: 0, | 162 | mediaSequence: 0, |
162 | endList: true, | 163 | endList: true, |
163 | segments: [{ | 164 | segments: [{ |
... | @@ -178,15 +179,15 @@ | ... | @@ -178,15 +179,15 @@ |
178 | }; | 179 | }; |
179 | 180 | ||
180 | duration = Playlist.duration(playlist, 3); | 181 | duration = Playlist.duration(playlist, 3); |
181 | equal(duration, 29.45, 'calculated duration'); | 182 | QUnit.equal(duration, 29.45, 'calculated duration'); |
182 | 183 | ||
183 | duration = Playlist.duration(playlist, 2); | 184 | duration = Playlist.duration(playlist, 2); |
184 | equal(duration, 19.5, 'calculated duration'); | 185 | QUnit.equal(duration, 19.5, 'calculated duration'); |
185 | }); | 186 | }); |
186 | 187 | ||
187 | test('uses timeline intervals when segments have them', function() { | 188 | QUnit.test('uses timeline intervals when segments have them', function() { |
188 | var playlist, duration; | 189 | let duration; |
189 | playlist = { | 190 | let playlist = { |
190 | mediaSequence: 0, | 191 | mediaSequence: 0, |
191 | segments: [{ | 192 | segments: [{ |
192 | start: 0, | 193 | start: 0, |
... | @@ -195,23 +196,25 @@ | ... | @@ -195,23 +196,25 @@ |
195 | }, { | 196 | }, { |
196 | duration: 9, | 197 | duration: 9, |
197 | uri: '1.ts' | 198 | uri: '1.ts' |
198 | },{ | 199 | }, { |
199 | start: 20.1, | 200 | start: 20.1, |
200 | end: 30.1, | 201 | end: 30.1, |
201 | duration: 10, | 202 | duration: 10, |
202 | uri: '2.ts' | 203 | uri: '2.ts' |
203 | }] | 204 | }] |
204 | }; | 205 | }; |
205 | duration = Playlist.duration(playlist, 2); | ||
206 | 206 | ||
207 | equal(duration, 20.1, 'used the timeline-based interval'); | 207 | duration = Playlist.duration(playlist, 2); |
208 | QUnit.equal(duration, 20.1, 'used the timeline-based interval'); | ||
208 | 209 | ||
209 | duration = Playlist.duration(playlist, 3); | 210 | duration = Playlist.duration(playlist, 3); |
210 | equal(duration, 30.1, 'used the timeline-based interval'); | 211 | QUnit.equal(duration, 30.1, 'used the timeline-based interval'); |
211 | }); | 212 | }); |
212 | 213 | ||
213 | test('counts the time between segments as part of the earlier segment\'s duration', function() { | 214 | QUnit.test( |
214 | var duration = Playlist.duration({ | 215 | 'counts the time between segments as part of the earlier segment\'s duration', |
216 | function() { | ||
217 | let duration = Playlist.duration({ | ||
215 | mediaSequence: 0, | 218 | mediaSequence: 0, |
216 | endList: true, | 219 | endList: true, |
217 | segments: [{ | 220 | segments: [{ |
... | @@ -226,11 +229,11 @@ | ... | @@ -226,11 +229,11 @@ |
226 | }] | 229 | }] |
227 | }, 1); | 230 | }, 1); |
228 | 231 | ||
229 | equal(duration, 10.1, 'included the segment gap'); | 232 | QUnit.equal(duration, 10.1, 'included the segment gap'); |
230 | }); | 233 | }); |
231 | 234 | ||
232 | test('accounts for discontinuities', function() { | 235 | QUnit.test('accounts for discontinuities', function() { |
233 | var duration = Playlist.duration({ | 236 | let duration = Playlist.duration({ |
234 | mediaSequence: 0, | 237 | mediaSequence: 0, |
235 | endList: true, | 238 | endList: true, |
236 | discontinuityStarts: [1], | 239 | discontinuityStarts: [1], |
... | @@ -244,11 +247,11 @@ | ... | @@ -244,11 +247,11 @@ |
244 | }] | 247 | }] |
245 | }, 2); | 248 | }, 2); |
246 | 249 | ||
247 | equal(duration, 10 + 10, 'handles discontinuities'); | 250 | QUnit.equal(duration, 10 + 10, 'handles discontinuities'); |
248 | }); | 251 | }); |
249 | 252 | ||
250 | test('a non-positive length interval has zero duration', function() { | 253 | QUnit.test('a non-positive length interval has zero duration', function() { |
251 | var playlist = { | 254 | let playlist = { |
252 | mediaSequence: 0, | 255 | mediaSequence: 0, |
253 | discontinuityStarts: [1], | 256 | discontinuityStarts: [1], |
254 | segments: [{ | 257 | segments: [{ |
... | @@ -261,15 +264,15 @@ | ... | @@ -261,15 +264,15 @@ |
261 | }] | 264 | }] |
262 | }; | 265 | }; |
263 | 266 | ||
264 | equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero'); | 267 | QUnit.equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero'); |
265 | equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero'); | 268 | QUnit.equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero'); |
266 | equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero'); | 269 | QUnit.equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero'); |
267 | }); | 270 | }); |
268 | 271 | ||
269 | QUnit.module('Playlist Seekable'); | 272 | QUnit.module('Playlist Seekable'); |
270 | 273 | ||
271 | test('calculates seekable time ranges from the available segments', function() { | 274 | QUnit.test('calculates seekable time ranges from the available segments', function() { |
272 | var playlist = { | 275 | let playlist = { |
273 | mediaSequence: 0, | 276 | mediaSequence: 0, |
274 | segments: [{ | 277 | segments: [{ |
275 | duration: 10, | 278 | duration: 10, |
... | @@ -279,26 +282,30 @@ | ... | @@ -279,26 +282,30 @@ |
279 | uri: '1.ts' | 282 | uri: '1.ts' |
280 | }], | 283 | }], |
281 | endList: true | 284 | endList: true |
282 | }, seekable = Playlist.seekable(playlist); | 285 | }; |
286 | let seekable = Playlist.seekable(playlist); | ||
283 | 287 | ||
284 | equal(seekable.length, 1, 'there are seekable ranges'); | 288 | QUnit.equal(seekable.length, 1, 'there are seekable ranges'); |
285 | equal(seekable.start(0), 0, 'starts at zero'); | 289 | QUnit.equal(seekable.start(0), 0, 'starts at zero'); |
286 | equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration'); | 290 | QUnit.equal(seekable.end(0), Playlist.duration(playlist), 'ends at the duration'); |
287 | }); | 291 | }); |
288 | 292 | ||
289 | test('master playlists have empty seekable ranges', function() { | 293 | QUnit.test('master playlists have empty seekable ranges', function() { |
290 | var seekable = Playlist.seekable({ | 294 | let seekable = Playlist.seekable({ |
291 | playlists: [{ | 295 | playlists: [{ |
292 | uri: 'low.m3u8' | 296 | uri: 'low.m3u8' |
293 | }, { | 297 | }, { |
294 | uri: 'high.m3u8' | 298 | uri: 'high.m3u8' |
295 | }] | 299 | }] |
296 | }); | 300 | }); |
297 | equal(seekable.length, 0, 'no seekable ranges from a master playlist'); | ||
298 | }); | ||
299 | 301 | ||
300 | test('seekable end is three target durations from the actual end of live playlists', function() { | 302 | QUnit.equal(seekable.length, 0, 'no seekable ranges from a master playlist'); |
301 | var seekable = Playlist.seekable({ | 303 | }); |
304 | |||
305 | QUnit.test( | ||
306 | 'seekable end is three target durations from the actual end of live playlists', | ||
307 | function() { | ||
308 | let seekable = Playlist.seekable({ | ||
302 | mediaSequence: 0, | 309 | mediaSequence: 0, |
303 | segments: [{ | 310 | segments: [{ |
304 | duration: 7, | 311 | duration: 7, |
... | @@ -314,13 +321,14 @@ | ... | @@ -314,13 +321,14 @@ |
314 | uri: '3.ts' | 321 | uri: '3.ts' |
315 | }] | 322 | }] |
316 | }); | 323 | }); |
317 | equal(seekable.length, 1, 'there are seekable ranges'); | ||
318 | equal(seekable.start(0), 0, 'starts at zero'); | ||
319 | equal(seekable.end(0), 7, 'ends three target durations from the last segment'); | ||
320 | }); | ||
321 | 324 | ||
322 | test('only considers available segments', function() { | 325 | QUnit.equal(seekable.length, 1, 'there are seekable ranges'); |
323 | var seekable = Playlist.seekable({ | 326 | QUnit.equal(seekable.start(0), 0, 'starts at zero'); |
327 | QUnit.equal(seekable.end(0), 7, 'ends three target durations from the last segment'); | ||
328 | }); | ||
329 | |||
330 | QUnit.test('only considers available segments', function() { | ||
331 | let seekable = Playlist.seekable({ | ||
324 | mediaSequence: 7, | 332 | mediaSequence: 7, |
325 | segments: [{ | 333 | segments: [{ |
326 | uri: '8.ts', | 334 | uri: '8.ts', |
... | @@ -336,13 +344,18 @@ | ... | @@ -336,13 +344,18 @@ |
336 | duration: 10 | 344 | duration: 10 |
337 | }] | 345 | }] |
338 | }); | 346 | }); |
339 | equal(seekable.length, 1, 'there are seekable ranges'); | ||
340 | equal(seekable.start(0), 0, 'starts at the earliest available segment'); | ||
341 | equal(seekable.end(0), 10, 'ends three target durations from the last available segment'); | ||
342 | }); | ||
343 | 347 | ||
344 | test('seekable end accounts for non-standard target durations', function() { | 348 | QUnit.equal(seekable.length, 1, 'there are seekable ranges'); |
345 | var seekable = Playlist.seekable({ | 349 | QUnit.equal(seekable.start(0), 0, 'starts at the earliest available segment'); |
350 | QUnit.equal( | ||
351 | seekable.end(0), | ||
352 | 10, | ||
353 | 'ends three target durations from the last available segment' | ||
354 | ); | ||
355 | }); | ||
356 | |||
357 | QUnit.test('seekable end accounts for non-standard target durations', function() { | ||
358 | let seekable = Playlist.seekable({ | ||
346 | targetDuration: 2, | 359 | targetDuration: 2, |
347 | mediaSequence: 0, | 360 | mediaSequence: 0, |
348 | segments: [{ | 361 | segments: [{ |
... | @@ -362,10 +375,11 @@ | ... | @@ -362,10 +375,11 @@ |
362 | uri: '4.ts' | 375 | uri: '4.ts' |
363 | }] | 376 | }] |
364 | }); | 377 | }); |
365 | equal(seekable.start(0), 0, 'starts at the earliest available segment'); | ||
366 | equal(seekable.end(0), | ||
367 | 9 - (2 + 2 + 1), | ||
368 | 'allows seeking no further than three segments from the end'); | ||
369 | }); | ||
370 | 378 | ||
371 | })(window, window.videojs); | 379 | QUnit.equal(seekable.start(0), 0, 'starts at the earliest available segment'); |
380 | QUnit.equal( | ||
381 | seekable.end(0), | ||
382 | 9 - (2 + 2 + 1), | ||
383 | 'allows seeking no further than three segments from the end' | ||
384 | ); | ||
385 | }); | ... | ... |
-
Please register or sign in to post a comment