5a33fc57 by brandonocasey

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