c694b4b7 by David LaPalomento

Merge pull request #544 from BrandonOCasey/browserify-p4

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