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
71 additions
and
152 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,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 | ... | ... |
This diff is collapsed.
Click to expand it.
-
Please register or sign in to post a comment