Determine the segment to load by looking at buffered
When playlists are not segment-aligned or began at different times, we could make bad decisions about which segment to load by just incrementing the media index. Instead, annotate segments in the playlist with timeline information as they are downloaded. When a decision about what segment to fetch is required, simply try to fetch the segment that lines up with the latest edge of the buffered time range that contains the current time. Add a utility to stringify TextRanges for debugging. This is a checkpoint commit; 35 tests are currently failing in Chrome.
Showing
4 changed files
with
183 additions
and
281 deletions
1 | (function(window) { | 1 | (function(window) { |
2 | var textRange = function(range, i) { | ||
3 | return range.start(i) + '-' + range.end(i); | ||
4 | }; | ||
2 | var module = { | 5 | var module = { |
3 | hexDump: function(data) { | 6 | hexDump: function(data) { |
4 | var | 7 | var |
... | @@ -26,6 +29,13 @@ | ... | @@ -26,6 +29,13 @@ |
26 | }, | 29 | }, |
27 | tagDump: function(tag) { | 30 | tagDump: function(tag) { |
28 | return module.hexDump(tag.bytes); | 31 | return module.hexDump(tag.bytes); |
32 | }, | ||
33 | textRanges: function(ranges) { | ||
34 | var result = '', i; | ||
35 | for (i = 0; i < ranges.length; i++) { | ||
36 | result += textRange(ranges, i) + ' '; | ||
37 | } | ||
38 | return result; | ||
29 | } | 39 | } |
30 | }; | 40 | }; |
31 | 41 | ... | ... |
... | @@ -411,7 +411,7 @@ | ... | @@ -411,7 +411,7 @@ |
411 | * closest playback position that is currently available. | 411 | * closest playback position that is currently available. |
412 | */ | 412 | */ |
413 | PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { | 413 | PlaylistLoader.prototype.getMediaIndexForTime_ = function(time) { |
414 | var i; | 414 | var i, j, segment, targetDuration; |
415 | 415 | ||
416 | if (!this.media_) { | 416 | if (!this.media_) { |
417 | return 0; | 417 | return 0; |
... | @@ -424,18 +424,47 @@ | ... | @@ -424,18 +424,47 @@ |
424 | return 0; | 424 | return 0; |
425 | } | 425 | } |
426 | 426 | ||
427 | for (i = 0; i < this.media_.segments.length; i++) { | 427 | // 1) Walk backward until we find the latest segment with timeline |
428 | time -= Playlist.duration(this.media_, | 428 | // information that is earlier than `time` |
429 | this.media_.mediaSequence + i, | 429 | targetDuration = this.media_.targetDuration || 10; |
430 | this.media_.mediaSequence + i + 1, | 430 | i = this.media_.segments.length; |
431 | false); | 431 | while (i--) { |
432 | segment = this.media_.segments[i]; | ||
433 | if (segment.end !== undefined && segment.end <= time) { | ||
434 | time -= segment.end; | ||
435 | break; | ||
436 | } | ||
437 | if (segment.start !== undefined && segment.start < time) { | ||
432 | 438 | ||
433 | // HLS version 3 and lower round segment durations to the | 439 | if (segment.end !== undefined && segment.end > time) { |
434 | // nearest decimal integer. When the correct media index is | 440 | // we've found the target segment exactly |
435 | // ambiguous, prefer the higher one. | ||
436 | if (time <= 0) { | ||
437 | return i; | 441 | return i; |
438 | } | 442 | } |
443 | |||
444 | time -= segment.start; | ||
445 | time -= segment.duration || targetDuration; | ||
446 | break; | ||
447 | } | ||
448 | } | ||
449 | i++; | ||
450 | |||
451 | // 2) Walk forward, testing each segment to see if `time` falls within it | ||
452 | for (j = i; j < this.media_.segments.length; j++) { | ||
453 | segment = this.media_.segments[j]; | ||
454 | time -= segment.duration || targetDuration; | ||
455 | |||
456 | if (time < 0) { | ||
457 | return j; | ||
458 | } | ||
459 | |||
460 | // 2a) If we discover a segment that has timeline information | ||
461 | // before finding the result segment, the playlist information | ||
462 | // must have been inaccurate. Start a binary search for the | ||
463 | // segment which contains `time`. If the guess turns out to be | ||
464 | // incorrect, we'll have more info to work with next time. | ||
465 | if (segment.start !== undefined || segment.end !== undefined) { | ||
466 | return Math.floor((j - i) * 0.5); | ||
467 | } | ||
439 | } | 468 | } |
440 | 469 | ||
441 | // the playback position is outside the range of available | 470 | // the playback position is outside the range of available | ... | ... |
... | @@ -5,7 +5,7 @@ | ... | @@ -5,7 +5,7 @@ |
5 | 'use strict'; | 5 | 'use strict'; |
6 | 6 | ||
7 | var DEFAULT_TARGET_DURATION = 10; | 7 | var DEFAULT_TARGET_DURATION = 10; |
8 | var accumulateDuration, ascendingNumeric, duration, intervalDuration, optionalMin, optionalMax, rangeDuration, seekable; | 8 | var duration, intervalDuration, optionalMin, optionalMax, seekable; |
9 | 9 | ||
10 | // Math.min that will return the alternative input if one of its | 10 | // Math.min that will return the alternative input if one of its |
11 | // parameters in undefined | 11 | // parameters in undefined |
... | @@ -23,133 +23,6 @@ | ... | @@ -23,133 +23,6 @@ |
23 | return Math.max(left, right); | 23 | return Math.max(left, right); |
24 | }; | 24 | }; |
25 | 25 | ||
26 | // Array.sort comparator to sort numbers in ascending order | ||
27 | ascendingNumeric = function(left, right) { | ||
28 | return left - right; | ||
29 | }; | ||
30 | |||
31 | /** | ||
32 | * Returns the media duration for the segments between a start and | ||
33 | * exclusive end index. The start and end parameters are interpreted | ||
34 | * as indices into the currently available segments. This method | ||
35 | * does not calculate durations for segments that have expired. | ||
36 | * @param playlist {object} a media playlist object | ||
37 | * @param start {number} an inclusive lower boundary for the | ||
38 | * segments to examine. | ||
39 | * @param end {number} an exclusive upper boundary for the segments | ||
40 | * to examine. | ||
41 | * @param includeTrailingTime {boolean} if false, the interval between | ||
42 | * the final segment and the subsequent segment will not be included | ||
43 | * in the result | ||
44 | * @return {number} the duration between the start index and end | ||
45 | * index in seconds. | ||
46 | */ | ||
47 | accumulateDuration = function(playlist, start, end, includeTrailingTime) { | ||
48 | var | ||
49 | ranges = [], | ||
50 | rangeEnds = (playlist.discontinuityStarts || []).concat(end), | ||
51 | result = 0, | ||
52 | i; | ||
53 | |||
54 | // short circuit if start and end don't specify a non-empty range | ||
55 | // of segments | ||
56 | if (start >= end) { | ||
57 | return 0; | ||
58 | } | ||
59 | |||
60 | // create a range object for each discontinuity sequence | ||
61 | rangeEnds.sort(ascendingNumeric); | ||
62 | for (i = 0; i < rangeEnds.length; i++) { | ||
63 | if (rangeEnds[i] > start) { | ||
64 | ranges.push({ start: start, end: rangeEnds[i] }); | ||
65 | i++; | ||
66 | break; | ||
67 | } | ||
68 | } | ||
69 | for (; i < rangeEnds.length; i++) { | ||
70 | // ignore times ranges later than end | ||
71 | if (rangeEnds[i] >= end) { | ||
72 | ranges.push({ start: rangeEnds[i - 1], end: end }); | ||
73 | break; | ||
74 | } | ||
75 | ranges.push({ start: ranges[ranges.length - 1].end, end: rangeEnds[i] }); | ||
76 | } | ||
77 | |||
78 | // add up the durations for each of the ranges | ||
79 | for (i = 0; i < ranges.length; i++) { | ||
80 | result += rangeDuration(playlist, | ||
81 | ranges[i], | ||
82 | i === ranges.length - 1 && includeTrailingTime); | ||
83 | } | ||
84 | |||
85 | return result; | ||
86 | }; | ||
87 | |||
88 | /** | ||
89 | * Returns the duration of the specified range of segments. The | ||
90 | * range *must not* cross a discontinuity. | ||
91 | * @param playlist {object} a media playlist object | ||
92 | * @param range {object} an object that specifies a starting and | ||
93 | * ending index into the available segments. | ||
94 | * @param includeTrailingTime {boolean} if false, the interval between | ||
95 | * the final segment and the subsequent segment will not be included | ||
96 | * in the result | ||
97 | * @return {number} the duration of the range in seconds. | ||
98 | */ | ||
99 | rangeDuration = function(playlist, range, includeTrailingTime) { | ||
100 | var | ||
101 | result = 0, | ||
102 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION, | ||
103 | segment, | ||
104 | left, right; | ||
105 | |||
106 | // accumulate while searching for the earliest segment with | ||
107 | // available PTS information | ||
108 | for (left = range.start; left < range.end; left++) { | ||
109 | segment = playlist.segments[left]; | ||
110 | if (segment.minVideoPts !== undefined || | ||
111 | segment.minAudioPts !== undefined) { | ||
112 | break; | ||
113 | } | ||
114 | result += segment.duration || targetDuration; | ||
115 | } | ||
116 | |||
117 | // see if there's enough information to include the trailing time | ||
118 | if (includeTrailingTime) { | ||
119 | segment = playlist.segments[range.end]; | ||
120 | if (segment && | ||
121 | (segment.minVideoPts !== undefined || | ||
122 | segment.minAudioPts !== undefined)) { | ||
123 | result += 0.001 * | ||
124 | (optionalMin(segment.minVideoPts, segment.minAudioPts) - | ||
125 | optionalMin(playlist.segments[left].minVideoPts, | ||
126 | playlist.segments[left].minAudioPts)); | ||
127 | return result; | ||
128 | } | ||
129 | } | ||
130 | |||
131 | // do the same thing while finding the latest segment | ||
132 | for (right = range.end - 1; right >= left; right--) { | ||
133 | segment = playlist.segments[right]; | ||
134 | if (segment.maxVideoPts !== undefined || | ||
135 | segment.maxAudioPts !== undefined) { | ||
136 | break; | ||
137 | } | ||
138 | result += segment.duration || targetDuration; | ||
139 | } | ||
140 | |||
141 | // add in the PTS interval in seconds between them | ||
142 | if (right >= left) { | ||
143 | result += 0.001 * | ||
144 | (optionalMax(playlist.segments[right].maxVideoPts, | ||
145 | playlist.segments[right].maxAudioPts) - | ||
146 | optionalMin(playlist.segments[left].minVideoPts, | ||
147 | playlist.segments[left].minAudioPts)); | ||
148 | } | ||
149 | |||
150 | return result; | ||
151 | }; | ||
152 | |||
153 | /** | 26 | /** |
154 | * Calculate the media duration from the segments associated with a | 27 | * Calculate the media duration from the segments associated with a |
155 | * playlist. The duration of a subinterval of the available segments | 28 | * playlist. The duration of a subinterval of the available segments |
... | @@ -160,14 +33,11 @@ | ... | @@ -160,14 +33,11 @@ |
160 | * boundary for the playlist. Defaults to 0. | 33 | * boundary for the playlist. Defaults to 0. |
161 | * @param endSequence {number} (optional) an exclusive upper boundary | 34 | * @param endSequence {number} (optional) an exclusive upper boundary |
162 | * for the playlist. Defaults to playlist length. | 35 | * for the playlist. Defaults to playlist length. |
163 | * @param includeTrailingTime {boolean} if false, the interval between | ||
164 | * the final segment and the subsequent segment will not be included | ||
165 | * in the result | ||
166 | * @return {number} the duration between the start index and end | 36 | * @return {number} the duration between the start index and end |
167 | * index. | 37 | * index. |
168 | */ | 38 | */ |
169 | intervalDuration = function(playlist, startSequence, endSequence, includeTrailingTime) { | 39 | intervalDuration = function(playlist, startSequence, endSequence) { |
170 | var result = 0, targetDuration, expiredSegmentCount; | 40 | var result = 0, targetDuration, i, start, end, expiredSegmentCount; |
171 | 41 | ||
172 | if (startSequence === undefined) { | 42 | if (startSequence === undefined) { |
173 | startSequence = playlist.mediaSequence || 0; | 43 | startSequence = playlist.mediaSequence || 0; |
... | @@ -177,16 +47,26 @@ | ... | @@ -177,16 +47,26 @@ |
177 | } | 47 | } |
178 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; | 48 | targetDuration = playlist.targetDuration || DEFAULT_TARGET_DURATION; |
179 | 49 | ||
180 | // estimate expired segment duration using the target duration | 50 | // accumulate while looking for the latest known segment-timeline mapping |
181 | expiredSegmentCount = optionalMax(playlist.mediaSequence - startSequence, 0); | 51 | expiredSegmentCount = optionalMax(playlist.mediaSequence - startSequence, 0); |
182 | result += expiredSegmentCount * targetDuration; | 52 | start = startSequence + expiredSegmentCount - playlist.mediaSequence; |
53 | end = endSequence - playlist.mediaSequence; | ||
54 | for (i = end - 1; i >= start; i--) { | ||
55 | if (playlist.segments[i].end !== undefined) { | ||
56 | result += playlist.segments[i].end; | ||
57 | return result; | ||
58 | } | ||
183 | 59 | ||
184 | // accumulate the segment durations into the result | 60 | result += playlist.segments[i].duration || targetDuration; |
185 | result += accumulateDuration(playlist, | ||
186 | startSequence + expiredSegmentCount - playlist.mediaSequence, | ||
187 | endSequence - playlist.mediaSequence, | ||
188 | includeTrailingTime); | ||
189 | 61 | ||
62 | if (playlist.segments[i].start !== undefined) { | ||
63 | result += playlist.segments[i].start; | ||
64 | return result; | ||
65 | } | ||
66 | } | ||
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; | ||
190 | return result; | 70 | return result; |
191 | }; | 71 | }; |
192 | 72 | ... | ... |
... | @@ -71,6 +71,9 @@ videojs.Hls = videojs.extend(Component, { | ... | @@ -71,6 +71,9 @@ videojs.Hls = videojs.extend(Component, { |
71 | this.on(this.tech_, 'seeking', function() { | 71 | this.on(this.tech_, 'seeking', function() { |
72 | this.setCurrentTime(this.tech_.currentTime()); | 72 | this.setCurrentTime(this.tech_.currentTime()); |
73 | }); | 73 | }); |
74 | this.on(this.tech_, 'error', function() { | ||
75 | this.stopCheckingBuffer_(); | ||
76 | }); | ||
74 | 77 | ||
75 | this.on(this.tech_, 'play', this.play); | 78 | this.on(this.tech_, 'play', this.play); |
76 | } | 79 | } |
... | @@ -146,12 +149,6 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -146,12 +149,6 @@ videojs.Hls.prototype.src = function(src) { |
146 | // load the MediaSource into the player | 149 | // load the MediaSource into the player |
147 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); | 150 | this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen.bind(this)); |
148 | 151 | ||
149 | // The index of the next segment to be downloaded in the current | ||
150 | // media playlist. When the current media playlist is live with | ||
151 | // expiring segments, it may be a different value from the media | ||
152 | // sequence number for a segment. | ||
153 | this.mediaIndex = 0; | ||
154 | |||
155 | this.options_ = {}; | 152 | this.options_ = {}; |
156 | if (this.source_.withCredentials !== undefined) { | 153 | if (this.source_.withCredentials !== undefined) { |
157 | this.options_.withCredentials = this.source_.withCredentials; | 154 | this.options_.withCredentials = this.source_.withCredentials; |
... | @@ -161,9 +158,6 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -161,9 +158,6 @@ videojs.Hls.prototype.src = function(src) { |
161 | this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials); | 158 | this.playlists = new videojs.Hls.PlaylistLoader(this.source_.src, this.options_.withCredentials); |
162 | 159 | ||
163 | this.playlists.on('loadedmetadata', function() { | 160 | this.playlists.on('loadedmetadata', function() { |
164 | var selectedPlaylist, loaderHandler, oldBitrate, newBitrate, segmentDuration, | ||
165 | segmentDlTime, threshold; | ||
166 | |||
167 | oldMediaPlaylist = this.playlists.media(); | 161 | oldMediaPlaylist = this.playlists.media(); |
168 | 162 | ||
169 | // if this isn't a live video and preload permits, start | 163 | // if this isn't a live video and preload permits, start |
... | @@ -174,56 +168,10 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -174,56 +168,10 @@ videojs.Hls.prototype.src = function(src) { |
174 | this.loadingState_ = 'segments'; | 168 | this.loadingState_ = 'segments'; |
175 | } | 169 | } |
176 | 170 | ||
177 | // the bandwidth estimate for the first segment is based on round | ||
178 | // trip time for the master playlist. the master playlist is | ||
179 | // almost always tiny so the round-trip time is dominated by | ||
180 | // latency and the computed bandwidth is much lower than | ||
181 | // steady-state. if the the downstream developer has a better way | ||
182 | // of detecting bandwidth and provided a number, use that instead. | ||
183 | if (this.bandwidth === undefined) { | ||
184 | // we're going to have to estimate initial bandwidth | ||
185 | // ourselves. scale the bandwidth estimate to account for the | ||
186 | // relatively high round-trip time from the master playlist. | ||
187 | this.setBandwidth({ | ||
188 | bandwidth: this.playlists.bandwidth * 5 | ||
189 | }); | ||
190 | } | ||
191 | |||
192 | this.setupSourceBuffer_(); | 171 | this.setupSourceBuffer_(); |
193 | |||
194 | selectedPlaylist = this.selectPlaylist(); | ||
195 | oldBitrate = oldMediaPlaylist.attributes && | ||
196 | oldMediaPlaylist.attributes.BANDWIDTH || 0; | ||
197 | newBitrate = selectedPlaylist.attributes && | ||
198 | selectedPlaylist.attributes.BANDWIDTH || 0; | ||
199 | segmentDuration = oldMediaPlaylist.segments && | ||
200 | oldMediaPlaylist.segments[this.mediaIndex].duration || | ||
201 | oldMediaPlaylist.targetDuration; | ||
202 | |||
203 | segmentDlTime = (segmentDuration * newBitrate) / this.bandwidth; | ||
204 | |||
205 | if (!segmentDlTime) { | ||
206 | segmentDlTime = Infinity; | ||
207 | } | ||
208 | |||
209 | // this threshold is to account for having a high latency on the manifest | ||
210 | // request which is a somewhat small file. | ||
211 | threshold = 10; | ||
212 | |||
213 | if (newBitrate > oldBitrate && segmentDlTime <= threshold) { | ||
214 | this.playlists.media(selectedPlaylist); | ||
215 | loaderHandler = function() { | ||
216 | this.setupFirstPlay(); | ||
217 | this.fillBuffer(); | ||
218 | this.tech_.trigger('loadedmetadata'); | ||
219 | this.playlists.off('loadedplaylist', loaderHandler); | ||
220 | }.bind(this); | ||
221 | this.playlists.on('loadedplaylist', loaderHandler); | ||
222 | } else { | ||
223 | this.setupFirstPlay(); | 172 | this.setupFirstPlay(); |
224 | this.fillBuffer(); | 173 | this.fillBuffer(); |
225 | this.tech_.trigger('loadedmetadata'); | 174 | this.tech_.trigger('loadedmetadata'); |
226 | } | ||
227 | }.bind(this)); | 175 | }.bind(this)); |
228 | 176 | ||
229 | this.playlists.on('error', function() { | 177 | this.playlists.on('error', function() { |
... | @@ -247,7 +195,6 @@ videojs.Hls.prototype.src = function(src) { | ... | @@ -247,7 +195,6 @@ videojs.Hls.prototype.src = function(src) { |
247 | } | 195 | } |
248 | 196 | ||
249 | this.updateDuration(this.playlists.media()); | 197 | this.updateDuration(this.playlists.media()); |
250 | this.mediaIndex = videojs.Hls.translateMediaIndex(this.mediaIndex, oldMediaPlaylist, updatedPlaylist); | ||
251 | oldMediaPlaylist = updatedPlaylist; | 198 | oldMediaPlaylist = updatedPlaylist; |
252 | 199 | ||
253 | this.fetchKeys_(); | 200 | this.fetchKeys_(); |
... | @@ -305,6 +252,48 @@ videojs.Hls.prototype.handleSourceOpen = function() { | ... | @@ -305,6 +252,48 @@ videojs.Hls.prototype.handleSourceOpen = function() { |
305 | } | 252 | } |
306 | }; | 253 | }; |
307 | 254 | ||
255 | // Returns the array of time range edge objects that were additively | ||
256 | // modified between two TimeRanges. | ||
257 | var bufferedAdditions = function(original, update) { | ||
258 | var result = [], edges = [], | ||
259 | i, inOriginalRanges; | ||
260 | |||
261 | // create a sorted array of time range start and end times | ||
262 | for (i = 0; i < original.length; i++) { | ||
263 | edges.push({ original: true, start: original.start(i) }); | ||
264 | edges.push({ original: true, end: original.end(i) }); | ||
265 | } | ||
266 | for (i = 0; i < update.length; i++) { | ||
267 | edges.push({ start: update.start(i) }); | ||
268 | edges.push({ end: update.end(i) }); | ||
269 | } | ||
270 | edges.sort(function(left, right) { | ||
271 | var leftTime, rightTime; | ||
272 | leftTime = left.start !== undefined ? left.start : left.end; | ||
273 | rightTime = right.start !== undefined ? right.start : right.end; | ||
274 | return leftTime - rightTime; | ||
275 | }); | ||
276 | |||
277 | // filter out all time range edges that occur during a period that | ||
278 | // was already covered by `original` | ||
279 | inOriginalRanges = false; | ||
280 | for (i = 0; i < edges.length; i++) { | ||
281 | // if this is a transition point for `original`, track whether | ||
282 | // subsequent edges are additions | ||
283 | if (edges[i].original) { | ||
284 | inOriginalRanges = edges[i].start !== undefined; | ||
285 | continue; | ||
286 | } | ||
287 | // if we're in a time range that was in `original`, ignore this edge | ||
288 | if (inOriginalRanges) { | ||
289 | continue; | ||
290 | } | ||
291 | // this edge occurred outside the range of `original` | ||
292 | result.push(edges[i]); | ||
293 | } | ||
294 | return result; | ||
295 | }; | ||
296 | |||
308 | videojs.Hls.prototype.setupSourceBuffer_ = function() { | 297 | videojs.Hls.prototype.setupSourceBuffer_ = function() { |
309 | var media = this.playlists.media(), mimeType; | 298 | var media = this.playlists.media(), mimeType; |
310 | 299 | ||
... | @@ -325,12 +314,13 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { | ... | @@ -325,12 +314,13 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { |
325 | // transition the sourcebuffer to the ended state if we've hit the end of | 314 | // transition the sourcebuffer to the ended state if we've hit the end of |
326 | // the playlist | 315 | // the playlist |
327 | this.sourceBuffer.addEventListener('updateend', function() { | 316 | this.sourceBuffer.addEventListener('updateend', function() { |
328 | var segmentInfo = this.pendingSegment_, i, currentBuffered; | 317 | var segmentInfo = this.pendingSegment_, segment, i, currentBuffered, timelineUpdates; |
329 | 318 | ||
330 | this.pendingSegment_ = null; | 319 | this.pendingSegment_ = null; |
331 | 320 | ||
332 | if (this.duration() !== Infinity && | 321 | // if we've buffered to the end of the video, let the MediaSource know |
333 | this.mediaIndex === this.playlists.media().segments.length) { | 322 | currentBuffered = this.findCurrentBuffered_(); |
323 | if (currentBuffered.length && this.duration() === currentBuffered.end(0)) { | ||
334 | this.mediaSource.endOfStream(); | 324 | this.mediaSource.endOfStream(); |
335 | } | 325 | } |
336 | 326 | ||
... | @@ -345,13 +335,31 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { | ... | @@ -345,13 +335,31 @@ videojs.Hls.prototype.setupSourceBuffer_ = function() { |
345 | if (this.tech_.currentTime() < this.tech_.buffered().start(i)) { | 335 | if (this.tech_.currentTime() < this.tech_.buffered().start(i)) { |
346 | // found the misidentified segment's buffered time range | 336 | // found the misidentified segment's buffered time range |
347 | // adjust the media index to fill the gap | 337 | // adjust the media index to fill the gap |
348 | currentBuffered = this.findCurrentBuffered_(); | 338 | this.playlists.updateTimelineOffset(segmentInfo.mediaIndex, |
349 | this.playlists.updateTimelineOffset(segmentInfo.mediaIndex, this.tech_.buffered().start(i)); | 339 | this.tech_.buffered().start(i)); |
350 | this.mediaIndex = this.playlists.getMediaIndexForTime_(currentBuffered.end(0) + 1); | ||
351 | break; | 340 | break; |
352 | } | 341 | } |
353 | } | 342 | } |
354 | } | 343 | } |
344 | |||
345 | if (!segmentInfo) { | ||
346 | return; | ||
347 | } | ||
348 | |||
349 | // annotate the segment with any start and end time information | ||
350 | // added by the media processing | ||
351 | segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex]; | ||
352 | timelineUpdates = bufferedAdditions(segmentInfo.buffered, | ||
353 | this.tech_.buffered()); | ||
354 | timelineUpdates.forEach(function(update) { | ||
355 | if (update.start !== undefined) { | ||
356 | segment.start = update.start; | ||
357 | } | ||
358 | if (update.end !== undefined) { | ||
359 | segment.end = update.end; | ||
360 | } | ||
361 | }); | ||
362 | |||
355 | }.bind(this)); | 363 | }.bind(this)); |
356 | }; | 364 | }; |
357 | 365 | ||
... | @@ -470,14 +478,13 @@ videojs.Hls.prototype.setupFirstPlay = function() { | ... | @@ -470,14 +478,13 @@ videojs.Hls.prototype.setupFirstPlay = function() { |
470 | }; | 478 | }; |
471 | 479 | ||
472 | /** | 480 | /** |
473 | * Reset the mediaIndex if play() is called after the video has | 481 | * Begin playing the video. |
474 | * ended. | ||
475 | */ | 482 | */ |
476 | videojs.Hls.prototype.play = function() { | 483 | videojs.Hls.prototype.play = function() { |
477 | this.loadingState_ = 'segments'; | 484 | this.loadingState_ = 'segments'; |
478 | 485 | ||
479 | if (this.tech_.ended()) { | 486 | if (this.tech_.ended()) { |
480 | this.mediaIndex = 0; | 487 | this.tech_.setCurrentTime(0); |
481 | } | 488 | } |
482 | 489 | ||
483 | if (this.tech_.played().length === 0) { | 490 | if (this.tech_.played().length === 0) { |
... | @@ -514,9 +521,6 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { | ... | @@ -514,9 +521,6 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
514 | return currentTime; | 521 | return currentTime; |
515 | } | 522 | } |
516 | 523 | ||
517 | // determine the requested segment | ||
518 | this.mediaIndex = this.playlists.getMediaIndexForTime_(currentTime); | ||
519 | |||
520 | // cancel outstanding requests and buffer appends | 524 | // cancel outstanding requests and buffer appends |
521 | this.cancelSegmentXhr(); | 525 | this.cancelSegmentXhr(); |
522 | 526 | ||
... | @@ -530,7 +534,7 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { | ... | @@ -530,7 +534,7 @@ videojs.Hls.prototype.setCurrentTime = function(currentTime) { |
530 | this.segmentBuffer_ = []; | 534 | this.segmentBuffer_ = []; |
531 | 535 | ||
532 | // begin filling the buffer at the new position | 536 | // begin filling the buffer at the new position |
533 | this.fillBuffer(currentTime * 1000); | 537 | this.fillBuffer(currentTime); |
534 | }; | 538 | }; |
535 | 539 | ||
536 | videojs.Hls.prototype.duration = function() { | 540 | videojs.Hls.prototype.duration = function() { |
... | @@ -785,7 +789,7 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { | ... | @@ -785,7 +789,7 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { |
785 | 789 | ||
786 | if (buffered && buffered.length) { | 790 | if (buffered && buffered.length) { |
787 | // Search for a range containing the play-head | 791 | // Search for a range containing the play-head |
788 | for (i = 0;i < buffered.length; i++) { | 792 | for (i = 0; i < buffered.length; i++) { |
789 | if (buffered.start(i) <= currentTime && | 793 | if (buffered.start(i) <= currentTime && |
790 | buffered.end(i) >= currentTime) { | 794 | buffered.end(i) >= currentTime) { |
791 | ranges = videojs.createTimeRanges(buffered.start(i), buffered.end(i)); | 795 | ranges = videojs.createTimeRanges(buffered.start(i), buffered.end(i)); |
... | @@ -805,14 +809,15 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { | ... | @@ -805,14 +809,15 @@ videojs.Hls.prototype.findCurrentBuffered_ = function() { |
805 | * Determines whether there is enough video data currently in the buffer | 809 | * Determines whether there is enough video data currently in the buffer |
806 | * and downloads a new segment if the buffered time is less than the goal. | 810 | * and downloads a new segment if the buffered time is less than the goal. |
807 | * @param seekToTime (optional) {number} the offset into the downloaded segment | 811 | * @param seekToTime (optional) {number} the offset into the downloaded segment |
808 | * to seek to, in milliseconds | 812 | * to seek to, in seconds |
809 | */ | 813 | */ |
810 | videojs.Hls.prototype.fillBuffer = function(seekToTime) { | 814 | videojs.Hls.prototype.fillBuffer = function(seekToTime) { |
811 | var | 815 | var |
812 | tech = this.tech_, | 816 | tech = this.tech_, |
813 | currentTime = tech.currentTime(), | 817 | currentTime = tech.currentTime(), |
814 | buffered = this.findCurrentBuffered_(), | 818 | currentBuffered = this.findCurrentBuffered_(), |
815 | bufferedTime = 0, | 819 | bufferedTime = 0, |
820 | mediaIndex = 0, | ||
816 | segment, | 821 | segment, |
817 | segmentUri; | 822 | segmentUri; |
818 | 823 | ||
... | @@ -831,6 +836,11 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { | ... | @@ -831,6 +836,11 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { |
831 | return; | 836 | return; |
832 | } | 837 | } |
833 | 838 | ||
839 | // wait until the buffer is up to date | ||
840 | if (this.segmentBuffer_.length || this.pendingSegment_) { | ||
841 | return; | ||
842 | } | ||
843 | |||
834 | // if no segments are available, do nothing | 844 | // if no segments are available, do nothing |
835 | if (this.playlists.state === "HAVE_NOTHING" || | 845 | if (this.playlists.state === "HAVE_NOTHING" || |
836 | !this.playlists.media() || | 846 | !this.playlists.media() || |
... | @@ -843,28 +853,33 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { | ... | @@ -843,28 +853,33 @@ videojs.Hls.prototype.fillBuffer = function(seekToTime) { |
843 | return; | 853 | return; |
844 | } | 854 | } |
845 | 855 | ||
856 | // find the next segment to download | ||
857 | if (typeof seekToTime === 'number') { | ||
858 | mediaIndex = this.playlists.getMediaIndexForTime_(seekToTime); | ||
859 | } else if (currentBuffered && currentBuffered.length) { | ||
860 | mediaIndex = this.playlists.getMediaIndexForTime_(currentBuffered.end(0)); | ||
861 | bufferedTime = Math.max(0, currentBuffered.end(0) - currentTime); | ||
862 | } else { | ||
863 | mediaIndex = this.playlists.getMediaIndexForTime_(this.tech_.currentTime()); | ||
864 | } | ||
865 | segment = this.playlists.media().segments[mediaIndex]; | ||
866 | |||
846 | // if the video has finished downloading, stop trying to buffer | 867 | // if the video has finished downloading, stop trying to buffer |
847 | segment = this.playlists.media().segments[this.mediaIndex]; | ||
848 | if (!segment) { | 868 | if (!segment) { |
849 | return; | 869 | return; |
850 | } | 870 | } |
851 | 871 | ||
852 | // To determine how much is buffered, we need to find the buffered region we | ||
853 | // are currently playing in and measure it's length | ||
854 | if (buffered && buffered.length) { | ||
855 | bufferedTime = Math.max(0, buffered.end(0) - currentTime); | ||
856 | } | ||
857 | |||
858 | // if there is plenty of content in the buffer and we're not | 872 | // if there is plenty of content in the buffer and we're not |
859 | // seeking, relax for awhile | 873 | // seeking, relax for awhile |
860 | if (typeof seekToTime !== 'number' && bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { | 874 | if (typeof seekToTime !== 'number' && |
875 | bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) { | ||
861 | return; | 876 | return; |
862 | } | 877 | } |
863 | 878 | ||
864 | // resolve the segment URL relative to the playlist | 879 | // resolve the segment URL relative to the playlist |
865 | segmentUri = this.playlistUriToUrl(segment.uri); | 880 | segmentUri = this.playlistUriToUrl(segment.uri); |
866 | 881 | ||
867 | this.loadSegment(segmentUri, seekToTime); | 882 | this.loadSegment(segmentUri, mediaIndex, seekToTime); |
868 | }; | 883 | }; |
869 | 884 | ||
870 | videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) { | 885 | videojs.Hls.prototype.playlistUriToUrl = function(segmentRelativeUrl) { |
... | @@ -895,7 +910,7 @@ videojs.Hls.prototype.setBandwidth = function(xhr) { | ... | @@ -895,7 +910,7 @@ videojs.Hls.prototype.setBandwidth = function(xhr) { |
895 | this.tech_.trigger('bandwidthupdate'); | 910 | this.tech_.trigger('bandwidthupdate'); |
896 | }; | 911 | }; |
897 | 912 | ||
898 | videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { | 913 | videojs.Hls.prototype.loadSegment = function(segmentUri, mediaIndex, seekToTime) { |
899 | var self = this; | 914 | var self = this; |
900 | 915 | ||
901 | // request the next segment | 916 | // request the next segment |
... | @@ -915,17 +930,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { | ... | @@ -915,17 +930,15 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { |
915 | return self.playlists.media(self.selectPlaylist()); | 930 | return self.playlists.media(self.selectPlaylist()); |
916 | } | 931 | } |
917 | 932 | ||
933 | // otherwise, trigger a network error | ||
918 | if (!request.aborted && error) { | 934 | if (!request.aborted && error) { |
919 | // otherwise, try jumping ahead to the next segment | ||
920 | self.error = { | 935 | self.error = { |
921 | status: request.status, | 936 | status: request.status, |
922 | message: 'HLS segment request error at URL: ' + segmentUri, | 937 | message: 'HLS segment request error at URL: ' + segmentUri, |
923 | code: (request.status >= 500) ? 4 : 2 | 938 | code: (request.status >= 500) ? 4 : 2 |
924 | }; | 939 | }; |
925 | 940 | ||
926 | // try moving on to the next segment | 941 | return self.mediaSource.endOfStream('network'); |
927 | self.mediaIndex++; | ||
928 | return; | ||
929 | } | 942 | } |
930 | 943 | ||
931 | // stop processing if the request was aborted | 944 | // stop processing if the request was aborted |
... | @@ -938,7 +951,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { | ... | @@ -938,7 +951,7 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { |
938 | // package up all the work to append the segment | 951 | // package up all the work to append the segment |
939 | segmentInfo = { | 952 | segmentInfo = { |
940 | // the segment's mediaIndex at the time it was received | 953 | // the segment's mediaIndex at the time it was received |
941 | mediaIndex: self.mediaIndex, | 954 | mediaIndex: mediaIndex, |
942 | // the segment's playlist | 955 | // the segment's playlist |
943 | playlist: self.playlists.media(), | 956 | playlist: self.playlists.media(), |
944 | // optionally, a time offset to seek to within the segment | 957 | // optionally, a time offset to seek to within the segment |
... | @@ -951,9 +964,13 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { | ... | @@ -951,9 +964,13 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { |
951 | decrypter: null, | 964 | decrypter: null, |
952 | // metadata events discovered during muxing that need to be | 965 | // metadata events discovered during muxing that need to be |
953 | // translated into cue points | 966 | // translated into cue points |
954 | pendingMetadata: [] | 967 | pendingMetadata: [], |
968 | // the state of the buffer before a segment is appended will be | ||
969 | // stored here so that the actual segment duration can be | ||
970 | // determined after it has been appended | ||
971 | buffered: null | ||
955 | }; | 972 | }; |
956 | if (segmentInfo.playlist.segments[segmentInfo.mediaIndex].key) { | 973 | if (segmentInfo.playlist.segments[mediaIndex].key) { |
957 | segmentInfo.encryptedBytes = new Uint8Array(request.response); | 974 | segmentInfo.encryptedBytes = new Uint8Array(request.response); |
958 | } else { | 975 | } else { |
959 | segmentInfo.bytes = new Uint8Array(request.response); | 976 | segmentInfo.bytes = new Uint8Array(request.response); |
... | @@ -962,8 +979,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { | ... | @@ -962,8 +979,6 @@ videojs.Hls.prototype.loadSegment = function(segmentUri, seekToTime) { |
962 | self.tech_.trigger('progress'); | 979 | self.tech_.trigger('progress'); |
963 | self.drainBuffer(); | 980 | self.drainBuffer(); |
964 | 981 | ||
965 | self.mediaIndex++; | ||
966 | |||
967 | // figure out what stream the next segment should be downloaded from | 982 | // figure out what stream the next segment should be downloaded from |
968 | // with the updated bandwidth information | 983 | // with the updated bandwidth information |
969 | self.playlists.media(self.selectPlaylist()); | 984 | self.playlists.media(self.selectPlaylist()); |
... | @@ -1098,8 +1113,15 @@ videojs.Hls.prototype.drainBuffer = function(event) { | ... | @@ -1098,8 +1113,15 @@ videojs.Hls.prototype.drainBuffer = function(event) { |
1098 | } | 1113 | } |
1099 | 1114 | ||
1100 | // the segment is asynchronously added to the current buffered data | 1115 | // the segment is asynchronously added to the current buffered data |
1101 | this.sourceBuffer.appendBuffer(bytes); | 1116 | if (currentBuffered.length) { |
1117 | this.sourceBuffer.videoBuffer_.appendWindowStart = Math.min(this.tech_.currentTime(), currentBuffered.end(0)); | ||
1118 | } else if (this.sourceBuffer.videoBuffer_) { | ||
1119 | this.sourceBuffer.videoBuffer_.appendWindowStart = 0; | ||
1120 | } | ||
1102 | this.pendingSegment_ = segmentBuffer.shift(); | 1121 | this.pendingSegment_ = segmentBuffer.shift(); |
1122 | this.pendingSegment_.buffered = this.tech_.buffered(); | ||
1123 | |||
1124 | this.sourceBuffer.appendBuffer(bytes); | ||
1103 | }; | 1125 | }; |
1104 | 1126 | ||
1105 | /** | 1127 | /** |
... | @@ -1228,45 +1250,6 @@ videojs.Hls.getPlaylistTotalDuration = function(playlist) { | ... | @@ -1228,45 +1250,6 @@ videojs.Hls.getPlaylistTotalDuration = function(playlist) { |
1228 | }; | 1250 | }; |
1229 | 1251 | ||
1230 | /** | 1252 | /** |
1231 | * Determine the media index in one playlist that corresponds to a | ||
1232 | * specified media index in another. This function can be used to | ||
1233 | * calculate a new segment position when a playlist is reloaded or a | ||
1234 | * variant playlist is becoming active. | ||
1235 | * @param mediaIndex {number} the index into the original playlist | ||
1236 | * to translate | ||
1237 | * @param original {object} the playlist to translate the media | ||
1238 | * index from | ||
1239 | * @param update {object} the playlist to translate the media index | ||
1240 | * to | ||
1241 | * @param {number} the corresponding media index in the updated | ||
1242 | * playlist | ||
1243 | */ | ||
1244 | videojs.Hls.translateMediaIndex = function(mediaIndex, original, update) { | ||
1245 | var translatedMediaIndex; | ||
1246 | |||
1247 | // no segments have been loaded from the original playlist | ||
1248 | if (mediaIndex === 0) { | ||
1249 | return 0; | ||
1250 | } | ||
1251 | |||
1252 | if (!(update && update.segments)) { | ||
1253 | // let the media index be zero when there are no segments defined | ||
1254 | return 0; | ||
1255 | } | ||
1256 | |||
1257 | // translate based on media sequence numbers. syncing up across | ||
1258 | // bitrate switches should be happening here. | ||
1259 | translatedMediaIndex = (mediaIndex + (original.mediaSequence - update.mediaSequence)); | ||
1260 | |||
1261 | if (translatedMediaIndex > update.segments.length || translatedMediaIndex < 0) { | ||
1262 | // recalculate the live point if the streams are too far out of sync | ||
1263 | return videojs.Hls.getMediaIndexForLive_(update) + 1; | ||
1264 | } | ||
1265 | |||
1266 | return translatedMediaIndex; | ||
1267 | }; | ||
1268 | |||
1269 | /** | ||
1270 | * Deprecated. | 1253 | * Deprecated. |
1271 | * | 1254 | * |
1272 | * @deprecated use player.hls.playlists.getMediaIndexForTime_() instead | 1255 | * @deprecated use player.hls.playlists.getMediaIndexForTime_() instead | ... | ... |
-
Please register or sign in to post a comment