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,17 +424,46 @@ ...@@ -424,17 +424,46 @@
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 432 segment = this.media_.segments[i];
433 // HLS version 3 and lower round segment durations to the 433 if (segment.end !== undefined && segment.end <= time) {
434 // nearest decimal integer. When the correct media index is 434 time -= segment.end;
435 // ambiguous, prefer the higher one. 435 break;
436 if (time <= 0) { 436 }
437 return i; 437 if (segment.start !== undefined && segment.start < time) {
438
439 if (segment.end !== undefined && segment.end > time) {
440 // we've found the target segment exactly
441 return i;
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);
438 } 467 }
439 } 468 }
440 469
......
...@@ -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
......