Remove expired time tracking
Media sources implicitly track expired time by retaining the mapping between presentation timestamp values and the media timeline in the buffer. That allows us to simplify a good deal of code by not tracking it ourselves. Finish updating tests to work against the new timeline start and end annotations on segments instead of the old PTS values. Remove metadata cue translation because that is now handled by contrib-media-sources. Update key fetching in HLSe to occur concurrently with the segment download. All tests are now passing.
Showing
6 changed files
with
287 additions
and
1321 deletions
... | @@ -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 | ||
... | @@ -360,43 +345,10 @@ | ... | @@ -360,43 +345,10 @@ |
360 | * @param update {object} the updated media playlist object | 345 | * @param update {object} the updated media playlist object |
361 | */ | 346 | */ |
362 | PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { | 347 | PlaylistLoader.prototype.updateMediaPlaylist_ = function(update) { |
363 | var expiredCount; | ||
364 | |||
365 | if (this.media_) { | ||
366 | expiredCount = update.mediaSequence - this.media_.mediaSequence; | ||
367 | |||
368 | // update the expired time count | ||
369 | this.expired_ += Playlist.duration(this.media_, | ||
370 | this.media_.mediaSequence, | ||
371 | update.mediaSequence); | ||
372 | } | ||
373 | |||
374 | this.media_ = this.master.playlists[update.uri]; | 348 | this.media_ = this.master.playlists[update.uri]; |
375 | }; | 349 | }; |
376 | 350 | ||
377 | /** | 351 | /** |
378 | * When switching variant playlists in a live stream, the player may | ||
379 | * discover that the new set of available segments is shifted in | ||
380 | * time relative to the old playlist. If that is the case, you can | ||
381 | * call this method to synchronize the playlist loader so that | ||
382 | * subsequent calls to getMediaIndexForTime_() return values | ||
383 | * appropriate for the new playlist. | ||
384 | * | ||
385 | * @param mediaIndex {integer} the index of the segment that will be | ||
386 | * the used to base timeline calculations on | ||
387 | * @param startTime {number} the media timeline position of the | ||
388 | * first moment of video data for the specified segment. That is, | ||
389 | * data from the specified segment will first be displayed when | ||
390 | * `currentTime` is equal to `startTime`. | ||
391 | */ | ||
392 | PlaylistLoader.prototype.updateTimelineOffset = function(mediaIndex, startingTime) { | ||
393 | var segmentOffset = Playlist.duration(this.media_, | ||
394 | this.media_.mediaSequence, | ||
395 | this.media_.mediaSequence + mediaIndex); | ||
396 | this.expired_ = startingTime - segmentOffset; | ||
397 | }; | ||
398 | |||
399 | /** | ||
400 | * Determine the index of the segment that contains a specified | 352 | * Determine the index of the segment that contains a specified |
401 | * playback position in the current media playlist. Early versions | 353 | * playback position in the current media playlist. Early versions |
402 | * of the HLS specification require segment durations to be rounded | 354 | * of the HLS specification require segment durations to be rounded |
... | @@ -423,7 +375,6 @@ | ... | @@ -423,7 +375,6 @@ |
423 | 375 | ||
424 | // when the requested position is earlier than the current set of | 376 | // when the requested position is earlier than the current set of |
425 | // segments, return the earliest segment index | 377 | // segments, return the earliest segment index |
426 | time -= this.expired_; | ||
427 | if (time < 0) { | 378 | if (time < 0) { |
428 | return 0; | 379 | return 0; |
429 | } | 380 | } |
... | @@ -447,6 +398,11 @@ | ... | @@ -447,6 +398,11 @@ |
447 | 398 | ||
448 | time -= segment.start; | 399 | time -= segment.start; |
449 | time -= segment.duration || targetDuration; | 400 | time -= segment.duration || targetDuration; |
401 | if (time < 0) { | ||
402 | // the segment with start information is also our best guess | ||
403 | // for the momment | ||
404 | return i; | ||
405 | } | ||
450 | break; | 406 | break; |
451 | } | 407 | } |
452 | } | 408 | } | ... | ... |
... | @@ -26,47 +26,46 @@ | ... | @@ -26,47 +26,46 @@ |
26 | /** | 26 | /** |
27 | * Calculate the media duration from the segments associated with a | 27 | * Calculate the media duration from the segments associated with a |
28 | * playlist. The duration of a subinterval of the available segments | 28 | * playlist. The duration of a subinterval of the available segments |
29 | * may be calculated by specifying a start and end index. | 29 | * may be calculated by specifying an end index. |
30 | * | 30 | * |
31 | * @param playlist {object} a media playlist object | 31 | * @param playlist {object} a media playlist object |
32 | * @param startSequence {number} (optional) an inclusive lower | ||
33 | * boundary for the playlist. Defaults to 0. | ||
34 | * @param endSequence {number} (optional) an exclusive upper boundary | 32 | * @param endSequence {number} (optional) an exclusive upper boundary |
35 | * for the playlist. Defaults to playlist length. | 33 | * for the playlist. Defaults to playlist length. |
36 | * @return {number} the duration between the start index and end | 34 | * @return {number} the duration between the start index and end |
37 | * index. | 35 | * index. |
38 | */ | 36 | */ |
39 | intervalDuration = function(playlist, startSequence, endSequence) { | 37 | intervalDuration = function(playlist, endSequence) { |
40 | var result = 0, targetDuration, i, start, end, expiredSegmentCount; | 38 | var result = 0, segment, targetDuration, i; |
41 | 39 | ||
42 | if (startSequence === undefined) { | ||
43 | startSequence = playlist.mediaSequence || 0; | ||
44 | } | ||
45 | if (endSequence === undefined) { | 40 | if (endSequence === undefined) { |
46 | endSequence = startSequence + (playlist.segments || []).length; | 41 | endSequence = playlist.mediaSequence + (playlist.segments || []).length; |
42 | } | ||
43 | if (endSequence < 0) { | ||
44 | return 0; | ||
47 | } | 45 | } |
48 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | 46 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; |
49 | 47 | ||
50 | // accumulate while looking for the latest known segment-timeline mapping | 48 | i = endSequence - playlist.mediaSequence; |
51 | expiredSegmentCount = optionalMax(playlist.mediaSequence - startSequence, 0); | 49 | // if a start time is available for segment immediately following |
52 | start = startSequence + expiredSegmentCount - playlist.mediaSequence; | 50 | // the interval, use it |
53 | end = endSequence - playlist.mediaSequence; | 51 | segment = playlist.segments[i]; |
54 | for (i = end - 1; i >= start; i--) { | 52 | // Walk backward until we find the latest segment with timeline |
55 | if (playlist.segments[i].end !== undefined) { | 53 | // information that is earlier than endSequence |
56 | result += playlist.segments[i].end; | 54 | if (segment && segment.start !== undefined) { |
57 | return result; | 55 | return segment.start; |
56 | } | ||
57 | while (i--) { | ||
58 | segment = playlist.segments[i]; | ||
59 | if (segment.end !== undefined) { | ||
60 | return result + segment.end; | ||
58 | } | 61 | } |
59 | 62 | ||
60 | result += playlist.segments[i].duration || targetDuration; | 63 | result += (segment.duration || targetDuration); |
61 | 64 | ||
62 | if (playlist.segments[i].start !== undefined) { | 65 | if (segment.start !== undefined) { |
63 | result += playlist.segments[i].start; | 66 | return result + segment.start; |
64 | return result; | ||
65 | } | 67 | } |
66 | } | 68 | } |
67 | // neither a start or end time was found in the interval so we | ||
68 | // have to estimate the expired duration | ||
69 | result += expiredSegmentCount * targetDuration; | ||
70 | return result; | 69 | return result; |
71 | }; | 70 | }; |
72 | 71 | ||
... | @@ -76,17 +75,16 @@ | ... | @@ -76,17 +75,16 @@ |
76 | * timeline between those two indices. The total duration for live | 75 | * timeline between those two indices. The total duration for live |
77 | * playlists is always Infinity. | 76 | * playlists is always Infinity. |
78 | * @param playlist {object} a media playlist object | 77 | * @param playlist {object} a media playlist object |
79 | * @param startSequence {number} (optional) an inclusive lower | 78 | * @param endSequence {number} (optional) an exclusive upper |
80 | * boundary for the playlist. Defaults to 0. | 79 | * boundary for the playlist. Defaults to the playlist media |
81 | * @param endSequence {number} (optional) an exclusive upper boundary | 80 | * sequence number plus its length. |
82 | * for the playlist. Defaults to playlist length. | 81 | * @param includeTrailingTime {boolean} (optional) if false, the |
83 | * @param includeTrailingTime {boolean} (optional) if false, the interval between | 82 | * interval between the final segment and the subsequent segment |
84 | * the final segment and the subsequent segment will not be included | 83 | * will not be included in the result |
85 | * in the result | ||
86 | * @return {number} the duration between the start index and end | 84 | * @return {number} the duration between the start index and end |
87 | * index. | 85 | * index. |
88 | */ | 86 | */ |
89 | duration = function(playlist, startSequence, endSequence, includeTrailingTime) { | 87 | duration = function(playlist, endSequence, includeTrailingTime) { |
90 | if (!playlist) { | 88 | if (!playlist) { |
91 | return 0; | 89 | return 0; |
92 | } | 90 | } |
... | @@ -97,7 +95,7 @@ | ... | @@ -97,7 +95,7 @@ |
97 | 95 | ||
98 | // if a slice of the total duration is not requested, use | 96 | // if a slice of the total duration is not requested, use |
99 | // playlist-level duration indicators when they're present | 97 | // playlist-level duration indicators when they're present |
100 | if (startSequence === undefined && endSequence === undefined) { | 98 | if (endSequence === undefined) { |
101 | // if present, use the duration specified in the playlist | 99 | // if present, use the duration specified in the playlist |
102 | if (playlist.totalDuration) { | 100 | if (playlist.totalDuration) { |
103 | return playlist.totalDuration; | 101 | return playlist.totalDuration; |
... | @@ -111,7 +109,6 @@ | ... | @@ -111,7 +109,6 @@ |
111 | 109 | ||
112 | // calculate the total duration based on the segment durations | 110 | // calculate the total duration based on the segment durations |
113 | return intervalDuration(playlist, | 111 | return intervalDuration(playlist, |
114 | startSequence, | ||
115 | endSequence, | 112 | endSequence, |
116 | includeTrailingTime); | 113 | includeTrailingTime); |
117 | }; | 114 | }; |
... | @@ -128,7 +125,7 @@ | ... | @@ -128,7 +125,7 @@ |
128 | * for seeking | 125 | * for seeking |
129 | */ | 126 | */ |
130 | seekable = function(playlist) { | 127 | seekable = function(playlist) { |
131 | var start, end, liveBuffer, targetDuration, segment, pending, i; | 128 | var start, end; |
132 | 129 | ||
133 | // without segments, there are no seekable ranges | 130 | // without segments, there are no seekable ranges |
134 | if (!playlist.segments) { | 131 | if (!playlist.segments) { |
... | @@ -139,33 +136,14 @@ | ... | @@ -139,33 +136,14 @@ |
139 | return videojs.createTimeRange(0, duration(playlist)); | 136 | return videojs.createTimeRange(0, duration(playlist)); |
140 | } | 137 | } |
141 | 138 | ||
142 | start = 0; | ||
143 | end = intervalDuration(playlist, | ||
144 | playlist.mediaSequence, | ||
145 | playlist.mediaSequence + playlist.segments.length); | ||
146 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | ||
147 | |||
148 | // live playlists should not expose three segment durations worth | 139 | // live playlists should not expose three segment durations worth |
149 | // of content from the end of the playlist | 140 | // of content from the end of the playlist |
150 | // 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 |
151 | if (!playlist.endList) { | 142 | start = intervalDuration(playlist, playlist.mediaSequence); |
152 | liveBuffer = targetDuration * 3; | 143 | end = intervalDuration(playlist, |
153 | // walk backward from the last available segment and track how | 144 | playlist.mediaSequence + playlist.segments.length); |
154 | // much media time has elapsed until three target durations have | 145 | end -= (playlist.targetDuration || DEFAULT_TARGET_DURATION) * 3; |
155 | // been traversed. if a segment is part of the interval being | 146 | end = Math.max(0, end); |
156 | // reported, subtract the overlapping portion of its duration | ||
157 | // from the result. | ||
158 | for (i = playlist.segments.length - 1; i >= 0 && liveBuffer > 0; i--) { | ||
159 | segment = playlist.segments[i]; | ||
160 | pending = optionalMin(duration(playlist, | ||
161 | playlist.mediaSequence + i, | ||
162 | playlist.mediaSequence + i + 1), | ||
163 | liveBuffer); | ||
164 | liveBuffer -= pending; | ||
165 | end -= pending; | ||
166 | } | ||
167 | } | ||
168 | |||
169 | return videojs.createTimeRange(start, end); | 147 | return videojs.createTimeRange(start, end); |
170 | }; | 148 | }; |
171 | 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_(); |
... | @@ -140,11 +138,6 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -140,11 +138,6 @@ videojs.Hls.prototype.src = function(src) { |
140 | } | 138 | } |
141 | 139 | ||
142 | this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); | 140 | this.mediaSource = new videojs.MediaSource({ mode: this.mode_ }); |
143 | this.segmentBuffer_ = []; | ||
144 | |||
145 | // if the stream contains ID3 metadata, expose that as a metadata | ||
146 | // text track | ||
147 | //this.setupMetadataCueTranslation_(); | ||
148 | 141 | ||
149 | // load the MediaSource into the player | 142 | // load the MediaSource into the player |
150 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); | 143 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); |
... | @@ -197,17 +190,13 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -197,17 +190,13 @@ videojs.Hls.prototype.src = function(src) { |
197 | 190 | ||
198 | this.updateDuration(this.playlists.media()); | 191 | this.updateDuration(this.playlists.media()); |
199 | oldMediaPlaylist = updatedPlaylist; | 192 | oldMediaPlaylist = updatedPlaylist; |
200 | |||
201 | this.fetchKeys_(); | ||
202 | }.bind(this)); | 193 | }.bind(this)); |
203 | 194 | ||
204 | this.playlists.on('mediachange', function() { | 195 | this.playlists.on('mediachange', function() { |
205 | // abort outstanding key requests and check if new keys need to be retrieved | 196 | this.tech_.trigger({ |
206 | if (keyXhr) { | 197 | type: 'mediachange', |
207 | this.cancelKeyXhr(); | 198 | bubbles: true |
208 | } | 199 | }); |
209 | |||
210 | this.tech_.trigger({ type: 'mediachange', bubbles: true }); | ||
211 | }.bind(this)); | 200 | }.bind(this)); |
212 | 201 | ||
213 | // do nothing if the tech has been disposed already | 202 | // do nothing if the tech has been disposed already |
... | @@ -219,26 +208,6 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -219,26 +208,6 @@ videojs.Hls.prototype.src = function(src) { |
219 | this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); | 208 | this.tech_.src(videojs.URL.createObjectURL(this.mediaSource)); |
220 | }; | 209 | }; |
221 | 210 | ||
222 | /* Returns the media index for the live point in the current playlist, and updates | ||
223 | the current time to go along with it. | ||
224 | */ | ||
225 | videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) { | ||
226 | if (!selectedPlaylist.segments) { | ||
227 | return 0; | ||
228 | } | ||
229 | |||
230 | var tailIterator = selectedPlaylist.segments.length, | ||
231 | tailDuration = 0, | ||
232 | targetTail = (selectedPlaylist.targetDuration || 10) * 3; | ||
233 | |||
234 | while (tailDuration < targetTail && tailIterator > 0) { | ||
235 | tailDuration += selectedPlaylist.segments[tailIterator - 1].duration; | ||
236 | tailIterator--; | ||
237 | } | ||
238 | |||
239 | return tailIterator; | ||
240 | }; | ||
241 | |||
242 | videojs.Hls.prototype.handleSourceOpen = function() { | 211 | videojs.Hls.prototype.handleSourceOpen = function() { |
243 | this.setupSourceBuffer_(); | 212 | this.setupSourceBuffer_(); |
244 | 213 | ||
... | @@ -324,7 +293,7 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { | ... | @@ -324,7 +293,7 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { |
324 | // transition the sourcebuffer to the ended state if we've hit the end of | 293 | // transition the sourcebuffer to the ended state if we've hit the end of |
325 | // the playlist | 294 | // the playlist |
326 | this.sourceBuffer.addEventListener('updateend', function() { | 295 | this.sourceBuffer.addEventListener('updateend', function() { |
327 | var segmentInfo = this.pendingSegment_, segment, i, currentBuffered, timelineUpdates; | 296 | var segmentInfo = this.pendingSegment_, segment, currentBuffered, timelineUpdates; |
328 | 297 | ||
329 | this.pendingSegment_ = null; | 298 | this.pendingSegment_ = null; |
330 | 299 | ||
... | @@ -334,24 +303,7 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { | ... | @@ -334,24 +303,7 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { |
334 | this.mediaSource.endOfStream(); | 303 | this.mediaSource.endOfStream(); |
335 | } | 304 | } |
336 | 305 | ||
337 | // When switching renditions or seeking, we may misjudge the media | 306 | // stop here if the update errored or was aborted |
338 | // index to request to continue playback. Check after each append | ||
339 | // that a gap hasn't appeared in the buffered region and adjust | ||
340 | // the media index to fill it if necessary | ||
341 | if (this.tech_.buffered().length === 2 && | ||
342 | segmentInfo.playlist === this.playlists.media()) { | ||
343 | i = this.tech_.buffered().length; | ||
344 | while (i--) { | ||
345 | if (this.tech_.currentTime() < this.tech_.buffered().start(i)) { | ||
346 | // found the misidentified segment's buffered time range | ||
347 | // adjust the media index to fill the gap | ||
348 | this.playlists.updateTimelineOffset(segmentInfo.mediaIndex, | ||
349 | this.tech_.buffered().start(i)); | ||
350 | break; | ||
351 | } | ||
352 | } | ||
353 | } | ||
354 | |||
355 | if (!segmentInfo) { | 307 | if (!segmentInfo) { |
356 | return; | 308 | return; |
357 | } | 309 | } |
... | @@ -370,90 +322,13 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { | ... | @@ -370,90 +322,13 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { |
370 | } | 322 | } |
371 | }); | 323 | }); |
372 | 324 | ||
373 | }.bind(this)); | 325 | if (timelineUpdates.length) { |
374 | }; | 326 | this.updateDuration(segmentInfo.playlist); |
375 | |||
376 | // register event listeners to transform in-band metadata events into | ||
377 | // VTTCues on a text track | ||
378 | videojs.Hls.prototype.setupMetadataCueTranslation_ = function() { | ||
379 | var | ||
380 | metadataStream = this.segmentParser_.metadataStream, | ||
381 | textTrack; | ||
382 | |||
383 | // add a metadata cue whenever a metadata event is triggered during | ||
384 | // segment parsing | ||
385 | metadataStream.on('data', function(metadata) { | ||
386 | var i, hexDigit; | ||
387 | |||
388 | // create the metadata track if this is the first ID3 tag we've | ||
389 | // seen | ||
390 | if (!textTrack) { | ||
391 | textTrack = this.tech_.addTextTrack('metadata', 'Timed Metadata'); | ||
392 | |||
393 | // build the dispatch type from the stream descriptor | ||
394 | // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track | ||
395 | textTrack.inBandMetadataTrackDispatchType = videojs.Hls.SegmentParser.STREAM_TYPES.metadata.toString(16).toUpperCase(); | ||
396 | for (i = 0; i < metadataStream.descriptor.length; i++) { | ||
397 | hexDigit = ('00' + metadataStream.descriptor[i].toString(16).toUpperCase()).slice(-2); | ||
398 | textTrack.inBandMetadataTrackDispatchType += hexDigit; | ||
399 | } | ||
400 | } | 327 | } |
401 | 328 | ||
402 | // store this event for processing once the muxing has finished | 329 | // check if it's time to download the next segment |
403 | this.tech_.segmentBuffer_[0].pendingMetadata.push({ | 330 | this.checkBuffer_(); |
404 | textTrack: textTrack, | ||
405 | metadata: metadata | ||
406 | }); | ||
407 | }.bind(this)); | 331 | }.bind(this)); |
408 | |||
409 | // when seeking, clear out all cues ahead of the earliest position | ||
410 | // in the new segment. keep earlier cues around so they can still be | ||
411 | // programmatically inspected even though they've already fired | ||
412 | this.on(this.tech_, 'seeking', function() { | ||
413 | var media, startTime, i; | ||
414 | if (!textTrack) { | ||
415 | return; | ||
416 | } | ||
417 | media = this.playlists.media(); | ||
418 | startTime = this.tech_.playlists.expired_; | ||
419 | startTime += videojs.Hls.Playlist.duration(media, | ||
420 | media.mediaSequence, | ||
421 | media.mediaSequence + this.tech_.mediaIndex); | ||
422 | |||
423 | i = textTrack.cues.length; | ||
424 | while (i--) { | ||
425 | if (textTrack.cues[i].startTime >= startTime) { | ||
426 | textTrack.removeCue(textTrack.cues[i]); | ||
427 | } | ||
428 | } | ||
429 | }); | ||
430 | }; | ||
431 | |||
432 | videojs.Hls.prototype.addCuesForMetadata_ = function(segmentInfo) { | ||
433 | var i, cue, frame, metadata, minPts, segment, segmentOffset, textTrack, time; | ||
434 | segmentOffset = this.playlists.expired_; | ||
435 | segmentOffset += videojs.Hls.Playlist.duration(segmentInfo.playlist, | ||
436 | segmentInfo.playlist.mediaSequence, | ||
437 | segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex); | ||
438 | segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; | ||
439 | minPts = Math.min(isFinite(segment.minVideoPts) ? segment.minVideoPts : Infinity, | ||
440 | isFinite(segment.minAudioPts) ? segment.minAudioPts : Infinity); | ||
441 | |||
442 | while (segmentInfo.pendingMetadata.length) { | ||
443 | metadata = segmentInfo.pendingMetadata[0].metadata; | ||
444 | textTrack = segmentInfo.pendingMetadata[0].textTrack; | ||
445 | |||
446 | // create cue points for all the ID3 frames in this metadata event | ||
447 | for (i = 0; i < metadata.frames.length; i++) { | ||
448 | frame = metadata.frames[i]; | ||
449 | time = segmentOffset + ((metadata.pts - minPts) * 0.001); | ||
450 | cue = new window.VTTCue(time, time, frame.value || frame.url || ''); | ||
451 | cue.frame = frame; | ||
452 | cue.pts_ = metadata.pts; | ||
453 | textTrack.addCue(cue); | ||
454 | } | ||
455 | segmentInfo.pendingMetadata.shift(); | ||
456 | } | ||
457 | }; | 332 | }; |
458 | 333 | ||
459 | /** | 334 | /** |
... | @@ -535,13 +410,13 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { | ... | @@ -535,13 +410,13 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
535 | this.cancelSegmentXhr(); | 410 | this.cancelSegmentXhr(); |
536 | 411 | ||
537 | // abort outstanding key requests, if necessary | 412 | // abort outstanding key requests, if necessary |
538 | if (keyXhr) { | 413 | if (this.keyXhr_) { |
539 | keyXhr.aborted = true; | 414 | this.keyXhr_.aborted = true; |
540 | this.cancelKeyXhr(); | 415 | this.cancelKeyXhr(); |
541 | } | 416 | } |
542 | 417 | ||
543 | // clear out any buffered segments | 418 | // clear out the segment being processed |
544 | this.segmentBuffer_ = []; | 419 | this.pendingSegment_ = null; |
545 | 420 | ||
546 | // begin filling the buffer at the new position | 421 | // begin filling the buffer at the new position |
547 | this.fillBuffer(currentTime); | 422 | this.fillBuffer(currentTime); |
... | @@ -556,7 +431,7 @@ videojs.Hls.prototype.duration = function() { | ... | @@ -556,7 +431,7 @@ videojs.Hls.prototype.duration = function() { |
556 | }; | 431 | }; |
557 | 432 | ||
558 | videojs.Hls.prototype.seekable = function() { | 433 | videojs.Hls.prototype.seekable = function() { |
559 | var currentSeekable, startOffset, media; | 434 | var media; |
560 | 435 | ||
561 | if (!this.playlists) { | 436 | if (!this.playlists) { |
562 | return videojs.createTimeRanges(); | 437 | return videojs.createTimeRanges(); |
... | @@ -566,17 +441,7 @@ videojs.Hls.prototype.seekable = function() { | ... | @@ -566,17 +441,7 @@ videojs.Hls.prototype.seekable = function() { |
566 | return videojs.createTimeRanges(); | 441 | return videojs.createTimeRanges(); |
567 | } | 442 | } |
568 | 443 | ||
569 | // report the seekable range relative to the earliest possible | 444 | return videojs.Hls.Playlist.seekable(media); |
570 | // position when the stream was first loaded | ||
571 | currentSeekable = videojs.Hls.Playlist.seekable(media); | ||
572 | |||
573 | if (!currentSeekable.length) { | ||
574 | return currentSeekable; | ||
575 | } | ||
576 | |||
577 | startOffset = this.playlists.expired_; | ||
578 | return videojs.createTimeRanges(startOffset, | ||
579 | startOffset + (currentSeekable.end(0) - currentSeekable.start(0))); | ||
580 | }; | 445 | }; |
581 | 446 | ||
582 | /** | 447 | /** |
... | @@ -617,10 +482,10 @@ videojs.Hls.prototype.resetSrc_ = function() { | ... | @@ -617,10 +482,10 @@ videojs.Hls.prototype.resetSrc_ = function() { |
617 | }; | 482 | }; |
618 | 483 | ||
619 | videojs.Hls.prototype.cancelKeyXhr = function() { | 484 | videojs.Hls.prototype.cancelKeyXhr = function() { |
620 | if (keyXhr) { | 485 | if (this.keyXhr_) { |
621 | keyXhr.onreadystatechange = null; | 486 | this.keyXhr_.onreadystatechange = null; |
622 | keyXhr.abort(); | 487 | this.keyXhr_.abort(); |
623 | keyXhr = null; | 488 | this.keyXhr_ = null; |
624 | } | 489 | } |
625 | }; | 490 | }; |
626 | 491 | ||
... | @@ -829,7 +694,7 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { | ... | @@ -829,7 +694,7 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { |
829 | bufferedTime = 0, | 694 | bufferedTime = 0, |
830 | mediaIndex = 0, | 695 | mediaIndex = 0, |
831 | segment, | 696 | segment, |
832 | segmentUri; | 697 | segmentInfo; |
833 | 698 | ||
834 | // if preload is set to "none", do not download segments until playback is requested | 699 | // if preload is set to "none", do not download segments until playback is requested |
835 | if (this.loadingState_ !== 'segments') { | 700 | if (this.loadingState_ !== 'segments') { |
... | @@ -847,7 +712,7 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { | ... | @@ -847,7 +712,7 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { |
847 | } | 712 | } |
848 | 713 | ||
849 | // wait until the buffer is up to date | 714 | // wait until the buffer is up to date |
850 | if (this.segmentBuffer_.length || this.pendingSegment_) { | 715 | if (this.pendingSegment_) { |
851 | return; | 716 | return; |
852 | } | 717 | } |
853 | 718 | ||
... | @@ -886,10 +751,29 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { | ... | @@ -886,10 +751,29 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { |
886 | return; | 751 | return; |
887 | } | 752 | } |
888 | 753 | ||
754 | // package up all the work to append the segment | ||
755 | segmentInfo = { | ||
889 | // resolve the segment URL relative to the playlist | 756 | // resolve the segment URL relative to the playlist |
890 | segmentUri = this.playlistUriToUrl(segment.uri); | 757 | uri: this.playlistUriToUrl(segment.uri), |
758 | // the segment's mediaIndex at the time it was received | ||
759 | mediaIndex: mediaIndex, | ||
760 | // the segment's playlist | ||
761 | playlist: this.playlists.media(), | ||
762 | // optionally, a time offset to seek to within the segment | ||
763 | offset: seekToTime, | ||
764 | // unencrypted bytes of the segment | ||
765 | bytes: null, | ||
766 | // when a key is defined for this segment, the encrypted bytes | ||
767 | encryptedBytes: null, | ||
768 | // optionally, the decrypter that is unencrypting the segment | ||
769 | decrypter: null, | ||
770 | // the state of the buffer before a segment is appended will be | ||
771 | // stored here so that the actual segment duration can be | ||
772 | // determined after it has been appended | ||
773 | buffered: null | ||
774 | }; | ||
891 | 775 | ||
892 | this.loadSegment(segmentUri, mediaIndex, seekToTime); | 776 | this.loadSegment(segmentInfo); |
893 | }; | 777 | }; |
894 | 778 | ||
895 | videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) { | 779 | videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) { |
... | @@ -920,17 +804,22 @@ videojs.Hls.prototype.setBandwidth = function(xhr) { | ... | @@ -920,17 +804,22 @@ videojs.Hls.prototype.setBandwidth = function(xhr) { |
920 | this.tech_.trigger('bandwidthupdate'); | 804 | this.tech_.trigger('bandwidthupdate'); |
921 | }; | 805 | }; |
922 | 806 | ||
923 | videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime) { | 807 | videojs.Hls.prototype.loadSegment = function(segmentInfo) { |
924 | var self = this; | 808 | var |
809 | self = this, | ||
810 | segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; | ||
811 | |||
812 | // if the segment is encrypted, request the key | ||
813 | if (segment.key) { | ||
814 | this.fetchKey_(segment); | ||
815 | } | ||
925 | 816 | ||
926 | // request the next segment | 817 | // request the next segment |
927 | this.segmentXhr_ = videojs.Hls.xhr({ | 818 | this.segmentXhr_ = videojs.Hls.xhr({ |
928 | uri: segmentUri, | 819 | uri: segmentInfo.uri, |
929 | responseType: 'arraybuffer', | 820 | responseType: 'arraybuffer', |
930 | withCredentials: this.source_.withCredentials | 821 | withCredentials: this.source_.withCredentials |
931 | }, function(error, request) { | 822 | }, function(error, request) { |
932 | var segmentInfo; | ||
933 | |||
934 | // the segment request is no longer outstanding | 823 | // the segment request is no longer outstanding |
935 | self.segmentXhr_ = null; | 824 | self.segmentXhr_ = null; |
936 | 825 | ||
... | @@ -944,7 +833,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime) | ... | @@ -944,7 +833,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime) |
944 | if (!request.aborted && error) { | 833 | if (!request.aborted && error) { |
945 | self.error = { | 834 | self.error = { |
946 | status: request.status, | 835 | status: request.status, |
947 | message: 'HLS segment request error at URL: ' + segmentUri, | 836 | message: 'HLS segment request error at URL: ' + segmentInfo.uri, |
948 | code: (request.status >= 500) ? 4 : 2 | 837 | code: (request.status >= 500) ? 4 : 2 |
949 | }; | 838 | }; |
950 | 839 | ||
... | @@ -958,34 +847,12 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime) | ... | @@ -958,34 +847,12 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime) |
958 | 847 | ||
959 | self.setBandwidth(request); | 848 | self.setBandwidth(request); |
960 | 849 | ||
961 | // package up all the work to append the segment | 850 | if (segment.key) { |
962 | segmentInfo = { | ||
963 | // the segment's mediaIndex at the time it was received | ||
964 | mediaIndex: mediaIndex, | ||
965 | // the segment's playlist | ||
966 | playlist: self.playlists.media(), | ||
967 | // optionally, a time offset to seek to within the segment | ||
968 | offset: seekToTime, | ||
969 | // unencrypted bytes of the segment | ||
970 | bytes: null, | ||
971 | // when a key is defined for this segment, the encrypted bytes | ||
972 | encryptedBytes: null, | ||
973 | // optionally, the decrypter that is unencrypting the segment | ||
974 | decrypter: null, | ||
975 | // metadata events discovered during muxing that need to be | ||
976 | // translated into cue points | ||
977 | pendingMetadata: [], | ||
978 | // the state of the buffer before a segment is appended will be | ||
979 | // stored here so that the actual segment duration can be | ||
980 | // determined after it has been appended | ||
981 | buffered: null | ||
982 | }; | ||
983 | if (segmentInfo.playlist.segments[mediaIndex].key) { | ||
984 | segmentInfo.encryptedBytes = new Uint8Array(request.response); | 851 | segmentInfo.encryptedBytes = new Uint8Array(request.response); |
985 | } else { | 852 | } else { |
986 | segmentInfo.bytes = new Uint8Array(request.response); | 853 | segmentInfo.bytes = new Uint8Array(request.response); |
987 | } | 854 | } |
988 | self.segmentBuffer_.push(segmentInfo); | 855 | self.pendingSegment_ = segmentInfo; |
989 | self.tech_.trigger('progress'); | 856 | self.tech_.trigger('progress'); |
990 | self.drainBuffer(); | 857 | self.drainBuffer(); |
991 | 858 | ||
... | @@ -1008,13 +875,11 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1008,13 +875,11 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1008 | segmentTimestampOffset = 0, | 875 | segmentTimestampOffset = 0, |
1009 | hasBufferedContent = (this.tech_.buffered().length !== 0), | 876 | hasBufferedContent = (this.tech_.buffered().length !== 0), |
1010 | currentBuffered = this.findCurrentBuffered_(), | 877 | currentBuffered = this.findCurrentBuffered_(), |
1011 | outsideBufferedRanges = !(currentBuffered && currentBuffered.length), | 878 | outsideBufferedRanges = !(currentBuffered && currentBuffered.length); |
1012 | // ptsTime, | ||
1013 | segmentBuffer = this.segmentBuffer_; | ||
1014 | 879 | ||
1015 | // if the buffer is empty or the source buffer hasn't been created | 880 | // if the buffer is empty or the source buffer hasn't been created |
1016 | // yet, do nothing | 881 | // yet, do nothing |
1017 | if (!segmentBuffer.length || !this.sourceBuffer) { | 882 | if (!this.pendingSegment_ || !this.sourceBuffer) { |
1018 | return; | 883 | return; |
1019 | } | 884 | } |
1020 | 885 | ||
... | @@ -1024,7 +889,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1024,7 +889,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1024 | return; | 889 | return; |
1025 | } | 890 | } |
1026 | 891 | ||
1027 | segmentInfo = segmentBuffer[0]; | 892 | segmentInfo = this.pendingSegment_; |
1028 | mediaIndex = segmentInfo.mediaIndex; | 893 | mediaIndex = segmentInfo.mediaIndex; |
1029 | playlist = segmentInfo.playlist; | 894 | playlist = segmentInfo.playlist; |
1030 | offset = segmentInfo.offset; | 895 | offset = segmentInfo.offset; |
... | @@ -1037,18 +902,19 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1037,18 +902,19 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1037 | // if the key download failed, we want to skip this segment | 902 | // if the key download failed, we want to skip this segment |
1038 | // but if the key hasn't downloaded yet, we want to try again later | 903 | // but if the key hasn't downloaded yet, we want to try again later |
1039 | if (keyFailed(segment.key)) { | 904 | if (keyFailed(segment.key)) { |
1040 | return segmentBuffer.shift(); | 905 | videojs.log.warn('Network error retrieving key from "' + |
906 | segment.key.uri + '"'); | ||
907 | return this.mediaSource.endOfStream('network'); | ||
1041 | } else if (!segment.key.bytes) { | 908 | } else if (!segment.key.bytes) { |
1042 | 909 | ||
1043 | // trigger a key request if one is not already in-flight | 910 | // waiting for the key bytes, try again later |
1044 | return this.fetchKeys_(); | 911 | return; |
1045 | |||
1046 | } else if (segmentInfo.decrypter) { | 912 | } else if (segmentInfo.decrypter) { |
1047 | 913 | ||
1048 | // decryption is in progress, try again later | 914 | // decryption is in progress, try again later |
1049 | return; | 915 | return; |
1050 | |||
1051 | } else { | 916 | } else { |
917 | |||
1052 | // if the media sequence is greater than 2^32, the IV will be incorrect | 918 | // if the media sequence is greater than 2^32, the IV will be incorrect |
1053 | // assuming 10s segments, that would be about 1300 years | 919 | // assuming 10s segments, that would be about 1300 years |
1054 | segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); | 920 | segIv = segment.key.iv || new Uint32Array([0, 0, 0, mediaIndex + playlist.mediaSequence]); |
... | @@ -1067,32 +933,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1067,32 +933,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1067 | 933 | ||
1068 | event = event || {}; | 934 | event = event || {}; |
1069 | 935 | ||
1070 | // if (this.segmentParser_.tagsAvailable()) { | ||
1071 | // // record PTS information for the segment so we can calculate | ||
1072 | // // accurate durations and seek reliably | ||
1073 | // if (this.segmentParser_.stats.h264Tags()) { | ||
1074 | // segment.minVideoPts = this.segmentParser_.stats.minVideoPts(); | ||
1075 | // segment.maxVideoPts = this.segmentParser_.stats.maxVideoPts(); | ||
1076 | // } | ||
1077 | // if (this.segmentParser_.stats.aacTags()) { | ||
1078 | // segment.minAudioPts = this.segmentParser_.stats.minAudioPts(); | ||
1079 | // segment.maxAudioPts = this.segmentParser_.stats.maxAudioPts(); | ||
1080 | // } | ||
1081 | // } | ||
1082 | |||
1083 | // while (this.segmentParser_.tagsAvailable()) { | ||
1084 | // tags.push(this.segmentParser_.getNextTag()); | ||
1085 | // } | ||
1086 | |||
1087 | this.addCuesForMetadata_(segmentInfo); | ||
1088 | //this.updateDuration(this.playlists.media()); | ||
1089 | |||
1090 | // // when we're crossing a discontinuity, inject metadata to indicate | ||
1091 | // // that the decoder should be reset appropriately | ||
1092 | // if (segment.discontinuity && tags.length) { | ||
1093 | // this.tech_.el().vjs_discontinuity(); | ||
1094 | // } | ||
1095 | |||
1096 | // If we have seeked into a non-buffered time-range, remove all buffered | 936 | // If we have seeked into a non-buffered time-range, remove all buffered |
1097 | // time-ranges because they could have been incorrectly placed originally | 937 | // time-ranges because they could have been incorrectly placed originally |
1098 | if (this.tech_.seeking() && outsideBufferedRanges) { | 938 | if (this.tech_.seeking() && outsideBufferedRanges) { |
... | @@ -1108,7 +948,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1108,7 +948,7 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1108 | // anew on every seek | 948 | // anew on every seek |
1109 | if (segmentInfo.playlist.discontinuityStarts.length) { | 949 | if (segmentInfo.playlist.discontinuityStarts.length) { |
1110 | if (segmentInfo.mediaIndex > 0) { | 950 | if (segmentInfo.mediaIndex > 0) { |
1111 | segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, 0, segmentInfo.mediaIndex); | 951 | segmentTimestampOffset = videojs.Hls.Playlist.duration(segmentInfo.playlist, segmentInfo.mediaIndex); |
1112 | } | 952 | } |
1113 | 953 | ||
1114 | // Now that the forward buffer is clear, we have to set timestamp offset to | 954 | // Now that the forward buffer is clear, we have to set timestamp offset to |
... | @@ -1128,7 +968,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1128,7 +968,6 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1128 | } else { | 968 | } else { |
1129 | this.sourceBuffer.appendWindowStart = 0; | 969 | this.sourceBuffer.appendWindowStart = 0; |
1130 | } | 970 | } |
1131 | this.pendingSegment_ = segmentBuffer.shift(); | ||
1132 | this.pendingSegment_.buffered = this.tech_.buffered(); | 971 | this.pendingSegment_.buffered = this.tech_.buffered(); |
1133 | 972 | ||
1134 | // the segment is asynchronously added to the current buffered data | 973 | // the segment is asynchronously added to the current buffered data |
... | @@ -1136,38 +975,33 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1136,38 +975,33 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1136 | }; | 975 | }; |
1137 | 976 | ||
1138 | /** | 977 | /** |
1139 | * Attempt to retrieve keys starting at a particular media | 978 | * Attempt to retrieve the key for a particular media segment. |
1140 | * segment. This method has no effect if segments are not yet | ||
1141 | * available or a key request is already in progress. | ||
1142 | * | ||
1143 | * @param playlist {object} the media playlist to fetch keys for | ||
1144 | * @param index {number} the media segment index to start from | ||
1145 | */ | 979 | */ |
1146 | videojs.Hls.prototype.fetchKeys_ = function() { | 980 | videojs.Hls.prototype.fetchKey_ = function(segment) { |
1147 | var i, key, tech, player, settings, segment, view, receiveKey; | 981 | var key, self, settings, receiveKey; |
1148 | 982 | ||
1149 | // if there is a pending XHR or no segments, don't do anything | 983 | // if there is a pending XHR or no segments, don't do anything |
1150 | if (keyXhr || !this.segmentBuffer_.length) { | 984 | if (this.keyXhr_) { |
1151 | return; | 985 | return; |
1152 | } | 986 | } |
1153 | 987 | ||
1154 | tech = this; | 988 | self = this; |
1155 | player = this.player(); | ||
1156 | settings = this.options_; | 989 | settings = this.options_; |
1157 | 990 | ||
1158 | /** | 991 | /** |
1159 | * Handle a key XHR response. This function needs to lookup the | 992 | * Handle a key XHR response. |
1160 | */ | 993 | */ |
1161 | receiveKey = function(key) { | 994 | receiveKey = function(key) { |
1162 | return function(error, request) { | 995 | return function(error, request) { |
1163 | keyXhr = null; | 996 | var view; |
997 | self.keyXhr_ = null; | ||
1164 | 998 | ||
1165 | if (error || !request.response || request.response.byteLength !== 16) { | 999 | if (error || !request.response || request.response.byteLength !== 16) { |
1166 | key.retries = key.retries || 0; | 1000 | key.retries = key.retries || 0; |
1167 | key.retries++; | 1001 | key.retries++; |
1168 | if (!request.aborted) { | 1002 | if (!request.aborted) { |
1169 | // try fetching again | 1003 | // try fetching again |
1170 | tech.fetchKeys_(); | 1004 | self.fetchKey_(segment); |
1171 | } | 1005 | } |
1172 | return; | 1006 | return; |
1173 | } | 1007 | } |
... | @@ -1181,28 +1015,25 @@ videojs.Hls.prototype.fetchKeys_ = function() { | ... | @@ -1181,28 +1015,25 @@ videojs.Hls.prototype.fetchKeys_ = function() { |
1181 | ]); | 1015 | ]); |
1182 | 1016 | ||
1183 | // check to see if this allows us to make progress buffering now | 1017 | // check to see if this allows us to make progress buffering now |
1184 | tech.checkBuffer_(); | 1018 | self.checkBuffer_(); |
1185 | }; | 1019 | }; |
1186 | }; | 1020 | }; |
1187 | 1021 | ||
1188 | for (i = 0; i < tech.segmentBuffer_.length; i++) { | ||
1189 | segment = tech.segmentBuffer_[i].playlist.segments[tech.segmentBuffer_[i].mediaIndex]; | ||
1190 | key = segment.key; | 1022 | key = segment.key; |
1191 | 1023 | ||
1192 | // continue looking if this segment is unencrypted | 1024 | // nothing to do if this segment is unencrypted |
1193 | if (!key) { | 1025 | if (!key) { |
1194 | continue; | 1026 | return; |
1195 | } | 1027 | } |
1196 | 1028 | ||
1197 | // request the key if the retry limit hasn't been reached | 1029 | // request the key if the retry limit hasn't been reached |
1198 | if (!key.bytes && !keyFailed(key)) { | 1030 | if (!key.bytes && !keyFailed(key)) { |
1199 | keyXhr = videojs.Hls.xhr({ | 1031 | this.keyXhr_ = videojs.Hls.xhr({ |
1200 | uri: this.playlistUriToUrl(key.uri), | 1032 | uri: this.playlistUriToUrl(key.uri), |
1201 | responseType: 'arraybuffer', | 1033 | responseType: 'arraybuffer', |
1202 | withCredentials: settings.withCredentials | 1034 | withCredentials: settings.withCredentials |
1203 | }, receiveKey(key)); | 1035 | }, receiveKey(key)); |
1204 | break; | 1036 | return; |
1205 | } | ||
1206 | } | 1037 | } |
1207 | }; | 1038 | }; |
1208 | 1039 | ||
... | @@ -1234,44 +1065,6 @@ videojs.Hls.isSupported = function() { | ... | @@ -1234,44 +1065,6 @@ videojs.Hls.isSupported = function() { |
1234 | }; | 1065 | }; |
1235 | 1066 | ||
1236 | /** | 1067 | /** |
1237 | * Calculate the duration of a playlist from a given start index to a given | ||
1238 | * end index. | ||
1239 | * @param playlist {object} a media playlist object | ||
1240 | * @param startIndex {number} an inclusive lower boundary for the playlist. | ||
1241 | * Defaults to 0. | ||
1242 | * @param endIndex {number} an exclusive upper boundary for the playlist. | ||
1243 | * Defaults to playlist length. | ||
1244 | * @return {number} the duration between the start index and end index. | ||
1245 | */ | ||
1246 | videojs.Hls.getPlaylistDuration = function(playlist, startIndex, endIndex) { | ||
1247 | videojs.log.warn('videojs.Hls.getPlaylistDuration is deprecated. ' + | ||
1248 | 'Use videojs.Hls.Playlist.duration instead'); | ||
1249 | return videojs.Hls.Playlist.duration(playlist, startIndex, endIndex); | ||
1250 | }; | ||
1251 | |||
1252 | /** | ||
1253 | * Calculate the total duration for a playlist based on segment metadata. | ||
1254 | * @param playlist {object} a media playlist object | ||
1255 | * @return {number} the currently known duration, in seconds | ||
1256 | */ | ||
1257 | videojs.Hls.getPlaylistTotalDuration = function(playlist) { | ||
1258 | videojs.log.warn('videojs.Hls.getPlaylistTotalDuration is deprecated. ' + | ||
1259 | 'Use videojs.Hls.Playlist.duration instead'); | ||
1260 | return videojs.Hls.Playlist.duration(playlist); | ||
1261 | }; | ||
1262 | |||
1263 | /** | ||
1264 | * Deprecated. | ||
1265 | * | ||
1266 | * @deprecated use player.hls.playlists.getMediaIndexForTime_() instead | ||
1267 | */ | ||
1268 | videojs.Hls.getMediaIndexByTime = function() { | ||
1269 | videojs.log.warn('getMediaIndexByTime is deprecated. ' + | ||
1270 | 'Use PlaylistLoader.getMediaIndexForTime_ instead.'); | ||
1271 | return 0; | ||
1272 | }; | ||
1273 | |||
1274 | /** | ||
1275 | * A comparator function to sort two playlist object by bandwidth. | 1068 | * A comparator function to sort two playlist object by bandwidth. |
1276 | * @param left {object} a media playlist object | 1069 | * @param left {object} a media playlist object |
1277 | * @param right {object} a media playlist object | 1070 | * @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'); |
... | @@ -175,101 +166,6 @@ | ... | @@ -175,101 +166,6 @@ |
175 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); | 166 | strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct'); |
176 | }); | 167 | }); |
177 | 168 | ||
178 | test('increments expired seconds after a segment is removed', function() { | ||
179 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
180 | requests.pop().respond(200, null, | ||
181 | '#EXTM3U\n' + | ||
182 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
183 | '#EXTINF:10,\n' + | ||
184 | '0.ts\n' + | ||
185 | '#EXTINF:10,\n' + | ||
186 | '1.ts\n' + | ||
187 | '#EXTINF:10,\n' + | ||
188 | '2.ts\n' + | ||
189 | '#EXTINF:10,\n' + | ||
190 | '3.ts\n'); | ||
191 | clock.tick(10 * 1000); // 10s, one target duration | ||
192 | requests.pop().respond(200, null, | ||
193 | '#EXTM3U\n' + | ||
194 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
195 | '#EXTINF:10,\n' + | ||
196 | '1.ts\n' + | ||
197 | '#EXTINF:10,\n' + | ||
198 | '2.ts\n' + | ||
199 | '#EXTINF:10,\n' + | ||
200 | '3.ts\n' + | ||
201 | '#EXTINF:10,\n' + | ||
202 | '4.ts\n'); | ||
203 | equal(loader.expired_, 10, 'expired one segment'); | ||
204 | }); | ||
205 | |||
206 | test('increments expired seconds after a discontinuity', function() { | ||
207 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
208 | requests.pop().respond(200, null, | ||
209 | '#EXTM3U\n' + | ||
210 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
211 | '#EXTINF:10,\n' + | ||
212 | '0.ts\n' + | ||
213 | '#EXTINF:3,\n' + | ||
214 | '1.ts\n' + | ||
215 | '#EXT-X-DISCONTINUITY\n' + | ||
216 | '#EXTINF:4,\n' + | ||
217 | '2.ts\n'); | ||
218 | clock.tick(10 * 1000); // 10s, one target duration | ||
219 | requests.pop().respond(200, null, | ||
220 | '#EXTM3U\n' + | ||
221 | '#EXT-X-MEDIA-SEQUENCE:1\n' + | ||
222 | '#EXTINF:3,\n' + | ||
223 | '1.ts\n' + | ||
224 | '#EXT-X-DISCONTINUITY\n' + | ||
225 | '#EXTINF:4,\n' + | ||
226 | '2.ts\n'); | ||
227 | equal(loader.expired_, 10, 'expired one segment'); | ||
228 | |||
229 | clock.tick(10 * 1000); // 10s, one target duration | ||
230 | requests.pop().respond(200, null, | ||
231 | '#EXTM3U\n' + | ||
232 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | ||
233 | '#EXT-X-DISCONTINUITY\n' + | ||
234 | '#EXTINF:4,\n' + | ||
235 | '2.ts\n'); | ||
236 | equal(loader.expired_, 13, 'no expirations after the discontinuity yet'); | ||
237 | |||
238 | clock.tick(10 * 1000); // 10s, one target duration | ||
239 | requests.pop().respond(200, null, | ||
240 | '#EXTM3U\n' + | ||
241 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
242 | '#EXT-X-DISCONTINUITY-SEQUENCE:1\n' + | ||
243 | '#EXTINF:10,\n' + | ||
244 | '3.ts\n'); | ||
245 | equal(loader.expired_, 13 + 4, 'tracked expired prior to the discontinuity'); | ||
246 | }); | ||
247 | |||
248 | test('tracks expired seconds properly when two discontinuities expire at once', function() { | ||
249 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
250 | requests.pop().respond(200, null, | ||
251 | '#EXTM3U\n' + | ||
252 | '#EXT-X-MEDIA-SEQUENCE:0\n' + | ||
253 | '#EXTINF:4,\n' + | ||
254 | '0.ts\n' + | ||
255 | '#EXT-X-DISCONTINUITY\n' + | ||
256 | '#EXTINF:5,\n' + | ||
257 | '1.ts\n' + | ||
258 | '#EXT-X-DISCONTINUITY\n' + | ||
259 | '#EXTINF:6,\n' + | ||
260 | '2.ts\n' + | ||
261 | '#EXTINF:7,\n' + | ||
262 | '3.ts\n'); | ||
263 | clock.tick(10 * 1000); | ||
264 | requests.pop().respond(200, null, | ||
265 | '#EXTM3U\n' + | ||
266 | '#EXT-X-MEDIA-SEQUENCE:3\n' + | ||
267 | '#EXT-X-DISCONTINUITY-SEQUENCE:2\n' + | ||
268 | '#EXTINF:7,\n' + | ||
269 | '3.ts\n'); | ||
270 | equal(loader.expired_, 4 + 5 + 6, 'tracked both expired discontinuities'); | ||
271 | }); | ||
272 | |||
273 | test('emits an error when an initial playlist request fails', function() { | 169 | test('emits an error when an initial playlist request fails', function() { |
274 | var | 170 | var |
275 | errors = [], | 171 | errors = [], |
... | @@ -757,8 +653,8 @@ | ... | @@ -757,8 +653,8 @@ |
757 | equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); | 653 | equal(loader.getMediaIndexForTime_(3), 0, 'time three is index zero'); |
758 | equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); | 654 | equal(loader.getMediaIndexForTime_(10), 2, 'time 10 is index 2'); |
759 | equal(loader.getMediaIndexForTime_(22), | 655 | equal(loader.getMediaIndexForTime_(22), |
760 | 2, | 656 | 3, |
761 | 'the index is never greater than the length'); | 657 | 'time greater than the length is index 3'); |
762 | }); | 658 | }); |
763 | 659 | ||
764 | 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() { |
... | @@ -785,7 +681,7 @@ | ... | @@ -785,7 +681,7 @@ |
785 | '1001.ts\n' + | 681 | '1001.ts\n' + |
786 | '#EXTINF:5,\n' + | 682 | '#EXTINF:5,\n' + |
787 | '1002.ts\n'); | 683 | '1002.ts\n'); |
788 | loader.expired_ = 150; | 684 | loader.media().segments[0].start = 150; |
789 | 685 | ||
790 | equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); | 686 | equal(loader.getMediaIndexForTime_(0), 0, 'the lowest returned value is zero'); |
791 | equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); | 687 | equal(loader.getMediaIndexForTime_(45), 0, 'expired content returns zero'); |
... | @@ -797,30 +693,6 @@ | ... | @@ -797,30 +693,6 @@ |
797 | 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'); |
798 | }); | 694 | }); |
799 | 695 | ||
800 | test('updating the timeline offset adjusts results from getMediaIndexForTime_', function() { | ||
801 | var loader = new videojs.Hls.PlaylistLoader('live.m3u8'); | ||
802 | requests.pop().respond(200, null, | ||
803 | '#EXTM3U\n' + | ||
804 | '#EXT-X-MEDIA-SEQUENCE:23\n' + | ||
805 | '#EXTINF:4,\n' + | ||
806 | '23.ts\n' + | ||
807 | '#EXTINF:5,\n' + | ||
808 | '24.ts\n' + | ||
809 | '#EXTINF:6,\n' + | ||
810 | '25.ts\n' + | ||
811 | '#EXTINF:7,\n' + | ||
812 | '26.ts\n'); | ||
813 | loader.updateTimelineOffset(0, 150); | ||
814 | equal(loader.getMediaIndexForTime_(150), 0, 'translated the first segment'); | ||
815 | equal(loader.getMediaIndexForTime_(130), 0, 'clamps the index to zero'); | ||
816 | equal(loader.getMediaIndexForTime_(155), 1, 'translated the second segment'); | ||
817 | |||
818 | loader.updateTimelineOffset(2, 30); | ||
819 | equal(loader.getMediaIndexForTime_(30 - 5 - 1), 0, 'translated the first segment'); | ||
820 | equal(loader.getMediaIndexForTime_(30 + 7), 3, 'translated the last segment'); | ||
821 | equal(loader.getMediaIndexForTime_(30 - 3), 1, 'translated an earlier segment'); | ||
822 | }); | ||
823 | |||
824 | test('does not misintrepret playlists missing newlines at the end', function() { | 696 | test('does not misintrepret playlists missing newlines at the end', function() { |
825 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); | 697 | var loader = new videojs.Hls.PlaylistLoader('media.m3u8'); |
826 | 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 | ||
... | @@ -889,10 +835,10 @@ test('reports an error if a segment is unreachable', function() { | ... | @@ -889,10 +835,10 @@ test('reports an error if a segment is unreachable', function() { |
889 | openMediaSource(player); | 835 | openMediaSource(player); |
890 | 836 | ||
891 | player.tech_.hls.bandwidth = 20000; | 837 | player.tech_.hls.bandwidth = 20000; |
892 | standardXHRResponse(requests[0]); | 838 | standardXHRResponse(requests[0]); // master |
893 | standardXHRResponse(requests[1]); | 839 | standardXHRResponse(requests[1]); // media |
894 | 840 | ||
895 | requests[2].respond(400); | 841 | requests[2].respond(400); // segment |
896 | strictEqual(player.tech_.hls.mediaSource.error_, 'network', 'network error is triggered'); | 842 | strictEqual(player.tech_.hls.mediaSource.error_, 'network', 'network error is triggered'); |
897 | }); | 843 | }); |
898 | 844 | ||
... | @@ -1172,18 +1118,18 @@ test('downloads the next segment if the buffer is getting low', function() { | ... | @@ -1172,18 +1118,18 @@ test('downloads the next segment if the buffer is getting low', function() { |
1172 | 1118 | ||
1173 | 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() { |
1174 | var currentTime, buffered; | 1120 | var currentTime, buffered; |
1121 | player.src({ | ||
1122 | src: 'manifest/media.m3u8', | ||
1123 | type: 'application/vnd.apple.mpegurl' | ||
1124 | }); | ||
1125 | openMediaSource(player); | ||
1126 | |||
1175 | player.tech_.currentTime = function() { | 1127 | player.tech_.currentTime = function() { |
1176 | return currentTime; | 1128 | return currentTime; |
1177 | }; | 1129 | }; |
1178 | player.tech_.buffered = function() { | 1130 | player.tech_.buffered = function() { |
1179 | return videojs.createTimeRange(buffered); | 1131 | return videojs.createTimeRange(buffered); |
1180 | }; | 1132 | }; |
1181 | |||
1182 | player.src({ | ||
1183 | src: 'manifest/media.m3u8', | ||
1184 | type: 'application/vnd.apple.mpegurl' | ||
1185 | }); | ||
1186 | openMediaSource(player); | ||
1187 | currentTime = 8; | 1133 | currentTime = 8; |
1188 | buffered = [[0, 10], [20, 40]]; | 1134 | buffered = [[0, 10], [20, 40]]; |
1189 | 1135 | ||
... | @@ -1238,7 +1184,6 @@ test('only makes one segment request at a time', function() { | ... | @@ -1238,7 +1184,6 @@ test('only makes one segment request at a time', function() { |
1238 | }); | 1184 | }); |
1239 | 1185 | ||
1240 | test('only appends one segment at a time', function() { | 1186 | test('only appends one segment at a time', function() { |
1241 | var appends = 0; | ||
1242 | player.src({ | 1187 | player.src({ |
1243 | src: 'manifest/media.m3u8', | 1188 | src: 'manifest/media.m3u8', |
1244 | type: 'application/vnd.apple.mpegurl' | 1189 | type: 'application/vnd.apple.mpegurl' |
... | @@ -1251,84 +1196,6 @@ test('only appends one segment at a time', function() { | ... | @@ -1251,84 +1196,6 @@ test('only appends one segment at a time', function() { |
1251 | equal(requests.length, 0, 'did not request while updating'); | 1196 | equal(requests.length, 0, 'did not request while updating'); |
1252 | }); | 1197 | }); |
1253 | 1198 | ||
1254 | QUnit.skip('records the min and max PTS values for a segment', function() { | ||
1255 | var tags = []; | ||
1256 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1257 | player.src({ | ||
1258 | src: 'manifest/media.m3u8', | ||
1259 | type: 'application/vnd.apple.mpegurl' | ||
1260 | }); | ||
1261 | openMediaSource(player); | ||
1262 | standardXHRResponse(requests.pop()); // media.m3u8 | ||
1263 | |||
1264 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1265 | tags.push({ pts: 10, bytes: new Uint8Array(1) }); | ||
1266 | standardXHRResponse(requests.pop()); // segment 0 | ||
1267 | |||
1268 | equal(player.tech_.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts'); | ||
1269 | equal(player.tech_.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts'); | ||
1270 | equal(player.tech_.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts'); | ||
1271 | equal(player.tech_.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); | ||
1272 | }); | ||
1273 | |||
1274 | QUnit.skip('records PTS values for video-only segments', function() { | ||
1275 | var tags = []; | ||
1276 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1277 | player.src({ | ||
1278 | src: 'manifest/media.m3u8', | ||
1279 | type: 'application/vnd.apple.mpegurl' | ||
1280 | }); | ||
1281 | openMediaSource(player); | ||
1282 | standardXHRResponse(requests.pop()); // media.m3u8 | ||
1283 | |||
1284 | player.tech_.hls.segmentParser_.stats.aacTags = function() { | ||
1285 | return 0; | ||
1286 | }; | ||
1287 | player.tech_.hls.segmentParser_.stats.minAudioPts = function() { | ||
1288 | throw new Error('No audio tags'); | ||
1289 | }; | ||
1290 | player.tech_.hls.segmentParser_.stats.maxAudioPts = function() { | ||
1291 | throw new Error('No audio tags'); | ||
1292 | }; | ||
1293 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1294 | tags.push({ pts: 10, bytes: new Uint8Array(1) }); | ||
1295 | standardXHRResponse(requests.pop()); // segment 0 | ||
1296 | |||
1297 | equal(player.tech_.hls.playlists.media().segments[0].minVideoPts, 0, 'recorded min video pts'); | ||
1298 | equal(player.tech_.hls.playlists.media().segments[0].maxVideoPts, 10, 'recorded max video pts'); | ||
1299 | strictEqual(player.tech_.hls.playlists.media().segments[0].minAudioPts, undefined, 'min audio pts is undefined'); | ||
1300 | strictEqual(player.tech_.hls.playlists.media().segments[0].maxAudioPts, undefined, 'max audio pts is undefined'); | ||
1301 | }); | ||
1302 | |||
1303 | QUnit.skip('records PTS values for audio-only segments', function() { | ||
1304 | var tags = []; | ||
1305 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1306 | player.src({ | ||
1307 | src: 'manifest/media.m3u8', | ||
1308 | type: 'application/vnd.apple.mpegurl' | ||
1309 | }); | ||
1310 | openMediaSource(player); | ||
1311 | standardXHRResponse(requests.pop()); // media.m3u8 | ||
1312 | |||
1313 | player.tech_.hls.segmentParser_.stats.h264Tags = function() { | ||
1314 | return 0; | ||
1315 | }; | ||
1316 | player.tech_.hls.segmentParser_.stats.minVideoPts = function() { | ||
1317 | throw new Error('No video tags'); | ||
1318 | }; | ||
1319 | player.tech_.hls.segmentParser_.stats.maxVideoPts = function() { | ||
1320 | throw new Error('No video tags'); | ||
1321 | }; | ||
1322 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1323 | tags.push({ pts: 10, bytes: new Uint8Array(1) }); | ||
1324 | standardXHRResponse(requests.pop()); // segment 0 | ||
1325 | |||
1326 | equal(player.tech_.hls.playlists.media().segments[0].minAudioPts, 0, 'recorded min audio pts'); | ||
1327 | equal(player.tech_.hls.playlists.media().segments[0].maxAudioPts, 10, 'recorded max audio pts'); | ||
1328 | strictEqual(player.tech_.hls.playlists.media().segments[0].minVideoPts, undefined, 'min video pts is undefined'); | ||
1329 | strictEqual(player.tech_.hls.playlists.media().segments[0].maxVideoPts, undefined, 'max video pts is undefined'); | ||
1330 | }); | ||
1331 | |||
1332 | 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() { |
1333 | player.src({ | 1200 | player.src({ |
1334 | src: 'manifest/master.m3u8', | 1201 | src: 'manifest/master.m3u8', |
... | @@ -1438,331 +1305,6 @@ test('segmentXhr is properly nulled out when dispose is called', function() { | ... | @@ -1438,331 +1305,6 @@ test('segmentXhr is properly nulled out when dispose is called', function() { |
1438 | Flash.prototype.dispose = oldDispose; | 1305 | Flash.prototype.dispose = oldDispose; |
1439 | }); | 1306 | }); |
1440 | 1307 | ||
1441 | QUnit.skip('exposes in-band metadata events as cues', function() { | ||
1442 | var track; | ||
1443 | videojs.Hls.SegmentParser = mockSegmentParser(); | ||
1444 | player.src({ | ||
1445 | src: 'manifest/media.m3u8', | ||
1446 | type: 'application/vnd.apple.mpegurl' | ||
1447 | }); | ||
1448 | openMediaSource(player); | ||
1449 | |||
1450 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1451 | // trigger a metadata event | ||
1452 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', { | ||
1453 | pts: 2000, | ||
1454 | data: new Uint8Array([]), | ||
1455 | frames: [{ | ||
1456 | id: 'TXXX', | ||
1457 | value: 'cue text' | ||
1458 | }, { | ||
1459 | id: 'WXXX', | ||
1460 | url: 'http://example.com' | ||
1461 | }, { | ||
1462 | id: 'PRIV', | ||
1463 | owner: 'owner@example.com', | ||
1464 | privateData: new Uint8Array([1, 2, 3]) | ||
1465 | }] | ||
1466 | }); | ||
1467 | }; | ||
1468 | |||
1469 | standardXHRResponse(requests[0]); | ||
1470 | standardXHRResponse(requests[1]); | ||
1471 | |||
1472 | equal(player.textTracks().length, 1, 'created a text track'); | ||
1473 | track = player.textTracks()[0]; | ||
1474 | equal(track.kind, 'metadata', 'kind is metadata'); | ||
1475 | equal(track.inBandMetadataTrackDispatchType, '15010203BB', 'set the dispatch type'); | ||
1476 | equal(track.cues.length, 3, 'created three cues'); | ||
1477 | equal(track.cues[0].startTime, 2, 'cue starts at 2 seconds'); | ||
1478 | equal(track.cues[0].endTime, 2, 'cue ends at 2 seconds'); | ||
1479 | equal(track.cues[0].pauseOnExit, false, 'cue does not pause on exit'); | ||
1480 | equal(track.cues[0].text, 'cue text', 'set cue text'); | ||
1481 | |||
1482 | equal(track.cues[1].startTime, 2, 'cue starts at 2 seconds'); | ||
1483 | equal(track.cues[1].endTime, 2, 'cue ends at 2 seconds'); | ||
1484 | equal(track.cues[1].pauseOnExit, false, 'cue does not pause on exit'); | ||
1485 | equal(track.cues[1].text, 'http://example.com', 'set cue text'); | ||
1486 | |||
1487 | equal(track.cues[2].startTime, 2, 'cue starts at 2 seconds'); | ||
1488 | equal(track.cues[2].endTime, 2, 'cue ends at 2 seconds'); | ||
1489 | equal(track.cues[2].pauseOnExit, false, 'cue does not pause on exit'); | ||
1490 | equal(track.cues[2].text, '', 'did not set cue text'); | ||
1491 | equal(track.cues[2].frame.owner, 'owner@example.com', 'set the owner'); | ||
1492 | deepEqual(track.cues[2].frame.privateData, | ||
1493 | new Uint8Array([1, 2, 3]), | ||
1494 | 'set the private data'); | ||
1495 | }); | ||
1496 | |||
1497 | QUnit.skip('only adds in-band cues the first time they are encountered', function() { | ||
1498 | var tags = [{ pts: 0, bytes: new Uint8Array(1) }], track; | ||
1499 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1500 | player.src({ | ||
1501 | src: 'manifest/media.m3u8', | ||
1502 | type: 'application/vnd.apple.mpegurl' | ||
1503 | }); | ||
1504 | openMediaSource(player); | ||
1505 | |||
1506 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1507 | // trigger a metadata event | ||
1508 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', { | ||
1509 | pts: 2000, | ||
1510 | data: new Uint8Array([]), | ||
1511 | frames: [{ | ||
1512 | id: 'TXXX', | ||
1513 | value: 'cue text' | ||
1514 | }] | ||
1515 | }); | ||
1516 | }; | ||
1517 | standardXHRResponse(requests.shift()); | ||
1518 | standardXHRResponse(requests.shift()); | ||
1519 | // seek back to the first segment | ||
1520 | player.currentTime(0); | ||
1521 | player.tech_.hls.trigger('seeking'); | ||
1522 | tags.push({ pts: 0, bytes: new Uint8Array(1) }); | ||
1523 | standardXHRResponse(requests.shift()); | ||
1524 | |||
1525 | track = player.textTracks()[0]; | ||
1526 | equal(track.cues.length, 1, 'only added the cue once'); | ||
1527 | }); | ||
1528 | |||
1529 | QUnit.skip('clears in-band cues ahead of current time on seek', function() { | ||
1530 | var | ||
1531 | tags = [], | ||
1532 | events = [], | ||
1533 | track; | ||
1534 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1535 | player.src({ | ||
1536 | src: 'manifest/media.m3u8', | ||
1537 | type: 'application/vnd.apple.mpegurl' | ||
1538 | }); | ||
1539 | openMediaSource(player); | ||
1540 | |||
1541 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1542 | // trigger a metadata event | ||
1543 | while (events.length) { | ||
1544 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | ||
1545 | } | ||
1546 | }; | ||
1547 | standardXHRResponse(requests.shift()); // media | ||
1548 | tags.push({ pts: 0, bytes: new Uint8Array(1) }, | ||
1549 | { pts: 10 * 1000, bytes: new Uint8Array(1) }); | ||
1550 | events.push({ | ||
1551 | pts: 9.9 * 1000, | ||
1552 | data: new Uint8Array([]), | ||
1553 | frames: [{ | ||
1554 | id: 'TXXX', | ||
1555 | value: 'cue 1' | ||
1556 | }] | ||
1557 | }); | ||
1558 | events.push({ | ||
1559 | pts: 20 * 1000, | ||
1560 | data: new Uint8Array([]), | ||
1561 | frames: [{ | ||
1562 | id: 'TXXX', | ||
1563 | value: 'cue 3' | ||
1564 | }] | ||
1565 | }); | ||
1566 | standardXHRResponse(requests.shift()); // segment 0 | ||
1567 | tags.push({ pts: 10 * 1000 + 1, bytes: new Uint8Array(1) }, | ||
1568 | { pts: 20 * 1000, bytes: new Uint8Array(1) }); | ||
1569 | events.push({ | ||
1570 | pts: 19.9 * 1000, | ||
1571 | data: new Uint8Array([]), | ||
1572 | frames: [{ | ||
1573 | id: 'TXXX', | ||
1574 | value: 'cue 2' | ||
1575 | }] | ||
1576 | }); | ||
1577 | player.tech_.hls.checkBuffer_(); | ||
1578 | standardXHRResponse(requests.shift()); // segment 1 | ||
1579 | |||
1580 | track = player.textTracks()[0]; | ||
1581 | equal(track.cues.length, 3, 'added the cues'); | ||
1582 | |||
1583 | // seek into segment 1 | ||
1584 | player.currentTime(11); | ||
1585 | player.trigger('seeking'); | ||
1586 | equal(track.cues.length, 1, 'removed later cues'); | ||
1587 | equal(track.cues[0].startTime, 9.9, 'retained the earlier cue'); | ||
1588 | }); | ||
1589 | |||
1590 | QUnit.skip('translates ID3 PTS values to cue media timeline positions', function() { | ||
1591 | var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; | ||
1592 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1593 | player.src({ | ||
1594 | src: 'manifest/media.m3u8', | ||
1595 | type: 'application/vnd.apple.mpegurl' | ||
1596 | }); | ||
1597 | openMediaSource(player); | ||
1598 | |||
1599 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1600 | // trigger a metadata event | ||
1601 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', { | ||
1602 | pts: 5 * 1000, | ||
1603 | data: new Uint8Array([]), | ||
1604 | frames: [{ | ||
1605 | id: 'TXXX', | ||
1606 | value: 'cue text' | ||
1607 | }] | ||
1608 | }); | ||
1609 | }; | ||
1610 | standardXHRResponse(requests.shift()); // media | ||
1611 | standardXHRResponse(requests.shift()); // segment 0 | ||
1612 | |||
1613 | track = player.textTracks()[0]; | ||
1614 | equal(track.cues[0].startTime, 1, 'translated startTime'); | ||
1615 | equal(track.cues[0].endTime, 1, 'translated startTime'); | ||
1616 | }); | ||
1617 | |||
1618 | QUnit.skip('translates ID3 PTS values with expired segments', function() { | ||
1619 | var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; | ||
1620 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1621 | player.src({ | ||
1622 | src: 'live.m3u8', | ||
1623 | type: 'application/vnd.apple.mpegurl' | ||
1624 | }); | ||
1625 | openMediaSource(player); | ||
1626 | player.play(); | ||
1627 | |||
1628 | // 20.9 seconds of content have expired | ||
1629 | player.hls.playlists.expiredPostDiscontinuity_ = 20.9; | ||
1630 | |||
1631 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1632 | // trigger a metadata event | ||
1633 | player.hls.segmentParser_.metadataStream.trigger('data', { | ||
1634 | pts: 5 * 1000, | ||
1635 | data: new Uint8Array([]), | ||
1636 | frames: [{ | ||
1637 | id: 'TXXX', | ||
1638 | value: 'cue text' | ||
1639 | }] | ||
1640 | }); | ||
1641 | }; | ||
1642 | requests.shift().respond(200, null, | ||
1643 | '#EXTM3U\n' + | ||
1644 | '#EXT-X-MEDIA-SEQUENCE:2\n' + | ||
1645 | '#EXTINF:10,\n' + | ||
1646 | '2.ts\n' + | ||
1647 | '#EXTINF:10,\n' + | ||
1648 | '3.ts\n'); // media | ||
1649 | standardXHRResponse(requests.shift()); // segment 0 | ||
1650 | |||
1651 | track = player.textTracks()[0]; | ||
1652 | equal(track.cues[0].startTime, 20.9 + 1, 'translated startTime'); | ||
1653 | equal(track.cues[0].endTime, 20.9 + 1, 'translated startTime'); | ||
1654 | }); | ||
1655 | |||
1656 | QUnit.skip('translates id3 PTS values for audio-only media', function() { | ||
1657 | var tags = [{ pts: 4 * 1000, bytes: new Uint8Array(1) }], track; | ||
1658 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1659 | player.src({ | ||
1660 | src: 'manifest/media.m3u8', | ||
1661 | type: 'application/vnd.apple.mpegurl' | ||
1662 | }); | ||
1663 | openMediaSource(player); | ||
1664 | |||
1665 | player.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1666 | // trigger a metadata event | ||
1667 | player.hls.segmentParser_.metadataStream.trigger('data', { | ||
1668 | pts: 5 * 1000, | ||
1669 | data: new Uint8Array([]), | ||
1670 | frames: [{ | ||
1671 | id: 'TXXX', | ||
1672 | value: 'cue text' | ||
1673 | }] | ||
1674 | }); | ||
1675 | }; | ||
1676 | player.hls.segmentParser_.stats.h264Tags = function() { return 0; }; | ||
1677 | player.hls.segmentParser_.stats.minVideoPts = null; | ||
1678 | standardXHRResponse(requests.shift()); // media | ||
1679 | standardXHRResponse(requests.shift()); // segment 0 | ||
1680 | |||
1681 | track = player.textTracks()[0]; | ||
1682 | equal(track.cues[0].startTime, 1, 'translated startTime'); | ||
1683 | }); | ||
1684 | |||
1685 | QUnit.skip('translates ID3 PTS values across discontinuities', function() { | ||
1686 | var tags = [], events = [], track; | ||
1687 | videojs.Hls.SegmentParser = mockSegmentParser(tags); | ||
1688 | player.src({ | ||
1689 | src: 'cues-and-discontinuities.m3u8', | ||
1690 | type: 'application/vnd.apple.mpegurl' | ||
1691 | }); | ||
1692 | openMediaSource(player); | ||
1693 | |||
1694 | player.tech_.hls.segmentParser_.parseSegmentBinaryData = function() { | ||
1695 | // trigger a metadata event | ||
1696 | if (events.length) { | ||
1697 | player.tech_.hls.segmentParser_.metadataStream.trigger('data', events.shift()); | ||
1698 | } | ||
1699 | }; | ||
1700 | |||
1701 | // media playlist | ||
1702 | player.trigger('play'); | ||
1703 | requests.shift().respond(200, null, | ||
1704 | '#EXTM3U\n' + | ||
1705 | '#EXTINF:10,\n' + | ||
1706 | '0.ts\n' + | ||
1707 | '#EXT-X-DISCONTINUITY\n' + | ||
1708 | '#EXTINF:10,\n' + | ||
1709 | '1.ts\n'); | ||
1710 | |||
1711 | // segment 0 starts at PTS 14000 and has a cue point at 15000 | ||
1712 | tags.push({ pts: 14 * 1000, bytes: new Uint8Array(1) }, | ||
1713 | { pts: 24 * 1000, bytes: new Uint8Array(1) }); | ||
1714 | events.push({ | ||
1715 | pts: 15 * 1000, | ||
1716 | data: new Uint8Array([]), | ||
1717 | frames: [{ | ||
1718 | id: 'TXXX', | ||
1719 | value: 'cue 0' | ||
1720 | }] | ||
1721 | }); | ||
1722 | standardXHRResponse(requests.shift()); // segment 0 | ||
1723 | |||
1724 | // segment 1 is after a discontinuity, starts at PTS 22000 | ||
1725 | // and has a cue point at 23000 | ||
1726 | tags.push({ pts: 22 * 1000, bytes: new Uint8Array(1) }); | ||
1727 | events.push({ | ||
1728 | pts: 23 * 1000, | ||
1729 | data: new Uint8Array([]), | ||
1730 | frames: [{ | ||
1731 | id: 'TXXX', | ||
1732 | value: 'cue 1' | ||
1733 | }] | ||
1734 | }); | ||
1735 | player.tech_.hls.checkBuffer_(); | ||
1736 | standardXHRResponse(requests.shift()); | ||
1737 | |||
1738 | track = player.textTracks()[0]; | ||
1739 | equal(track.cues.length, 2, 'created cues'); | ||
1740 | equal(track.cues[0].startTime, 1, 'first cue started at the correct time'); | ||
1741 | equal(track.cues[0].endTime, 1, 'first cue ended at the correct time'); | ||
1742 | equal(track.cues[1].startTime, 11, 'second cue started at the correct time'); | ||
1743 | equal(track.cues[1].endTime, 11, 'second cue ended at the correct time'); | ||
1744 | }); | ||
1745 | |||
1746 | test('seeks between buffered time ranges', function() { | ||
1747 | player.src({ | ||
1748 | src: 'media.m3u8', | ||
1749 | type: 'application/vnd.apple.mpegurl' | ||
1750 | }); | ||
1751 | openMediaSource(player); | ||
1752 | standardXHRResponse(requests.shift()); // media | ||
1753 | player.tech_.buffered = function() { | ||
1754 | return videojs.createTimeRange([[0, 10], [20, 30]]); | ||
1755 | }; | ||
1756 | |||
1757 | player.tech_.setCurrentTime(15); | ||
1758 | clock.tick(1); | ||
1759 | // drop the aborted segment | ||
1760 | requests.shift(); | ||
1761 | equal(requests[0].url, | ||
1762 | absoluteUrl('media-00002.ts'), | ||
1763 | 'requested the correct segment'); | ||
1764 | }); | ||
1765 | |||
1766 | 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() { |
1767 | var mediaIndex; | 1309 | var mediaIndex; |
1768 | player.src({ | 1310 | player.src({ |
... | @@ -2036,26 +1578,21 @@ test('does not break if the playlist has no segments', function() { | ... | @@ -2036,26 +1578,21 @@ test('does not break if the playlist has no segments', function() { |
2036 | 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'); |
2037 | }); | 1579 | }); |
2038 | 1580 | ||
2039 | test('clears the segment buffer on seek', function() { | 1581 | test('aborts segment processing on seek', function() { |
2040 | var currentTime, oldCurrentTime; | 1582 | var currentTime = 0; |
2041 | |||
2042 | player.src({ | 1583 | player.src({ |
2043 | src: 'discontinuity.m3u8', | 1584 | src: 'discontinuity.m3u8', |
2044 | type: 'application/vnd.apple.mpegurl' | 1585 | type: 'application/vnd.apple.mpegurl' |
2045 | }); | 1586 | }); |
2046 | openMediaSource(player); | 1587 | openMediaSource(player); |
2047 | oldCurrentTime = player.currentTime; | 1588 | player.tech_.currentTime = function() { |
2048 | player.currentTime = function(time) { | ||
2049 | if (time !== undefined) { | ||
2050 | return oldCurrentTime.call(player, time); | ||
2051 | } | ||
2052 | return currentTime; | 1589 | return currentTime; |
2053 | }; | 1590 | }; |
2054 | player.tech_.buffered = function() { | 1591 | player.tech_.buffered = function() { |
2055 | return videojs.createTimeRange(); | 1592 | return videojs.createTimeRange(); |
2056 | }; | 1593 | }; |
2057 | 1594 | ||
2058 | requests.pop().respond(200, null, | 1595 | requests.shift().respond(200, null, |
2059 | '#EXTM3U\n' + | 1596 | '#EXTM3U\n' + |
2060 | '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + | 1597 | '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + |
2061 | '#EXTINF:10,0\n' + | 1598 | '#EXTINF:10,0\n' + |
... | @@ -2063,23 +1600,19 @@ test('clears the segment buffer on seek', function() { | ... | @@ -2063,23 +1600,19 @@ test('clears the segment buffer on seek', function() { |
2063 | '#EXT-X-DISCONTINUITY\n' + | 1600 | '#EXT-X-DISCONTINUITY\n' + |
2064 | '#EXTINF:10,0\n' + | 1601 | '#EXTINF:10,0\n' + |
2065 | '2.ts\n' + | 1602 | '2.ts\n' + |
2066 | '#EXT-X-ENDLIST\n'); | 1603 | '#EXT-X-ENDLIST\n'); // media |
2067 | standardXHRResponse(requests.pop()); // 1.ts | 1604 | standardXHRResponse(requests.shift()); // 1.ts |
2068 | 1605 | standardXHRResponse(requests.shift()); // key.php | |
2069 | // play to 6s to trigger the next segment request | 1606 | ok(player.tech_.hls.pendingSegment_, 'decrypting the segment'); |
2070 | currentTime = 6; | ||
2071 | clock.tick(6000); | ||
2072 | |||
2073 | standardXHRResponse(requests.pop()); // 2.ts | ||
2074 | equal(player.tech_.hls.segmentBuffer_.length, 2, 'started fetching segments'); | ||
2075 | 1607 | ||
2076 | // seek back to the beginning | 1608 | // seek back to the beginning |
2077 | player.currentTime(0); | 1609 | player.currentTime(0); |
2078 | clock.tick(1); | 1610 | clock.tick(1); |
2079 | equal(player.tech_.hls.segmentBuffer_.length, 0, 'cleared the segment buffer'); | 1611 | ok(!player.tech_.hls.pendingSegment_, 'aborted processing'); |
2080 | }); | 1612 | }); |
2081 | 1613 | ||
2082 | test('calls mediaSource\'s timestampOffset on discontinuity', function() { | 1614 | test('calls mediaSource\'s timestampOffset on discontinuity', function() { |
1615 | var buffered = [[]]; | ||
2083 | player.src({ | 1616 | player.src({ |
2084 | src: 'discontinuity.m3u8', | 1617 | src: 'discontinuity.m3u8', |
2085 | type: 'application/vnd.apple.mpegurl' | 1618 | type: 'application/vnd.apple.mpegurl' |
... | @@ -2087,10 +1620,10 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { | ... | @@ -2087,10 +1620,10 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { |
2087 | openMediaSource(player); | 1620 | openMediaSource(player); |
2088 | player.play(); | 1621 | player.play(); |
2089 | player.tech_.buffered = function() { | 1622 | player.tech_.buffered = function() { |
2090 | return videojs.createTimeRange(0, 10); | 1623 | return videojs.createTimeRange(buffered); |
2091 | }; | 1624 | }; |
2092 | 1625 | ||
2093 | requests.pop().respond(200, null, | 1626 | requests.shift().respond(200, null, |
2094 | '#EXTM3U\n' + | 1627 | '#EXTM3U\n' + |
2095 | '#EXTINF:10,0\n' + | 1628 | '#EXTINF:10,0\n' + |
2096 | '1.ts\n' + | 1629 | '1.ts\n' + |
... | @@ -2099,14 +1632,14 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { | ... | @@ -2099,14 +1632,14 @@ test('calls mediaSource\'s timestampOffset on discontinuity', function() { |
2099 | '2.ts\n' + | 1632 | '2.ts\n' + |
2100 | '#EXT-X-ENDLIST\n'); | 1633 | '#EXT-X-ENDLIST\n'); |
2101 | player.tech_.hls.sourceBuffer.timestampOffset = 0; | 1634 | player.tech_.hls.sourceBuffer.timestampOffset = 0; |
2102 | standardXHRResponse(requests.pop()); // 1.ts | 1635 | standardXHRResponse(requests.shift()); // 1.ts |
2103 | 1636 | equal(player.tech_.hls.sourceBuffer.timestampOffset, | |
2104 | equal(player.tech_.hls.sourceBuffer.timestampOffset, 0, 'timestampOffset starts at zero'); | 1637 | 0, |
2105 | 1638 | 'timestampOffset starts at zero'); | |
2106 | // play to 6s to trigger the next segment request | ||
2107 | clock.tick(6000); | ||
2108 | 1639 | ||
2109 | standardXHRResponse(requests.pop()); // 2.ts | 1640 | buffered = [[0, 10]]; |
1641 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
1642 | standardXHRResponse(requests.shift()); // 2.ts | ||
2110 | equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity'); | 1643 | equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity'); |
2111 | }); | 1644 | }); |
2112 | 1645 | ||
... | @@ -2198,7 +1731,7 @@ QUnit.skip('sets the timestampOffset after seeking to discontinuity', function() | ... | @@ -2198,7 +1731,7 @@ QUnit.skip('sets the timestampOffset after seeking to discontinuity', function() |
2198 | 'set the timestamp offset'); | 1731 | 'set the timestamp offset'); |
2199 | }); | 1732 | }); |
2200 | 1733 | ||
2201 | QUnit.skip('tracks segment end times as they are buffered', function() { | 1734 | test('tracks segment end times as they are buffered', function() { |
2202 | var bufferEnd = 0; | 1735 | var bufferEnd = 0; |
2203 | player.src({ | 1736 | player.src({ |
2204 | src: 'media.m3u8', | 1737 | src: 'media.m3u8', |
... | @@ -2225,8 +1758,7 @@ QUnit.skip('tracks segment end times as they are buffered', function() { | ... | @@ -2225,8 +1758,7 @@ QUnit.skip('tracks segment end times as they are buffered', function() { |
2225 | bufferEnd = 9.5; | 1758 | bufferEnd = 9.5; |
2226 | player.tech_.hls.sourceBuffer.trigger('update'); | 1759 | player.tech_.hls.sourceBuffer.trigger('update'); |
2227 | player.tech_.hls.sourceBuffer.trigger('updateend'); | 1760 | player.tech_.hls.sourceBuffer.trigger('updateend'); |
2228 | equal(player.tech_.duration(), 10 + 9.5, 'updated duration'); | 1761 | equal(player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration'); |
2229 | equal(player.tech_.hls.appendingSegmentInfo_, null, 'cleared the appending segment'); | ||
2230 | }); | 1762 | }); |
2231 | 1763 | ||
2232 | QUnit.skip('seeking does not fail when targeted between segments', function() { | 1764 | QUnit.skip('seeking does not fail when targeted between segments', function() { |
... | @@ -2274,7 +1806,7 @@ test('resets the switching algorithm if a request times out', function() { | ... | @@ -2274,7 +1806,7 @@ test('resets the switching algorithm if a request times out', function() { |
2274 | type: 'application/vnd.apple.mpegurl' | 1806 | type: 'application/vnd.apple.mpegurl' |
2275 | }); | 1807 | }); |
2276 | openMediaSource(player); | 1808 | openMediaSource(player); |
2277 | player.tech_.hls.bandwidth = 20000; | 1809 | player.tech_.hls.bandwidth = 1e20; |
2278 | 1810 | ||
2279 | standardXHRResponse(requests.shift()); // master | 1811 | standardXHRResponse(requests.shift()); // master |
2280 | standardXHRResponse(requests.shift()); // media.m3u8 | 1812 | standardXHRResponse(requests.shift()); // media.m3u8 |
... | @@ -2455,6 +1987,7 @@ test('tracks the bytes downloaded', function() { | ... | @@ -2455,6 +1987,7 @@ test('tracks the bytes downloaded', function() { |
2455 | // transmit some segment bytes | 1987 | // transmit some segment bytes |
2456 | requests[0].response = new ArrayBuffer(17); | 1988 | requests[0].response = new ArrayBuffer(17); |
2457 | requests.shift().respond(200, null, ''); | 1989 | requests.shift().respond(200, null, ''); |
1990 | player.tech_.hls.sourceBuffer.trigger('updateend'); | ||
2458 | 1991 | ||
2459 | strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received'); | 1992 | strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received'); |
2460 | 1993 | ||
... | @@ -2509,12 +2042,15 @@ test('can be disposed before finishing initialization', function() { | ... | @@ -2509,12 +2042,15 @@ test('can be disposed before finishing initialization', function() { |
2509 | }); | 2042 | }); |
2510 | 2043 | ||
2511 | 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() { |
2512 | var endOfStreams = 0; | 2045 | var endOfStreams = 0, buffered = [[]]; |
2513 | player.src({ | 2046 | player.src({ |
2514 | src: 'http://example.com/media.m3u8', | 2047 | src: 'http://example.com/media.m3u8', |
2515 | type: 'application/vnd.apple.mpegurl' | 2048 | type: 'application/vnd.apple.mpegurl' |
2516 | }); | 2049 | }); |
2517 | openMediaSource(player); | 2050 | openMediaSource(player); |
2051 | player.tech_.buffered = function() { | ||
2052 | return videojs.createTimeRanges(buffered); | ||
2053 | }; | ||
2518 | player.tech_.hls.mediaSource.endOfStream = function() { | 2054 | player.tech_.hls.mediaSource.endOfStream = function() { |
2519 | endOfStreams++; | 2055 | endOfStreams++; |
2520 | }; | 2056 | }; |
... | @@ -2529,70 +2065,61 @@ test('calls ended() on the media source at the end of a playlist', function() { | ... | @@ -2529,70 +2065,61 @@ test('calls ended() on the media source at the end of a playlist', function() { |
2529 | requests.shift().respond(200, null, ''); | 2065 | requests.shift().respond(200, null, ''); |
2530 | strictEqual(endOfStreams, 0, 'waits for the buffer update to finish'); | 2066 | strictEqual(endOfStreams, 0, 'waits for the buffer update to finish'); |
2531 | 2067 | ||
2068 | buffered =[[0, 10]]; | ||
2532 | player.tech_.hls.sourceBuffer.trigger('updateend'); | 2069 | player.tech_.hls.sourceBuffer.trigger('updateend'); |
2533 | strictEqual(endOfStreams, 1, 'ended media source'); | 2070 | strictEqual(endOfStreams, 1, 'ended media source'); |
2534 | }); | 2071 | }); |
2535 | 2072 | ||
2536 | 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; | ||
2537 | player.src({ | 2075 | player.src({ |
2538 | src: 'http://example.com/media.m3u8', | 2076 | src: 'http://example.com/media.m3u8', |
2539 | type: 'application/vnd.apple.mpegurl' | 2077 | type: 'application/vnd.apple.mpegurl' |
2540 | }); | 2078 | }); |
2541 | openMediaSource(player); | 2079 | openMediaSource(player); |
2080 | player.tech_.setCurrentTime = function(time) { | ||
2081 | if (time !== undefined) { | ||
2082 | seekTime = time; | ||
2083 | } | ||
2084 | return 0; | ||
2085 | }; | ||
2542 | requests.shift().respond(200, null, | 2086 | requests.shift().respond(200, null, |
2543 | '#EXTM3U\n' + | 2087 | '#EXTM3U\n' + |
2544 | '#EXTINF:10,\n' + | 2088 | '#EXTINF:10,\n' + |
2545 | '0.ts\n' + | 2089 | '0.ts\n' + |
2546 | '#EXT-X-ENDLIST\n'); | 2090 | '#EXT-X-ENDLIST\n'); |
2547 | standardXHRResponse(requests.shift()); | 2091 | standardXHRResponse(requests.shift()); |
2548 | |||
2549 | strictEqual(player.tech_.hls.mediaIndex, 1, 'index is 1 after the first segment'); | ||
2550 | player.tech_.ended = function() { | 2092 | player.tech_.ended = function() { |
2551 | return true; | 2093 | return true; |
2552 | }; | 2094 | }; |
2553 | 2095 | ||
2554 | player.tech_.trigger('play'); | 2096 | player.tech_.trigger('play'); |
2555 | strictEqual(player.tech_.hls.mediaIndex, 0, 'index is 0 after the first segment'); | 2097 | equal(seekTime, 0, 'seeked to the beginning'); |
2556 | }); | 2098 | }); |
2557 | 2099 | ||
2558 | test('drainBuffer will not proceed with empty source buffer', function() { | 2100 | test('segments remain pending without a source buffer', function() { |
2559 | var oldMedia, newMedia, compareBuffer; | ||
2560 | player.src({ | 2101 | player.src({ |
2561 | src: 'https://example.com/encrypted-media.m3u8', | 2102 | src: 'https://example.com/encrypted-media.m3u8', |
2562 | type: 'application/vnd.apple.mpegurl' | 2103 | type: 'application/vnd.apple.mpegurl' |
2563 | }); | 2104 | }); |
2564 | openMediaSource(player); | 2105 | openMediaSource(player); |
2565 | 2106 | ||
2566 | oldMedia = player.tech_.hls.playlists.media; | 2107 | requests.shift().respond(200, null, |
2567 | newMedia = {segments: [{ | 2108 | '#EXTM3U\n' + |
2568 | key: { | 2109 | '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=52"\n' + |
2569 | 'retries': 5 | 2110 | '#EXTINF:10,\n' + |
2570 | }, | 2111 | 'http://media.example.com/fileSequence52-A.ts' + |
2571 | uri: 'http://media.example.com/fileSequence52-A.ts' | 2112 | '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=53"\n' + |
2572 | }, { | 2113 | '#EXTINF:10,\n' + |
2573 | key: { | 2114 | 'http://media.example.com/fileSequence53-B.ts\n' + |
2574 | 'method': 'AES-128', | 2115 | '#EXT-X-ENDLIST\n'); |
2575 | 'uri': 'https://priv.example.com/key.php?r=53' | ||
2576 | }, | ||
2577 | uri: 'http://media.example.com/fileSequence53-B.ts' | ||
2578 | }]}; | ||
2579 | player.tech_.hls.playlists.media = function() { | ||
2580 | return newMedia; | ||
2581 | }; | ||
2582 | 2116 | ||
2583 | player.tech_.hls.sourceBuffer = undefined; | 2117 | player.tech_.hls.sourceBuffer = undefined; |
2584 | compareBuffer = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}]; | ||
2585 | player.tech_.hls.segmentBuffer_ = [{mediaIndex: 0, playlist: newMedia, offset: 0, bytes: new Uint8Array(3)}]; | ||
2586 | 2118 | ||
2587 | player.tech_.hls.drainBuffer(); | 2119 | standardXHRResponse(requests.shift()); // key |
2588 | 2120 | standardXHRResponse(requests.shift()); // segment | |
2589 | /* Normally, drainBuffer() calls segmentBuffer.shift(), removing a segment from the stack. | 2121 | player.tech_.hls.checkBuffer_(); |
2590 | * Comparing two buffers to ensure no segment was popped verifies that we returned early | 2122 | ok(player.tech_.hls.pendingSegment_, 'waiting for the source buffer'); |
2591 | * from drainBuffer() because sourceBuffer was empty. | ||
2592 | */ | ||
2593 | deepEqual(player.tech_.hls.segmentBuffer_, compareBuffer, 'playlist remains unchanged'); | ||
2594 | |||
2595 | player.tech_.hls.playlists.media = oldMedia; | ||
2596 | }); | 2123 | }); |
2597 | 2124 | ||
2598 | test('keys are requested when an encrypted segment is loaded', function() { | 2125 | test('keys are requested when an encrypted segment is loaded', function() { |
... | @@ -2603,12 +2130,14 @@ test('keys are requested when an encrypted segment is loaded', function() { | ... | @@ -2603,12 +2130,14 @@ test('keys are requested when an encrypted segment is loaded', function() { |
2603 | openMediaSource(player); | 2130 | openMediaSource(player); |
2604 | player.tech_.trigger('play'); | 2131 | player.tech_.trigger('play'); |
2605 | standardXHRResponse(requests.shift()); // playlist | 2132 | standardXHRResponse(requests.shift()); // playlist |
2606 | standardXHRResponse(requests.shift()); // first segment | ||
2607 | 2133 | ||
2608 | strictEqual(requests.length, 1, 'a key XHR is created'); | 2134 | strictEqual(requests.length, 2, 'a key XHR is created'); |
2609 | strictEqual(requests[0].url, | 2135 | strictEqual(requests[0].url, |
2610 | player.tech_.hls.playlists.media().segments[0].key.uri, | 2136 | player.tech_.hls.playlists.media().segments[0].key.uri, |
2611 | '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'); | ||
2612 | }); | 2141 | }); |
2613 | 2142 | ||
2614 | test('keys are resolved relative to the master playlist', function() { | 2143 | test('keys are resolved relative to the master playlist', function() { |
... | @@ -2629,10 +2158,9 @@ test('keys are resolved relative to the master playlist', function() { | ... | @@ -2629,10 +2158,9 @@ test('keys are resolved relative to the master playlist', function() { |
2629 | '#EXTINF:2.833,\n' + | 2158 | '#EXTINF:2.833,\n' + |
2630 | 'http://media.example.com/fileSequence1.ts\n' + | 2159 | 'http://media.example.com/fileSequence1.ts\n' + |
2631 | '#EXT-X-ENDLIST\n'); | 2160 | '#EXT-X-ENDLIST\n'); |
2632 | 2161 | equal(requests.length, 2, 'requested the key'); | |
2633 | standardXHRResponse(requests.shift()); | 2162 | equal(requests[0].url, |
2634 | equal(requests.length, 1, 'requested the key'); | 2163 | absoluteUrl('video/playlist/keys/key.php'), |
2635 | ok((/video\/playlist\/keys\/key\.php$/).test(requests[0].url), | ||
2636 | 'resolves multiple relative paths'); | 2164 | 'resolves multiple relative paths'); |
2637 | }); | 2165 | }); |
2638 | 2166 | ||
... | @@ -2649,13 +2177,13 @@ test('keys are resolved relative to their containing playlist', function() { | ... | @@ -2649,13 +2177,13 @@ test('keys are resolved relative to their containing playlist', function() { |
2649 | '#EXTINF:2.833,\n' + | 2177 | '#EXTINF:2.833,\n' + |
2650 | 'http://media.example.com/fileSequence1.ts\n' + | 2178 | 'http://media.example.com/fileSequence1.ts\n' + |
2651 | '#EXT-X-ENDLIST\n'); | 2179 | '#EXT-X-ENDLIST\n'); |
2652 | standardXHRResponse(requests.shift()); | 2180 | equal(requests.length, 2, 'requested a key'); |
2653 | equal(requests.length, 1, 'requested a key'); | 2181 | equal(requests[0].url, |
2654 | ok((/video\/keys\/key\.php$/).test(requests[0].url), | 2182 | absoluteUrl('video/keys/key.php'), |
2655 | 'resolves multiple relative paths'); | 2183 | 'resolves multiple relative paths'); |
2656 | }); | 2184 | }); |
2657 | 2185 | ||
2658 | 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() { |
2659 | player.src({ | 2187 | player.src({ |
2660 | src: 'https://example.com/encrypted-media.m3u8', | 2188 | src: 'https://example.com/encrypted-media.m3u8', |
2661 | type: 'application/vnd.apple.mpegurl' | 2189 | type: 'application/vnd.apple.mpegurl' |
... | @@ -2672,15 +2200,17 @@ test('a new key XHR is created when a the segment is received', function() { | ... | @@ -2672,15 +2200,17 @@ test('a new key XHR is created when a the segment is received', function() { |
2672 | '#EXTINF:2.833,\n' + | 2200 | '#EXTINF:2.833,\n' + |
2673 | 'http://media.example.com/fileSequence2.ts\n' + | 2201 | 'http://media.example.com/fileSequence2.ts\n' + |
2674 | '#EXT-X-ENDLIST\n'); | 2202 | '#EXT-X-ENDLIST\n'); |
2675 | standardXHRResponse(requests.shift()); // segment 1 | ||
2676 | standardXHRResponse(requests.shift()); // key 1 | 2203 | standardXHRResponse(requests.shift()); // key 1 |
2204 | standardXHRResponse(requests.shift()); // segment 1 | ||
2677 | // "finish" decrypting segment 1 | 2205 | // "finish" decrypting segment 1 |
2678 | player.tech_.hls.segmentBuffer_[0].bytes = new Uint8Array(16); | 2206 | player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16); |
2679 | 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'); | ||
2680 | 2212 | ||
2681 | standardXHRResponse(requests.shift()); // segment 2 | 2213 | strictEqual(requests.length, 2, 'a key XHR is created'); |
2682 | |||
2683 | strictEqual(requests.length, 1, 'a key XHR is created'); | ||
2684 | strictEqual(requests[0].url, | 2214 | strictEqual(requests[0].url, |
2685 | 'https://example.com/' + | 2215 | 'https://example.com/' + |
2686 | player.tech_.hls.playlists.media().segments[1].key.uri, | 2216 | player.tech_.hls.playlists.media().segments[1].key.uri, |
... | @@ -2704,16 +2234,14 @@ test('seeking should abort an outstanding key request and create a new one', fun | ... | @@ -2704,16 +2234,14 @@ test('seeking should abort an outstanding key request and create a new one', fun |
2704 | '#EXTINF:9,\n' + | 2234 | '#EXTINF:9,\n' + |
2705 | 'http://media.example.com/fileSequence2.ts\n' + | 2235 | 'http://media.example.com/fileSequence2.ts\n' + |
2706 | '#EXT-X-ENDLIST\n'); | 2236 | '#EXT-X-ENDLIST\n'); |
2707 | standardXHRResponse(requests.shift()); // segment 1 | 2237 | standardXHRResponse(requests.pop()); // segment 1 |
2708 | 2238 | ||
2709 | player.currentTime(11); | 2239 | player.currentTime(11); |
2710 | clock.tick(1); | 2240 | clock.tick(1); |
2711 | ok(requests[0].aborted, 'the key XHR should be aborted'); | 2241 | ok(requests[0].aborted, 'the key XHR should be aborted'); |
2712 | requests.shift(); // aborted key 1 | 2242 | requests.shift(); // aborted key 1 |
2713 | 2243 | ||
2714 | equal(requests.length, 1, 'requested the new segment'); | 2244 | equal(requests.length, 2, 'requested the new key'); |
2715 | standardXHRResponse(requests.shift()); // segment 2 | ||
2716 | equal(requests.length, 1, 'requested the new key'); | ||
2717 | equal(requests[0].url, | 2245 | equal(requests[0].url, |
2718 | 'https://example.com/' + | 2246 | 'https://example.com/' + |
2719 | player.tech_.hls.playlists.media().segments[1].key.uri, | 2247 | player.tech_.hls.playlists.media().segments[1].key.uri, |
... | @@ -2736,7 +2264,7 @@ test('retries key requests once upon failure', function() { | ... | @@ -2736,7 +2264,7 @@ test('retries key requests once upon failure', function() { |
2736 | '#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' + |
2737 | '#EXTINF:15.0,\n' + | 2265 | '#EXTINF:15.0,\n' + |
2738 | 'http://media.example.com/fileSequence53-A.ts\n'); | 2266 | 'http://media.example.com/fileSequence53-A.ts\n'); |
2739 | standardXHRResponse(requests.shift()); // segment | 2267 | standardXHRResponse(requests.pop()); // segment |
2740 | requests[0].respond(404); | 2268 | requests[0].respond(404); |
2741 | 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'); |
2742 | 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'); |
... | @@ -2745,7 +2273,7 @@ test('retries key requests once upon failure', function() { | ... | @@ -2745,7 +2273,7 @@ test('retries key requests once upon failure', function() { |
2745 | equal(requests.length, 2, 'gives up after one retry'); | 2273 | equal(requests.length, 2, 'gives up after one retry'); |
2746 | }); | 2274 | }); |
2747 | 2275 | ||
2748 | test('skip segments if key requests fail more than once', function() { | 2276 | test('errors if key requests fail more than once', function() { |
2749 | var bytes = []; | 2277 | var bytes = []; |
2750 | 2278 | ||
2751 | player.src({ | 2279 | player.src({ |
... | @@ -2766,23 +2294,14 @@ test('skip segments if key requests fail more than once', function() { | ... | @@ -2766,23 +2294,14 @@ test('skip segments if key requests fail more than once', function() { |
2766 | player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) { | 2294 | player.tech_.hls.sourceBuffer.appendBuffer = function(chunk) { |
2767 | bytes.push(chunk); | 2295 | bytes.push(chunk); |
2768 | }; | 2296 | }; |
2769 | standardXHRResponse(requests.shift()); // segment 1 | 2297 | standardXHRResponse(requests.pop()); // segment 1 |
2770 | requests.shift().respond(404); // fail key | 2298 | requests.shift().respond(404); // fail key |
2771 | requests.shift().respond(404); // fail key, again | 2299 | requests.shift().respond(404); // fail key, again |
2772 | |||
2773 | player.tech_.hls.checkBuffer_(); | 2300 | player.tech_.hls.checkBuffer_(); |
2774 | standardXHRResponse(requests.shift()); // segment 2 | ||
2775 | equal(bytes.length, 0, 'did not append encrypted bytes'); | ||
2776 | 2301 | ||
2777 | // key for second segment | 2302 | equal(player.tech_.hls.mediaSource.error_, |
2778 | requests[0].response = new Uint32Array([0,0,0,0]).buffer; | 2303 | 'network', |
2779 | requests.shift().respond(200, null, ''); | 2304 | 'triggered a network error'); |
2780 | // "finish" decryption | ||
2781 | player.tech_.hls.segmentBuffer_[0].bytes = new Uint8Array(16); | ||
2782 | player.tech_.hls.checkBuffer_(); | ||
2783 | |||
2784 | equal(bytes.length, 1, 'appended cleartext bytes from the second segment'); | ||
2785 | deepEqual(bytes[0], new Uint8Array(16), 'appended bytes from the second segment, not the first'); | ||
2786 | }); | 2305 | }); |
2787 | 2306 | ||
2788 | 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() { |
... | @@ -2804,12 +2323,11 @@ test('the key is supplied to the decrypter in the correct format', function() { | ... | @@ -2804,12 +2323,11 @@ test('the key is supplied to the decrypter in the correct format', function() { |
2804 | '#EXTINF:15.0,\n' + | 2323 | '#EXTINF:15.0,\n' + |
2805 | 'http://media.example.com/fileSequence52-B.ts\n'); | 2324 | 'http://media.example.com/fileSequence52-B.ts\n'); |
2806 | 2325 | ||
2807 | |||
2808 | videojs.Hls.Decrypter = function(encrypted, key) { | 2326 | videojs.Hls.Decrypter = function(encrypted, key) { |
2809 | keys.push(key); | 2327 | keys.push(key); |
2810 | }; | 2328 | }; |
2811 | 2329 | ||
2812 | standardXHRResponse(requests.shift()); // segment | 2330 | standardXHRResponse(requests.pop()); // segment |
2813 | requests[0].response = new Uint32Array([0,1,2,3]).buffer; | 2331 | requests[0].response = new Uint32Array([0,1,2,3]).buffer; |
2814 | requests[0].respond(200, null, ''); | 2332 | requests[0].respond(200, null, ''); |
2815 | requests.shift(); // key | 2333 | requests.shift(); // key |
... | @@ -2856,6 +2374,7 @@ test('supplies the media sequence of current segment as the IV by default, if no | ... | @@ -2856,6 +2374,7 @@ test('supplies the media sequence of current segment as the IV by default, if no |
2856 | }); | 2374 | }); |
2857 | 2375 | ||
2858 | 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 = []; | ||
2859 | var media = '#EXTM3U\n' + | 2378 | var media = '#EXTM3U\n' + |
2860 | '#EXT-X-MEDIA-SEQUENCE:5\n' + | 2379 | '#EXT-X-MEDIA-SEQUENCE:5\n' + |
2861 | '#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' + |
... | @@ -2870,31 +2389,37 @@ test('switching playlists with an outstanding key request does not stall playbac | ... | @@ -2870,31 +2389,37 @@ test('switching playlists with an outstanding key request does not stall playbac |
2870 | openMediaSource(player); | 2389 | openMediaSource(player); |
2871 | player.tech_.trigger('play'); | 2390 | player.tech_.trigger('play'); |
2872 | 2391 | ||
2392 | player.tech_.hls.bandwidth = 1; | ||
2393 | player.tech_.buffered = function() { | ||
2394 | return videojs.createTimeRange(buffered); | ||
2395 | }; | ||
2873 | // master playlist | 2396 | // master playlist |
2874 | standardXHRResponse(requests.shift()); | 2397 | standardXHRResponse(requests.shift()); |
2875 | // media playlist | 2398 | // media playlist |
2876 | requests.shift().respond(200, null, media); | 2399 | requests.shift().respond(200, null, media); |
2877 | // mock out media switching from this point on | 2400 | // mock out media switching from this point on |
2878 | player.tech_.hls.playlists.media = function() { | 2401 | player.tech_.hls.playlists.media = function() { |
2879 | return player.tech_.hls.playlists.master.playlists[0]; | 2402 | return player.tech_.hls.playlists.master.playlists[1]; |
2880 | }; | 2403 | }; |
2881 | // first segment of the original media playlist | 2404 | // first segment of the original media playlist |
2882 | standardXHRResponse(requests.shift()); | 2405 | standardXHRResponse(requests.pop()); |
2883 | // don't respond to the initial key request | ||
2884 | requests.shift(); | ||
2885 | 2406 | ||
2886 | // "switch" media | 2407 | // "switch" media |
2887 | player.tech_.hls.playlists.trigger('mediachange'); | 2408 | player.tech_.hls.playlists.trigger('mediachange'); |
2409 | ok(!requests[0].aborted, 'did not abort the key request'); | ||
2888 | 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'); | ||
2889 | player.tech_.hls.checkBuffer_(); | 2417 | player.tech_.hls.checkBuffer_(); |
2890 | 2418 | ||
2891 | ok(requests.length, 'made a request'); | 2419 | equal(requests.length, 1, 'made a request'); |
2892 | equal(requests[0].url, | 2420 | equal(requests[0].url, |
2893 | 'http://media.example.com/fileSequence52-B.ts', | 2421 | 'http://media.example.com/fileSequence52-B.ts', |
2894 | 'requested the segment'); | 2422 | 'requested the segment'); |
2895 | equal(requests[1].url, | ||
2896 | 'https://priv.example.com/key.php?r=52', | ||
2897 | 'requested the key'); | ||
2898 | }); | 2423 | }); |
2899 | 2424 | ||
2900 | test('resolves relative key URLs against the playlist', function() { | 2425 | test('resolves relative key URLs against the playlist', function() { |
... | @@ -2911,8 +2436,6 @@ test('resolves relative key URLs against the playlist', function() { | ... | @@ -2911,8 +2436,6 @@ test('resolves relative key URLs against the playlist', function() { |
2911 | '#EXTINF:2.833,\n' + | 2436 | '#EXTINF:2.833,\n' + |
2912 | 'http://media.example.com/fileSequence52-A.ts\n' + | 2437 | 'http://media.example.com/fileSequence52-A.ts\n' + |
2913 | '#EXT-X-ENDLIST\n'); | 2438 | '#EXT-X-ENDLIST\n'); |
2914 | standardXHRResponse(requests.shift()); // segment | ||
2915 | |||
2916 | 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'); |
2917 | }); | 2440 | }); |
2918 | 2441 | ||
... | @@ -2937,7 +2460,7 @@ test('treats invalid keys as a key request failure', function() { | ... | @@ -2937,7 +2460,7 @@ test('treats invalid keys as a key request failure', function() { |
2937 | bytes.push(chunk); | 2460 | bytes.push(chunk); |
2938 | }; | 2461 | }; |
2939 | // segment request | 2462 | // segment request |
2940 | standardXHRResponse(requests.shift()); | 2463 | standardXHRResponse(requests.pop()); |
2941 | // keys should be 16 bytes long | 2464 | // keys should be 16 bytes long |
2942 | requests[0].response = new Uint8Array(1).buffer; | 2465 | requests[0].response = new Uint8Array(1).buffer; |
2943 | requests.shift().respond(200, null, ''); | 2466 | requests.shift().respond(200, null, ''); |
... | @@ -2947,17 +2470,12 @@ test('treats invalid keys as a key request failure', function() { | ... | @@ -2947,17 +2470,12 @@ test('treats invalid keys as a key request failure', function() { |
2947 | // the retried response is invalid, too | 2470 | // the retried response is invalid, too |
2948 | requests[0].response = new Uint8Array(1); | 2471 | requests[0].response = new Uint8Array(1); |
2949 | requests.shift().respond(200, null, ''); | 2472 | requests.shift().respond(200, null, ''); |
2950 | |||
2951 | // the first segment should be dropped and playback moves on | ||
2952 | player.tech_.hls.checkBuffer_(); | 2473 | player.tech_.hls.checkBuffer_(); |
2953 | equal(bytes.length, 0, 'did not append bytes'); | ||
2954 | |||
2955 | // second segment request | ||
2956 | requests[0].response = new Uint8Array([1, 2]); | ||
2957 | requests.shift().respond(200, null, ''); | ||
2958 | 2474 | ||
2959 | equal(bytes.length, 1, 'appended bytes'); | 2475 | // two failed attempts is a network error |
2960 | 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'); | ||
2961 | }); | 2479 | }); |
2962 | 2480 | ||
2963 | test('live stream should not call endOfStream', function(){ | 2481 | test('live stream should not call endOfStream', function(){ | ... | ... |
-
Please register or sign in to post a comment