5ee4363a by David LaPalomento

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.
1 parent ad82ecc0
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
......