Merge pull request #411 from dmlap/next-segment-calculation
Determine the segment to load by looking at buffered
Showing
7 changed files
with
581 additions
and
1847 deletions
1 | (function(window) { | 1 | (function(window) { |
2 | var textRange = function(range, i) { | ||
3 | return range.start(i) + '-' + range.end(i); | ||
4 | }; | ||
2 | var module = { | 5 | var module = { |
3 | hexDump: function(data) { | 6 | hexDump: function(data) { |
4 | var | 7 | var |
... | @@ -26,6 +29,13 @@ | ... | @@ -26,6 +29,13 @@ |
26 | }, | 29 | }, |
27 | tagDump: function(tag) { | 30 | tagDump: function(tag) { |
28 | return module.hexDump(tag.bytes); | 31 | return module.hexDump(tag.bytes); |
32 | }, | ||
33 | textRanges: function(ranges) { | ||
34 | var result = '', i; | ||
35 | for (i = 0; i < ranges.length; i++) { | ||
36 | result += textRange(ranges, i) + ' '; | ||
37 | } | ||
38 | return result; | ||
29 | } | 39 | } |
30 | }; | 40 | }; |
31 | 41 | ... | ... |
... | @@ -2,13 +2,7 @@ | ... | @@ -2,13 +2,7 @@ |
2 | * playlist-loader | 2 | * playlist-loader |
3 | * | 3 | * |
4 | * A state machine that manages the loading, caching, and updating of | 4 | * A state machine that manages the loading, caching, and updating of |
5 | * M3U8 playlists. When tracking a live playlist, loaders will keep | 5 | * M3U8 playlists. |
6 | * track of the duration of content that expired since the loader was | ||
7 | * initialized and when the current discontinuity sequence was | ||
8 | * encountered. A complete media timeline for a live playlist with | ||
9 | * expiring segments looks like this: | ||
10 | * | ||
11 | * |-- expired --|-- segments --| | ||
12 | * | 6 | * |
13 | */ | 7 | */ |
14 | (function(window, videojs) { | 8 | (function(window, videojs) { |
... | @@ -16,7 +10,6 @@ | ... | @@ -16,7 +10,6 @@ |
16 | var | 10 | var |
17 | resolveUrl = videojs.Hls.resolveUrl, | 11 | resolveUrl = videojs.Hls.resolveUrl, |
18 | xhr = videojs.Hls.xhr, | 12 | xhr = videojs.Hls.xhr, |
19 | Playlist = videojs.Hls.Playlist, | ||
20 | mergeOptions = videojs.mergeOptions, | 13 | mergeOptions = videojs.mergeOptions, |
21 | 14 | ||
22 | /** | 15 | /** |
... | @@ -158,14 +151,6 @@ | ... | @@ -158,14 +151,6 @@ |
158 | // initialize the loader state | 151 | // initialize the loader state |
159 | loader.state = 'HAVE_NOTHING'; | 152 | loader.state = 'HAVE_NOTHING'; |
160 | 153 | ||
161 | // The total duration of all segments that expired and have been | ||
162 | // removed from the current playlist, in seconds. This property | ||
163 | // should always be zero for non-live playlists. In a live | ||
164 | // playlist, this is the total amount of time that has been | ||
165 | // removed from the stream since the playlist loader began | ||
166 | // tracking it. | ||
167 | loader.expired_ = 0; | ||
168 | |||
169 | // capture the prototype dispose function | 154 | // capture the prototype dispose function |
170 | dispose = this.dispose; | 155 | dispose = this.dispose; |
171 | 156 | ||
... | @@ -187,20 +172,20 @@ | ... | @@ -187,20 +172,20 @@ |
187 | * active media playlist. When called with a single argument, | 172 | * active media playlist. When called with a single argument, |
188 | * triggers the playlist loader to asynchronously switch to the | 173 | * triggers the playlist loader to asynchronously switch to the |
189 | * specified media playlist. Calling this method while the | 174 | * specified media playlist. Calling this method while the |
190 | * loader is in the HAVE_NOTHING or HAVE_MASTER states causes an | 175 | * loader is in the HAVE_NOTHING causes an error to be emitted |
191 | * error to be emitted but otherwise has no effect. | 176 | * but otherwise has no effect. |
192 | * @param playlist (optional) {object} the parsed media playlist | 177 | * @param playlist (optional) {object} the parsed media playlist |
193 | * object to switch to | 178 | * object to switch to |
194 | */ | 179 | */ |
195 | loader.media = function(playlist) { | 180 | loader.media = function(playlist) { |
196 | var mediaChange = false; | 181 | var startingState = loader.state, mediaChange; |
197 | // getter | 182 | // getter |
198 | if (!playlist) { | 183 | if (!playlist) { |
199 | return loader.media_; | 184 | return loader.media_; |
200 | } | 185 | } |
201 | 186 | ||
202 | // setter | 187 | // setter |
203 | if (loader.state === 'HAVE_NOTHING' || loader.state === 'HAVE_MASTER') { | 188 | if (loader.state === 'HAVE_NOTHING') { |
204 | throw new Error('Cannot switch media playlist from ' + loader.state); | 189 | throw new Error('Cannot switch media playlist from ' + loader.state); |
205 | } | 190 | } |
206 | 191 | ||
... | @@ -213,7 +198,7 @@ | ... | @@ -213,7 +198,7 @@ |
213 | playlist = loader.master.playlists[playlist]; | 198 | playlist = loader.master.playlists[playlist]; |
214 | } | 199 | } |
215 | 200 | ||
216 | mediaChange = playlist.uri !== loader.media_.uri; | 201 | mediaChange = !loader.media_ || playlist.uri !== loader.media_.uri; |
217 | 202 | ||
218 | // switch to fully loaded playlists immediately | 203 | // switch to fully loaded playlists immediately |
219 | if (loader.master.playlists[playlist.uri].endList) { | 204 | if (loader.master.playlists[playlist.uri].endList) { |
... | @@ -258,7 +243,17 @@ | ... | @@ -258,7 +243,17 @@ |
258 | withCredentials: withCredentials | 243 | withCredentials: withCredentials |
259 | }, function(error, request) { | 244 | }, function(error, request) { |
260 | haveMetadata(error, request, playlist.uri); | 245 | haveMetadata(error, request, playlist.uri); |
246 | |||
247 | if (error) { | ||
248 | return; | ||
249 | } | ||
250 | |||
251 | // fire loadedmetadata the first time a media playlist is loaded | ||
252 | if (startingState === 'HAVE_MASTER') { | ||
253 | loader.trigger('loadedmetadata'); | ||
254 | } else { | ||
261 | loader.trigger('mediachange'); | 255 | loader.trigger('mediachange'); |
256 | } | ||
262 | }); | 257 | }); |
263 | }; | 258 | }; |
264 | 259 | ||
... | @@ -320,19 +315,13 @@ | ... | @@ -320,19 +315,13 @@ |
320 | loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; | 315 | loader.master.playlists[loader.master.playlists[i].uri] = loader.master.playlists[i]; |
321 | } | 316 | } |
322 | 317 | ||
323 | request = xhr({ | 318 | loader.trigger('loadedplaylist'); |
324 | uri: resolveUrl(srcUrl, parser.manifest.playlists[0].uri), | 319 | if (!request) { |
325 | withCredentials: withCredentials | 320 | // no media playlist was specifically selected so start |
326 | }, function(error, request) { | 321 | // from the first listed one |
327 | // pass along the URL specified in the master playlist | 322 | loader.media(parser.manifest.playlists[0]); |
328 | haveMetadata(error, | ||
329 | request, | ||
330 | parser.manifest.playlists[0].uri); | ||
331 | if (!error) { | ||
332 | loader.trigger('loadedmetadata'); | ||
333 | } | 323 | } |
334 | }); | 324 | return; |
335 | return loader.trigger('loadedplaylist'); | ||
336 | } | 325 | } |
337 | 326 | ||
338 | // loaded a media playlist | 327 | // loaded a media playlist |
... | @@ -356,43 +345,10 @@ | ... | @@ -356,43 +345,10 @@ |
356 | * @param update {object} the updated media playlist object | 345 | * @param update {object} the updated media playlist object |
357 | */ | 346 | */ |
358 | PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { | 347 | PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { |
359 | var expiredCount; | ||
360 | |||
361 | if (this.media_) { | ||
362 | expiredCount = update.mediaSequence - this.media_.mediaSequence; | ||
363 | |||
364 | // update the expired time count | ||
365 | this.expired_ += Playlist.duration(this.media_, | ||
366 | this.media_.mediaSequence, | ||
367 | update.mediaSequence); | ||
368 | } | ||
369 | |||
370 | this.media_ = this.master.playlists[update.uri]; | 348 | this.media_ = this.master.playlists[update.uri]; |
371 | }; | 349 | }; |
372 | 350 | ||
373 | /** | 351 | /** |
374 | * When switching variant playlists in a live stream, the player may | ||
375 | * discover that the new set of available segments is shifted in | ||
376 | * time relative to the old playlist. If that is the case, you can | ||
377 | * call this method to synchronize the playlist loader so that | ||
378 | * subsequent calls to getMediaIndexForTime_() return values | ||
379 | * appropriate for the new playlist. | ||
380 | * | ||
381 | * @param mediaIndex {integer} the index of the segment that will be | ||
382 | * the used to base timeline calculations on | ||
383 | * @param startTime {number} the media timeline position of the | ||
384 | * first moment of video data for the specified segment. That is, | ||
385 | * data from the specified segment will first be displayed when | ||
386 | * `currentTime` is equal to `startTime`. | ||
387 | */ | ||
388 | PlaylistLoader.prototype.updateTimelineOffset = function(mediaIndex, startingTime) { | ||
389 | var segmentOffset = Playlist.duration(this.media_, | ||
390 | this.media_.mediaSequence, | ||
391 | this.media_.mediaSequence + mediaIndex); | ||
392 | this.expired_ = startingTime - segmentOffset; | ||
393 | }; | ||
394 | |||
395 | /** | ||
396 | * Determine the index of the segment that contains a specified | 352 | * Determine the index of the segment that contains a specified |
397 | * playback position in the current media playlist. Early versions | 353 | * playback position in the current media playlist. Early versions |
398 | * of the HLS specification require segment durations to be rounded | 354 | * of the HLS specification require segment durations to be rounded |
... | @@ -411,7 +367,7 @@ | ... | @@ -411,7 +367,7 @@ |
411 | * closest playback position that is currently available. | 367 | * closest playback position that is currently available. |
412 | */ | 368 | */ |
413 | PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { | 369 | PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { |
414 | var i; | 370 | var i, j, segment, targetDuration; |
415 | 371 | ||
416 | if (!this.media_) { | 372 | if (!this.media_) { |
417 | return 0; | 373 | return 0; |
... | @@ -419,28 +375,61 @@ | ... | @@ -419,28 +375,61 @@ |
419 | 375 | ||
420 | // when the requested position is earlier than the current set of | 376 | // when the requested position is earlier than the current set of |
421 | // segments, return the earliest segment index | 377 | // segments, return the earliest segment index |
422 | time -= this.expired_; | ||
423 | if (time < 0) { | 378 | if (time < 0) { |
424 | return 0; | 379 | return 0; |
425 | } | 380 | } |
426 | 381 | ||
427 | for (i = 0; i < this.media_.segments.length; i++) { | 382 | // 1) Walk backward until we find the latest segment with timeline |
428 | time -= Playlist.duration(this.media_, | 383 | // information that is earlier than `time` |
429 | this.media_.mediaSequence + i, | 384 | targetDuration = this.media_.targetDuration || 10; |
430 | this.media_.mediaSequence + i + 1, | 385 | i = this.media_.segments.length; |
431 | false); | 386 | while (i--) { |
387 | segment = this.media_.segments[i]; | ||
388 | if (segment.end !== undefined && segment.end <= time) { | ||
389 | time -= segment.end; | ||
390 | break; | ||
391 | } | ||
392 | if (segment.start !== undefined && segment.start < time) { | ||
393 | |||
394 | if (segment.end !== undefined && segment.end > time) { | ||
395 | // we've found the target segment exactly | ||
396 | return i; | ||
397 | } | ||
432 | 398 | ||
433 | // HLS version 3 and lower round segment durations to the | 399 | time -= segment.start; |
434 | // nearest decimal integer. When the correct media index is | 400 | time -= segment.duration || targetDuration; |
435 | // ambiguous, prefer the higher one. | 401 | if (time < 0) { |
436 | if (time <= 0) { | 402 | // the segment with start information is also our best guess |
403 | // for the momment | ||
437 | return i; | 404 | return i; |
438 | } | 405 | } |
406 | break; | ||
407 | } | ||
408 | } | ||
409 | i++; | ||
410 | |||
411 | // 2) Walk forward, testing each segment to see if `time` falls within it | ||
412 | for (j = i; j < this.media_.segments.length; j++) { | ||
413 | segment = this.media_.segments[j]; | ||
414 | time -= segment.duration || targetDuration; | ||
415 | |||
416 | if (time < 0) { | ||
417 | return j; | ||
418 | } | ||
419 | |||
420 | // 2a) If we discover a segment that has timeline information | ||
421 | // before finding the result segment, the playlist information | ||
422 | // must have been inaccurate. Start a binary search for the | ||
423 | // segment which contains `time`. If the guess turns out to be | ||
424 | // incorrect, we'll have more info to work with next time. | ||
425 | if (segment.start !== undefined || segment.end !== undefined) { | ||
426 | return Math.floor((j - i) * 0.5); | ||
427 | } | ||
439 | } | 428 | } |
440 | 429 | ||
441 | // the playback position is outside the range of available | 430 | // the playback position is outside the range of available |
442 | // segments so return the last one | 431 | // segments so return the length |
443 | return this.media_.segments.length - 1; | 432 | return this.media_.segments.length; |
444 | }; | 433 | }; |
445 | 434 | ||
446 | videojs.Hls.PlaylistLoader = PlaylistLoader; | 435 | videojs.Hls.PlaylistLoader = PlaylistLoader; | ... | ... |
... | @@ -5,7 +5,7 @@ | ... | @@ -5,7 +5,7 @@ |
5 | 'use strict'; | 5 | 'use strict'; |
6 | 6 | ||
7 | var DEFAULT_TARGET_DURATION = 10; | 7 | var DEFAULT_TARGET_DURATION = 10; |
8 | var accumulateDuration, ascendingNumeric, duration, intervalDuration, optionalMin, optionalMax, rangeDuration, seekable; | 8 | var duration, intervalDuration, optionalMin, optionalMax, seekable; |
9 | 9 | ||
10 | // Math.min that will return the alternative input if one of its | 10 | // Math.min that will return the alternative input if one of its |
11 | // parameters in undefined | 11 | // parameters in undefined |
... | @@ -23,170 +23,49 @@ | ... | @@ -23,170 +23,49 @@ |
23 | return Math.max(left, right); | 23 | return Math.max(left, right); |
24 | }; | 24 | }; |
25 | 25 | ||
26 | // Array.sort comparator to sort numbers in ascending order | ||
27 | ascendingNumeric = function(left, right) { | ||
28 | return left - right; | ||
29 | }; | ||
30 | |||
31 | /** | ||
32 | * Returns the media duration for the segments between a start and | ||
33 | * exclusive end index. The start and end parameters are interpreted | ||
34 | * as indices into the currently available segments. This method | ||
35 | * does not calculate durations for segments that have expired. | ||
36 | * @param playlist {object} a media playlist object | ||
37 | * @param start {number} an inclusive lower boundary for the | ||
38 | * segments to examine. | ||
39 | * @param end {number} an exclusive upper boundary for the segments | ||
40 | * to examine. | ||
41 | * @param includeTrailingTime {boolean} if false, the interval between | ||
42 | * the final segment and the subsequent segment will not be included | ||
43 | * in the result | ||
44 | * @return {number} the duration between the start index and end | ||
45 | * index in seconds. | ||
46 | */ | ||
47 | accumulateDuration = function(playlist, start, end, includeTrailingTime) { | ||
48 | var | ||
49 | ranges = [], | ||
50 | rangeEnds = (playlist.discontinuityStarts || []).concat(end), | ||
51 | result = 0, | ||
52 | i; | ||
53 | |||
54 | // short circuit if start and end don't specify a non-empty range | ||
55 | // of segments | ||
56 | if (start >= end) { | ||
57 | return 0; | ||
58 | } | ||
59 | |||
60 | // create a range object for each discontinuity sequence | ||
61 | rangeEnds.sort(ascendingNumeric); | ||
62 | for (i = 0; i < rangeEnds.length; i++) { | ||
63 | if (rangeEnds[i] > start) { | ||
64 | ranges.push({ start: start, end: rangeEnds[i] }); | ||
65 | i++; | ||
66 | break; | ||
67 | } | ||
68 | } | ||
69 | for (; i < rangeEnds.length; i++) { | ||
70 | // ignore times ranges later than end | ||
71 | if (rangeEnds[i] >= end) { | ||
72 | ranges.push({ start: rangeEnds[i - 1], end: end }); | ||
73 | break; | ||
74 | } | ||
75 | ranges.push({ start: ranges[ranges.length - 1].end, end: rangeEnds[i] }); | ||
76 | } | ||
77 | |||
78 | // add up the durations for each of the ranges | ||
79 | for (i = 0; i < ranges.length; i++) { | ||
80 | result += rangeDuration(playlist, | ||
81 | ranges[i], | ||
82 | i === ranges.length - 1 && includeTrailingTime); | ||
83 | } | ||
84 | |||
85 | return result; | ||
86 | }; | ||
87 | |||
88 | /** | ||
89 | * Returns the duration of the specified range of segments. The | ||
90 | * range *must not* cross a discontinuity. | ||
91 | * @param playlist {object} a media playlist object | ||
92 | * @param range {object} an object that specifies a starting and | ||
93 | * ending index into the available segments. | ||
94 | * @param includeTrailingTime {boolean} if false, the interval between | ||
95 | * the final segment and the subsequent segment will not be included | ||
96 | * in the result | ||
97 | * @return {number} the duration of the range in seconds. | ||
98 | */ | ||
99 | rangeDuration = function(playlist, range, includeTrailingTime) { | ||
100 | var | ||
101 | result = 0, | ||
102 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION, | ||
103 | segment, | ||
104 | left, right; | ||
105 | |||
106 | // accumulate while searching for the earliest segment with | ||
107 | // available PTS information | ||
108 | for (left = range.start; left < range.end; left++) { | ||
109 | segment = playlist.segments[left]; | ||
110 | if (segment.minVideoPts !== undefined || | ||
111 | segment.minAudioPts !== undefined) { | ||
112 | break; | ||
113 | } | ||
114 | result += segment.duration || targetDuration; | ||
115 | } | ||
116 | |||
117 | // see if there's enough information to include the trailing time | ||
118 | if (includeTrailingTime) { | ||
119 | segment = playlist.segments[range.end]; | ||
120 | if (segment && | ||
121 | (segment.minVideoPts !== undefined || | ||
122 | segment.minAudioPts !== undefined)) { | ||
123 | result += 0.001 * | ||
124 | (optionalMin(segment.minVideoPts, segment.minAudioPts) - | ||
125 | optionalMin(playlist.segments[left].minVideoPts, | ||
126 | playlist.segments[left].minAudioPts)); | ||
127 | return result; | ||
128 | } | ||
129 | } | ||
130 | |||
131 | // do the same thing while finding the latest segment | ||
132 | for (right = range.end - 1; right >= left; right--) { | ||
133 | segment = playlist.segments[right]; | ||
134 | if (segment.maxVideoPts !== undefined || | ||
135 | segment.maxAudioPts !== undefined) { | ||
136 | break; | ||
137 | } | ||
138 | result += segment.duration || targetDuration; | ||
139 | } | ||
140 | |||
141 | // add in the PTS interval in seconds between them | ||
142 | if (right >= left) { | ||
143 | result += 0.001 * | ||
144 | (optionalMax(playlist.segments[right].maxVideoPts, | ||
145 | playlist.segments[right].maxAudioPts) - | ||
146 | optionalMin(playlist.segments[left].minVideoPts, | ||
147 | playlist.segments[left].minAudioPts)); | ||
148 | } | ||
149 | |||
150 | return result; | ||
151 | }; | ||
152 | |||
153 | /** | 26 | /** |
154 | * Calculate the media duration from the segments associated with a | 27 | * Calculate the media duration from the segments associated with a |
155 | * playlist. The duration of a subinterval of the available segments | 28 | * playlist. The duration of a subinterval of the available segments |
156 | * may be calculated by specifying a start and end index. | 29 | * may be calculated by specifying an end index. |
157 | * | 30 | * |
158 | * @param playlist {object} a media playlist object | 31 | * @param playlist {object} a media playlist object |
159 | * @param startSequence {number} (optional) an inclusive lower | ||
160 | * boundary for the playlist. Defaults to 0. | ||
161 | * @param endSequence {number} (optional) an exclusive upper boundary | 32 | * @param endSequence {number} (optional) an exclusive upper boundary |
162 | * for the playlist. Defaults to playlist length. | 33 | * for the playlist. Defaults to playlist length. |
163 | * @param includeTrailingTime {boolean} if false, the interval between | ||
164 | * the final segment and the subsequent segment will not be included | ||
165 | * in the result | ||
166 | * @return {number} the duration between the start index and end | 34 | * @return {number} the duration between the start index and end |
167 | * index. | 35 | * index. |
168 | */ | 36 | */ |
169 | intervalDuration = function(playlist, startSequence, endSequence, includeTrailingTime) { | 37 | intervalDuration = function(playlist, endSequence) { |
170 | var result = 0, targetDuration, expiredSegmentCount; | 38 | var result = 0, segment, targetDuration, i; |
171 | 39 | ||
172 | if (startSequence === undefined) { | ||
173 | startSequence = playlist.mediaSequence || 0; | ||
174 | } | ||
175 | if (endSequence === undefined) { | 40 | if (endSequence === undefined) { |
176 | endSequence = startSequence + (playlist.segments || []).length; | 41 | endSequence = playlist.mediaSequence + (playlist.segments || []).length; |
42 | } | ||
43 | if (endSequence < 0) { | ||
44 | return 0; | ||
177 | } | 45 | } |
178 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | 46 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; |
179 | 47 | ||
180 | // estimate expired segment duration using the target duration | 48 | i = endSequence - playlist.mediaSequence; |
181 | expiredSegmentCount = optionalMax(playlist.mediaSequence - startSequence, 0); | 49 | // if a start time is available for segment immediately following |
182 | result += expiredSegmentCount * targetDuration; | 50 | // the interval, use it |
51 | segment = playlist.segments[i]; | ||
52 | // Walk backward until we find the latest segment with timeline | ||
53 | // information that is earlier than endSequence | ||
54 | if (segment && segment.start !== undefined) { | ||
55 | return segment.start; | ||
56 | } | ||
57 | while (i--) { | ||
58 | segment = playlist.segments[i]; | ||
59 | if (segment.end !== undefined) { | ||
60 | return result + segment.end; | ||
61 | } | ||
183 | 62 | ||
184 | // accumulate the segment durations into the result | 63 | result += (segment.duration || targetDuration); |
185 | result += accumulateDuration(playlist, | ||
186 | startSequence + expiredSegmentCount - playlist.mediaSequence, | ||
187 | endSequence - playlist.mediaSequence, | ||
188 | includeTrailingTime); | ||
189 | 64 | ||
65 | if (segment.start !== undefined) { | ||
66 | return result + segment.start; | ||
67 | } | ||
68 | } | ||
190 | return result; | 69 | return result; |
191 | }; | 70 | }; |
192 | 71 | ||
... | @@ -196,17 +75,16 @@ | ... | @@ -196,17 +75,16 @@ |
196 | * timeline between those two indices. The total duration for live | 75 | * timeline between those two indices. The total duration for live |
197 | * playlists is always Infinity. | 76 | * playlists is always Infinity. |
198 | * @param playlist {object} a media playlist object | 77 | * @param playlist {object} a media playlist object |
199 | * @param startSequence {number} (optional) an inclusive lower | 78 | * @param endSequence {number} (optional) an exclusive upper |
200 | * boundary for the playlist. Defaults to 0. | 79 | * boundary for the playlist. Defaults to the playlist media |
201 | * @param endSequence {number} (optional) an exclusive upper boundary | 80 | * sequence number plus its length. |
202 | * for the playlist. Defaults to playlist length. | 81 | * @param includeTrailingTime {boolean} (optional) if false, the |
203 | * @param includeTrailingTime {boolean} (optional) if false, the interval between | 82 | * interval between the final segment and the subsequent segment |
204 | * the final segment and the subsequent segment will not be included | 83 | * will not be included in the result |
205 | * in the result | ||
206 | * @return {number} the duration between the start index and end | 84 | * @return {number} the duration between the start index and end |
207 | * index. | 85 | * index. |
208 | */ | 86 | */ |
209 | duration = function(playlist, startSequence, endSequence, includeTrailingTime) { | 87 | duration = function(playlist, endSequence, includeTrailingTime) { |
210 | if (!playlist) { | 88 | if (!playlist) { |
211 | return 0; | 89 | return 0; |
212 | } | 90 | } |
... | @@ -217,7 +95,7 @@ | ... | @@ -217,7 +95,7 @@ |
217 | 95 | ||
218 | // if a slice of the total duration is not requested, use | 96 | // if a slice of the total duration is not requested, use |
219 | // playlist-level duration indicators when they're present | 97 | // playlist-level duration indicators when they're present |
220 | if (startSequence === undefined && endSequence === undefined) { | 98 | if (endSequence === undefined) { |
221 | // if present, use the duration specified in the playlist | 99 | // if present, use the duration specified in the playlist |
222 | if (playlist.totalDuration) { | 100 | if (playlist.totalDuration) { |
223 | return playlist.totalDuration; | 101 | return playlist.totalDuration; |
... | @@ -231,7 +109,6 @@ | ... | @@ -231,7 +109,6 @@ |
231 | 109 | ||
232 | // calculate the total duration based on the segment durations | 110 | // calculate the total duration based on the segment durations |
233 | return intervalDuration(playlist, | 111 | return intervalDuration(playlist, |
234 | startSequence, | ||
235 | endSequence, | 112 | endSequence, |
236 | includeTrailingTime); | 113 | includeTrailingTime); |
237 | }; | 114 | }; |
... | @@ -248,7 +125,7 @@ | ... | @@ -248,7 +125,7 @@ |
248 | * for seeking | 125 | * for seeking |
249 | */ | 126 | */ |
250 | seekable = function(playlist) { | 127 | seekable = function(playlist) { |
251 | var start, end, liveBuffer, targetDuration, segment, pending, i; | 128 | var start, end; |
252 | 129 | ||
253 | // without segments, there are no seekable ranges | 130 | // without segments, there are no seekable ranges |
254 | if (!playlist.segments) { | 131 | if (!playlist.segments) { |
... | @@ -259,33 +136,14 @@ | ... | @@ -259,33 +136,14 @@ |
259 | return videojs.createTimeRange(0, duration(playlist)); | 136 | return videojs.createTimeRange(0, duration(playlist)); |
260 | } | 137 | } |
261 | 138 | ||
262 | start = 0; | ||
263 | end = intervalDuration(playlist, | ||
264 | playlist.mediaSequence, | ||
265 | playlist.mediaSequence + playlist.segments.length); | ||
266 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | ||
267 | |||
268 | // live playlists should not expose three segment durations worth | 139 | // live playlists should not expose three segment durations worth |
269 | // of content from the end of the playlist | 140 | // of content from the end of the playlist |
270 | // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3 | 141 | // https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3 |
271 | if (!playlist.endList) { | 142 | start = intervalDuration(playlist, playlist.mediaSequence); |
272 | liveBuffer = targetDuration * 3; | 143 | end = intervalDuration(playlist, |
273 | // walk backward from the last available segment and track how | 144 | playlist.mediaSequence + playlist.segments.length); |
274 | // much media time has elapsed until three target durations have | 145 | end -= (playlist.targetDuration || DEFAULT_TARGET_DURATION) * 3; |
275 | // been traversed. if a segment is part of the interval being | 146 | end = Math.max(0, end); |
276 | // reported, subtract the overlapping portion of its duration | ||
277 | // from the result. | ||
278 | for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) { | ||
279 | segment = playlist.segments[i]; | ||
280 | pending = optionalMin(duration(playlist, | ||
281 | playlist.mediaSequence + i, | ||
282 | playlist.mediaSequence + i + 1), | ||
283 | liveBuffer); | ||
284 | liveBuffer -= pending; | ||
285 | end -= pending; | ||
286 | } | ||
287 | } | ||
288 | |||
289 | return videojs.createTimeRange(start, end); | 147 | return videojs.createTimeRange(start, end); |
290 | }; | 148 | }; |
291 | 149 | ... | ... |
... | @@ -15,7 +15,6 @@ var | ... | @@ -15,7 +15,6 @@ var |
15 | // the amount of time to wait between checking the state of the buffer | 15 | // the amount of time to wait between checking the state of the buffer |
16 | bufferCheckInterval = 500, | 16 | bufferCheckInterval = 500, |
17 | 17 | ||
18 | keyXhr, | ||
19 | keyFailed, | 18 | keyFailed, |
20 | resolveUrl; | 19 | resolveUrl; |
21 | 20 | ||
... | @@ -46,6 +45,8 @@ videojs.Hls = videojs.extend(Component, { | ... | @@ -46,6 +45,8 @@ videojs.Hls = videojs.extend(Component, { |
46 | this.tech_ = tech; | 45 | this.tech_ = tech; |
47 | this.source_ = options.source; | 46 | this.source_ = options.source; |
48 | this.mode_ = options.mode; | 47 | this.mode_ = options.mode; |
48 | // the segment info object for a segment that is in the process of | ||
49 | // being downloaded or processed | ||
49 | this.pendingSegment_ = null; | 50 | this.pendingSegment_ = null; |
50 | 51 | ||
51 | this.bytesReceived = 0; | 52 | this.bytesReceived = 0; |
... | @@ -61,9 +62,6 @@ videojs.Hls = videojs.extend(Component, { | ... | @@ -61,9 +62,6 @@ videojs.Hls = videojs.extend(Component, { |
61 | this.loadingState_ = 'meta'; | 62 | this.loadingState_ = 'meta'; |
62 | } | 63 | } |
63 | 64 | ||
64 | // a queue of segments that need to be transmuxed and processed, | ||
65 | // and then fed to the source buffer | ||
66 | this.segmentBuffer_ = []; | ||
67 | // periodically check if new data needs to be downloaded or | 65 | // periodically check if new data needs to be downloaded or |
68 | // buffered data should be appended to the source buffer | 66 | // buffered data should be appended to the source buffer |
69 | this.startCheckingBuffer_(); | 67 | this.startCheckingBuffer_(); |
... | @@ -71,6 +69,9 @@ videojs.Hls = videojs.extend(Component, { | ... | @@ -71,6 +69,9 @@ videojs.Hls = videojs.extend(Component, { |
71 | this.on(this.tech_, 'seeking', function() { | 69 | this.on(this.tech_, 'seeking', function() { |
72 | this.setCurrentTime(this.tech_.currentTime()); | 70 | this.setCurrentTime(this.tech_.currentTime()); |
73 | }); | 71 | }); |
72 | this.on(this.tech_, 'error', function() { | ||
73 | this.stopCheckingBuffer_(); | ||
74 | }); | ||
74 | 75 | ||
75 | this.on(this.tech_, 'play', this.play); | 76 | this.on(this.tech_, 'play', this.play); |
76 | } | 77 | } |
... | @@ -137,21 +138,10 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -137,21 +138,10 @@ videojs.Hls.prototype.src = function(src) { |
137 | } | 138 | } |
138 | 139 | ||
139 | this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); | 140 | this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); |
140 | this.segmentBuffer_ = []; | ||
141 | |||
142 | // if the stream contains ID3 metadata, expose that as a metadata | ||
143 | // text track | ||
144 | //this.setupMetadataCueTranslation_(); | ||
145 | 141 | ||
146 | // load the MediaSource into the player | 142 | // load the MediaSource into the player |
147 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); | 143 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); |
148 | 144 | ||
149 | // The index of the next segment to be downloaded in the current | ||
150 | // media playlist. When the current media playlist is live with | ||
151 | // expiring segments, it may be a different value from the media | ||
152 | // sequence number for a segment. | ||
153 | this.mediaIndex = 0; | ||
154 | |||
155 | this.options_ = {}; | 145 | this.options_ = {}; |
156 | if (this.source_.withCredentials !== undefined) { | 146 | if (this.source_.withCredentials !== undefined) { |
157 | this.options_.withCredentials = this.source_.withCredentials; | 147 | this.options_.withCredentials = this.source_.withCredentials; |
... | @@ -161,9 +151,6 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -161,9 +151,6 @@ videojs.Hls.prototype.src = function(src) { |
161 | this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials); | 151 | this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials); |
162 | 152 | ||
163 | this.playlists.on('loadedmetadata', function() { | 153 | this.playlists.on('loadedmetadata', function() { |
164 | var selectedPlaylist, loaderHandler, oldBitrate, newBitrate, segmentDuration, | ||
165 | segmentDlTime, threshold; | ||
166 | |||
167 | oldMediaPlaylist = this.playlists.media(); | 154 | oldMediaPlaylist = this.playlists.media(); |
168 | 155 | ||
169 | // if this isn't a live video and preload permits, start | 156 | // if this isn't a live video and preload permits, start |
... | @@ -174,56 +161,10 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -174,56 +161,10 @@ videojs.Hls.prototype.src = function(src) { |
174 | this.loadingState_ = 'segments'; | 161 | this.loadingState_ = 'segments'; |
175 | } | 162 | } |
176 | 163 | ||
177 | // the bandwidth estimate for the first segment is based on round | ||
178 | // trip time for the master playlist. the master playlist is | ||
179 | // almost always tiny so the round-trip time is dominated by | ||
180 | // latency and the computed bandwidth is much lower than | ||
181 | // steady-state. if the the downstream developer has a better way | ||
182 | // of detecting bandwidth and provided a number, use that instead. | ||
183 | if (this.bandwidth === undefined) { | ||
184 | // we're going to have to estimate initial bandwidth | ||
185 | // ourselves. scale the bandwidth estimate to account for the | ||
186 | // relatively high round-trip time from the master playlist. | ||
187 | this.setBandwidth({ | ||
188 | bandwidth: this.playlists.bandwidth * 5 | ||
189 | }); | ||
190 | } | ||
191 | |||
192 | this.setupSourceBuffer_(); | 164 | this.setupSourceBuffer_(); |
193 | |||
194 | selectedPlaylist = this.selectPlaylist(); | ||
195 | oldBitrate = oldMediaPlaylist.attributes && | ||
196 | oldMediaPlaylist.attributes.BANDWIDTH || 0; | ||
197 | newBitrate = selectedPlaylist.attributes && | ||
198 | selectedPlaylist.attributes.BANDWIDTH || 0; | ||
199 | segmentDuration = oldMediaPlaylist.segments && | ||
200 | oldMediaPlaylist.segments[this.mediaIndex].duration || | ||
201 | oldMediaPlaylist.targetDuration; | ||
202 | |||
203 | segmentDlTime = (segmentDuration * newBitrate) / this.bandwidth; | ||
204 | |||
205 | if (!segmentDlTime) { | ||
206 | segmentDlTime = Infinity; | ||
207 | } | ||
208 | |||
209 | // this threshold is to account for having a high latency on the manifest | ||
210 | // request which is a somewhat small file. | ||
211 | threshold = 10; | ||
212 | |||
213 | if (newBitrate > oldBitrate && segmentDlTime <= threshold) { | ||
214 | this.playlists.media(selectedPlaylist); | ||
215 | loaderHandler = function() { | ||
216 | this.setupFirstPlay(); | 165 | this.setupFirstPlay(); |
217 | this.fillBuffer(); | 166 | this.fillBuffer(); |
218 | this.tech_.trigger('loadedmetadata'); | 167 | this.tech_.trigger('loadedmetadata'); |
219 | this.playlists.off('loadedplaylist', loaderHandler); | ||
220 | }.bind(this); | ||
221 | this.playlists.on('loadedplaylist', loaderHandler); | ||
222 | } else { | ||
223 | this.setupFirstPlay(); | ||
224 | this.fillBuffer(); | ||
225 | this.tech_.trigger('loadedmetadata'); | ||
226 | } | ||
227 | }.bind(this)); | 168 | }.bind(this)); |
228 | 169 | ||
229 | this.playlists.on('error', function() { | 170 | this.playlists.on('error', function() { |
... | @@ -242,24 +183,20 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -242,24 +183,20 @@ videojs.Hls.prototype.src = function(src) { |
242 | var updatedPlaylist = this.playlists.media(); | 183 | var updatedPlaylist = this.playlists.media(); |
243 | 184 | ||
244 | if (!updatedPlaylist) { | 185 | if (!updatedPlaylist) { |
245 | // do nothing before an initial media playlist has been activated | 186 | // select the initial variant |
187 | this.playlists.media(this.selectPlaylist()); | ||
246 | return; | 188 | return; |
247 | } | 189 | } |
248 | 190 | ||
249 | this.updateDuration(this.playlists.media()); | 191 | this.updateDuration(this.playlists.media()); |
250 | this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist); | ||
251 | oldMediaPlaylist = updatedPlaylist; | 192 | oldMediaPlaylist = updatedPlaylist; |
252 | |||
253 | this.fetchKeys_(); | ||
254 | }.bind(this)); | 193 | }.bind(this)); |
255 | 194 | ||
256 | this.playlists.on('mediachange', function() { | 195 | this.playlists.on('mediachange', function() { |
257 | // abort outstanding key requests and check if new keys need to be retrieved | 196 | this.tech_.trigger({ |
258 | if (keyXhr) { | 197 | type: 'mediachange', |
259 | this.cancelKeyXhr(); | 198 | bubbles: true |
260 | } | 199 | }); |
261 | |||
262 | this.tech_.trigger({ type: 'mediachange', bubbles: true }); | ||
263 | }.bind(this)); | 200 | }.bind(this)); |
264 | 201 | ||
265 | // do nothing if the tech has been disposed already | 202 | // do nothing if the tech has been disposed already |
... | @@ -271,26 +208,6 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -271,26 +208,6 @@ videojs.Hls.prototype.src = function(src) { |
271 | this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); | 208 | this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); |
272 | }; | 209 | }; |
273 | 210 | ||
274 | /* Returns the media index for the live point in the current playlist, and updates | ||
275 | the current time to go along with it. | ||
276 | */ | ||
277 | videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) { | ||
278 | if (!selectedPlaylist.segments) { | ||
279 | return 0; | ||
280 | } | ||
281 | |||
282 | var tailIterator = selectedPlaylist.segments.length, | ||
283 | tailDuration = 0, | ||
284 | targetTail = (selectedPlaylist.targetDuration || 10) * 3; | ||
285 | |||
286 | while (tailDuration < targetTail && tailIterator > 0) { | ||
287 | tailDuration += selectedPlaylist.segments[tailIterator - 1].duration; | ||
288 | tailIterator--; | ||
289 | } | ||
290 | |||
291 | return tailIterator; | ||
292 | }; | ||
293 | |||
294 | videojs.Hls.prototype.handleSourceOpen = function() { | 211 | videojs.Hls.prototype.handleSourceOpen = function() { |
295 | // Only attempt to create the source buffer if none already exist. | 212 | // Only attempt to create the source buffer if none already exist. |
296 | // handleSourceOpen is also called when we are "re-opening" a source buffer | 213 | // handleSourceOpen is also called when we are "re-opening" a source buffer |
... | @@ -310,6 +227,63 @@ videojs.Hls.prototype.handleSourceOpen = function() { | ... | @@ -310,6 +227,63 @@ videojs.Hls.prototype.handleSourceOpen = function() { |
310 | } | 227 | } |
311 | }; | 228 | }; |
312 | 229 | ||
230 | // Returns the array of time range edge objects that were additively | ||
231 | // modified between two TimeRanges. | ||
232 | videojs.Hls.bufferedAdditions_ = function(original, update) { | ||
233 | var result = [], edges = [], | ||
234 | i, inOriginalRanges; | ||
235 | |||
236 | // if original or update are falsey, return an empty list of | ||
237 | // additions | ||
238 | if (!original || !update) { | ||
239 | return result; | ||
240 | } | ||
241 | |||
242 | // create a sorted array of time range start and end times | ||
243 | for (i = 0; i < original.length; i++) { | ||
244 | edges.push({ original: true, start: original.start(i) }); | ||
245 | edges.push({ original: true, end: original.end(i) }); | ||
246 | } | ||
247 | for (i = 0; i < update.length; i++) { | ||
248 | edges.push({ start: update.start(i) }); | ||
249 | edges.push({ end: update.end(i) }); | ||
250 | } | ||
251 | edges.sort(function(left, right) { | ||
252 | var leftTime, rightTime; | ||
253 | leftTime = left.start !== undefined ? left.start : left.end; | ||
254 | rightTime = right.start !== undefined ? right.start : right.end; | ||
255 | |||
256 | // when two times are equal, ensure the original edge covers the | ||
257 | // update | ||
258 | if (leftTime === rightTime) { | ||
259 | if (left.original) { | ||
260 | return left.start !== undefined ? -1 : 1; | ||
261 | } | ||
262 | return right.start !== undefined ? -1 : 1; | ||
263 | } | ||
264 | return leftTime - rightTime; | ||
265 | }); | ||
266 | |||
267 | // filter out all time range edges that occur during a period that | ||
268 | // was already covered by `original` | ||
269 | inOriginalRanges = false; | ||
270 | for (i = 0; i < edges.length; i++) { | ||
271 | // if this is a transition point for `original`, track whether | ||
272 | // subsequent edges are additions | ||
273 | if (edges[i].original) { | ||
274 | inOriginalRanges = edges[i].start !== undefined; | ||
275 | continue; | ||
276 | } | ||
277 | // if we're in a time range that was in `original`, ignore this edge | ||
278 | if (inOriginalRanges) { | ||
279 | continue; | ||
280 | } | ||
281 | // this edge occurred outside the range of `original` | ||
282 | result.push(edges[i]); | ||
283 | } | ||
284 | return result; | ||
285 | }; | ||
286 | |||
313 | videojs.Hls.prototype.setupSourceBuffer_ = function() { | 287 | videojs.Hls.prototype.setupSourceBuffer_ = function() { |
314 | var media = this.playlists.media(), mimeType; | 288 | var media = this.playlists.media(), mimeType; |
315 | 289 | ||
... | @@ -330,117 +304,42 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { | ... | @@ -330,117 +304,42 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { |
330 | // transition the sourcebuffer to the ended state if we've hit the end of | 304 | // transition the sourcebuffer to the ended state if we've hit the end of |
331 | // the playlist | 305 | // the playlist |
332 | this.sourceBuffer.addEventListener('updateend', function() { | 306 | this.sourceBuffer.addEventListener('updateend', function() { |
333 | var segmentInfo = this.pendingSegment_, i, currentBuffered; | 307 | var segmentInfo = this.pendingSegment_, segment, currentBuffered, timelineUpdates; |
334 | 308 | ||
335 | this.pendingSegment_ = null; | 309 | this.pendingSegment_ = null; |
336 | 310 | ||
337 | if (this.duration() !== Infinity && | 311 | // if we've buffered to the end of the video, let the MediaSource know |
338 | this.mediaIndex === this.playlists.media().segments.length) { | ||
339 | this.mediaSource.endOfStream(); | ||
340 | } | ||
341 | |||
342 | // When switching renditions or seeking, we may misjudge the media | ||
343 | // index to request to continue playback. Check after each append | ||
344 | // that a gap hasn't appeared in the buffered region and adjust | ||
345 | // the media index to fill it if necessary | ||
346 | if (this.tech_.buffered().length === 2 && | ||
347 | segmentInfo.playlist === this.playlists.media()) { | ||
348 | i = this.tech_.buffered().length; | ||
349 | while (i--) { | ||
350 | if (this.tech_.currentTime() < this.tech_.buffered().start(i)) { | ||
351 | // found the misidentified segment's buffered time range | ||
352 | // adjust the media index to fill the gap | ||
353 | currentBuffered = this.findCurrentBuffered_(); | 312 | currentBuffered = this.findCurrentBuffered_(); |
354 | this.playlists.updateTimelineOffset(segmentInfo.mediaIndex, this.tech_.buffered().start(i)); | 313 | if (currentBuffered.length && this.duration() === currentBuffered.end(0)) { |
355 | this.mediaIndex = this.playlists.getMediaIndexForTime_(currentBuffered.end(0) + 1); | 314 | this.mediaSource.endOfStream(); |
356 | break; | ||
357 | } | ||
358 | } | ||
359 | } | ||
360 | }.bind(this)); | ||
361 | }; | ||
362 | |||
363 | // register event listeners to transform in-band metadata events into | ||
364 | // VTTCues on a text track | ||
365 | videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { | ||
366 | var | ||
367 | metadataStream = this.segmentParser_.metadataStream, | ||
368 | textTrack; | ||
369 | |||
370 | // add a metadata cue whenever a metadata event is triggered during | ||
371 | // segment parsing | ||
372 | metadataStream.on('data', function(metadata) { | ||
373 | var i, hexDigit; | ||
374 | |||
375 | // create the metadata track if this is the first ID3 tag we've | ||
376 | // seen | ||
377 | if (!textTrack) { | ||
378 | textTrack = this.tech_.addTextTrack('metadata', 'Timed Metadata'); | ||
379 | |||
380 | // build the dispatch type from the stream descriptor | ||
381 | // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track | ||
382 | textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase(); | ||
383 | for (i = 0; i < metadataStream.descriptor.length; i++) { | ||
384 | hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2); | ||
385 | textTrack.inBandMetadataTrackDispatchType += hexDigit; | ||
386 | } | ||
387 | } | 315 | } |
388 | 316 | ||
389 | // store this event for processing once the muxing has finished | 317 | // stop here if the update errored or was aborted |
390 | this.tech_.segmentBuffer_[0].pendingMetadata.push({ | 318 | if (!segmentInfo) { |
391 | textTrack: textTrack, | ||
392 | metadata: metadata | ||
393 | }); | ||
394 | }.bind(this)); | ||
395 | |||
396 | // when seeking, clear out all cues ahead of the earliest position | ||
397 | // in the new segment. keep earlier cues around so they can still be | ||
398 | // programmatically inspected even though they've already fired | ||
399 | this.on(this.tech_, 'seeking', function() { | ||
400 | var media, startTime, i; | ||
401 | if (!textTrack) { | ||
402 | return; | 319 | return; |
403 | } | 320 | } |
404 | media = this.playlists.media(); | ||
405 | startTime = this.tech_.playlists.expired_; | ||
406 | startTime += videojs.Hls.Playlist.duration(media, | ||
407 | media.mediaSequence, | ||
408 | media.mediaSequence + this.tech_.mediaIndex); | ||
409 | 321 | ||
410 | i = textTrack.cues.length; | 322 | // annotate the segment with any start and end time information |
411 | while (i--) { | 323 | // added by the media processing |
412 | if (textTrack.cues[i].startTime >= startTime) { | 324 | segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; |
413 | textTrack.removeCue(textTrack.cues[i]); | 325 | timelineUpdates = videojs.Hls.bufferedAdditions_(segmentInfo.buffered, |
326 | this.tech_.buffered()); | ||
327 | timelineUpdates.forEach(function(update) { | ||
328 | if (update.start !== undefined) { | ||
329 | segment.start = update.start; | ||
414 | } | 330 | } |
331 | if (update.end !== undefined) { | ||
332 | segment.end = update.end; | ||
415 | } | 333 | } |
416 | }); | 334 | }); |
417 | }; | ||
418 | |||
419 | videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { | ||
420 | var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time; | ||
421 | segmentOffset = this.playlists.expired_; | ||
422 | segmentOffset += videojs.Hls.Playlist.duration(segmentInfo.playlist, | ||
423 | segmentInfo.playlist.mediaSequence, | ||
424 | segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex); | ||
425 | segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; | ||
426 | minPts = Math.min(isFinite(segment.minVideoPts) ? segment.minVideoPts : Infinity, | ||
427 | isFinite(segment.minAudioPts) ? segment.minAudioPts : Infinity); | ||
428 | 335 | ||
429 | while (segmentInfo.pendingMetadata.length) { | 336 | if (timelineUpdates.length) { |
430 | metadata = segmentInfo.pendingMetadata[0].metadata; | 337 | this.updateDuration(segmentInfo.playlist); |
431 | textTrack = segmentInfo.pendingMetadata[0].textTrack; | ||
432 | |||
433 | // create cue points for all the ID3 frames in this metadata event | ||
434 | for (i = 0; i < metadata.frames.length; i++) { | ||
435 | frame = metadata.frames[i]; | ||
436 | time = segmentOffset + ((metadata.pts - minPts) * 0.001); | ||
437 | cue = new window.VTTCue(time, time, frame.value || frame.url || ''); | ||
438 | cue.frame = frame; | ||
439 | cue.pts_ = metadata.pts; | ||
440 | textTrack.addCue(cue); | ||
441 | } | ||
442 | segmentInfo.pendingMetadata.shift(); | ||
443 | } | 338 | } |
339 | |||
340 | // check if it's time to download the next segment | ||
341 | this.checkBuffer_(); | ||
342 | }.bind(this)); | ||
444 | }; | 343 | }; |
445 | 344 | ||
446 | /** | 345 | /** |
... | @@ -475,14 +374,13 @@ videojs.Hls.prototype.setupFirstPlay = function() { | ... | @@ -475,14 +374,13 @@ videojs.Hls.prototype.setupFirstPlay = function() { |
475 | }; | 374 | }; |
476 | 375 | ||
477 | /** | 376 | /** |
478 | * Reset the mediaIndex if play() is called after the video has | 377 | * Begin playing the video. |
479 | * ended. | ||
480 | */ | 378 | */ |
481 | videojs.Hls.prototype.play = function() { | 379 | videojs.Hls.prototype.play = function() { |
482 | this.loadingState_ = 'segments'; | 380 | this.loadingState_ = 'segments'; |
483 | 381 | ||
484 | if (this.tech_.ended()) { | 382 | if (this.tech_.ended()) { |
485 | this.mediaIndex = 0; | 383 | this.tech_.setCurrentTime(0); |
486 | } | 384 | } |
487 | 385 | ||
488 | if (this.tech_.played().length === 0) { | 386 | if (this.tech_.played().length === 0) { |
... | @@ -519,23 +417,20 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { | ... | @@ -519,23 +417,20 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
519 | return currentTime; | 417 | return currentTime; |
520 | } | 418 | } |
521 | 419 | ||
522 | // determine the requested segment | ||
523 | this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime); | ||
524 | |||
525 | // cancel outstanding requests and buffer appends | 420 | // cancel outstanding requests and buffer appends |
526 | this.cancelSegmentXhr(); | 421 | this.cancelSegmentXhr(); |
527 | 422 | ||
528 | // abort outstanding key requests, if necessary | 423 | // abort outstanding key requests, if necessary |
529 | if (keyXhr) { | 424 | if (this.keyXhr_) { |
530 | keyXhr.aborted = true; | 425 | this.keyXhr_.aborted = true; |
531 | this.cancelKeyXhr(); | 426 | this.cancelKeyXhr(); |
532 | } | 427 | } |
533 | 428 | ||
534 | // clear out any buffered segments | 429 | // clear out the segment being processed |
535 | this.segmentBuffer_ = []; | 430 | this.pendingSegment_ = null; |
536 | 431 | ||
537 | // begin filling the buffer at the new position | 432 | // begin filling the buffer at the new position |
538 | this.fillBuffer(currentTime * 1000); | 433 | this.fillBuffer(currentTime); |
539 | }; | 434 | }; |
540 | 435 | ||
541 | videojs.Hls.prototype.duration = function() { | 436 | videojs.Hls.prototype.duration = function() { |
... | @@ -547,7 +442,7 @@ videojs.Hls.prototype.duration = function() { | ... | @@ -547,7 +442,7 @@ videojs.Hls.prototype.duration = function() { |
547 | }; | 442 | }; |
548 | 443 | ||
549 | videojs.Hls.prototype.seekable = function() { | 444 | videojs.Hls.prototype.seekable = function() { |
550 | var currentSeekable, startOffset, media; | 445 | var media; |
551 | 446 | ||
552 | if (!this.playlists) { | 447 | if (!this.playlists) { |
553 | return videojs.createTimeRanges(); | 448 | return videojs.createTimeRanges(); |
... | @@ -557,17 +452,7 @@ videojs.Hls.prototype.seekable = function() { | ... | @@ -557,17 +452,7 @@ videojs.Hls.prototype.seekable = function() { |
557 | return videojs.createTimeRanges(); | 452 | return videojs.createTimeRanges(); |
558 | } | 453 | } |
559 | 454 | ||
560 | // report the seekable range relative to the earliest possible | 455 | return videojs.Hls.Playlist.seekable(media); |
561 | // position when the stream was first loaded | ||
562 | currentSeekable = videojs.Hls.Playlist.seekable(media); | ||
563 | |||
564 | if (!currentSeekable.length) { | ||
565 | return currentSeekable; | ||
566 | } | ||
567 | |||
568 | startOffset = this.playlists.expired_; | ||
569 | return videojs.createTimeRanges(startOffset, | ||
570 | startOffset + (currentSeekable.end(0) - currentSeekable.start(0))); | ||
571 | }; | 456 | }; |
572 | 457 | ||
573 | /** | 458 | /** |
... | @@ -608,10 +493,10 @@ videojs.Hls.prototype.resetSrc_ = function() { | ... | @@ -608,10 +493,10 @@ videojs.Hls.prototype.resetSrc_ = function() { |
608 | }; | 493 | }; |
609 | 494 | ||
610 | videojs.Hls.prototype.cancelKeyXhr = function() { | 495 | videojs.Hls.prototype.cancelKeyXhr = function() { |
611 | if (keyXhr) { | 496 | if (this.keyXhr_) { |
612 | keyXhr.onreadystatechange = null; | 497 | this.keyXhr_.onreadystatechange = null; |
613 | keyXhr.abort(); | 498 | this.keyXhr_.abort(); |
614 | keyXhr = null; | 499 | this.keyXhr_ = null; |
615 | } | 500 | } |
616 | }; | 501 | }; |
617 | 502 | ||
... | @@ -790,7 +675,7 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { | ... | @@ -790,7 +675,7 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { |
790 | 675 | ||
791 | if (buffered && buffered.length) { | 676 | if (buffered && buffered.length) { |
792 | // Search for a range containing the play-head | 677 | // Search for a range containing the play-head |
793 | for (i = 0;i < buffered.length; i++) { | 678 | for (i = 0; i < buffered.length; i++) { |
794 | if (buffered.start(i) <= currentTime && | 679 | if (buffered.start(i) <= currentTime && |
795 | buffered.end(i) >= currentTime) { | 680 | buffered.end(i) >= currentTime) { |
796 | ranges = videojs.createTimeRanges(buffered.start(i), buffered.end(i)); | 681 | ranges = videojs.createTimeRanges(buffered.start(i), buffered.end(i)); |
... | @@ -810,16 +695,17 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { | ... | @@ -810,16 +695,17 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { |
810 | * Determines whether there is enough video data currently in the buffer | 695 | * Determines whether there is enough video data currently in the buffer |
811 | * and downloads a new segment if the buffered time is less than the goal. | 696 | * and downloads a new segment if the buffered time is less than the goal. |
812 | * @param seekToTime (optional) {number} the offset into the downloaded segment | 697 | * @param seekToTime (optional) {number} the offset into the downloaded segment |
813 | * to seek to, in milliseconds | 698 | * to seek to, in seconds |
814 | */ | 699 | */ |
815 | videojs.Hls.prototype.fillBuffer = function(seekToTime) { | 700 | videojs.Hls.prototype.fillBuffer = function(seekToTime) { |
816 | var | 701 | var |
817 | tech = this.tech_, | 702 | tech = this.tech_, |
818 | currentTime = tech.currentTime(), | 703 | currentTime = tech.currentTime(), |
819 | buffered = this.findCurrentBuffered_(), | 704 | currentBuffered = this.findCurrentBuffered_(), |
820 | bufferedTime = 0, | 705 | bufferedTime = 0, |
706 | mediaIndex = 0, | ||
821 | segment, | 707 | segment, |
822 | segmentUri; | 708 | segmentInfo; |
823 | 709 | ||
824 | // if preload is set to "none", do not download segments until playback is requested | 710 | // if preload is set to "none", do not download segments until playback is requested |
825 | if (this.loadingState_ !== 'segments') { | 711 | if (this.loadingState_ !== 'segments') { |
... | @@ -836,6 +722,11 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { | ... | @@ -836,6 +722,11 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { |
836 | return; | 722 | return; |
837 | } | 723 | } |
838 | 724 | ||
725 | // wait until the buffer is up to date | ||
726 | if (this.pendingSegment_) { | ||
727 | return; | ||
728 | } | ||
729 | |||
839 | // if no segments are available, do nothing | 730 | // if no segments are available, do nothing |
840 | if (this.playlists.state === "HAVE_NOTHING" || | 731 | if (this.playlists.state === "HAVE_NOTHING" || |
841 | !this.playlists.media() || | 732 | !this.playlists.media() || |
... | @@ -848,28 +739,52 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { | ... | @@ -848,28 +739,52 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { |
848 | return; | 739 | return; |
849 | } | 740 | } |
850 | 741 | ||
742 | // find the next segment to download | ||
743 | if (typeof seekToTime === 'number') { | ||
744 | mediaIndex = this.playlists.getMediaIndexForTime_(seekToTime); | ||
745 | } else if (currentBuffered && currentBuffered.length) { | ||
746 | mediaIndex = this.playlists.getMediaIndexForTime_(currentBuffered.end(0)); | ||
747 | bufferedTime = Math.max(0, currentBuffered.end(0) - currentTime); | ||
748 | } else { | ||
749 | mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime()); | ||
750 | } | ||
751 | segment = this.playlists.media().segments[mediaIndex]; | ||
752 | |||
851 | // if the video has finished downloading, stop trying to buffer | 753 | // if the video has finished downloading, stop trying to buffer |
852 | segment = this.playlists.media().segments[this.mediaIndex]; | ||
853 | if (!segment) { | 754 | if (!segment) { |
854 | return; | 755 | return; |
855 | } | 756 | } |
856 | 757 | ||
857 | // To determine how much is buffered, we need to find the buffered region we | ||
858 | // are currently playing in and measure it's length | ||
859 | if (buffered && buffered.length) { | ||
860 | bufferedTime = Math.max(0, buffered.end(0) - currentTime); | ||
861 | } | ||
862 | |||
863 | // if there is plenty of content in the buffer and we're not | 758 | // if there is plenty of content in the buffer and we're not |
864 | // seeking, relax for awhile | 759 | // seeking, relax for awhile |
865 | if (typeof seekToTime !== 'number' && bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { | 760 | if (typeof seekToTime !== 'number' && |
761 | bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { | ||
866 | return; | 762 | return; |
867 | } | 763 | } |
868 | 764 | ||
765 | // package up all the work to append the segment | ||
766 | segmentInfo = { | ||
869 | // resolve the segment URL relative to the playlist | 767 | // resolve the segment URL relative to the playlist |
870 | segmentUri = this.playlistUriToUrl(segment.uri); | 768 | uri: this.playlistUriToUrl(segment.uri), |
769 | // the segment's mediaIndex at the time it was received | ||
770 | mediaIndex: mediaIndex, | ||
771 | // the segment's playlist | ||
772 | playlist: this.playlists.media(), | ||
773 | // optionally, a time offset to seek to within the segment | ||
774 | offset: seekToTime, | ||
775 | // unencrypted bytes of the segment | ||
776 | bytes: null, | ||
777 | // when a key is defined for this segment, the encrypted bytes | ||
778 | encryptedBytes: null, | ||
779 | // optionally, the decrypter that is unencrypting the segment | ||
780 | decrypter: null, | ||
781 | // the state of the buffer before a segment is appended will be | ||
782 | // stored here so that the actual segment duration can be | ||
783 | // determined after it has been appended | ||
784 | buffered: null | ||
785 | }; | ||
871 | 786 | ||
872 | this.loadSegment(segmentUri, seekToTime); | 787 | this.loadSegment(segmentInfo); |
873 | }; | 788 | }; |
874 | 789 | ||
875 | videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) { | 790 | videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) { |
... | @@ -900,17 +815,22 @@ videojs.Hls.prototype.setBandwidth = function(xhr) { | ... | @@ -900,17 +815,22 @@ videojs.Hls.prototype.setBandwidth = function(xhr) { |
900 | this.tech_.trigger('bandwidthupdate'); | 815 | this.tech_.trigger('bandwidthupdate'); |
901 | }; | 816 | }; |
902 | 817 | ||
903 | videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { | 818 | videojs.Hls.prototype.loadSegment = function(segmentInfo) { |
904 | var self = this; | 819 | var |
820 | self = this, | ||
821 | segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; | ||
822 | |||
823 | // if the segment is encrypted, request the key | ||
824 | if (segment.key) { | ||
825 | this.fetchKey_(segment); | ||
826 | } | ||
905 | 827 | ||
906 | // request the next segment | 828 | // request the next segment |
907 | this.segmentXhr_ = videojs.Hls.xhr({ | 829 | this.segmentXhr_ = videojs.Hls.xhr({ |
908 | uri: segmentUri, | 830 | uri: segmentInfo.uri, |
909 | responseType: 'arraybuffer', | 831 | responseType: 'arraybuffer', |
910 | withCredentials: this.source_.withCredentials | 832 | withCredentials: this.source_.withCredentials |
911 | }, function(error, request) { | 833 | }, function(error, request) { |
912 | var segmentInfo; | ||
913 | |||
914 | // the segment request is no longer outstanding | 834 | // the segment request is no longer outstanding |
915 | self.segmentXhr_ = null; | 835 | self.segmentXhr_ = null; |
916 | 836 | ||
... | @@ -920,17 +840,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { | ... | @@ -920,17 +840,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { |
920 | return self.playlists.media(self.selectPlaylist()); | 840 | return self.playlists.media(self.selectPlaylist()); |
921 | } | 841 | } |
922 | 842 | ||
843 | // otherwise, trigger a network error | ||
923 | if (!request.aborted && error) { | 844 | if (!request.aborted && error) { |
924 | // otherwise, try jumping ahead to the next segment | ||
925 | self.error = { | 845 | self.error = { |
926 | status: request.status, | 846 | status: request.status, |
927 | message: 'HLS segment request error at URL: ' + segmentUri, | 847 | message: 'HLS segment request error at URL: ' + segmentInfo.uri, |
928 | code: (request.status >= 500) ? 4 : 2 | 848 | code: (request.status >= 500) ? 4 : 2 |
929 | }; | 849 | }; |
930 | 850 | ||
931 | // try moving on to the next segment | 851 | return self.mediaSource.endOfStream('network'); |
932 | self.mediaIndex++; | ||
933 | return; | ||
934 | } | 852 | } |
935 | 853 | ||
936 | // stop processing if the request was aborted | 854 | // stop processing if the request was aborted |
... | @@ -940,35 +858,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { | ... | @@ -940,35 +858,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { |
940 | 858 | ||
941 | self.setBandwidth(request); | 859 | self.setBandwidth(request); |
942 | 860 | ||
943 | // package up all the work to append the segment | 861 | if (segment.key) { |
944 | segmentInfo = { | ||
945 | // the segment's mediaIndex at the time it was received | ||
946 | mediaIndex: self.mediaIndex, | ||
947 | // the segment's playlist | ||
948 | playlist: self.playlists.media(), | ||
949 | // optionally, a time offset to seek to within the segment | ||
950 | offset: seekToTime, | ||
951 | // unencrypted bytes of the segment | ||
952 | bytes: null, | ||
953 | // when a key is defined for this segment, the encrypted bytes | ||
954 | encryptedBytes: null, | ||
955 | // optionally, the decrypter that is unencrypting the segment | ||
956 | decrypter: null, | ||
957 | // metadata events discovered during muxing that need to be | ||
958 | // translated into cue points | ||
959 | pendingMetadata: [] | ||
960 | }; | ||
961 | if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) { | ||
962 | segmentInfo.encryptedBytes = new Uint8Array(request.response); | 862 | segmentInfo.encryptedBytes = new Uint8Array(request.response); |
963 | } else { | 863 | } else { |
964 | segmentInfo.bytes = new Uint8Array(request.response); | 864 | segmentInfo.bytes = new Uint8Array(request.response); |
965 | } | 865 | } |
966 | self.segmentBuffer_.push(segmentInfo); | 866 | self.pendingSegment_ = segmentInfo; |
967 | self.tech_.trigger('progress'); | 867 | self.tech_.trigger('progress'); |
968 | self.drainBuffer(); | 868 | self.drainBuffer(); |
969 | 869 | ||
970 | self.mediaIndex++; | ||
971 | |||
972 | // figure out what stream the next segment should be downloaded from | 870 | // figure out what stream the next segment should be downloaded from |
973 | // with the updated bandwidth information | 871 | // with the updated bandwidth information |
974 | self.playlists.media(self.selectPlaylist()); | 872 | self.playlists.media(self.selectPlaylist()); |
... | @@ -988,13 +886,11 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -988,13 +886,11 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
988 | segmentTimestampOffset = 0, | 886 | segmentTimestampOffset = 0, |
989 | hasBufferedContent = (this.tech_.buffered().length !== 0), | 887 | hasBufferedContent = (this.tech_.buffered().length !== 0), |
990 | currentBuffered = this.findCurrentBuffered_(), | 888 | currentBuffered = this.findCurrentBuffered_(), |
991 | outsideBufferedRanges = !(currentBuffered && currentBuffered.length), | 889 | outsideBufferedRanges = !(currentBuffered && currentBuffered.length); |
992 | // ptsTime, | ||
993 | segmentBuffer = this.segmentBuffer_; | ||
994 | 890 | ||
995 | // if the buffer is empty or the source buffer hasn't been created | 891 | // if the buffer is empty or the source buffer hasn't been created |
996 | // yet, do nothing | 892 | // yet, do nothing |
997 | if (!segmentBuffer.length || !this.sourceBuffer) { | 893 | if (!this.pendingSegment_ || !this.sourceBuffer) { |
998 | return; | 894 | return; |
999 | } | 895 | } |
1000 | 896 | ||
... | @@ -1004,7 +900,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1004,7 +900,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1004 | return; | 900 | return; |
1005 | } | 901 | } |
1006 | 902 | ||
1007 | segmentInfo = segmentBuffer[0]; | 903 | segmentInfo = this.pendingSegment_; |
1008 | mediaIndex = segmentInfo.mediaIndex; | 904 | mediaIndex = segmentInfo.mediaIndex; |
1009 | playlist = segmentInfo.playlist; | 905 | playlist = segmentInfo.playlist; |
1010 | offset = segmentInfo.offset; | 906 | offset = segmentInfo.offset; |
... | @@ -1017,18 +913,19 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1017,18 +913,19 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1017 | // if the key download failed, we want to skip this segment | 913 | // if the key download failed, we want to skip this segment |
1018 | // but if the key hasn't downloaded yet, we want to try again later | 914 | // but if the key hasn't downloaded yet, we want to try again later |
1019 | if (keyFailed(segment.key)) { | 915 | if (keyFailed(segment.key)) { |
1020 | return segmentBuffer.shift(); | 916 | videojs.log.warn('Network error retrieving key from "' + |
917 | segment.key.uri + '"'); | ||
918 | return this.mediaSource.endOfStream('network'); | ||
1021 | } else if (!segment.key.bytes) { | 919 | } else if (!segment.key.bytes) { |
1022 | 920 | ||
1023 | // trigger a key request if one is not already in-flight | 921 | // waiting for the key bytes, try again later |
1024 | return this.fetchKeys_(); | 922 | return; |
1025 | |||
1026 | } else if (segmentInfo.decrypter) { | 923 | } else if (segmentInfo.decrypter) { |
1027 | 924 | ||
1028 | // decryption is in progress, try again later | 925 | // decryption is in progress, try again later |
1029 | return; | 926 | return; |
1030 | |||
1031 | } else { | 927 | } else { |
928 | |||
1032 | // if the media sequence is greater than 2^32, the IV will be incorrect | 929 | // if the media sequence is greater than 2^32, the IV will be incorrect |
1033 | // assuming 10s segments, that would be about 1300 years | 930 | // assuming 10s segments, that would be about 1300 years |
1034 | segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); | 931 | segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); |
... | @@ -1047,32 +944,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1047,32 +944,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1047 | 944 | ||
1048 | event = event || {}; | 945 | event = event || {}; |
1049 | 946 | ||
1050 | // if (this.segmentParser_.tagsAvailable()) { | ||
1051 | // // record PTS information for the segment so we can calculate | ||
1052 | // // accurate durations and seek reliably | ||
1053 | // if (this.segmentParser_.stats.h264Tags()) { | ||
1054 | // segment.minVideoPts = this.segmentParser_.stats.minVideoPts(); | ||
1055 | // segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts(); | ||
1056 | // } | ||
1057 | // if (this.segmentParser_.stats.aacTags()) { | ||
1058 | // segment.minAudioPts = this.segmentParser_.stats.minAudioPts(); | ||
1059 | // segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts(); | ||
1060 | // } | ||
1061 | // } | ||
1062 | |||
1063 | // while (this.segmentParser_.tagsAvailable()) { | ||
1064 | // tags.push(this.segmentParser_.getNextTag()); | ||
1065 | // } | ||
1066 | |||
1067 | this.addCuesForMetadata_(segmentInfo); | ||
1068 | //this.updateDuration(this.playlists.media()); | ||
1069 | |||
1070 | // // when we're crossing a discontinuity, inject metadata to indicate | ||
1071 | // // that the decoder should be reset appropriately | ||
1072 | // if (segment.discontinuity && tags.length) { | ||
1073 | // this.tech_.el().vjs_discontinuity(); | ||
1074 | // } | ||
1075 | |||
1076 | // If we have seeked into a non-buffered time-range, remove all buffered | 947 | // If we have seeked into a non-buffered time-range, remove all buffered |
1077 | // time-ranges because they could have been incorrectly placed originally | 948 | // time-ranges because they could have been incorrectly placed originally |
1078 | if (this.tech_.seeking() && outsideBufferedRanges) { | 949 | if (this.tech_.seeking() && outsideBufferedRanges) { |
... | @@ -1088,7 +959,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1088,7 +959,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1088 | // anew on every seek | 959 | // anew on every seek |
1089 | if (segmentInfo.playlist.discontinuityStarts.length) { | 960 | if (segmentInfo.playlist.discontinuityStarts.length) { |
1090 | if (segmentInfo.mediaIndex > 0) { | 961 | if (segmentInfo.mediaIndex > 0) { |
1091 | segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, 0, segmentInfo.mediaIndex); | 962 | segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, segmentInfo.mediaIndex); |
1092 | } | 963 | } |
1093 | 964 | ||
1094 | // Now that the forward buffer is clear, we have to set timestamp offset to | 965 | // Now that the forward buffer is clear, we have to set timestamp offset to |
... | @@ -1102,44 +973,46 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1102,44 +973,46 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1102 | this.sourceBuffer.timestampOffset = currentBuffered.end(0); | 973 | this.sourceBuffer.timestampOffset = currentBuffered.end(0); |
1103 | } | 974 | } |
1104 | 975 | ||
976 | if (currentBuffered.length) { | ||
977 | // Chrome 45 stalls if appends overlap the playhead | ||
978 | this.sourceBuffer.appendWindowStart = Math.min(this.tech_.currentTime(), currentBuffered.end(0)); | ||
979 | } else { | ||
980 | this.sourceBuffer.appendWindowStart = 0; | ||
981 | } | ||
982 | this.pendingSegment_.buffered = this.tech_.buffered(); | ||
983 | |||
1105 | // the segment is asynchronously added to the current buffered data | 984 | // the segment is asynchronously added to the current buffered data |
1106 | this.sourceBuffer.appendBuffer(bytes); | 985 | this.sourceBuffer.appendBuffer(bytes); |
1107 | this.pendingSegment_ = segmentBuffer.shift(); | ||
1108 | }; | 986 | }; |
1109 | 987 | ||
1110 | /** | 988 | /** |
1111 | * Attempt to retrieve keys starting at a particular media | 989 | * Attempt to retrieve the key for a particular media segment. |
1112 | * segment. This method has no effect if segments are not yet | ||
1113 | * available or a key request is already in progress. | ||
1114 | * | ||
1115 | * @param playlist {object} the media playlist to fetch keys for | ||
1116 | * @param index {number} the media segment index to start from | ||
1117 | */ | 990 | */ |
1118 | videojs.Hls.prototype.fetchKeys_ = function() { | 991 | videojs.Hls.prototype.fetchKey_ = function(segment) { |
1119 | var i, key, tech, player, settings, segment, view, receiveKey; | 992 | var key, self, settings, receiveKey; |
1120 | 993 | ||
1121 | // if there is a pending XHR or no segments, don't do anything | 994 | // if there is a pending XHR or no segments, don't do anything |
1122 | if (keyXhr || !this.segmentBuffer_.length) { | 995 | if (this.keyXhr_) { |
1123 | return; | 996 | return; |
1124 | } | 997 | } |
1125 | 998 | ||
1126 | tech = this; | 999 | self = this; |
1127 | player = this.player(); | ||
1128 | settings = this.options_; | 1000 | settings = this.options_; |
1129 | 1001 | ||
1130 | /** | 1002 | /** |
1131 | * Handle a key XHR response. This function needs to lookup the | 1003 | * Handle a key XHR response. |
1132 | */ | 1004 | */ |
1133 | receiveKey = function(key) { | 1005 | receiveKey = function(key) { |
1134 | return function(error, request) { | 1006 | return function(error, request) { |
1135 | keyXhr = null; | 1007 | var view; |
1008 | self.keyXhr_ = null; | ||
1136 | 1009 | ||
1137 | if (error || !request.response || request.response.byteLength !== 16) { | 1010 | if (error || !request.response || request.response.byteLength !== 16) { |
1138 | key.retries = key.retries || 0; | 1011 | key.retries = key.retries || 0; |
1139 | key.retries++; | 1012 | key.retries++; |
1140 | if (!request.aborted) { | 1013 | if (!request.aborted) { |
1141 | // try fetching again | 1014 | // try fetching again |
1142 | tech.fetchKeys_(); | 1015 | self.fetchKey_(segment); |
1143 | } | 1016 | } |
1144 | return; | 1017 | return; |
1145 | } | 1018 | } |
... | @@ -1153,28 +1026,25 @@ videojs.Hls.prototype.fetchKeys_ = function() { | ... | @@ -1153,28 +1026,25 @@ videojs.Hls.prototype.fetchKeys_ = function() { |
1153 | ]); | 1026 | ]); |
1154 | 1027 | ||
1155 | // check to see if this allows us to make progress buffering now | 1028 | // check to see if this allows us to make progress buffering now |
1156 | tech.checkBuffer_(); | 1029 | self.checkBuffer_(); |
1157 | }; | 1030 | }; |
1158 | }; | 1031 | }; |
1159 | 1032 | ||
1160 | for (i = 0; i < tech.segmentBuffer_.length; i++) { | ||
1161 | segment = tech.segmentBuffer_[i].playlist.segments[tech.segmentBuffer_[i].mediaIndex]; | ||
1162 | key = segment.key; | 1033 | key = segment.key; |
1163 | 1034 | ||
1164 | // continue looking if this segment is unencrypted | 1035 | // nothing to do if this segment is unencrypted |
1165 | if (!key) { | 1036 | if (!key) { |
1166 | continue; | 1037 | return; |
1167 | } | 1038 | } |
1168 | 1039 | ||
1169 | // request the key if the retry limit hasn't been reached | 1040 | // request the key if the retry limit hasn't been reached |
1170 | if (!key.bytes && !keyFailed(key)) { | 1041 | if (!key.bytes && !keyFailed(key)) { |
1171 | keyXhr = videojs.Hls.xhr({ | 1042 | this.keyXhr_ = videojs.Hls.xhr({ |
1172 | uri: this.playlistUriToUrl(key.uri), | 1043 | uri: this.playlistUriToUrl(key.uri), |
1173 | responseType: 'arraybuffer', | 1044 | responseType: 'arraybuffer', |
1174 | withCredentials: settings.withCredentials | 1045 | withCredentials: settings.withCredentials |
1175 | }, receiveKey(key)); | 1046 | }, receiveKey(key)); |
1176 | break; | 1047 | return; |
1177 | } | ||
1178 | } | 1048 | } |
1179 | }; | 1049 | }; |
1180 | 1050 | ||
... | @@ -1206,83 +1076,6 @@ videojs.Hls.isSupported = function() { | ... | @@ -1206,83 +1076,6 @@ videojs.Hls.isSupported = function() { |
1206 | }; | 1076 | }; |
1207 | 1077 | ||
1208 | /** | 1078 | /** |
1209 | * Calculate the duration of a playlist from a given start index to a given | ||
1210 | * end index. | ||
1211 | * @param playlist {object} a media playlist object | ||
1212 | * @param startIndex {number} an inclusive lower boundary for the playlist. | ||
1213 | * Defaults to 0. | ||
1214 | * @param endIndex {number} an exclusive upper boundary for the playlist. | ||
1215 | * Defaults to playlist length. | ||
1216 | * @return {number} the duration between the start index and end index. | ||
1217 | */ | ||
1218 | videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) { | ||
1219 | videojs.log.warn('videojs.Hls.getPlaylistDuration is deprecated. ' + | ||
1220 | 'Use videojs.Hls.Playlist.duration instead'); | ||
1221 | return videojs.Hls.Playlist.duration(playlist, startIndex, endIndex); | ||
1222 | }; | ||
1223 | |||
1224 | /** | ||
1225 | * Calculate the total duration for a playlist based on segment metadata. | ||
1226 | * @param playlist {object} a media playlist object | ||
1227 | * @return {number} the currently known duration, in seconds | ||
1228 | */ | ||
1229 | videojs.Hls.getPlaylistTotalDuration = function(playlist) { | ||
1230 | videojs.log.warn('videojs.Hls.getPlaylistTotalDuration is deprecated. ' + | ||
1231 | 'Use videojs.Hls.Playlist.duration instead'); | ||
1232 | return videojs.Hls.Playlist.duration(playlist); | ||
1233 | }; | ||
1234 | |||
1235 | /** | ||
1236 | * Determine the media index in one playlist that corresponds to a | ||
1237 | * specified media index in another. This function can be used to | ||
1238 | * calculate a new segment position when a playlist is reloaded or a | ||
1239 | * variant playlist is becoming active. | ||
1240 | * @param mediaIndex {number} the index into the original playlist | ||
1241 | * to translate | ||
1242 | * @param original {object} the playlist to translate the media | ||
1243 | * index from | ||
1244 | * @param update {object} the playlist to translate the media index | ||
1245 | * to | ||
1246 | * @param {number} the corresponding media index in the updated | ||
1247 | * playlist | ||
1248 | */ | ||
1249 | videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { | ||
1250 | var translatedMediaIndex; | ||
1251 | |||
1252 | // no segments have been loaded from the original playlist | ||
1253 | if (mediaIndex === 0) { | ||
1254 | return 0; | ||
1255 | } | ||
1256 | |||
1257 | if (!(update && update.segments)) { | ||
1258 | // let the media index be zero when there are no segments defined | ||
1259 | return 0; | ||
1260 | } | ||
1261 | |||
1262 | // translate based on media sequence numbers. syncing up across | ||
1263 | // bitrate switches should be happening here. | ||
1264 | translatedMediaIndex = (mediaIndex + (original.mediaSequence - update.mediaSequence)); | ||
1265 | |||
1266 | if (translatedMediaIndex > update.segments.length || translatedMediaIndex < 0) { | ||
1267 | // recalculate the live point if the streams are too far out of sync | ||
1268 | return videojs.Hls.getMediaIndexForLive_(update) + 1; | ||
1269 | } | ||
1270 | |||
1271 | return translatedMediaIndex; | ||
1272 | }; | ||
1273 | |||
1274 | /** | ||
1275 | * Deprecated. | ||
1276 | * | ||
1277 | * @deprecated use player.hls.playlists.getMediaIndexForTime_() instead | ||
1278 | */ | ||
1279 | videojs.Hls.getMediaIndexByTime = function() { | ||
1280 | videojs.log.warn('getMediaIndexByTime is deprecated. ' + | ||
1281 | 'Use PlaylistLoader.getMediaIndexForTime_ instead.'); | ||
1282 | return 0; | ||
1283 | }; | ||
1284 | |||
1285 | /** | ||
1286 | * A comparator function to sort two playlist object by bandwidth. | 1079 | * A comparator function to sort two playlist object by bandwidth. |
1287 | * @param left {object} a media playlist object | 1080 | * @param left {object} a media playlist object |
1288 | * @param right {object} a media playlist object | 1081 | * @param right {object} a media playlist object | ... | ... |
... | @@ -53,15 +53,6 @@ | ... | @@ -53,15 +53,6 @@ |
53 | strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); | 53 | strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet'); |
54 | }); | 54 | }); |
55 | 55 | ||
56 | test('starts with no expired time', function() { | ||
57 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | ||
58 | requests.pop().respond(200, null, | ||
59 | '#EXTM3U\n' + | ||
60 | '#EXTINF:10,\n' + | ||
61 | '0.ts\n'); | ||
62 | equal(loader.expired_, 0, 'zero seconds expired'); | ||
63 | }); | ||
64 | |||
65 | test('requests the initial playlist immediately', function() { | 56 | test('requests the initial playlist immediately', function() { |
66 | new videojs.Hls.PlaylistLoader('master.m3u8'); | 57 | new videojs.Hls.PlaylistLoader('master.m3u8'); |
67 | strictEqual(requests.length, 1, 'made a request'); | 58 | strictEqual(requests.length, 1, 'made a request'); |
... | @@ -69,13 +60,16 @@ | ... | @@ -69,13 +60,16 @@ |
69 | }); | 60 | }); |
70 | 61 | ||
71 | test('moves to HAVE_MASTER after loading a master playlist', function() { | 62 | test('moves to HAVE_MASTER after loading a master playlist', function() { |
72 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | 63 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'), state; |
64 | loader.on('loadedplaylist', function() { | ||
65 | state = loader.state; | ||
66 | }); | ||
73 | requests.pop().respond(200, null, | 67 | requests.pop().respond(200, null, |
74 | '#EXTM3U\n' + | 68 | '#EXTM3U\n' + |
75 | '#EXT-X-STREAM-INF:\n' + | 69 | '#EXT-X-STREAM-INF:\n' + |
76 | 'media.m3u8\n'); | 70 | 'media.m3u8\n'); |
77 | ok(loader.master, 'the master playlist is available'); | 71 | ok(loader.master, 'the master playlist is available'); |
78 | strictEqual(loader.state, 'HAVE_MASTER', 'the state is correct'); | 72 | strictEqual(state, 'HAVE_MASTER', 'the state at loadedplaylist correct'); |
79 | }); | 73 | }); |
80 | 74 | ||
81 | test('jumps to HAVE_METADATA when initialized with a media playlist', function() { | 75 | test('jumps to HAVE_METADATA when initialized with a media playlist', function() { |
... | @@ -172,101 +166,6 @@ | ... | @@ -172,101 +166,6 @@ |
172 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 166 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
173 | }); | 167 | }); |
174 | 168 | ||
175 | test('increments expired seconds after a segment is removed', function() { | ||
176 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
177 | requests.pop().respond(200, null, | ||
178 | '#EXTM3U\n' + | ||
179 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
180 | '#EXTINF:10,\n' + | ||
181 | '0.ts\n' + | ||
182 | '#EXTINF:10,\n' + | ||
183 | '1.ts\n' + | ||
184 | '#EXTINF:10,\n' + | ||
185 | '2.ts\n' + | ||
186 | '#EXTINF:10,\n' + | ||
187 | '3.ts\n'); | ||
188 | clock.tick(10 * 1000); // 10s, one target duration | ||
189 | requests.pop().respond(200, null, | ||
190 | '#EXTM3U\n' + | ||
191 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
192 | '#EXTINF:10,\n' + | ||
193 | '1.ts\n' + | ||
194 | '#EXTINF:10,\n' + | ||
195 | '2.ts\n' + | ||
196 | '#EXTINF:10,\n' + | ||
197 | '3.ts\n' + | ||
198 | '#EXTINF:10,\n' + | ||
199 | '4.ts\n'); | ||
200 | equal(loader.expired_, 10, 'expired one segment'); | ||
201 | }); | ||
202 | |||
203 | test('increments expired seconds after a discontinuity', function() { | ||
204 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
205 | requests.pop().respond(200, null, | ||
206 | '#EXTM3U\n' + | ||
207 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
208 | '#EXTINF:10,\n' + | ||
209 | '0.ts\n' + | ||
210 | '#EXTINF:3,\n' + | ||
211 | '1.ts\n' + | ||
212 | '#EXT-X-DISCONTINUITY\n' + | ||
213 | '#EXTINF:4,\n' + | ||
214 | '2.ts\n'); | ||
215 | clock.tick(10 * 1000); // 10s, one target duration | ||
216 | requests.pop().respond(200, null, | ||
217 | '#EXTM3U\n' + | ||
218 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
219 | '#EXTINF:3,\n' + | ||
220 | '1.ts\n' + | ||
221 | '#EXT-X-DISCONTINUITY\n' + | ||
222 | '#EXTINF:4,\n' + | ||
223 | '2.ts\n'); | ||
224 | equal(loader.expired_, 10, 'expired one segment'); | ||
225 | |||
226 | clock.tick(10 * 1000); // 10s, one target duration | ||
227 | requests.pop().respond(200, null, | ||
228 | '#EXTM3U\n' + | ||
229 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | ||
230 | '#EXT-X-DISCONTINUITY\n' + | ||
231 | '#EXTINF:4,\n' + | ||
232 | '2.ts\n'); | ||
233 | equal(loader.expired_, 13, 'no expirations after the discontinuity yet'); | ||
234 | |||
235 | clock.tick(10 * 1000); // 10s, one target duration | ||
236 | requests.pop().respond(200, null, | ||
237 | '#EXTM3U\n' + | ||
238 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
239 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + | ||
240 | '#EXTINF:10,\n' + | ||
241 | '3.ts\n'); | ||
242 | equal(loader.expired_, 13 + 4, 'tracked expired prior to the discontinuity'); | ||
243 | }); | ||
244 | |||
245 | test('tracks expired seconds properly when two discontinuities expire at once', function() { | ||
246 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
247 | requests.pop().respond(200, null, | ||
248 | '#EXTM3U\n' + | ||
249 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
250 | '#EXTINF:4,\n' + | ||
251 | '0.ts\n' + | ||
252 | '#EXT-X-DISCONTINUITY\n' + | ||
253 | '#EXTINF:5,\n' + | ||
254 | '1.ts\n' + | ||
255 | '#EXT-X-DISCONTINUITY\n' + | ||
256 | '#EXTINF:6,\n' + | ||
257 | '2.ts\n' + | ||
258 | '#EXTINF:7,\n' + | ||
259 | '3.ts\n'); | ||
260 | clock.tick(10 * 1000); | ||
261 | requests.pop().respond(200, null, | ||
262 | '#EXTM3U\n' + | ||
263 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
264 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + | ||
265 | '#EXTINF:7,\n' + | ||
266 | '3.ts\n'); | ||
267 | equal(loader.expired_, 4 + 5 + 6, 'tracked both expired discontinuities'); | ||
268 | }); | ||
269 | |||
270 | test('emits an error when an initial playlist request fails', function() { | 169 | test('emits an error when an initial playlist request fails', function() { |
271 | var | 170 | var |
272 | errors = [], | 171 | errors = [], |
... | @@ -453,6 +352,20 @@ | ... | @@ -453,6 +352,20 @@ |
453 | 'updated the active media'); | 352 | 'updated the active media'); |
454 | }); | 353 | }); |
455 | 354 | ||
355 | test('can switch playlists immediately after the master is downloaded', function() { | ||
356 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | ||
357 | loader.on('loadedplaylist', function() { | ||
358 | loader.media('high.m3u8'); | ||
359 | }); | ||
360 | requests.pop().respond(200, null, | ||
361 | '#EXTM3U\n' + | ||
362 | '#EXT-X-STREAM-INF:BANDWIDTH=1\n' + | ||
363 | 'low.m3u8\n' + | ||
364 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | ||
365 | 'high.m3u8\n'); | ||
366 | equal(requests[0].url, urlTo('high.m3u8'), 'switched variants immediately'); | ||
367 | }); | ||
368 | |||
456 | test('can switch media playlists based on URI', function() { | 369 | test('can switch media playlists based on URI', function() { |
457 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); | 370 | var loader = new videojs.Hls.PlaylistLoader('master.m3u8'); |
458 | requests.pop().respond(200, null, | 371 | requests.pop().respond(200, null, |
... | @@ -624,9 +537,6 @@ | ... | @@ -624,9 +537,6 @@ |
624 | 'low.m3u8\n' + | 537 | 'low.m3u8\n' + |
625 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + | 538 | '#EXT-X-STREAM-INF:BANDWIDTH=2\n' + |
626 | 'high.m3u8\n'); | 539 | 'high.m3u8\n'); |
627 | throws(function() { | ||
628 | loader.media('high.m3u8'); | ||
629 | }, 'throws an error from HAVE_MASTER'); | ||
630 | }); | 540 | }); |
631 | 541 | ||
632 | test('throws an error if a switch to an unrecognized playlist is requested', function() { | 542 | test('throws an error if a switch to an unrecognized playlist is requested', function() { |
... | @@ -743,8 +653,8 @@ | ... | @@ -743,8 +653,8 @@ |
743 | equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); | 653 | equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); |
744 | equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); | 654 | equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); |
745 | equal(loader.getMediaIndexForTime_(22), | 655 | equal(loader.getMediaIndexForTime_(22), |
746 | 2, | 656 | 3, |
747 | 'the index is never greater than the length'); | 657 | 'time greater than the length is index 3'); |
748 | }); | 658 | }); |
749 | 659 | ||
750 | test('returns the lower index when calculating for a segment boundary', function() { | 660 | test('returns the lower index when calculating for a segment boundary', function() { |
... | @@ -757,10 +667,8 @@ | ... | @@ -757,10 +667,8 @@ |
757 | '#EXTINF:5,\n' + | 667 | '#EXTINF:5,\n' + |
758 | '1.ts\n' + | 668 | '1.ts\n' + |
759 | '#EXT-X-ENDLIST\n'); | 669 | '#EXT-X-ENDLIST\n'); |
760 | equal(loader.getMediaIndexForTime_(4), 0, 'rounds down exact matches'); | 670 | equal(loader.getMediaIndexForTime_(4), 1, 'rounds up exact matches'); |
761 | equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); | 671 | equal(loader.getMediaIndexForTime_(3.7), 0, 'rounds down'); |
762 | // FIXME: the test below should pass for HLSv3 | ||
763 | //equal(loader.getMediaIndexForTime_(4.2), 0, 'rounds down'); | ||
764 | equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); | 672 | equal(loader.getMediaIndexForTime_(4.5), 1, 'rounds up at 0.5'); |
765 | }); | 673 | }); |
766 | 674 | ||
... | @@ -773,7 +681,7 @@ | ... | @@ -773,7 +681,7 @@ |
773 | '1001.ts\n' + | 681 | '1001.ts\n' + |
774 | '#EXTINF:5,\n' + | 682 | '#EXTINF:5,\n' + |
775 | '1002.ts\n'); | 683 | '1002.ts\n'); |
776 | loader.expired_ = 150; | 684 | loader.media().segments[0].start = 150; |
777 | 685 | ||
778 | equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); | 686 | equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); |
779 | equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); | 687 | equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); |
... | @@ -785,30 +693,6 @@ | ... | @@ -785,30 +693,6 @@ |
785 | equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); | 693 | equal(loader.getMediaIndexForTime_(50 + 100 + 6), 1, 'calculates within the second segment'); |
786 | }); | 694 | }); |
787 | 695 | ||
788 | test('updating the timeline offset adjusts results from getMediaIndexForTime_', function() { | ||
789 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
790 | requests.pop().respond(200, null, | ||
791 | '#EXTM3U\n' + | ||
792 | '#EXT-X-MEDIA-SEQUENCE:23\n' + | ||
793 | '#EXTINF:4,\n' + | ||
794 | '23.ts\n' + | ||
795 | '#EXTINF:5,\n' + | ||
796 | '24.ts\n' + | ||
797 | '#EXTINF:6,\n' + | ||
798 | '25.ts\n' + | ||
799 | '#EXTINF:7,\n' + | ||
800 | '26.ts\n'); | ||
801 | loader.updateTimelineOffset(0, 150); | ||
802 | equal(loader.getMediaIndexForTime_(150), 0, 'translated the first segment'); | ||
803 | equal(loader.getMediaIndexForTime_(130), 0, 'clamps the index to zero'); | ||
804 | equal(loader.getMediaIndexForTime_(155), 1, 'translated the second segment'); | ||
805 | |||
806 | loader.updateTimelineOffset(2, 30); | ||
807 | equal(loader.getMediaIndexForTime_(30 - 5 - 1), 0, 'translated the first segment'); | ||
808 | equal(loader.getMediaIndexForTime_(30 + 7), 3, 'translated the last segment'); | ||
809 | equal(loader.getMediaIndexForTime_(30 - 3), 1, 'translated an earlier segment'); | ||
810 | }); | ||
811 | |||
812 | test('does not misintrepret playlists missing newlines at the end', function() { | 696 | test('does not misintrepret playlists missing newlines at the end', function() { |
813 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | 697 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); |
814 | requests.shift().respond(200, null, | 698 | requests.shift().respond(200, null, | ... | ... |
... | @@ -18,27 +18,6 @@ | ... | @@ -18,27 +18,6 @@ |
18 | 18 | ||
19 | module('Playlist Interval Duration'); | 19 | module('Playlist Interval Duration'); |
20 | 20 | ||
21 | test('accounts expired duration for live playlists', function() { | ||
22 | var duration = Playlist.duration({ | ||
23 | mediaSequence: 10, | ||
24 | segments: [{ | ||
25 | duration: 10, | ||
26 | uri: '10.ts' | ||
27 | }, { | ||
28 | duration: 10, | ||
29 | uri: '11.ts' | ||
30 | }, { | ||
31 | duration: 10, | ||
32 | uri: '12.ts' | ||
33 | }, { | ||
34 | duration: 10, | ||
35 | uri: '13.ts' | ||
36 | }] | ||
37 | }, 0, 14); | ||
38 | |||
39 | equal(duration, 14 * 10, 'duration includes dropped segments'); | ||
40 | }); | ||
41 | |||
42 | test('accounts for non-zero starting VOD media sequences', function() { | 21 | test('accounts for non-zero starting VOD media sequences', function() { |
43 | var duration = Playlist.duration({ | 22 | var duration = Playlist.duration({ |
44 | mediaSequence: 10, | 23 | mediaSequence: 10, |
... | @@ -61,47 +40,37 @@ | ... | @@ -61,47 +40,37 @@ |
61 | equal(duration, 4 * 10, 'includes only listed segments'); | 40 | equal(duration, 4 * 10, 'includes only listed segments'); |
62 | }); | 41 | }); |
63 | 42 | ||
64 | test('uses PTS values when available', function() { | 43 | test('uses timeline values when available', function() { |
65 | var duration = Playlist.duration({ | 44 | var duration = Playlist.duration({ |
66 | mediaSequence: 0, | 45 | mediaSequence: 0, |
67 | endList: true, | 46 | endList: true, |
68 | segments: [{ | 47 | segments: [{ |
69 | minVideoPts: 1, | 48 | start: 0, |
70 | minAudioPts: 2, | ||
71 | uri: '0.ts' | 49 | uri: '0.ts' |
72 | }, { | 50 | }, { |
73 | duration: 10, | 51 | duration: 10, |
74 | maxVideoPts: 2 * 10 * 1000 + 1, | 52 | end: 2 * 10 + 2, |
75 | maxAudioPts: 2 * 10 * 1000 + 2, | ||
76 | uri: '1.ts' | 53 | uri: '1.ts' |
77 | }, { | 54 | }, { |
78 | duration: 10, | 55 | duration: 10, |
79 | maxVideoPts: 3 * 10 * 1000 + 1, | 56 | end: 3 * 10 + 2, |
80 | maxAudioPts: 3 * 10 * 1000 + 2, | ||
81 | uri: '2.ts' | 57 | uri: '2.ts' |
82 | }, { | 58 | }, { |
83 | duration: 10, | 59 | duration: 10, |
84 | maxVideoPts: 4 * 10 * 1000 + 1, | 60 | end: 4 * 10 + 2, |
85 | maxAudioPts: 4 * 10 * 1000 + 2, | ||
86 | uri: '3.ts' | 61 | uri: '3.ts' |
87 | }] | 62 | }] |
88 | }, 0, 4); | 63 | }, 4); |
89 | 64 | ||
90 | equal(duration, ((4 * 10 * 1000 + 2) - 1) * 0.001, 'used PTS values'); | 65 | equal(duration, 4 * 10 + 2, 'used timeline values'); |
91 | }); | 66 | }); |
92 | 67 | ||
93 | test('works when partial PTS information is available', function() { | 68 | test('works when partial timeline information is available', function() { |
94 | var duration = Playlist.duration({ | 69 | var duration = Playlist.duration({ |
95 | mediaSequence: 0, | 70 | mediaSequence: 0, |
96 | endList: true, | 71 | endList: true, |
97 | segments: [{ | 72 | segments: [{ |
98 | minVideoPts: 1, | 73 | start: 0, |
99 | minAudioPts: 2, | ||
100 | maxVideoPts: 10 * 1000 + 1, | ||
101 | |||
102 | // intentionally less duration than video | ||
103 | // the max stream duration should be used | ||
104 | maxAudioPts: 10 * 1000 + 1, | ||
105 | uri: '0.ts' | 74 | uri: '0.ts' |
106 | }, { | 75 | }, { |
107 | duration: 9, | 76 | duration: 9, |
... | @@ -111,67 +80,17 @@ | ... | @@ -111,67 +80,17 @@ |
111 | uri: '2.ts' | 80 | uri: '2.ts' |
112 | }, { | 81 | }, { |
113 | duration: 10, | 82 | duration: 10, |
114 | minVideoPts: 30 * 1000 + 7, | 83 | start: 30.007, |
115 | minAudioPts: 30 * 1000 + 10, | 84 | end: 40.002, |
116 | maxVideoPts: 40 * 1000 + 1, | ||
117 | maxAudioPts: 40 * 1000 + 2, | ||
118 | uri: '3.ts' | 85 | uri: '3.ts' |
119 | }, { | 86 | }, { |
120 | duration: 10, | 87 | duration: 10, |
121 | maxVideoPts: 50 * 1000 + 1, | 88 | end: 50.0002, |
122 | maxAudioPts: 50 * 1000 + 2, | ||
123 | uri: '4.ts' | 89 | uri: '4.ts' |
124 | }] | 90 | }] |
125 | }, 0, 5); | 91 | }, 5); |
126 | 92 | ||
127 | equal(duration, | 93 | equal(duration, 50.0002, 'calculated with mixed intervals'); |
128 | ((50 * 1000 + 2) - 1) * 0.001, | ||
129 | 'calculated with mixed intervals'); | ||
130 | }); | ||
131 | |||
132 | test('ignores segments before the start', function() { | ||
133 | var duration = Playlist.duration({ | ||
134 | mediaSequence: 0, | ||
135 | segments: [{ | ||
136 | duration: 10, | ||
137 | uri: '0.ts' | ||
138 | }, { | ||
139 | duration: 10, | ||
140 | uri: '1.ts' | ||
141 | }, { | ||
142 | duration: 10, | ||
143 | uri: '2.ts' | ||
144 | }] | ||
145 | }, 1, 3); | ||
146 | |||
147 | equal(duration, 10 + 10, 'ignored the first segment'); | ||
148 | }); | ||
149 | |||
150 | test('ignores discontinuity sequences earlier than the start', function() { | ||
151 | var duration = Playlist.duration({ | ||
152 | mediaSequence: 0, | ||
153 | discontinuityStarts: [1, 3], | ||
154 | segments: [{ | ||
155 | minVideoPts: 0, | ||
156 | minAudioPts: 0, | ||
157 | maxVideoPts: 10 * 1000, | ||
158 | maxAudioPts: 10 * 1000, | ||
159 | uri: '0.ts' | ||
160 | }, { | ||
161 | discontinuity: true, | ||
162 | duration: 9, | ||
163 | uri: '1.ts' | ||
164 | }, { | ||
165 | duration: 10, | ||
166 | uri: '2.ts' | ||
167 | }, { | ||
168 | discontinuity: true, | ||
169 | duration: 10, | ||
170 | uri: '3.ts' | ||
171 | }] | ||
172 | }, 2, 4); | ||
173 | |||
174 | equal(duration, 10 + 10, 'excluded the earlier segments'); | ||
175 | }); | 94 | }); |
176 | 95 | ||
177 | test('ignores discontinuity sequences later than the end', function() { | 96 | test('ignores discontinuity sequences later than the end', function() { |
... | @@ -196,20 +115,19 @@ | ... | @@ -196,20 +115,19 @@ |
196 | duration: 10, | 115 | duration: 10, |
197 | uri: '3.ts' | 116 | uri: '3.ts' |
198 | }] | 117 | }] |
199 | }, 0, 2); | 118 | }, 2); |
200 | 119 | ||
201 | equal(duration, 19, 'excluded the later segments'); | 120 | equal(duration, 19, 'excluded the later segments'); |
202 | }); | 121 | }); |
203 | 122 | ||
204 | test('handles trailing segments without PTS information', function() { | 123 | test('handles trailing segments without timeline information', function() { |
205 | var duration = Playlist.duration({ | 124 | var playlist, duration; |
125 | playlist = { | ||
206 | mediaSequence: 0, | 126 | mediaSequence: 0, |
207 | endList: true, | 127 | endList: true, |
208 | segments: [{ | 128 | segments: [{ |
209 | minVideoPts: 0, | 129 | start: 0, |
210 | minAudioPts: 0, | 130 | end: 10.5, |
211 | maxVideoPts: 10 * 1000, | ||
212 | maxAudioPts: 10 * 1000, | ||
213 | uri: '0.ts' | 131 | uri: '0.ts' |
214 | }, { | 132 | }, { |
215 | duration: 9, | 133 | duration: 9, |
... | @@ -218,107 +136,43 @@ | ... | @@ -218,107 +136,43 @@ |
218 | duration: 10, | 136 | duration: 10, |
219 | uri: '2.ts' | 137 | uri: '2.ts' |
220 | }, { | 138 | }, { |
221 | minVideoPts: 29.5 * 1000, | 139 | start: 29.45, |
222 | minAudioPts: 29.5 * 1000, | 140 | end: 39.5, |
223 | maxVideoPts: 39.5 * 1000, | ||
224 | maxAudioPts: 39.5 * 1000, | ||
225 | uri: '3.ts' | 141 | uri: '3.ts' |
226 | }] | 142 | }] |
227 | }, 0, 3); | 143 | }; |
144 | |||
145 | duration = Playlist.duration(playlist, 3); | ||
146 | equal(duration, 29.45, 'calculated duration'); | ||
228 | 147 | ||
229 | equal(duration, 29.5, 'calculated duration'); | 148 | duration = Playlist.duration(playlist, 2); |
149 | equal(duration, 19.5, 'calculated duration'); | ||
230 | }); | 150 | }); |
231 | 151 | ||
232 | test('uses PTS intervals when the start and end segment have them', function() { | 152 | test('uses timeline intervals when segments have them', function() { |
233 | var playlist, duration; | 153 | var playlist, duration; |
234 | playlist = { | 154 | playlist = { |
235 | mediaSequence: 0, | 155 | mediaSequence: 0, |
236 | segments: [{ | 156 | segments: [{ |
237 | minVideoPts: 0, | 157 | start: 0, |
238 | minAudioPts: 0, | 158 | end: 10, |
239 | maxVideoPts: 10 * 1000, | ||
240 | maxAudioPts: 10 * 1000, | ||
241 | uri: '0.ts' | 159 | uri: '0.ts' |
242 | }, { | 160 | }, { |
243 | duration: 9, | 161 | duration: 9, |
244 | uri: '1.ts' | 162 | uri: '1.ts' |
245 | },{ | 163 | },{ |
246 | minVideoPts: 20 * 1000 + 100, | 164 | start: 20.1, |
247 | minAudioPts: 20 * 1000 + 100, | 165 | end: 30.1, |
248 | maxVideoPts: 30 * 1000 + 100, | ||
249 | maxAudioPts: 30 * 1000 + 100, | ||
250 | duration: 10, | 166 | duration: 10, |
251 | uri: '2.ts' | 167 | uri: '2.ts' |
252 | }] | 168 | }] |
253 | }; | 169 | }; |
254 | duration = Playlist.duration(playlist, 0, 2); | 170 | duration = Playlist.duration(playlist, 2); |
255 | 171 | ||
256 | equal(duration, 20.1, 'used the PTS-based interval'); | 172 | equal(duration, 20.1, 'used the timeline-based interval'); |
257 | 173 | ||
258 | duration = Playlist.duration(playlist, 0, 3); | 174 | duration = Playlist.duration(playlist, 3); |
259 | equal(duration, 30.1, 'used the PTS-based interval'); | 175 | equal(duration, 30.1, 'used the timeline-based interval'); |
260 | }); | ||
261 | |||
262 | test('works for media without audio', function() { | ||
263 | equal(Playlist.duration({ | ||
264 | mediaSequence: 0, | ||
265 | endList: true, | ||
266 | segments: [{ | ||
267 | minVideoPts: 0, | ||
268 | maxVideoPts: 9 * 1000, | ||
269 | uri: 'no-audio.ts' | ||
270 | }] | ||
271 | }), 9, 'used video PTS values'); | ||
272 | }); | ||
273 | |||
274 | test('works for media without video', function() { | ||
275 | equal(Playlist.duration({ | ||
276 | mediaSequence: 0, | ||
277 | endList: true, | ||
278 | segments: [{ | ||
279 | minAudioPts: 0, | ||
280 | maxAudioPts: 9 * 1000, | ||
281 | uri: 'no-video.ts' | ||
282 | }] | ||
283 | }), 9, 'used video PTS values'); | ||
284 | }); | ||
285 | |||
286 | test('uses the largest continuous available PTS ranges', function() { | ||
287 | var playlist = { | ||
288 | mediaSequence: 0, | ||
289 | segments: [{ | ||
290 | minVideoPts: 0, | ||
291 | minAudioPts: 0, | ||
292 | maxVideoPts: 10 * 1000, | ||
293 | maxAudioPts: 10 * 1000, | ||
294 | uri: '0.ts' | ||
295 | }, { | ||
296 | duration: 10, | ||
297 | uri: '1.ts' | ||
298 | }, { | ||
299 | // starts 0.5s earlier than the previous segment indicates | ||
300 | minVideoPts: 19.5 * 1000, | ||
301 | minAudioPts: 19.5 * 1000, | ||
302 | maxVideoPts: 29.5 * 1000, | ||
303 | maxAudioPts: 29.5 * 1000, | ||
304 | uri: '2.ts' | ||
305 | }, { | ||
306 | duration: 10, | ||
307 | uri: '3.ts' | ||
308 | }, { | ||
309 | // ... but by the last segment, there is actual 0.5s more | ||
310 | // content than duration indicates | ||
311 | minVideoPts: 40.5 * 1000, | ||
312 | minAudioPts: 40.5 * 1000, | ||
313 | maxVideoPts: 50.5 * 1000, | ||
314 | maxAudioPts: 50.5 * 1000, | ||
315 | uri: '4.ts' | ||
316 | }] | ||
317 | }; | ||
318 | |||
319 | equal(Playlist.duration(playlist, 0, 5), | ||
320 | 50.5, | ||
321 | 'calculated across the larger PTS interval'); | ||
322 | }); | 176 | }); |
323 | 177 | ||
324 | test('counts the time between segments as part of the earlier segment\'s duration', function() { | 178 | test('counts the time between segments as part of the earlier segment\'s duration', function() { |
... | @@ -326,22 +180,18 @@ | ... | @@ -326,22 +180,18 @@ |
326 | mediaSequence: 0, | 180 | mediaSequence: 0, |
327 | endList: true, | 181 | endList: true, |
328 | segments: [{ | 182 | segments: [{ |
329 | minVideoPts: 0, | 183 | start: 0, |
330 | minAudioPts: 0, | 184 | end: 10, |
331 | maxVideoPts: 1 * 10 * 1000, | ||
332 | maxAudioPts: 1 * 10 * 1000, | ||
333 | uri: '0.ts' | 185 | uri: '0.ts' |
334 | }, { | 186 | }, { |
335 | minVideoPts: 1 * 10 * 1000 + 100, | 187 | start: 10.1, |
336 | minAudioPts: 1 * 10 * 1000 + 100, | 188 | end: 20.1, |
337 | maxVideoPts: 2 * 10 * 1000 + 100, | ||
338 | maxAudioPts: 2 * 10 * 1000 + 100, | ||
339 | duration: 10, | 189 | duration: 10, |
340 | uri: '1.ts' | 190 | uri: '1.ts' |
341 | }] | 191 | }] |
342 | }, 0, 1); | 192 | }, 1); |
343 | 193 | ||
344 | equal(duration, (1 * 10 * 1000 + 100) * 0.001, 'included the segment gap'); | 194 | equal(duration, 10.1, 'included the segment gap'); |
345 | }); | 195 | }); |
346 | 196 | ||
347 | test('accounts for discontinuities', function() { | 197 | test('accounts for discontinuities', function() { |
... | @@ -364,7 +214,7 @@ | ... | @@ -364,7 +214,7 @@ |
364 | duration: 10, | 214 | duration: 10, |
365 | uri: '1.ts' | 215 | uri: '1.ts' |
366 | }] | 216 | }] |
367 | }, 0, 2); | 217 | }, 2); |
368 | 218 | ||
369 | equal(duration, 10 + 10, 'handles discontinuities'); | 219 | equal(duration, 10 + 10, 'handles discontinuities'); |
370 | }); | 220 | }); |
... | @@ -389,7 +239,7 @@ | ... | @@ -389,7 +239,7 @@ |
389 | duration: 10, | 239 | duration: 10, |
390 | uri: '1.ts' | 240 | uri: '1.ts' |
391 | }] | 241 | }] |
392 | }, 0, 1); | 242 | }, 1); |
393 | 243 | ||
394 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); | 244 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); |
395 | }); | 245 | }); |
... | @@ -412,7 +262,7 @@ | ... | @@ -412,7 +262,7 @@ |
412 | duration: 10, | 262 | duration: 10, |
413 | uri: '1.ts' | 263 | uri: '1.ts' |
414 | }] | 264 | }] |
415 | }, 0, 1, false); | 265 | }, 1, false); |
416 | 266 | ||
417 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); | 267 | equal(duration, (1 * 10 * 1000) * 0.001, 'did not include the segment gap'); |
418 | }); | 268 | }); |
... | @@ -431,10 +281,9 @@ | ... | @@ -431,10 +281,9 @@ |
431 | }] | 281 | }] |
432 | }; | 282 | }; |
433 | 283 | ||
434 | equal(Playlist.duration(playlist, 0, 0), 0, 'zero-length duration is zero'); | 284 | equal(Playlist.duration(playlist, 0), 0, 'zero-length duration is zero'); |
435 | equal(Playlist.duration(playlist, 0, 0, false), 0, 'zero-length duration is zero'); | 285 | equal(Playlist.duration(playlist, 0, false), 0, 'zero-length duration is zero'); |
436 | equal(Playlist.duration(playlist, 0, -1), 0, 'negative length duration is zero'); | 286 | equal(Playlist.duration(playlist, -1), 0, 'negative length duration is zero'); |
437 | equal(Playlist.duration(playlist, 2, 1, false), 0, 'negative length duration is zero'); | ||
438 | }); | 287 | }); |
439 | 288 | ||
440 | module('Playlist Seekable'); | 289 | module('Playlist Seekable'); | ... | ... |
... | @@ -167,60 +167,6 @@ var | ... | @@ -167,60 +167,6 @@ var |
167 | window.manifests[manifestName]); | 167 | window.manifests[manifestName]); |
168 | }, | 168 | }, |
169 | 169 | ||
170 | mockSegmentParser = function(tags) { | ||
171 | var MockSegmentParser; | ||
172 | |||
173 | if (tags === undefined) { | ||
174 | tags = [{ pts: 0, bytes: new Uint8Array(1) }]; | ||
175 | } | ||
176 | MockSegmentParser = function() { | ||
177 | this.getFlvHeader = function() { | ||
178 | return 'flv'; | ||
179 | }; | ||
180 | this.parseSegmentBinaryData = function() {}; | ||
181 | this.flushTags = function() {}; | ||
182 | this.tagsAvailable = function() { | ||
183 | return tags.length; | ||
184 | }; | ||
185 | this.getTags = function() { | ||
186 | return tags; | ||
187 | }; | ||
188 | this.getNextTag = function() { | ||
189 | return tags.shift(); | ||
190 | }; | ||
191 | this.metadataStream = new videojs.Hls.Stream(); | ||
192 | this.metadataStream.init(); | ||
193 | this.metadataStream.descriptor = new Uint8Array([ | ||
194 | 1, 2, 3, 0xbb | ||
195 | ]); | ||
196 | |||
197 | this.stats = { | ||
198 | h264Tags: function() { | ||
199 | return tags.length; | ||
200 | }, | ||
201 | minVideoPts: function() { | ||
202 | return tags[0].pts; | ||
203 | }, | ||
204 | maxVideoPts: function() { | ||
205 | return tags[tags.length - 1].pts; | ||
206 | }, | ||
207 | aacTags: function() { | ||
208 | return tags.length; | ||
209 | }, | ||
210 | minAudioPts: function() { | ||
211 | return tags[0].pts; | ||
212 | }, | ||
213 | maxAudioPts: function() { | ||
214 | return tags[tags.length - 1].pts; | ||
215 | }, | ||
216 | }; | ||
217 | }; | ||
218 | |||
219 | MockSegmentParser.STREAM_TYPES = videojs.Hls.SegmentParser.STREAM_TYPES; | ||
220 | |||
221 | return MockSegmentParser; | ||
222 | }, | ||
223 | |||
224 | // a no-op MediaSource implementation to allow synchronous testing | 170 | // a no-op MediaSource implementation to allow synchronous testing |
225 | MockMediaSource = videojs.extend(videojs.EventTarget, { | 171 | MockMediaSource = videojs.extend(videojs.EventTarget, { |
226 | constructor: function() {}, | 172 | constructor: function() {}, |
... | @@ -524,7 +470,7 @@ test('sets the duration if one is available on the playlist', function() { | ... | @@ -524,7 +470,7 @@ test('sets the duration if one is available on the playlist', function() { |
524 | equal(events, 1, 'durationchange is fired'); | 470 | equal(events, 1, 'durationchange is fired'); |
525 | }); | 471 | }); |
526 | 472 | ||
527 | QUnit.skip('calculates the duration if needed', function() { | 473 | test('estimates individual segment durations if needed', function() { |
528 | var changes = 0; | 474 | var changes = 0; |
529 | player.src({ | 475 | player.src({ |
530 | src: 'http://example.com/manifest/missingExtinf.m3u8', | 476 | src: 'http://example.com/manifest/missingExtinf.m3u8', |
... | @@ -532,7 +478,7 @@ QUnit.skip('calculates the duration if needed', function() { | ... | @@ -532,7 +478,7 @@ QUnit.skip('calculates the duration if needed', function() { |
532 | }); | 478 | }); |
533 | openMediaSource(player); | 479 | openMediaSource(player); |
534 | player.tech_.hls.mediaSource.duration = NaN; | 480 | player.tech_.hls.mediaSource.duration = NaN; |
535 | player.on('durationchange', function() { | 481 | player.tech_.on('durationchange', function() { |
536 | changes++; | 482 | changes++; |
537 | }); | 483 | }); |
538 | 484 | ||
... | @@ -686,73 +632,59 @@ test('downloads media playlists after loading the master', function() { | ... | @@ -686,73 +632,59 @@ test('downloads media playlists after loading the master', function() { |
686 | }); | 632 | }); |
687 | openMediaSource(player); | 633 | openMediaSource(player); |
688 | 634 | ||
689 | // set bandwidth to an appropriate number so we don't switch | 635 | player.tech_.hls.bandwidth = 20e10; |
690 | player.tech_.hls.bandwidth = 200000; | ||
691 | standardXHRResponse(requests[0]); | 636 | standardXHRResponse(requests[0]); |
692 | standardXHRResponse(requests[1]); | 637 | standardXHRResponse(requests[1]); |
693 | standardXHRResponse(requests[2]); | 638 | standardXHRResponse(requests[2]); |
694 | 639 | ||
695 | strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); | 640 | strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); |
696 | strictEqual(requests[1].url, | 641 | strictEqual(requests[1].url, |
697 | absoluteUrl('manifest/media.m3u8'), | 642 | absoluteUrl('manifest/media3.m3u8'), |
698 | 'media playlist requested'); | 643 | 'media playlist requested'); |
699 | strictEqual(requests[2].url, | 644 | strictEqual(requests[2].url, |
700 | absoluteUrl('manifest/media-00001.ts'), | 645 | absoluteUrl('manifest/media3-00001.ts'), |
701 | 'first segment requested'); | 646 | 'first segment requested'); |
702 | }); | 647 | }); |
703 | 648 | ||
704 | test('upshift if initial bandwidth is high', function() { | 649 | test('upshifts if the initial bandwidth hint is high', function() { |
705 | player.src({ | 650 | player.src({ |
706 | src: 'manifest/master.m3u8', | 651 | src: 'manifest/master.m3u8', |
707 | type: 'application/vnd.apple.mpegurl' | 652 | type: 'application/vnd.apple.mpegurl' |
708 | }); | 653 | }); |
709 | openMediaSource(player); | 654 | openMediaSource(player); |
710 | 655 | ||
656 | player.tech_.hls.bandwidth = 10e20; | ||
711 | standardXHRResponse(requests[0]); | 657 | standardXHRResponse(requests[0]); |
712 | |||
713 | player.tech_.hls.playlists.setBandwidth = function() { | ||
714 | player.tech_.hls.playlists.bandwidth = 1000000000; | ||
715 | }; | ||
716 | |||
717 | standardXHRResponse(requests[1]); | 658 | standardXHRResponse(requests[1]); |
718 | standardXHRResponse(requests[2]); | 659 | standardXHRResponse(requests[2]); |
719 | 660 | ||
720 | standardXHRResponse(requests[3]); | ||
721 | |||
722 | strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); | 661 | strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); |
723 | strictEqual(requests[1].url, | 662 | strictEqual(requests[1].url, |
724 | absoluteUrl('manifest/media.m3u8'), | ||
725 | 'media playlist requested'); | ||
726 | strictEqual(requests[2].url, | ||
727 | absoluteUrl('manifest/media3.m3u8'), | 663 | absoluteUrl('manifest/media3.m3u8'), |
728 | 'media playlist requested'); | 664 | 'media playlist requested'); |
729 | strictEqual(requests[3].url, | 665 | strictEqual(requests[2].url, |
730 | absoluteUrl('manifest/media3-00001.ts'), | 666 | absoluteUrl('manifest/media3-00001.ts'), |
731 | 'first segment requested'); | 667 | 'first segment requested'); |
732 | }); | 668 | }); |
733 | 669 | ||
734 | test('dont downshift if bandwidth is low', function() { | 670 | test('downshifts if the initial bandwidth hint is low', function() { |
735 | player.src({ | 671 | player.src({ |
736 | src: 'manifest/master.m3u8', | 672 | src: 'manifest/master.m3u8', |
737 | type: 'application/vnd.apple.mpegurl' | 673 | type: 'application/vnd.apple.mpegurl' |
738 | }); | 674 | }); |
739 | openMediaSource(player); | 675 | openMediaSource(player); |
740 | 676 | ||
677 | player.tech_.hls.bandwidth = 100; | ||
741 | standardXHRResponse(requests[0]); | 678 | standardXHRResponse(requests[0]); |
742 | |||
743 | player.tech_.hls.playlists.setBandwidth = function() { | ||
744 | player.tech_.hls.playlists.bandwidth = 100; | ||
745 | }; | ||
746 | |||
747 | standardXHRResponse(requests[1]); | 679 | standardXHRResponse(requests[1]); |
748 | standardXHRResponse(requests[2]); | 680 | standardXHRResponse(requests[2]); |
749 | 681 | ||
750 | strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); | 682 | strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); |
751 | strictEqual(requests[1].url, | 683 | strictEqual(requests[1].url, |
752 | absoluteUrl('manifest/media.m3u8'), | 684 | absoluteUrl('manifest/media1.m3u8'), |
753 | 'media playlist requested'); | 685 | 'media playlist requested'); |
754 | strictEqual(requests[2].url, | 686 | strictEqual(requests[2].url, |
755 | absoluteUrl('manifest/media-00001.ts'), | 687 | absoluteUrl('manifest/media1-00001.ts'), |
756 | 'first segment requested'); | 688 | 'first segment requested'); |
757 | }); | 689 | }); |
758 | 690 | ||
... | @@ -822,7 +754,7 @@ test('buffer checks are noops when only the master is ready', function() { | ... | @@ -822,7 +754,7 @@ test('buffer checks are noops when only the master is ready', function() { |
822 | 754 | ||
823 | strictEqual(1, requests.length, 'one request was made'); | 755 | strictEqual(1, requests.length, 'one request was made'); |
824 | strictEqual(requests[0].url, | 756 | strictEqual(requests[0].url, |
825 | absoluteUrl('manifest/media.m3u8'), | 757 | absoluteUrl('manifest/media1.m3u8'), |
826 | 'media playlist requested'); | 758 | 'media playlist requested'); |
827 | }); | 759 | }); |
828 | 760 | ||
... | @@ -876,11 +808,9 @@ test('selects a playlist after segment downloads', function() { | ... | @@ -876,11 +808,9 @@ test('selects a playlist after segment downloads', function() { |
876 | return player.tech_.hls.playlists.master.playlists[0]; | 808 | return player.tech_.hls.playlists.master.playlists[0]; |
877 | }; | 809 | }; |
878 | 810 | ||
879 | standardXHRResponse(requests[0]); | 811 | standardXHRResponse(requests[0]); // master |
880 | 812 | standardXHRResponse(requests[1]); // media | |
881 | player.tech_.hls.bandwidth = 3000000; | 813 | standardXHRResponse(requests[2]); // segment |
882 | standardXHRResponse(requests[1]); | ||
883 | standardXHRResponse(requests[2]); | ||
884 | 814 | ||
885 | strictEqual(calls, 2, 'selects after the initial segment'); | 815 | strictEqual(calls, 2, 'selects after the initial segment'); |
886 | player.currentTime = function() { | 816 | player.currentTime = function() { |
... | @@ -889,6 +819,7 @@ test('selects a playlist after segment downloads', function() { | ... | @@ -889,6 +819,7 @@ test('selects a playlist after segment downloads', function() { |
889 | player.buffered = function() { | 819 | player.buffered = function() { |
890 | return videojs.createTimeRange(0, 2); | 820 | return videojs.createTimeRange(0, 2); |
891 | }; | 821 | }; |
822 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
892 | player.tech_.hls.checkBuffer_(); | 823 | player.tech_.hls.checkBuffer_(); |
893 | 824 | ||
894 | standardXHRResponse(requests[3]); | 825 | standardXHRResponse(requests[3]); |
... | @@ -896,9 +827,7 @@ test('selects a playlist after segment downloads', function() { | ... | @@ -896,9 +827,7 @@ test('selects a playlist after segment downloads', function() { |
896 | strictEqual(calls, 3, 'selects after additional segments'); | 827 | strictEqual(calls, 3, 'selects after additional segments'); |
897 | }); | 828 | }); |
898 | 829 | ||
899 | test('moves to the next segment if there is a network error', function() { | 830 | test('reports an error if a segment is unreachable', function() { |
900 | var mediaIndex; | ||
901 | |||
902 | player.src({ | 831 | player.src({ |
903 | src: 'manifest/master.m3u8', | 832 | src: 'manifest/master.m3u8', |
904 | type: 'application/vnd.apple.mpegurl' | 833 | type: 'application/vnd.apple.mpegurl' |
... | @@ -906,65 +835,11 @@ test('moves to the next segment if there is a network error', function() { | ... | @@ -906,65 +835,11 @@ test('moves to the next segment if there is a network error', function() { |
906 | openMediaSource(player); | 835 | openMediaSource(player); |
907 | 836 | ||
908 | player.tech_.hls.bandwidth = 20000; | 837 | player.tech_.hls.bandwidth = 20000; |
909 | standardXHRResponse(requests[0]); | 838 | standardXHRResponse(requests[0]); // master |
910 | standardXHRResponse(requests[1]); | 839 | standardXHRResponse(requests[1]); // media |
911 | 840 | ||
912 | mediaIndex = player.tech_.hls.mediaIndex; | 841 | requests[2].respond(400); // segment |
913 | player.trigger('timeupdate'); | 842 | strictEqual(player.tech_.hls.mediaSource.error_, 'network', 'network error is triggered'); |
914 | |||
915 | requests[2].respond(400); | ||
916 | strictEqual(mediaIndex + 1, player.tech_.hls.mediaIndex, 'media index is incremented'); | ||
917 | }); | ||
918 | |||
919 | test('updates playlist timeline offsets if it detects a desynchronization', function() { | ||
920 | var buffered = [], currentTime = 0; | ||
921 | |||
922 | player.src({ | ||
923 | src: 'manifest/master.m3u8', | ||
924 | type: 'application/vnd.apple.mpegurl' | ||
925 | }); | ||
926 | openMediaSource(player); | ||
927 | standardXHRResponse(requests.shift()); // master | ||
928 | requests.shift().respond(200, null, | ||
929 | '#EXTM3U\n' + | ||
930 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | ||
931 | '#EXTINF:10,\n' + | ||
932 | '2.ts\n' + | ||
933 | '#EXTINF:10,\n' + | ||
934 | '3.ts\n'); // media | ||
935 | player.tech_.buffered = function() { return videojs.createTimeRange(buffered); }; | ||
936 | player.tech_.currentTime = function() { return currentTime; }; | ||
937 | player.tech_.paused = function() { return false; }; | ||
938 | player.tech_.trigger('play'); | ||
939 | clock.tick(1); | ||
940 | standardXHRResponse(requests.shift()); // segment 0 | ||
941 | equal(player.tech_.hls.mediaIndex, 1, 'incremented mediaIndex'); | ||
942 | |||
943 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
944 | buffered.push([0, 10]); | ||
945 | |||
946 | // force a playlist switch | ||
947 | player.tech_.hls.playlists.media('media1.m3u8'); | ||
948 | requests = requests.filter(function(request) { | ||
949 | return !request.aborted; | ||
950 | }); | ||
951 | requests.shift().respond(200, null, | ||
952 | '#EXTM3U\n' + | ||
953 | '#EXT-X-MEDIA-SEQUENCE:9999\n' + | ||
954 | '#EXTINF:10,\n' + | ||
955 | '3.ts\n' + | ||
956 | '#EXTINF:10,\n' + | ||
957 | '4.ts\n' + | ||
958 | '#EXTINF:10,\n' + | ||
959 | '5.ts\n'); // media1 | ||
960 | player.tech_.hls.checkBuffer_(); | ||
961 | standardXHRResponse(requests.shift()); | ||
962 | |||
963 | buffered.push([20, 30]); | ||
964 | currentTime = 8; | ||
965 | |||
966 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
967 | equal(player.tech_.hls.mediaIndex, 0, 'prepared to request the missing segment'); | ||
968 | }); | 843 | }); |
969 | 844 | ||
970 | test('updates the duration after switching playlists', function() { | 845 | test('updates the duration after switching playlists', function() { |
... | @@ -974,19 +849,22 @@ test('updates the duration after switching playlists', function() { | ... | @@ -974,19 +849,22 @@ test('updates the duration after switching playlists', function() { |
974 | type: 'application/vnd.apple.mpegurl' | 849 | type: 'application/vnd.apple.mpegurl' |
975 | }); | 850 | }); |
976 | openMediaSource(player); | 851 | openMediaSource(player); |
852 | |||
853 | player.tech_.hls.bandwidth = 1e20; | ||
854 | standardXHRResponse(requests[0]); // master | ||
855 | standardXHRResponse(requests[1]); // media3 | ||
856 | |||
977 | player.tech_.hls.selectPlaylist = function() { | 857 | player.tech_.hls.selectPlaylist = function() { |
978 | selectedPlaylist = true; | 858 | selectedPlaylist = true; |
979 | 859 | ||
980 | // this duraiton should be overwritten by the playlist change | 860 | // this duration should be overwritten by the playlist change |
981 | player.tech_.hls.mediaSource.duration = -Infinity; | 861 | player.tech_.hls.mediaSource.duration = -Infinity; |
982 | 862 | ||
983 | return player.tech_.hls.playlists.master.playlists[1]; | 863 | return player.tech_.hls.playlists.master.playlists[1]; |
984 | }; | 864 | }; |
985 | 865 | ||
986 | standardXHRResponse(requests[0]); | 866 | standardXHRResponse(requests[2]); // segment 0 |
987 | standardXHRResponse(requests[1]); | 867 | standardXHRResponse(requests[3]); // media1 |
988 | standardXHRResponse(requests[2]); | ||
989 | standardXHRResponse(requests[3]); | ||
990 | ok(selectedPlaylist, 'selected playlist'); | 868 | ok(selectedPlaylist, 'selected playlist'); |
991 | ok(player.tech_.hls.mediaSource.duration !== -Infinity, 'updates the duration'); | 869 | ok(player.tech_.hls.mediaSource.duration !== -Infinity, 'updates the duration'); |
992 | }); | 870 | }); |
... | @@ -1058,21 +936,6 @@ test('selects a playlist below the current bandwidth', function() { | ... | @@ -1058,21 +936,6 @@ test('selects a playlist below the current bandwidth', function() { |
1058 | 'the low bitrate stream is selected'); | 936 | 'the low bitrate stream is selected'); |
1059 | }); | 937 | }); |
1060 | 938 | ||
1061 | test('scales the bandwidth estimate for the first segment', function() { | ||
1062 | player.src({ | ||
1063 | src: 'manifest/master.m3u8', | ||
1064 | type: 'application/vnd.apple.mpegurl' | ||
1065 | }); | ||
1066 | openMediaSource(player); | ||
1067 | |||
1068 | requests[0].bandwidth = 500; | ||
1069 | requests.shift().respond(200, null, | ||
1070 | '#EXTM3U\n' + | ||
1071 | '#EXT-X-PLAYLIST-TYPE:VOD\n' + | ||
1072 | '#EXT-X-TARGETDURATION:10\n'); | ||
1073 | equal(player.tech_.hls.bandwidth, 500 * 5, 'scaled the bandwidth estimate by 5'); | ||
1074 | }); | ||
1075 | |||
1076 | test('allows initial bandwidth to be provided', function() { | 939 | test('allows initial bandwidth to be provided', function() { |
1077 | player.src({ | 940 | player.src({ |
1078 | src: 'manifest/master.m3u8', | 941 | src: 'manifest/master.m3u8', |
... | @@ -1242,6 +1105,7 @@ test('downloads the next segment if the buffer is getting low', function() { | ... | @@ -1242,6 +1105,7 @@ test('downloads the next segment if the buffer is getting low', function() { |
1242 | player.tech_.buffered = function() { | 1105 | player.tech_.buffered = function() { |
1243 | return videojs.createTimeRange(0, 19.999); | 1106 | return videojs.createTimeRange(0, 19.999); |
1244 | }; | 1107 | }; |
1108 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
1245 | player.tech_.hls.checkBuffer_(); | 1109 | player.tech_.hls.checkBuffer_(); |
1246 | 1110 | ||
1247 | standardXHRResponse(requests[2]); | 1111 | standardXHRResponse(requests[2]); |
... | @@ -1253,46 +1117,41 @@ test('downloads the next segment if the buffer is getting low', function() { | ... | @@ -1253,46 +1117,41 @@ test('downloads the next segment if the buffer is getting low', function() { |
1253 | }); | 1117 | }); |
1254 | 1118 | ||
1255 | test('buffers based on the correct TimeRange if multiple ranges exist', function() { | 1119 | test('buffers based on the correct TimeRange if multiple ranges exist', function() { |
1256 | player.tech_.currentTime = function() { | 1120 | var currentTime, buffered; |
1257 | return 8; | ||
1258 | }; | ||
1259 | |||
1260 | player.src({ | 1121 | player.src({ |
1261 | src: 'manifest/media.m3u8', | 1122 | src: 'manifest/media.m3u8', |
1262 | type: 'application/vnd.apple.mpegurl' | 1123 | type: 'application/vnd.apple.mpegurl' |
1263 | }); | 1124 | }); |
1264 | openMediaSource(player); | 1125 | openMediaSource(player); |
1126 | |||
1127 | player.tech_.currentTime = function() { | ||
1128 | return currentTime; | ||
1129 | }; | ||
1265 | player.tech_.buffered = function() { | 1130 | player.tech_.buffered = function() { |
1266 | return videojs.createTimeRange([[0, 10], [50, 160]]); | 1131 | return videojs.createTimeRange(buffered); |
1267 | }; | 1132 | }; |
1133 | currentTime = 8; | ||
1134 | buffered = [[0, 10], [20, 40]]; | ||
1268 | 1135 | ||
1269 | standardXHRResponse(requests[0]); | 1136 | standardXHRResponse(requests[0]); |
1270 | standardXHRResponse(requests[1]); | 1137 | standardXHRResponse(requests[1]); |
1271 | 1138 | ||
1272 | strictEqual(requests.length, 2, 'made two requests'); | 1139 | strictEqual(requests.length, 2, 'made two requests'); |
1273 | strictEqual(requests[1].url, | 1140 | strictEqual(requests[1].url, |
1274 | absoluteUrl('manifest/media-00001.ts'), | 1141 | absoluteUrl('manifest/media-00002.ts'), |
1275 | 'made segment request'); | 1142 | 'made segment request'); |
1276 | 1143 | ||
1277 | player.tech_.currentTime = function() { | 1144 | currentTime = 22; |
1278 | return 55; | 1145 | player.tech_.hls.sourceBuffer.trigger('updateend'); |
1279 | }; | ||
1280 | |||
1281 | player.tech_.hls.checkBuffer_(); | 1146 | player.tech_.hls.checkBuffer_(); |
1282 | |||
1283 | strictEqual(requests.length, 2, 'made no additional requests'); | 1147 | strictEqual(requests.length, 2, 'made no additional requests'); |
1284 | 1148 | ||
1285 | player.tech_.currentTime = function() { | 1149 | buffered = [[0, 10], [20, 30]]; |
1286 | return 134; | ||
1287 | }; | ||
1288 | |||
1289 | player.tech_.hls.checkBuffer_(); | 1150 | player.tech_.hls.checkBuffer_(); |
1290 | standardXHRResponse(requests[2]); | 1151 | standardXHRResponse(requests[2]); |
1291 | |||
1292 | strictEqual(requests.length, 3, 'made three requests'); | 1152 | strictEqual(requests.length, 3, 'made three requests'); |
1293 | |||
1294 | strictEqual(requests[2].url, | 1153 | strictEqual(requests[2].url, |
1295 | absoluteUrl('manifest/media-00002.ts'), | 1154 | absoluteUrl('manifest/media-00004.ts'), |
1296 | 'made segment request'); | 1155 | 'made segment request'); |
1297 | }); | 1156 | }); |
1298 | 1157 | ||
... | @@ -1325,7 +1184,6 @@ test('only makes one segment request at a time', function() { | ... | @@ -1325,7 +1184,6 @@ test('only makes one segment request at a time', function() { |
1325 | }); | 1184 | }); |
1326 | 1185 | ||
1327 | test('only appends one segment at a time', function() { | 1186 | test('only appends one segment at a time', function() { |
1328 | var appends = 0; | ||
1329 | player.src({ | 1187 | player.src({ |
1330 | src: 'manifest/media.m3u8', | 1188 | src: 'manifest/media.m3u8', |
1331 | type: 'application/vnd.apple.mpegurl' | 1189 | type: 'application/vnd.apple.mpegurl' |
... | @@ -1334,97 +1192,11 @@ test('only appends one segment at a time', function() { | ... | @@ -1334,97 +1192,11 @@ test('only appends one segment at a time', function() { |
1334 | standardXHRResponse(requests.pop()); // media.m3u8 | 1192 | standardXHRResponse(requests.pop()); // media.m3u8 |
1335 | standardXHRResponse(requests.pop()); // segment 0 | 1193 | standardXHRResponse(requests.pop()); // segment 0 |
1336 | 1194 | ||
1337 | player.tech_.hls.sourceBuffer.updating = true; | ||
1338 | player.tech_.hls.sourceBuffer.appendBuffer = function() { | ||
1339 | appends++; | ||
1340 | }; | ||
1341 | |||
1342 | player.tech_.hls.checkBuffer_(); | 1195 | player.tech_.hls.checkBuffer_(); |
1343 | standardXHRResponse(requests.pop()); // segment 1 | 1196 | equal(requests.length, 0, 'did not request while updating'); |
1344 | player.tech_.hls.checkBuffer_(); // should be a no-op | ||
1345 | equal(appends, 0, 'did not append while updating'); | ||
1346 | }); | ||
1347 | |||
1348 | QUnit.skip('records the min and max PTS values for a segment', function() { | ||
1349 | var tags = []; | ||
1350 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1351 | player.src({ | ||
1352 | src: 'manifest/media.m3u8', | ||
1353 | type: 'application/vnd.apple.mpegurl' | ||
1354 | }); | ||
1355 | openMediaSource(player); | ||
1356 | standardXHRResponse(requests.pop()); // media.m3u8 | ||
1357 | |||
1358 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1359 | tags.push({ pts: 10, bytes: new Uint8Array(1) }); | ||
1360 | standardXHRResponse(requests.pop()); // segment 0 | ||
1361 | |||
1362 | equal(player.tech_.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts'); | ||
1363 | equal(player.tech_.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts'); | ||
1364 | equal(player.tech_.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts'); | ||
1365 | equal(player.tech_.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); | ||
1366 | }); | ||
1367 | |||
1368 | QUnit.skip('records PTS values for video-only segments', function() { | ||
1369 | var tags = []; | ||
1370 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1371 | player.src({ | ||
1372 | src: 'manifest/media.m3u8', | ||
1373 | type: 'application/vnd.apple.mpegurl' | ||
1374 | }); | ||
1375 | openMediaSource(player); | ||
1376 | standardXHRResponse(requests.pop()); // media.m3u8 | ||
1377 | |||
1378 | player.tech_.hls.segmentParser_.stats.aacTags = function() { | ||
1379 | return 0; | ||
1380 | }; | ||
1381 | player.tech_.hls.segmentParser_.stats.minAudioPts = function() { | ||
1382 | throw new Error('No audio tags'); | ||
1383 | }; | ||
1384 | player.tech_.hls.segmentParser_.stats.maxAudioPts = function() { | ||
1385 | throw new Error('No audio tags'); | ||
1386 | }; | ||
1387 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1388 | tags.push({ pts: 10, bytes: new Uint8Array(1) }); | ||
1389 | standardXHRResponse(requests.pop()); // segment 0 | ||
1390 | |||
1391 | equal(player.tech_.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts'); | ||
1392 | equal(player.tech_.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts'); | ||
1393 | strictEqual(player.tech_.hls.playlists.media().segments[0].minAudioPts, undefined, 'min audio pts is undefined'); | ||
1394 | strictEqual(player.tech_.hls.playlists.media().segments[0].maxAudioPts, undefined, 'max audio pts is undefined'); | ||
1395 | }); | ||
1396 | |||
1397 | QUnit.skip('records PTS values for audio-only segments', function() { | ||
1398 | var tags = []; | ||
1399 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1400 | player.src({ | ||
1401 | src: 'manifest/media.m3u8', | ||
1402 | type: 'application/vnd.apple.mpegurl' | ||
1403 | }); | ||
1404 | openMediaSource(player); | ||
1405 | standardXHRResponse(requests.pop()); // media.m3u8 | ||
1406 | |||
1407 | player.tech_.hls.segmentParser_.stats.h264Tags = function() { | ||
1408 | return 0; | ||
1409 | }; | ||
1410 | player.tech_.hls.segmentParser_.stats.minVideoPts = function() { | ||
1411 | throw new Error('No video tags'); | ||
1412 | }; | ||
1413 | player.tech_.hls.segmentParser_.stats.maxVideoPts = function() { | ||
1414 | throw new Error('No video tags'); | ||
1415 | }; | ||
1416 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1417 | tags.push({ pts: 10, bytes: new Uint8Array(1) }); | ||
1418 | standardXHRResponse(requests.pop()); // segment 0 | ||
1419 | |||
1420 | equal(player.tech_.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts'); | ||
1421 | equal(player.tech_.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); | ||
1422 | strictEqual(player.tech_.hls.playlists.media().segments[0].minVideoPts, undefined, 'min video pts is undefined'); | ||
1423 | strictEqual(player.tech_.hls.playlists.media().segments[0].maxVideoPts, undefined, 'max video pts is undefined'); | ||
1424 | }); | 1197 | }); |
1425 | 1198 | ||
1426 | test('waits to download new segments until the media playlist is stable', function() { | 1199 | test('waits to download new segments until the media playlist is stable', function() { |
1427 | var media; | ||
1428 | player.src({ | 1200 | player.src({ |
1429 | src: 'manifest/master.m3u8', | 1201 | src: 'manifest/master.m3u8', |
1430 | type: 'application/vnd.apple.mpegurl' | 1202 | type: 'application/vnd.apple.mpegurl' |
... | @@ -1432,22 +1204,19 @@ test('waits to download new segments until the media playlist is stable', functi | ... | @@ -1432,22 +1204,19 @@ test('waits to download new segments until the media playlist is stable', functi |
1432 | openMediaSource(player); | 1204 | openMediaSource(player); |
1433 | standardXHRResponse(requests.shift()); // master | 1205 | standardXHRResponse(requests.shift()); // master |
1434 | player.tech_.hls.bandwidth = 1; // make sure we stay on the lowest variant | 1206 | player.tech_.hls.bandwidth = 1; // make sure we stay on the lowest variant |
1435 | standardXHRResponse(requests.shift()); // media | 1207 | standardXHRResponse(requests.shift()); // media1 |
1436 | 1208 | ||
1437 | // mock a playlist switch | 1209 | // force a playlist switch |
1438 | media = player.tech_.hls.playlists.media(); | 1210 | player.tech_.hls.playlists.media('media3.m3u8'); |
1439 | player.tech_.hls.playlists.media = function() { | ||
1440 | return media; | ||
1441 | }; | ||
1442 | player.tech_.hls.playlists.state = 'SWITCHING_MEDIA'; | ||
1443 | 1211 | ||
1444 | standardXHRResponse(requests.shift()); // segment 0 | 1212 | standardXHRResponse(requests.shift()); // segment 0 |
1213 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
1445 | 1214 | ||
1446 | equal(requests.length, 0, 'no requests outstanding'); | 1215 | equal(requests.length, 1, 'only the playlist request outstanding'); |
1447 | player.tech_.hls.checkBuffer_(); | 1216 | player.tech_.hls.checkBuffer_(); |
1448 | equal(requests.length, 0, 'delays segment fetching'); | 1217 | equal(requests.length, 1, 'delays segment fetching'); |
1449 | 1218 | ||
1450 | player.tech_.hls.playlists.state = 'LOADED_METADATA'; | 1219 | standardXHRResponse(requests.shift()); // media3 |
1451 | player.tech_.hls.checkBuffer_(); | 1220 | player.tech_.hls.checkBuffer_(); |
1452 | equal(requests.length, 1, 'resumes segment fetching'); | 1221 | equal(requests.length, 1, 'resumes segment fetching'); |
1453 | }); | 1222 | }); |
... | @@ -1536,359 +1305,6 @@ test('segmentXhr is properly nulled out when dispose is called', function() { | ... | @@ -1536,359 +1305,6 @@ test('segmentXhr is properly nulled out when dispose is called', function() { |
1536 | Flash.prototype.dispose = oldDispose; | 1305 | Flash.prototype.dispose = oldDispose; |
1537 | }); | 1306 | }); |
1538 | 1307 | ||
1539 | QUnit.skip('exposes in-band metadata events as cues', function() { | ||
1540 | var track; | ||
1541 | videojs.Hls.SegmentParser = mockSegmentParser(); | ||
1542 | player.src({ | ||
1543 | src: 'manifest/media.m3u8', | ||
1544 | type: 'application/vnd.apple.mpegurl' | ||
1545 | }); | ||
1546 | openMediaSource(player); | ||
1547 | |||
1548 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1549 | // trigger a metadata event | ||
1550 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', { | ||
1551 | pts: 2000, | ||
1552 | data: new Uint8Array([]), | ||
1553 | frames: [{ | ||
1554 | id: 'TXXX', | ||
1555 | value: 'cue text' | ||
1556 | }, { | ||
1557 | id: 'WXXX', | ||
1558 | url: 'http://example.com' | ||
1559 | }, { | ||
1560 | id: 'PRIV', | ||
1561 | owner: 'owner@example.com', | ||
1562 | privateData: new Uint8Array([1, 2, 3]) | ||
1563 | }] | ||
1564 | }); | ||
1565 | }; | ||
1566 | |||
1567 | standardXHRResponse(requests[0]); | ||
1568 | standardXHRResponse(requests[1]); | ||
1569 | |||
1570 | equal(player.textTracks().length, 1, 'created a text track'); | ||
1571 | track = player.textTracks()[0]; | ||
1572 | equal(track.kind, 'metadata', 'kind is metadata'); | ||
1573 | equal(track.inBandMetadataTrackDispatchType, '15010203BB', 'set the dispatch type'); | ||
1574 | equal(track.cues.length, 3, 'created three cues'); | ||
1575 | equal(track.cues[0].startTime, 2, 'cue starts at 2 seconds'); | ||
1576 | equal(track.cues[0].endTime, 2, 'cue ends at 2 seconds'); | ||
1577 | equal(track.cues[0].pauseOnExit, false, 'cue does not pause on exit'); | ||
1578 | equal(track.cues[0].text, 'cue text', 'set cue text'); | ||
1579 | |||
1580 | equal(track.cues[1].startTime, 2, 'cue starts at 2 seconds'); | ||
1581 | equal(track.cues[1].endTime, 2, 'cue ends at 2 seconds'); | ||
1582 | equal(track.cues[1].pauseOnExit, false, 'cue does not pause on exit'); | ||
1583 | equal(track.cues[1].text, 'http://example.com', 'set cue text'); | ||
1584 | |||
1585 | equal(track.cues[2].startTime, 2, 'cue starts at 2 seconds'); | ||
1586 | equal(track.cues[2].endTime, 2, 'cue ends at 2 seconds'); | ||
1587 | equal(track.cues[2].pauseOnExit, false, 'cue does not pause on exit'); | ||
1588 | equal(track.cues[2].text, '', 'did not set cue text'); | ||
1589 | equal(track.cues[2].frame.owner, 'owner@example.com', 'set the owner'); | ||
1590 | deepEqual(track.cues[2].frame.privateData, | ||
1591 | new Uint8Array([1, 2, 3]), | ||
1592 | 'set the private data'); | ||
1593 | }); | ||
1594 | |||
1595 | QUnit.skip('only adds in-band cues the first time they are encountered', function() { | ||
1596 | var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track; | ||
1597 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1598 | player.src({ | ||
1599 | src: 'manifest/media.m3u8', | ||
1600 | type: 'application/vnd.apple.mpegurl' | ||
1601 | }); | ||
1602 | openMediaSource(player); | ||
1603 | |||
1604 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1605 | // trigger a metadata event | ||
1606 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', { | ||
1607 | pts: 2000, | ||
1608 | data: new Uint8Array([]), | ||
1609 | frames: [{ | ||
1610 | id: 'TXXX', | ||
1611 | value: 'cue text' | ||
1612 | }] | ||
1613 | }); | ||
1614 | }; | ||
1615 | standardXHRResponse(requests.shift()); | ||
1616 | standardXHRResponse(requests.shift()); | ||
1617 | // seek back to the first segment | ||
1618 | player.currentTime(0); | ||
1619 | player.tech_.hls.trigger('seeking'); | ||
1620 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1621 | standardXHRResponse(requests.shift()); | ||
1622 | |||
1623 | track = player.textTracks()[0]; | ||
1624 | equal(track.cues.length, 1, 'only added the cue once'); | ||
1625 | }); | ||
1626 | |||
1627 | QUnit.skip('clears in-band cues ahead of current time on seek', function() { | ||
1628 | var | ||
1629 | tags = [], | ||
1630 | events = [], | ||
1631 | track; | ||
1632 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1633 | player.src({ | ||
1634 | src: 'manifest/media.m3u8', | ||
1635 | type: 'application/vnd.apple.mpegurl' | ||
1636 | }); | ||
1637 | openMediaSource(player); | ||
1638 | |||
1639 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1640 | // trigger a metadata event | ||
1641 | while (events.length) { | ||
1642 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | ||
1643 | } | ||
1644 | }; | ||
1645 | standardXHRResponse(requests.shift()); // media | ||
1646 | tags.push({ pts: 0, bytes: new Uint8Array(1) }, | ||
1647 | { pts: 10 * 1000, bytes: new Uint8Array(1) }); | ||
1648 | events.push({ | ||
1649 | pts: 9.9 * 1000, | ||
1650 | data: new Uint8Array([]), | ||
1651 | frames: [{ | ||
1652 | id: 'TXXX', | ||
1653 | value: 'cue 1' | ||
1654 | }] | ||
1655 | }); | ||
1656 | events.push({ | ||
1657 | pts: 20 * 1000, | ||
1658 | data: new Uint8Array([]), | ||
1659 | frames: [{ | ||
1660 | id: 'TXXX', | ||
1661 | value: 'cue 3' | ||
1662 | }] | ||
1663 | }); | ||
1664 | standardXHRResponse(requests.shift()); // segment 0 | ||
1665 | tags.push({ pts: 10 * 1000 + 1, bytes: new Uint8Array(1) }, | ||
1666 | { pts: 20 * 1000, bytes: new Uint8Array(1) }); | ||
1667 | events.push({ | ||
1668 | pts: 19.9 * 1000, | ||
1669 | data: new Uint8Array([]), | ||
1670 | frames: [{ | ||
1671 | id: 'TXXX', | ||
1672 | value: 'cue 2' | ||
1673 | }] | ||
1674 | }); | ||
1675 | player.tech_.hls.checkBuffer_(); | ||
1676 | standardXHRResponse(requests.shift()); // segment 1 | ||
1677 | |||
1678 | track = player.textTracks()[0]; | ||
1679 | equal(track.cues.length, 3, 'added the cues'); | ||
1680 | |||
1681 | // seek into segment 1 | ||
1682 | player.currentTime(11); | ||
1683 | player.trigger('seeking'); | ||
1684 | equal(track.cues.length, 1, 'removed later cues'); | ||
1685 | equal(track.cues[0].startTime, 9.9, 'retained the earlier cue'); | ||
1686 | }); | ||
1687 | |||
1688 | QUnit.skip('translates ID3 PTS values to cue media timeline positions', function() { | ||
1689 | var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; | ||
1690 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1691 | player.src({ | ||
1692 | src: 'manifest/media.m3u8', | ||
1693 | type: 'application/vnd.apple.mpegurl' | ||
1694 | }); | ||
1695 | openMediaSource(player); | ||
1696 | |||
1697 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1698 | // trigger a metadata event | ||
1699 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', { | ||
1700 | pts: 5 * 1000, | ||
1701 | data: new Uint8Array([]), | ||
1702 | frames: [{ | ||
1703 | id: 'TXXX', | ||
1704 | value: 'cue text' | ||
1705 | }] | ||
1706 | }); | ||
1707 | }; | ||
1708 | standardXHRResponse(requests.shift()); // media | ||
1709 | standardXHRResponse(requests.shift()); // segment 0 | ||
1710 | |||
1711 | track = player.textTracks()[0]; | ||
1712 | equal(track.cues[0].startTime, 1, 'translated startTime'); | ||
1713 | equal(track.cues[0].endTime, 1, 'translated startTime'); | ||
1714 | }); | ||
1715 | |||
1716 | QUnit.skip('translates ID3 PTS values with expired segments', function() { | ||
1717 | var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; | ||
1718 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1719 | player.src({ | ||
1720 | src: 'live.m3u8', | ||
1721 | type: 'application/vnd.apple.mpegurl' | ||
1722 | }); | ||
1723 | openMediaSource(player); | ||
1724 | player.play(); | ||
1725 | |||
1726 | // 20.9 seconds of content have expired | ||
1727 | player.hls.playlists.expiredPostDiscontinuity_ = 20.9; | ||
1728 | |||
1729 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1730 | // trigger a metadata event | ||
1731 | player.hls.segmentParser_.metadataStream.trigger('data', { | ||
1732 | pts: 5 * 1000, | ||
1733 | data: new Uint8Array([]), | ||
1734 | frames: [{ | ||
1735 | id: 'TXXX', | ||
1736 | value: 'cue text' | ||
1737 | }] | ||
1738 | }); | ||
1739 | }; | ||
1740 | requests.shift().respond(200, null, | ||
1741 | '#EXTM3U\n' + | ||
1742 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | ||
1743 | '#EXTINF:10,\n' + | ||
1744 | '2.ts\n' + | ||
1745 | '#EXTINF:10,\n' + | ||
1746 | '3.ts\n'); // media | ||
1747 | standardXHRResponse(requests.shift()); // segment 0 | ||
1748 | |||
1749 | track = player.textTracks()[0]; | ||
1750 | equal(track.cues[0].startTime, 20.9 + 1, 'translated startTime'); | ||
1751 | equal(track.cues[0].endTime, 20.9 + 1, 'translated startTime'); | ||
1752 | }); | ||
1753 | |||
1754 | QUnit.skip('translates id3 PTS values for audio-only media', function() { | ||
1755 | var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; | ||
1756 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1757 | player.src({ | ||
1758 | src: 'manifest/media.m3u8', | ||
1759 | type: 'application/vnd.apple.mpegurl' | ||
1760 | }); | ||
1761 | openMediaSource(player); | ||
1762 | |||
1763 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1764 | // trigger a metadata event | ||
1765 | player.hls.segmentParser_.metadataStream.trigger('data', { | ||
1766 | pts: 5 * 1000, | ||
1767 | data: new Uint8Array([]), | ||
1768 | frames: [{ | ||
1769 | id: 'TXXX', | ||
1770 | value: 'cue text' | ||
1771 | }] | ||
1772 | }); | ||
1773 | }; | ||
1774 | player.hls.segmentParser_.stats.h264Tags = function() { return 0; }; | ||
1775 | player.hls.segmentParser_.stats.minVideoPts = null; | ||
1776 | standardXHRResponse(requests.shift()); // media | ||
1777 | standardXHRResponse(requests.shift()); // segment 0 | ||
1778 | |||
1779 | track = player.textTracks()[0]; | ||
1780 | equal(track.cues[0].startTime, 1, 'translated startTime'); | ||
1781 | }); | ||
1782 | |||
1783 | QUnit.skip('translates ID3 PTS values across discontinuities', function() { | ||
1784 | var tags = [], events = [], track; | ||
1785 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1786 | player.src({ | ||
1787 | src: 'cues-and-discontinuities.m3u8', | ||
1788 | type: 'application/vnd.apple.mpegurl' | ||
1789 | }); | ||
1790 | openMediaSource(player); | ||
1791 | |||
1792 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1793 | // trigger a metadata event | ||
1794 | if (events.length) { | ||
1795 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | ||
1796 | } | ||
1797 | }; | ||
1798 | |||
1799 | // media playlist | ||
1800 | player.trigger('play'); | ||
1801 | requests.shift().respond(200, null, | ||
1802 | '#EXTM3U\n' + | ||
1803 | '#EXTINF:10,\n' + | ||
1804 | '0.ts\n' + | ||
1805 | '#EXT-X-DISCONTINUITY\n' + | ||
1806 | '#EXTINF:10,\n' + | ||
1807 | '1.ts\n'); | ||
1808 | |||
1809 | // segment 0 starts at PTS 14000 and has a cue point at 15000 | ||
1810 | tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }, | ||
1811 | { pts: 24 * 1000, bytes: new Uint8Array(1) }); | ||
1812 | events.push({ | ||
1813 | pts: 15 * 1000, | ||
1814 | data: new Uint8Array([]), | ||
1815 | frames: [{ | ||
1816 | id: 'TXXX', | ||
1817 | value: 'cue 0' | ||
1818 | }] | ||
1819 | }); | ||
1820 | standardXHRResponse(requests.shift()); // segment 0 | ||
1821 | |||
1822 | // segment 1 is after a discontinuity, starts at PTS 22000 | ||
1823 | // and has a cue point at 23000 | ||
1824 | tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) }); | ||
1825 | events.push({ | ||
1826 | pts: 23 * 1000, | ||
1827 | data: new Uint8Array([]), | ||
1828 | frames: [{ | ||
1829 | id: 'TXXX', | ||
1830 | value: 'cue 1' | ||
1831 | }] | ||
1832 | }); | ||
1833 | player.tech_.hls.checkBuffer_(); | ||
1834 | standardXHRResponse(requests.shift()); | ||
1835 | |||
1836 | track = player.textTracks()[0]; | ||
1837 | equal(track.cues.length, 2, 'created cues'); | ||
1838 | equal(track.cues[0].startTime, 1, 'first cue started at the correct time'); | ||
1839 | equal(track.cues[0].endTime, 1, 'first cue ended at the correct time'); | ||
1840 | equal(track.cues[1].startTime, 11, 'second cue started at the correct time'); | ||
1841 | equal(track.cues[1].endTime, 11, 'second cue ended at the correct time'); | ||
1842 | }); | ||
1843 | |||
1844 | test('adjusts the segment offsets for out-of-buffer seeking', function() { | ||
1845 | player.src({ | ||
1846 | src: 'manifest/media.m3u8', | ||
1847 | type: 'application/vnd.apple.mpegurl' | ||
1848 | }); | ||
1849 | openMediaSource(player); | ||
1850 | standardXHRResponse(requests.shift()); // media | ||
1851 | player.tech_.hls.sourceBuffer.buffered = function() { | ||
1852 | return videojs.createTimeRange(0, 20); | ||
1853 | }; | ||
1854 | equal(player.tech_.hls.mediaIndex, 0, 'starts at zero'); | ||
1855 | |||
1856 | player.tech_.setCurrentTime(35); | ||
1857 | clock.tick(1); | ||
1858 | // drop the aborted segment | ||
1859 | requests.shift(); | ||
1860 | equal(player.tech_.hls.mediaIndex, 3, 'moved the mediaIndex'); | ||
1861 | standardXHRResponse(requests.shift()); | ||
1862 | }); | ||
1863 | |||
1864 | test('seeks between buffered time ranges', function() { | ||
1865 | player.src({ | ||
1866 | src: 'manifest/media.m3u8', | ||
1867 | type: 'application/vnd.apple.mpegurl' | ||
1868 | }); | ||
1869 | openMediaSource(player); | ||
1870 | standardXHRResponse(requests.shift()); // media | ||
1871 | player.tech_.buffered = function() { | ||
1872 | return { | ||
1873 | length: 2, | ||
1874 | ranges_: [[0, 10], [20, 30]], | ||
1875 | start: function(i) { | ||
1876 | return this.ranges_[i][0]; | ||
1877 | }, | ||
1878 | end: function(i) { | ||
1879 | return this.ranges_[i][1]; | ||
1880 | } | ||
1881 | }; | ||
1882 | }; | ||
1883 | |||
1884 | player.tech_.setCurrentTime(15); | ||
1885 | clock.tick(1); | ||
1886 | // drop the aborted segment | ||
1887 | requests.shift(); | ||
1888 | equal(player.tech_.hls.mediaIndex, 1, 'updated the mediaIndex'); | ||
1889 | standardXHRResponse(requests.shift()); | ||
1890 | }); | ||
1891 | |||
1892 | test('does not modify the media index for in-buffer seeking', function() { | 1308 | test('does not modify the media index for in-buffer seeking', function() { |
1893 | var mediaIndex; | 1309 | var mediaIndex; |
1894 | player.src({ | 1310 | player.src({ |
... | @@ -1979,40 +1395,6 @@ test('duration is Infinity for live playlists', function() { | ... | @@ -1979,40 +1395,6 @@ test('duration is Infinity for live playlists', function() { |
1979 | 'duration is infinity'); | 1395 | 'duration is infinity'); |
1980 | }); | 1396 | }); |
1981 | 1397 | ||
1982 | test('updates the media index when a playlist reloads', function() { | ||
1983 | player.src({ | ||
1984 | src: 'http://example.com/live-updating.m3u8', | ||
1985 | type: 'application/vnd.apple.mpegurl' | ||
1986 | }); | ||
1987 | openMediaSource(player); | ||
1988 | player.tech_.trigger('play'); | ||
1989 | |||
1990 | requests[0].respond(200, null, | ||
1991 | '#EXTM3U\n' + | ||
1992 | '#EXTINF:10,\n' + | ||
1993 | '0.ts\n' + | ||
1994 | '#EXTINF:10,\n' + | ||
1995 | '1.ts\n' + | ||
1996 | '#EXTINF:10,\n' + | ||
1997 | '2.ts\n'); | ||
1998 | standardXHRResponse(requests[1]); | ||
1999 | // play the stream until 2.ts is playing | ||
2000 | player.tech_.hls.mediaIndex = 3; | ||
2001 | // trigger a playlist refresh | ||
2002 | player.tech_.hls.playlists.trigger('mediaupdatetimeout'); | ||
2003 | requests[2].respond(200, null, | ||
2004 | '#EXTM3U\n' + | ||
2005 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
2006 | '#EXTINF:10,\n' + | ||
2007 | '1.ts\n' + | ||
2008 | '#EXTINF:10,\n' + | ||
2009 | '2.ts\n' + | ||
2010 | '#EXTINF:10,\n' + | ||
2011 | '3.ts\n'); | ||
2012 | |||
2013 | strictEqual(player.tech_.hls.mediaIndex, 2, 'mediaIndex is updated after the reload'); | ||
2014 | }); | ||
2015 | |||
2016 | test('live playlist starts three target durations before live', function() { | 1398 | test('live playlist starts three target durations before live', function() { |
2017 | var mediaPlaylist; | 1399 | var mediaPlaylist; |
2018 | player.src({ | 1400 | player.src({ |
... | @@ -2040,30 +1422,11 @@ test('live playlist starts three target durations before live', function() { | ... | @@ -2040,30 +1422,11 @@ test('live playlist starts three target durations before live', function() { |
2040 | player.tech_.trigger('play'); | 1422 | player.tech_.trigger('play'); |
2041 | clock.tick(1); | 1423 | clock.tick(1); |
2042 | mediaPlaylist = player.tech_.hls.playlists.media(); | 1424 | mediaPlaylist = player.tech_.hls.playlists.media(); |
2043 | equal(player.tech_.hls.mediaIndex, 1, 'mediaIndex is updated at play'); | ||
2044 | equal(player.currentTime(), player.tech_.hls.seekable().end(0), 'seeked to the seekable end'); | 1425 | equal(player.currentTime(), player.tech_.hls.seekable().end(0), 'seeked to the seekable end'); |
2045 | 1426 | ||
2046 | equal(requests.length, 1, 'begins buffering'); | 1427 | equal(requests.length, 1, 'begins buffering'); |
2047 | }); | 1428 | }); |
2048 | 1429 | ||
2049 | test('does not reset live currentTime if mediaIndex is one beyond the last available segment', function() { | ||
2050 | var playlist = { | ||
2051 | mediaSequence: 20, | ||
2052 | targetDuration: 9, | ||
2053 | segments: [{ | ||
2054 | duration: 3 | ||
2055 | }, { | ||
2056 | duration: 3 | ||
2057 | }, { | ||
2058 | duration: 3 | ||
2059 | }] | ||
2060 | }; | ||
2061 | |||
2062 | equal(playlist.segments.length, | ||
2063 | videojs.Hls.translateMediaIndex(playlist.segments.length, playlist, playlist), | ||
2064 | 'did not change mediaIndex'); | ||
2065 | }); | ||
2066 | |||
2067 | test('live playlist starts with correct currentTime value', function() { | 1430 | test('live playlist starts with correct currentTime value', function() { |
2068 | player.src({ | 1431 | player.src({ |
2069 | src: 'http://example.com/manifest/liveStart30sBefore.m3u8', | 1432 | src: 'http://example.com/manifest/liveStart30sBefore.m3u8', |
... | @@ -2117,39 +1480,6 @@ test('resets the time to a seekable position when resuming a live stream ' + | ... | @@ -2117,39 +1480,6 @@ test('resets the time to a seekable position when resuming a live stream ' + |
2117 | player.tech_.trigger('seeked'); | 1480 | player.tech_.trigger('seeked'); |
2118 | }); | 1481 | }); |
2119 | 1482 | ||
2120 | test('mediaIndex is zero before the first segment loads', function() { | ||
2121 | window.manifests['first-seg-load'] = | ||
2122 | '#EXTM3U\n' + | ||
2123 | '#EXTINF:10,\n' + | ||
2124 | '0.ts\n'; | ||
2125 | player.src({ | ||
2126 | src: 'http://example.com/first-seg-load.m3u8', | ||
2127 | type: 'application/vnd.apple.mpegurl' | ||
2128 | }); | ||
2129 | openMediaSource(player); | ||
2130 | |||
2131 | strictEqual(player.tech_.hls.mediaIndex, 0, 'mediaIndex is zero'); | ||
2132 | }); | ||
2133 | |||
2134 | test('mediaIndex returns correctly at playlist boundaries', function() { | ||
2135 | player.src({ | ||
2136 | src: 'http://example.com/master.m3u8', | ||
2137 | type: 'application/vnd.apple.mpegurl' | ||
2138 | }); | ||
2139 | |||
2140 | openMediaSource(player); | ||
2141 | standardXHRResponse(requests.shift()); // master | ||
2142 | standardXHRResponse(requests.shift()); // media | ||
2143 | |||
2144 | strictEqual(player.tech_.hls.mediaIndex, 0, 'mediaIndex is zero at first segment'); | ||
2145 | |||
2146 | // seek to end | ||
2147 | player.tech_.setCurrentTime(40); | ||
2148 | clock.tick(1); | ||
2149 | |||
2150 | strictEqual(player.tech_.hls.mediaIndex, 3, 'mediaIndex is 3 at last segment'); | ||
2151 | }); | ||
2152 | |||
2153 | test('reloads out-of-date live playlists when switching variants', function() { | 1483 | test('reloads out-of-date live playlists when switching variants', function() { |
2154 | player.src({ | 1484 | player.src({ |
2155 | src: 'http://example.com/master.m3u8', | 1485 | src: 'http://example.com/master.m3u8', |
... | @@ -2248,26 +1578,21 @@ test('does not break if the playlist has no segments', function() { | ... | @@ -2248,26 +1578,21 @@ test('does not break if the playlist has no segments', function() { |
2248 | strictEqual(requests.length, 1, 'no requests for non-existent segments were queued'); | 1578 | strictEqual(requests.length, 1, 'no requests for non-existent segments were queued'); |
2249 | }); | 1579 | }); |
2250 | 1580 | ||
2251 | test('clears the segment buffer on seek', function() { | 1581 | test('aborts segment processing on seek', function() { |
2252 | var currentTime, oldCurrentTime; | 1582 | var currentTime = 0; |
2253 | |||
2254 | player.src({ | 1583 | player.src({ |
2255 | src: 'discontinuity.m3u8', | 1584 | src: 'discontinuity.m3u8', |
2256 | type: 'application/vnd.apple.mpegurl' | 1585 | type: 'application/vnd.apple.mpegurl' |
2257 | }); | 1586 | }); |
2258 | openMediaSource(player); | 1587 | openMediaSource(player); |
2259 | oldCurrentTime = player.currentTime; | 1588 | player.tech_.currentTime = function() { |
2260 | player.currentTime = function(time) { | ||
2261 | if (time !== undefined) { | ||
2262 | return oldCurrentTime.call(player, time); | ||
2263 | } | ||
2264 | return currentTime; | 1589 | return currentTime; |
2265 | }; | 1590 | }; |
2266 | player.tech_.buffered = function() { | 1591 | player.tech_.buffered = function() { |
2267 | return videojs.createTimeRange(); | 1592 | return videojs.createTimeRange(); |
2268 | }; | 1593 | }; |
2269 | 1594 | ||
2270 | requests.pop().respond(200, null, | 1595 | requests.shift().respond(200, null, |
2271 | '#EXTM3U\n' + | 1596 | '#EXTM3U\n' + |
2272 | '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + | 1597 | '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + |
2273 | '#EXTINF:10,0\n' + | 1598 | '#EXTINF:10,0\n' + |
... | @@ -2275,23 +1600,19 @@ test('clears the segment buffer on seek', function() { | ... | @@ -2275,23 +1600,19 @@ test('clears the segment buffer on seek', function() { |
2275 | '#EXT-X-DISCONTINUITY\n' + | 1600 | '#EXT-X-DISCONTINUITY\n' + |
2276 | '#EXTINF:10,0\n' + | 1601 | '#EXTINF:10,0\n' + |
2277 | '2.ts\n' + | 1602 | '2.ts\n' + |
2278 | '#EXT-X-ENDLIST\n'); | 1603 | '#EXT-X-ENDLIST\n'); // media |
2279 | standardXHRResponse(requests.pop()); // 1.ts | 1604 | standardXHRResponse(requests.shift()); // 1.ts |
2280 | 1605 | standardXHRResponse(requests.shift()); // key.php | |
2281 | // play to 6s to trigger the next segment request | 1606 | ok(player.tech_.hls.pendingSegment_, 'decrypting the segment'); |
2282 | currentTime = 6; | ||
2283 | clock.tick(6000); | ||
2284 | |||
2285 | standardXHRResponse(requests.pop()); // 2.ts | ||
2286 | equal(player.tech_.hls.segmentBuffer_.length, 2, 'started fetching segments'); | ||
2287 | 1607 | ||
2288 | // seek back to the beginning | 1608 | // seek back to the beginning |
2289 | player.currentTime(0); | 1609 | player.currentTime(0); |
2290 | clock.tick(1); | 1610 | clock.tick(1); |
2291 | equal(player.tech_.hls.segmentBuffer_.length, 0, 'cleared the segment buffer'); | 1611 | ok(!player.tech_.hls.pendingSegment_, 'aborted processing'); |
2292 | }); | 1612 | }); |
2293 | 1613 | ||
2294 | test('calls mediaSource\'s timestampOffset on discontinuity', function() { | 1614 | test('calls mediaSource\'s timestampOffset on discontinuity', function() { |
1615 | var buffered = [[]]; | ||
2295 | player.src({ | 1616 | player.src({ |
2296 | src: 'discontinuity.m3u8', | 1617 | src: 'discontinuity.m3u8', |
2297 | type: 'application/vnd.apple.mpegurl' | 1618 | type: 'application/vnd.apple.mpegurl' |
... | @@ -2299,10 +1620,10 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { | ... | @@ -2299,10 +1620,10 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { |
2299 | openMediaSource(player); | 1620 | openMediaSource(player); |
2300 | player.play(); | 1621 | player.play(); |
2301 | player.tech_.buffered = function() { | 1622 | player.tech_.buffered = function() { |
2302 | return videojs.createTimeRange(0, 10); | 1623 | return videojs.createTimeRange(buffered); |
2303 | }; | 1624 | }; |
2304 | 1625 | ||
2305 | requests.pop().respond(200, null, | 1626 | requests.shift().respond(200, null, |
2306 | '#EXTM3U\n' + | 1627 | '#EXTM3U\n' + |
2307 | '#EXTINF:10,0\n' + | 1628 | '#EXTINF:10,0\n' + |
2308 | '1.ts\n' + | 1629 | '1.ts\n' + |
... | @@ -2311,14 +1632,14 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { | ... | @@ -2311,14 +1632,14 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { |
2311 | '2.ts\n' + | 1632 | '2.ts\n' + |
2312 | '#EXT-X-ENDLIST\n'); | 1633 | '#EXT-X-ENDLIST\n'); |
2313 | player.tech_.hls.sourceBuffer.timestampOffset = 0; | 1634 | player.tech_.hls.sourceBuffer.timestampOffset = 0; |
2314 | standardXHRResponse(requests.pop()); // 1.ts | 1635 | standardXHRResponse(requests.shift()); // 1.ts |
2315 | 1636 | equal(player.tech_.hls.sourceBuffer.timestampOffset, | |
2316 | equal(player.tech_.hls.sourceBuffer.timestampOffset, 0, 'timestampOffset starts at zero'); | 1637 | 0, |
2317 | 1638 | 'timestampOffset starts at zero'); | |
2318 | // play to 6s to trigger the next segment request | ||
2319 | clock.tick(6000); | ||
2320 | 1639 | ||
2321 | standardXHRResponse(requests.pop()); // 2.ts | 1640 | buffered = [[0, 10]]; |
1641 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
1642 | standardXHRResponse(requests.shift()); // 2.ts | ||
2322 | equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity'); | 1643 | equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity'); |
2323 | }); | 1644 | }); |
2324 | 1645 | ||
... | @@ -2410,7 +1731,7 @@ QUnit.skip('sets the timestampOffset after seeking to discontinuity', function() | ... | @@ -2410,7 +1731,7 @@ QUnit.skip('sets the timestampOffset after seeking to discontinuity', function() |
2410 | 'set the timestamp offset'); | 1731 | 'set the timestamp offset'); |
2411 | }); | 1732 | }); |
2412 | 1733 | ||
2413 | QUnit.skip('tracks segment end times as they are buffered', function() { | 1734 | test('tracks segment end times as they are buffered', function() { |
2414 | var bufferEnd = 0; | 1735 | var bufferEnd = 0; |
2415 | player.src({ | 1736 | player.src({ |
2416 | src: 'media.m3u8', | 1737 | src: 'media.m3u8', |
... | @@ -2437,8 +1758,7 @@ QUnit.skip('tracks segment end times as they are buffered', function() { | ... | @@ -2437,8 +1758,7 @@ QUnit.skip('tracks segment end times as they are buffered', function() { |
2437 | bufferEnd = 9.5; | 1758 | bufferEnd = 9.5; |
2438 | player.tech_.hls.sourceBuffer.trigger('update'); | 1759 | player.tech_.hls.sourceBuffer.trigger('update'); |
2439 | player.tech_.hls.sourceBuffer.trigger('updateend'); | 1760 | player.tech_.hls.sourceBuffer.trigger('updateend'); |
2440 | equal(player.tech_.duration(), 10 + 9.5, 'updated duration'); | 1761 | equal(player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration'); |
2441 | equal(player.tech_.hls.appendingSegmentInfo_, null, 'cleared the appending segment'); | ||
2442 | }); | 1762 | }); |
2443 | 1763 | ||
2444 | QUnit.skip('seeking does not fail when targeted between segments', function() { | 1764 | QUnit.skip('seeking does not fail when targeted between segments', function() { |
... | @@ -2486,7 +1806,7 @@ test('resets the switching algorithm if a request times out', function() { | ... | @@ -2486,7 +1806,7 @@ test('resets the switching algorithm if a request times out', function() { |
2486 | type: 'application/vnd.apple.mpegurl' | 1806 | type: 'application/vnd.apple.mpegurl' |
2487 | }); | 1807 | }); |
2488 | openMediaSource(player); | 1808 | openMediaSource(player); |
2489 | player.tech_.hls.bandwidth = 20000; | 1809 | player.tech_.hls.bandwidth = 1e20; |
2490 | 1810 | ||
2491 | standardXHRResponse(requests.shift()); // master | 1811 | standardXHRResponse(requests.shift()); // master |
2492 | standardXHRResponse(requests.shift()); // media.m3u8 | 1812 | standardXHRResponse(requests.shift()); // media.m3u8 |
... | @@ -2667,6 +1987,7 @@ test('tracks the bytes downloaded', function() { | ... | @@ -2667,6 +1987,7 @@ test('tracks the bytes downloaded', function() { |
2667 | // transmit some segment bytes | 1987 | // transmit some segment bytes |
2668 | requests[0].response = new ArrayBuffer(17); | 1988 | requests[0].response = new ArrayBuffer(17); |
2669 | requests.shift().respond(200, null, ''); | 1989 | requests.shift().respond(200, null, ''); |
1990 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2670 | 1991 | ||
2671 | strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received'); | 1992 | strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received'); |
2672 | 1993 | ||
... | @@ -2721,12 +2042,15 @@ test('can be disposed before finishing initialization', function() { | ... | @@ -2721,12 +2042,15 @@ test('can be disposed before finishing initialization', function() { |
2721 | }); | 2042 | }); |
2722 | 2043 | ||
2723 | test('calls ended() on the media source at the end of a playlist', function() { | 2044 | test('calls ended() on the media source at the end of a playlist', function() { |
2724 | var endOfStreams = 0; | 2045 | var endOfStreams = 0, buffered = [[]]; |
2725 | player.src({ | 2046 | player.src({ |
2726 | src: 'http://example.com/media.m3u8', | 2047 | src: 'http://example.com/media.m3u8', |
2727 | type: 'application/vnd.apple.mpegurl' | 2048 | type: 'application/vnd.apple.mpegurl' |
2728 | }); | 2049 | }); |
2729 | openMediaSource(player); | 2050 | openMediaSource(player); |
2051 | player.tech_.buffered = function() { | ||
2052 | return videojs.createTimeRanges(buffered); | ||
2053 | }; | ||
2730 | player.tech_.hls.mediaSource.endOfStream = function() { | 2054 | player.tech_.hls.mediaSource.endOfStream = function() { |
2731 | endOfStreams++; | 2055 | endOfStreams++; |
2732 | }; | 2056 | }; |
... | @@ -2741,70 +2065,61 @@ test('calls ended() on the media source at the end of a playlist', function() { | ... | @@ -2741,70 +2065,61 @@ test('calls ended() on the media source at the end of a playlist', function() { |
2741 | requests.shift().respond(200, null, ''); | 2065 | requests.shift().respond(200, null, ''); |
2742 | strictEqual(endOfStreams, 0, 'waits for the buffer update to finish'); | 2066 | strictEqual(endOfStreams, 0, 'waits for the buffer update to finish'); |
2743 | 2067 | ||
2068 | buffered =[[0, 10]]; | ||
2744 | player.tech_.hls.sourceBuffer.trigger('updateend'); | 2069 | player.tech_.hls.sourceBuffer.trigger('updateend'); |
2745 | strictEqual(endOfStreams, 1, 'ended media source'); | 2070 | strictEqual(endOfStreams, 1, 'ended media source'); |
2746 | }); | 2071 | }); |
2747 | 2072 | ||
2748 | test('calling play() at the end of a video resets the media index', function() { | 2073 | test('calling play() at the end of a video replays', function() { |
2074 | var seekTime = -1; | ||
2749 | player.src({ | 2075 | player.src({ |
2750 | src: 'http://example.com/media.m3u8', | 2076 | src: 'http://example.com/media.m3u8', |
2751 | type: 'application/vnd.apple.mpegurl' | 2077 | type: 'application/vnd.apple.mpegurl' |
2752 | }); | 2078 | }); |
2753 | openMediaSource(player); | 2079 | openMediaSource(player); |
2080 | player.tech_.setCurrentTime = function(time) { | ||
2081 | if (time !== undefined) { | ||
2082 | seekTime = time; | ||
2083 | } | ||
2084 | return 0; | ||
2085 | }; | ||
2754 | requests.shift().respond(200, null, | 2086 | requests.shift().respond(200, null, |
2755 | '#EXTM3U\n' + | 2087 | '#EXTM3U\n' + |
2756 | '#EXTINF:10,\n' + | 2088 | '#EXTINF:10,\n' + |
2757 | '0.ts\n' + | 2089 | '0.ts\n' + |
2758 | '#EXT-X-ENDLIST\n'); | 2090 | '#EXT-X-ENDLIST\n'); |
2759 | standardXHRResponse(requests.shift()); | 2091 | standardXHRResponse(requests.shift()); |
2760 | |||
2761 | strictEqual(player.tech_.hls.mediaIndex, 1, 'index is 1 after the first segment'); | ||
2762 | player.tech_.ended = function() { | 2092 | player.tech_.ended = function() { |
2763 | return true; | 2093 | return true; |
2764 | }; | 2094 | }; |
2765 | 2095 | ||
2766 | player.tech_.trigger('play'); | 2096 | player.tech_.trigger('play'); |
2767 | strictEqual(player.tech_.hls.mediaIndex, 0, 'index is 0 after the first segment'); | 2097 | equal(seekTime, 0, 'seeked to the beginning'); |
2768 | }); | 2098 | }); |
2769 | 2099 | ||
2770 | test('drainBuffer will not proceed with empty source buffer', function() { | 2100 | test('segments remain pending without a source buffer', function() { |
2771 | var oldMedia, newMedia, compareBuffer; | ||
2772 | player.src({ | 2101 | player.src({ |
2773 | src: 'https://example.com/encrypted-media.m3u8', | 2102 | src: 'https://example.com/encrypted-media.m3u8', |
2774 | type: 'application/vnd.apple.mpegurl' | 2103 | type: 'application/vnd.apple.mpegurl' |
2775 | }); | 2104 | }); |
2776 | openMediaSource(player); | 2105 | openMediaSource(player); |
2777 | 2106 | ||
2778 | oldMedia = player.tech_.hls.playlists.media; | 2107 | requests.shift().respond(200, null, |
2779 | newMedia = {segments: [{ | 2108 | '#EXTM3U\n' + |
2780 | key: { | 2109 | '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=52"\n' + |
2781 | 'retries': 5 | 2110 | '#EXTINF:10,\n' + |
2782 | }, | 2111 | 'http://media.example.com/fileSequence52-A.ts' + |
2783 | uri: 'http://media.example.com/fileSequence52-A.ts' | 2112 | '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=53"\n' + |
2784 | }, { | 2113 | '#EXTINF:10,\n' + |
2785 | key: { | 2114 | 'http://media.example.com/fileSequence53-B.ts\n' + |
2786 | 'method': 'AES-128', | 2115 | '#EXT-X-ENDLIST\n'); |
2787 | 'uri': 'https://priv.example.com/key.php?r=53' | ||
2788 | }, | ||
2789 | uri: 'http://media.example.com/fileSequence53-B.ts' | ||
2790 | }]}; | ||
2791 | player.tech_.hls.playlists.media = function() { | ||
2792 | return newMedia; | ||
2793 | }; | ||
2794 | 2116 | ||
2795 | player.tech_.hls.sourceBuffer = undefined; | 2117 | player.tech_.hls.sourceBuffer = undefined; |
2796 | compareBuffer = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}]; | ||
2797 | player.tech_.hls.segmentBuffer_ = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}]; | ||
2798 | |||
2799 | player.tech_.hls.drainBuffer(); | ||
2800 | |||
2801 | /* Normally, drainBuffer() calls segmentBuffer.shift(), removing a segment from the stack. | ||
2802 | * Comparing two buffers to ensure no segment was popped verifies that we returned early | ||
2803 | * from drainBuffer() because sourceBuffer was empty. | ||
2804 | */ | ||
2805 | deepEqual(player.tech_.hls.segmentBuffer_, compareBuffer, 'playlist remains unchanged'); | ||
2806 | 2118 | ||
2807 | player.tech_.hls.playlists.media = oldMedia; | 2119 | standardXHRResponse(requests.shift()); // key |
2120 | standardXHRResponse(requests.shift()); // segment | ||
2121 | player.tech_.hls.checkBuffer_(); | ||
2122 | ok(player.tech_.hls.pendingSegment_, 'waiting for the source buffer'); | ||
2808 | }); | 2123 | }); |
2809 | 2124 | ||
2810 | test('keys are requested when an encrypted segment is loaded', function() { | 2125 | test('keys are requested when an encrypted segment is loaded', function() { |
... | @@ -2815,12 +2130,14 @@ test('keys are requested when an encrypted segment is loaded', function() { | ... | @@ -2815,12 +2130,14 @@ test('keys are requested when an encrypted segment is loaded', function() { |
2815 | openMediaSource(player); | 2130 | openMediaSource(player); |
2816 | player.tech_.trigger('play'); | 2131 | player.tech_.trigger('play'); |
2817 | standardXHRResponse(requests.shift()); // playlist | 2132 | standardXHRResponse(requests.shift()); // playlist |
2818 | standardXHRResponse(requests.shift()); // first segment | ||
2819 | 2133 | ||
2820 | strictEqual(requests.length, 1, 'a key XHR is created'); | 2134 | strictEqual(requests.length, 2, 'a key XHR is created'); |
2821 | strictEqual(requests[0].url, | 2135 | strictEqual(requests[0].url, |
2822 | player.tech_.hls.playlists.media().segments[0].key.uri, | 2136 | player.tech_.hls.playlists.media().segments[0].key.uri, |
2823 | 'a key XHR is created with correct uri'); | 2137 | 'key XHR is created with correct uri'); |
2138 | strictEqual(requests[1].url, | ||
2139 | player.tech_.hls.playlists.media().segments[0].uri, | ||
2140 | 'segment XHR is created with correct uri'); | ||
2824 | }); | 2141 | }); |
2825 | 2142 | ||
2826 | test('keys are resolved relative to the master playlist', function() { | 2143 | test('keys are resolved relative to the master playlist', function() { |
... | @@ -2841,10 +2158,9 @@ test('keys are resolved relative to the master playlist', function() { | ... | @@ -2841,10 +2158,9 @@ test('keys are resolved relative to the master playlist', function() { |
2841 | '#EXTINF:2.833,\n' + | 2158 | '#EXTINF:2.833,\n' + |
2842 | 'http://media.example.com/fileSequence1.ts\n' + | 2159 | 'http://media.example.com/fileSequence1.ts\n' + |
2843 | '#EXT-X-ENDLIST\n'); | 2160 | '#EXT-X-ENDLIST\n'); |
2844 | 2161 | equal(requests.length, 2, 'requested the key'); | |
2845 | standardXHRResponse(requests.shift()); | 2162 | equal(requests[0].url, |
2846 | equal(requests.length, 1, 'requested the key'); | 2163 | absoluteUrl('video/playlist/keys/key.php'), |
2847 | ok((/video\/playlist\/keys\/key\.php$/).test(requests[0].url), | ||
2848 | 'resolves multiple relative paths'); | 2164 | 'resolves multiple relative paths'); |
2849 | }); | 2165 | }); |
2850 | 2166 | ||
... | @@ -2861,13 +2177,13 @@ test('keys are resolved relative to their containing playlist', function() { | ... | @@ -2861,13 +2177,13 @@ test('keys are resolved relative to their containing playlist', function() { |
2861 | '#EXTINF:2.833,\n' + | 2177 | '#EXTINF:2.833,\n' + |
2862 | 'http://media.example.com/fileSequence1.ts\n' + | 2178 | 'http://media.example.com/fileSequence1.ts\n' + |
2863 | '#EXT-X-ENDLIST\n'); | 2179 | '#EXT-X-ENDLIST\n'); |
2864 | standardXHRResponse(requests.shift()); | 2180 | equal(requests.length, 2, 'requested a key'); |
2865 | equal(requests.length, 1, 'requested a key'); | 2181 | equal(requests[0].url, |
2866 | ok((/video\/keys\/key\.php$/).test(requests[0].url), | 2182 | absoluteUrl('video/keys/key.php'), |
2867 | 'resolves multiple relative paths'); | 2183 | 'resolves multiple relative paths'); |
2868 | }); | 2184 | }); |
2869 | 2185 | ||
2870 | test('a new key XHR is created when a the segment is received', function() { | 2186 | test('a new key XHR is created when a the segment is requested', function() { |
2871 | player.src({ | 2187 | player.src({ |
2872 | src: 'https://example.com/encrypted-media.m3u8', | 2188 | src: 'https://example.com/encrypted-media.m3u8', |
2873 | type: 'application/vnd.apple.mpegurl' | 2189 | type: 'application/vnd.apple.mpegurl' |
... | @@ -2884,15 +2200,17 @@ test('a new key XHR is created when a the segment is received', function() { | ... | @@ -2884,15 +2200,17 @@ test('a new key XHR is created when a the segment is received', function() { |
2884 | '#EXTINF:2.833,\n' + | 2200 | '#EXTINF:2.833,\n' + |
2885 | 'http://media.example.com/fileSequence2.ts\n' + | 2201 | 'http://media.example.com/fileSequence2.ts\n' + |
2886 | '#EXT-X-ENDLIST\n'); | 2202 | '#EXT-X-ENDLIST\n'); |
2887 | standardXHRResponse(requests.shift()); // segment 1 | ||
2888 | standardXHRResponse(requests.shift()); // key 1 | 2203 | standardXHRResponse(requests.shift()); // key 1 |
2204 | standardXHRResponse(requests.shift()); // segment 1 | ||
2889 | // "finish" decrypting segment 1 | 2205 | // "finish" decrypting segment 1 |
2890 | player.tech_.hls.segmentBuffer_[0].bytes = new Uint8Array(16); | 2206 | player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16); |
2891 | player.tech_.hls.checkBuffer_(); | 2207 | player.tech_.hls.checkBuffer_(); |
2208 | player.tech_.buffered = function() { | ||
2209 | return videojs.createTimeRange(0, 2.833); | ||
2210 | }; | ||
2211 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2892 | 2212 | ||
2893 | standardXHRResponse(requests.shift()); // segment 2 | 2213 | strictEqual(requests.length, 2, 'a key XHR is created'); |
2894 | |||
2895 | strictEqual(requests.length, 1, 'a key XHR is created'); | ||
2896 | strictEqual(requests[0].url, | 2214 | strictEqual(requests[0].url, |
2897 | 'https://example.com/' + | 2215 | 'https://example.com/' + |
2898 | player.tech_.hls.playlists.media().segments[1].key.uri, | 2216 | player.tech_.hls.playlists.media().segments[1].key.uri, |
... | @@ -2916,16 +2234,14 @@ test('seeking should abort an outstanding key request and create a new one', fun | ... | @@ -2916,16 +2234,14 @@ test('seeking should abort an outstanding key request and create a new one', fun |
2916 | '#EXTINF:9,\n' + | 2234 | '#EXTINF:9,\n' + |
2917 | 'http://media.example.com/fileSequence2.ts\n' + | 2235 | 'http://media.example.com/fileSequence2.ts\n' + |
2918 | '#EXT-X-ENDLIST\n'); | 2236 | '#EXT-X-ENDLIST\n'); |
2919 | standardXHRResponse(requests.shift()); // segment 1 | 2237 | standardXHRResponse(requests.pop()); // segment 1 |
2920 | 2238 | ||
2921 | player.currentTime(11); | 2239 | player.currentTime(11); |
2922 | clock.tick(1); | 2240 | clock.tick(1); |
2923 | ok(requests[0].aborted, 'the key XHR should be aborted'); | 2241 | ok(requests[0].aborted, 'the key XHR should be aborted'); |
2924 | requests.shift(); // aborted key 1 | 2242 | requests.shift(); // aborted key 1 |
2925 | 2243 | ||
2926 | equal(requests.length, 1, 'requested the new segment'); | 2244 | equal(requests.length, 2, 'requested the new key'); |
2927 | standardXHRResponse(requests.shift()); // segment 2 | ||
2928 | equal(requests.length, 1, 'requested the new key'); | ||
2929 | equal(requests[0].url, | 2245 | equal(requests[0].url, |
2930 | 'https://example.com/' + | 2246 | 'https://example.com/' + |
2931 | player.tech_.hls.playlists.media().segments[1].key.uri, | 2247 | player.tech_.hls.playlists.media().segments[1].key.uri, |
... | @@ -2948,7 +2264,7 @@ test('retries key requests once upon failure', function() { | ... | @@ -2948,7 +2264,7 @@ test('retries key requests once upon failure', function() { |
2948 | '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' + | 2264 | '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' + |
2949 | '#EXTINF:15.0,\n' + | 2265 | '#EXTINF:15.0,\n' + |
2950 | 'http://media.example.com/fileSequence53-A.ts\n'); | 2266 | 'http://media.example.com/fileSequence53-A.ts\n'); |
2951 | standardXHRResponse(requests.shift()); // segment | 2267 | standardXHRResponse(requests.pop()); // segment |
2952 | requests[0].respond(404); | 2268 | requests[0].respond(404); |
2953 | equal(requests.length, 2, 'create a new XHR for the same key'); | 2269 | equal(requests.length, 2, 'create a new XHR for the same key'); |
2954 | equal(requests[1].url, requests[0].url, 'should be the same key'); | 2270 | equal(requests[1].url, requests[0].url, 'should be the same key'); |
... | @@ -2957,7 +2273,7 @@ test('retries key requests once upon failure', function() { | ... | @@ -2957,7 +2273,7 @@ test('retries key requests once upon failure', function() { |
2957 | equal(requests.length, 2, 'gives up after one retry'); | 2273 | equal(requests.length, 2, 'gives up after one retry'); |
2958 | }); | 2274 | }); |
2959 | 2275 | ||
2960 | test('skip segments if key requests fail more than once', function() { | 2276 | test('errors if key requests fail more than once', function() { |
2961 | var bytes = []; | 2277 | var bytes = []; |
2962 | 2278 | ||
2963 | player.src({ | 2279 | player.src({ |
... | @@ -2978,23 +2294,14 @@ test('skip segments if key requests fail more than once', function() { | ... | @@ -2978,23 +2294,14 @@ test('skip segments if key requests fail more than once', function() { |
2978 | player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) { | 2294 | player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) { |
2979 | bytes.push(chunk); | 2295 | bytes.push(chunk); |
2980 | }; | 2296 | }; |
2981 | standardXHRResponse(requests.shift()); // segment 1 | 2297 | standardXHRResponse(requests.pop()); // segment 1 |
2982 | requests.shift().respond(404); // fail key | 2298 | requests.shift().respond(404); // fail key |
2983 | requests.shift().respond(404); // fail key, again | 2299 | requests.shift().respond(404); // fail key, again |
2984 | |||
2985 | player.tech_.hls.checkBuffer_(); | 2300 | player.tech_.hls.checkBuffer_(); |
2986 | standardXHRResponse(requests.shift()); // segment 2 | ||
2987 | equal(bytes.length, 0, 'did not append encrypted bytes'); | ||
2988 | 2301 | ||
2989 | // key for second segment | 2302 | equal(player.tech_.hls.mediaSource.error_, |
2990 | requests[0].response = new Uint32Array([0,0,0,0]).buffer; | 2303 | 'network', |
2991 | requests.shift().respond(200, null, ''); | 2304 | 'triggered a network error'); |
2992 | // "finish" decryption | ||
2993 | player.tech_.hls.segmentBuffer_[0].bytes = new Uint8Array(16); | ||
2994 | player.tech_.hls.checkBuffer_(); | ||
2995 | |||
2996 | equal(bytes.length, 1, 'appended cleartext bytes from the second segment'); | ||
2997 | deepEqual(bytes[0], new Uint8Array(16), 'appended bytes from the second segment, not the first'); | ||
2998 | }); | 2305 | }); |
2999 | 2306 | ||
3000 | test('the key is supplied to the decrypter in the correct format', function() { | 2307 | test('the key is supplied to the decrypter in the correct format', function() { |
... | @@ -3016,12 +2323,11 @@ test('the key is supplied to the decrypter in the correct format', function() { | ... | @@ -3016,12 +2323,11 @@ test('the key is supplied to the decrypter in the correct format', function() { |
3016 | '#EXTINF:15.0,\n' + | 2323 | '#EXTINF:15.0,\n' + |
3017 | 'http://media.example.com/fileSequence52-B.ts\n'); | 2324 | 'http://media.example.com/fileSequence52-B.ts\n'); |
3018 | 2325 | ||
3019 | |||
3020 | videojs.Hls.Decrypter = function(encrypted, key) { | 2326 | videojs.Hls.Decrypter = function(encrypted, key) { |
3021 | keys.push(key); | 2327 | keys.push(key); |
3022 | }; | 2328 | }; |
3023 | 2329 | ||
3024 | standardXHRResponse(requests.shift()); // segment | 2330 | standardXHRResponse(requests.pop()); // segment |
3025 | requests[0].response = new Uint32Array([0,1,2,3]).buffer; | 2331 | requests[0].response = new Uint32Array([0,1,2,3]).buffer; |
3026 | requests[0].respond(200, null, ''); | 2332 | requests[0].respond(200, null, ''); |
3027 | requests.shift(); // key | 2333 | requests.shift(); // key |
... | @@ -3068,6 +2374,7 @@ test('supplies the media sequence of current segment as the IV by default, if no | ... | @@ -3068,6 +2374,7 @@ test('supplies the media sequence of current segment as the IV by default, if no |
3068 | }); | 2374 | }); |
3069 | 2375 | ||
3070 | test('switching playlists with an outstanding key request does not stall playback', function() { | 2376 | test('switching playlists with an outstanding key request does not stall playback', function() { |
2377 | var buffered = []; | ||
3071 | var media = '#EXTM3U\n' + | 2378 | var media = '#EXTM3U\n' + |
3072 | '#EXT-X-MEDIA-SEQUENCE:5\n' + | 2379 | '#EXT-X-MEDIA-SEQUENCE:5\n' + |
3073 | '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' + | 2380 | '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' + |
... | @@ -3082,31 +2389,37 @@ test('switching playlists with an outstanding key request does not stall playbac | ... | @@ -3082,31 +2389,37 @@ test('switching playlists with an outstanding key request does not stall playbac |
3082 | openMediaSource(player); | 2389 | openMediaSource(player); |
3083 | player.tech_.trigger('play'); | 2390 | player.tech_.trigger('play'); |
3084 | 2391 | ||
2392 | player.tech_.hls.bandwidth = 1; | ||
2393 | player.tech_.buffered = function() { | ||
2394 | return videojs.createTimeRange(buffered); | ||
2395 | }; | ||
3085 | // master playlist | 2396 | // master playlist |
3086 | standardXHRResponse(requests.shift()); | 2397 | standardXHRResponse(requests.shift()); |
3087 | // media playlist | 2398 | // media playlist |
3088 | requests.shift().respond(200, null, media); | 2399 | requests.shift().respond(200, null, media); |
3089 | // mock out media switching from this point on | 2400 | // mock out media switching from this point on |
3090 | player.tech_.hls.playlists.media = function() { | 2401 | player.tech_.hls.playlists.media = function() { |
3091 | return player.tech_.hls.playlists.master.playlists[0]; | 2402 | return player.tech_.hls.playlists.master.playlists[1]; |
3092 | }; | 2403 | }; |
3093 | // first segment of the original media playlist | 2404 | // first segment of the original media playlist |
3094 | standardXHRResponse(requests.shift()); | 2405 | standardXHRResponse(requests.pop()); |
3095 | // don't respond to the initial key request | ||
3096 | requests.shift(); | ||
3097 | 2406 | ||
3098 | // "switch" media | 2407 | // "switch" media |
3099 | player.tech_.hls.playlists.trigger('mediachange'); | 2408 | player.tech_.hls.playlists.trigger('mediachange'); |
2409 | ok(!requests[0].aborted, 'did not abort the key request'); | ||
3100 | 2410 | ||
2411 | // "finish" decrypting segment 1 | ||
2412 | standardXHRResponse(requests.shift()); // key | ||
2413 | player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16); | ||
2414 | player.tech_.hls.checkBuffer_(); | ||
2415 | buffered = [[0, 2.833]]; | ||
2416 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
3101 | player.tech_.hls.checkBuffer_(); | 2417 | player.tech_.hls.checkBuffer_(); |
3102 | 2418 | ||
3103 | ok(requests.length, 'made a request'); | 2419 | equal(requests.length, 1, 'made a request'); |
3104 | equal(requests[0].url, | 2420 | equal(requests[0].url, |
3105 | 'http://media.example.com/fileSequence52-B.ts', | 2421 | 'http://media.example.com/fileSequence52-B.ts', |
3106 | 'requested the segment'); | 2422 | 'requested the segment'); |
3107 | equal(requests[1].url, | ||
3108 | 'https://priv.example.com/key.php?r=52', | ||
3109 | 'requested the key'); | ||
3110 | }); | 2423 | }); |
3111 | 2424 | ||
3112 | test('resolves relative key URLs against the playlist', function() { | 2425 | test('resolves relative key URLs against the playlist', function() { |
... | @@ -3123,8 +2436,6 @@ test('resolves relative key URLs against the playlist', function() { | ... | @@ -3123,8 +2436,6 @@ test('resolves relative key URLs against the playlist', function() { |
3123 | '#EXTINF:2.833,\n' + | 2436 | '#EXTINF:2.833,\n' + |
3124 | 'http://media.example.com/fileSequence52-A.ts\n' + | 2437 | 'http://media.example.com/fileSequence52-A.ts\n' + |
3125 | '#EXT-X-ENDLIST\n'); | 2438 | '#EXT-X-ENDLIST\n'); |
3126 | standardXHRResponse(requests.shift()); // segment | ||
3127 | |||
3128 | equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL'); | 2439 | equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL'); |
3129 | }); | 2440 | }); |
3130 | 2441 | ||
... | @@ -3149,7 +2460,7 @@ test('treats invalid keys as a key request failure', function() { | ... | @@ -3149,7 +2460,7 @@ test('treats invalid keys as a key request failure', function() { |
3149 | bytes.push(chunk); | 2460 | bytes.push(chunk); |
3150 | }; | 2461 | }; |
3151 | // segment request | 2462 | // segment request |
3152 | standardXHRResponse(requests.shift()); | 2463 | standardXHRResponse(requests.pop()); |
3153 | // keys should be 16 bytes long | 2464 | // keys should be 16 bytes long |
3154 | requests[0].response = new Uint8Array(1).buffer; | 2465 | requests[0].response = new Uint8Array(1).buffer; |
3155 | requests.shift().respond(200, null, ''); | 2466 | requests.shift().respond(200, null, ''); |
... | @@ -3159,17 +2470,12 @@ test('treats invalid keys as a key request failure', function() { | ... | @@ -3159,17 +2470,12 @@ test('treats invalid keys as a key request failure', function() { |
3159 | // the retried response is invalid, too | 2470 | // the retried response is invalid, too |
3160 | requests[0].response = new Uint8Array(1); | 2471 | requests[0].response = new Uint8Array(1); |
3161 | requests.shift().respond(200, null, ''); | 2472 | requests.shift().respond(200, null, ''); |
3162 | |||
3163 | // the first segment should be dropped and playback moves on | ||
3164 | player.tech_.hls.checkBuffer_(); | 2473 | player.tech_.hls.checkBuffer_(); |
3165 | equal(bytes.length, 0, 'did not append bytes'); | ||
3166 | |||
3167 | // second segment request | ||
3168 | requests[0].response = new Uint8Array([1, 2]); | ||
3169 | requests.shift().respond(200, null, ''); | ||
3170 | 2474 | ||
3171 | equal(bytes.length, 1, 'appended bytes'); | 2475 | // two failed attempts is a network error |
3172 | deepEqual(bytes[0], new Uint8Array([1, 2]), 'skipped to the second segment'); | 2476 | equal(player.tech_.hls.mediaSource.error_, |
2477 | 'network', | ||
2478 | 'triggered a network error'); | ||
3173 | }); | 2479 | }); |
3174 | 2480 | ||
3175 | test('live stream should not call endOfStream', function(){ | 2481 | test('live stream should not call endOfStream', function(){ |
... | @@ -3209,4 +2515,49 @@ test('does not download segments if preload option set to none', function() { | ... | @@ -3209,4 +2515,49 @@ test('does not download segments if preload option set to none', function() { |
3209 | equal(requests.length, 0, 'did not download any segments'); | 2515 | equal(requests.length, 0, 'did not download any segments'); |
3210 | }); | 2516 | }); |
3211 | 2517 | ||
2518 | module('Buffer Inspection'); | ||
2519 | |||
2520 | test('detects time range edges added by updates', function() { | ||
2521 | var edges; | ||
2522 | |||
2523 | edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]), | ||
2524 | videojs.createTimeRange([[0, 11]])); | ||
2525 | deepEqual(edges, [{ end: 11 }], 'detected a forward addition'); | ||
2526 | |||
2527 | edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[5, 10]]), | ||
2528 | videojs.createTimeRange([[0, 10]])); | ||
2529 | deepEqual(edges, [{ start: 0 }], 'detected a backward addition'); | ||
2530 | |||
2531 | edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[5, 10]]), | ||
2532 | videojs.createTimeRange([[0, 11]])); | ||
2533 | deepEqual(edges, [ | ||
2534 | { start: 0 }, { end: 11 } | ||
2535 | ], 'detected forward and backward additions'); | ||
2536 | |||
2537 | edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]), | ||
2538 | videojs.createTimeRange([[0, 10]])); | ||
2539 | deepEqual(edges, [], 'detected no addition'); | ||
2540 | |||
2541 | edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([]), | ||
2542 | videojs.createTimeRange([[0, 10]])); | ||
2543 | deepEqual(edges, [ | ||
2544 | { start: 0 }, | ||
2545 | { end: 10 } | ||
2546 | ], 'detected an initial addition'); | ||
2547 | |||
2548 | edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]), | ||
2549 | videojs.createTimeRange([[0, 10], [20, 30]])); | ||
2550 | deepEqual(edges, [ | ||
2551 | { start: 20 }, | ||
2552 | { end: 30} | ||
2553 | ], 'detected a non-contiguous addition'); | ||
2554 | }); | ||
2555 | |||
2556 | test('treats null buffered ranges as no addition', function() { | ||
2557 | var edges = videojs.Hls.bufferedAdditions_(null, | ||
2558 | videojs.createTimeRange([[0, 11]])); | ||
2559 | |||
2560 | equal(edges.length, 0, 'no additions'); | ||
2561 | }); | ||
2562 | |||
3212 | })(window, window.videojs); | 2563 | })(window, window.videojs); | ... | ... |
-
Please register or sign in to post a comment