Merge pull request #544 from BrandonOCasey/browserify-p4
browserify-p4: playlist*, xhr, and resolve-url
Showing
12 changed files
with
766 additions
and
616 deletions
... | @@ -48,10 +48,7 @@ | ... | @@ -48,10 +48,7 @@ |
48 | <script src="/node_modules/video.js/dist/video.js"></script> | 48 | <script src="/node_modules/video.js/dist/video.js"></script> |
49 | <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script> | 49 | <script src="/node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script> |
50 | <script src="/src/videojs-contrib-hls.js"></script> | 50 | <script src="/src/videojs-contrib-hls.js"></script> |
51 | <script src="/src/xhr.js"></script> | ||
52 | <script src="/dist/videojs-contrib-hls.js"></script> | 51 | <script src="/dist/videojs-contrib-hls.js"></script> |
53 | <script src="/src/playlist.js"></script> | ||
54 | <script src="/src/playlist-loader.js"></script> | ||
55 | <script src="/src/bin-utils.js"></script> | 52 | <script src="/src/bin-utils.js"></script> |
56 | <script> | 53 | <script> |
57 | (function(window, videojs) { | 54 | (function(window, videojs) { | ... | ... |
... | @@ -2,7 +2,7 @@ var browserify = require('browserify'); | ... | @@ -2,7 +2,7 @@ var browserify = require('browserify'); |
2 | var fs = require('fs'); | 2 | var fs = require('fs'); |
3 | var glob = require('glob'); | 3 | var glob = require('glob'); |
4 | 4 | ||
5 | glob('test/{decryper,m3u8,stub}.test.js', function(err, files) { | 5 | glob('test/{playlist*,decryper,m3u8,stub}.test.js', function(err, files) { |
6 | browserify(files) | 6 | browserify(files) |
7 | .transform('babelify') | 7 | .transform('babelify') |
8 | .bundle() | 8 | .bundle() | ... | ... |
... | @@ -3,7 +3,7 @@ var fs = require('fs'); | ... | @@ -3,7 +3,7 @@ var fs = require('fs'); |
3 | var glob = require('glob'); | 3 | var glob = require('glob'); |
4 | var watchify = require('watchify'); | 4 | var watchify = require('watchify'); |
5 | 5 | ||
6 | glob('test/{decrypter,m3u8,stub}.test.js', function(err, files) { | 6 | glob('test/{playlist*,decrypter,m3u8,stub}.test.js', function(err, files) { |
7 | var b = browserify(files, { | 7 | var b = browserify(files, { |
8 | cache: {}, | 8 | cache: {}, |
9 | packageCache: {}, | 9 | packageCache: {}, | ... | ... |
... | @@ -5,14 +5,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 | }; | ... | ... |
src/resolve-url.js
0 → 100644
1 | import document from 'global/document'; | ||
2 | /* eslint-disable max-len */ | ||
3 | /** | ||
4 | * Constructs a new URI by interpreting a path relative to another | ||
5 | * URI. | ||
6 | * @param basePath {string} a relative or absolute URI | ||
7 | * @param path {string} a path part to combine with the base | ||
8 | * @return {string} a URI that is equivalent to composing `base` | ||
9 | * with `path` | ||
10 | * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue | ||
11 | */ | ||
12 | /* eslint-enable max-len */ | ||
13 | const resolveUrl = function(basePath, path) { | ||
14 | // use the base element to get the browser to handle URI resolution | ||
15 | let oldBase = document.querySelector('base'); | ||
16 | let docHead = document.querySelector('head'); | ||
17 | let a = document.createElement('a'); | ||
18 | let base = oldBase; | ||
19 | let oldHref; | ||
20 | let result; | ||
21 | |||
22 | // prep the document | ||
23 | if (oldBase) { | ||
24 | oldHref = oldBase.href; | ||
25 | } else { | ||
26 | base = docHead.appendChild(document.createElement('base')); | ||
27 | } | ||
28 | |||
29 | base.href = basePath; | ||
30 | a.href = path; | ||
31 | result = a.href; | ||
32 | |||
33 | // clean up | ||
34 | if (oldBase) { | ||
35 | oldBase.href = oldHref; | ||
36 | } else { | ||
37 | docHead.removeChild(base); | ||
38 | } | ||
39 | return result; | ||
40 | }; | ||
41 | |||
42 | export default resolveUrl; |
... | @@ -2,6 +2,10 @@ import m3u8 from './m3u8'; | ... | @@ -2,6 +2,10 @@ import m3u8 from './m3u8'; |
2 | import Stream from './stream'; | 2 | import Stream from './stream'; |
3 | import videojs from 'video.js'; | 3 | import videojs from 'video.js'; |
4 | import {Decrypter, decrypt, AsyncStream} from './decrypter'; | 4 | import {Decrypter, decrypt, AsyncStream} from './decrypter'; |
5 | import Playlist from './playlist'; | ||
6 | import PlaylistLoader from './playlist-loader'; | ||
7 | import xhr from './xhr'; | ||
8 | |||
5 | 9 | ||
6 | if(typeof window.videojs.Hls === 'undefined') { | 10 | if(typeof window.videojs.Hls === 'undefined') { |
7 | videojs.Hls = {}; | 11 | videojs.Hls = {}; |
... | @@ -11,3 +15,6 @@ videojs.m3u8 = m3u8; | ... | @@ -11,3 +15,6 @@ videojs.m3u8 = m3u8; |
11 | videojs.Hls.decrypt = decrypt; | 15 | videojs.Hls.decrypt = decrypt; |
12 | videojs.Hls.Decrypter = Decrypter; | 16 | videojs.Hls.Decrypter = Decrypter; |
13 | videojs.Hls.AsyncStream = AsyncStream; | 17 | videojs.Hls.AsyncStream = AsyncStream; |
18 | videojs.Hls.xhr = xhr; | ||
19 | videojs.Hls.Playlist = Playlist; | ||
20 | videojs.Hls.PlaylistLoader = PlaylistLoader; | ... | ... |
1 | (function(videojs) { | 1 | /** |
2 | 'use strict'; | ||
3 | |||
4 | /** | ||
5 | * A wrapper for videojs.xhr that tracks bandwidth. | 2 | * A wrapper for videojs.xhr that tracks bandwidth. |
6 | */ | 3 | */ |
7 | videojs.Hls.xhr = function(options, callback) { | 4 | import {xhr as videojsXHR, mergeOptions} from 'video.js'; |
5 | const xhr = function(options, callback) { | ||
8 | // Add a default timeout for all hls requests | 6 | // Add a default timeout for all hls requests |
9 | options = videojs.mergeOptions({ | 7 | options = mergeOptions({ |
10 | timeout: 45e3 | 8 | timeout: 45e3 |
11 | }, options); | 9 | }, options); |
12 | 10 | ||
13 | var request = videojs.xhr(options, function(error, response) { | 11 | let request = videojsXHR(options, function(error, response) { |
14 | if (!error && request.response) { | 12 | if (!error && request.response) { |
15 | request.responseTime = (new Date()).getTime(); | 13 | request.responseTime = (new Date()).getTime(); |
16 | request.roundTripTime = request.responseTime - request.requestTime; | 14 | request.roundTripTime = request.responseTime - request.requestTime; |
17 | request.bytesReceived = request.response.byteLength || request.response.length; | 15 | request.bytesReceived = request.response.byteLength || request.response.length; |
18 | if (!request.bandwidth) { | 16 | if (!request.bandwidth) { |
19 | request.bandwidth = Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000); | 17 | request.bandwidth = |
18 | Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000); | ||
20 | } | 19 | } |
21 | } | 20 | } |
22 | 21 | ||
23 | // videojs.xhr now uses a specific code on the error object to signal that a request has | 22 | // videojs.xhr now uses a specific code |
23 | // on the error object to signal that a request has | ||
24 | // timed out errors of setting a boolean on the request object | 24 | // timed out errors of setting a boolean on the request object |
25 | if (error || request.timedout) { | 25 | if (error || request.timedout) { |
26 | request.timedout = request.timedout || (error.code === 'ETIMEDOUT'); | 26 | request.timedout = request.timedout || (error.code === 'ETIMEDOUT'); |
... | @@ -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); | ... | ... |
-
Please register or sign in to post a comment