4ef1ede5 by David LaPalomento

Simple, bandwidth-only adaptive streaming

Use the calculated bandwidth after every segment download to select the appropriate bitrate playlist. Allow playlist selection logic to be configurable at runtime. Add tests to verify adaptive behavior. Currently, switching is pretty choppy but testing with a higher goalBufferLength (for instance, 30 seconds) looked pretty good.
1 parent a2ff61a5
...@@ -14,6 +14,24 @@ var ...@@ -14,6 +14,24 @@ var
14 // the desired length of video to maintain in the buffer, in seconds 14 // the desired length of video to maintain in the buffer, in seconds
15 goalBufferLength = 5, 15 goalBufferLength = 5,
16 16
17 // a fudge factor to apply to advertised playlist bitrates to account for
18 // temporary flucations in client bandwidth
19 bandwidthVariance = 1.1,
20
21 playlistBandwidth = function(left, right) {
22 var leftBandwidth, rightBandwidth;
23 if (left.attributes && left.attributes.BANDWIDTH) {
24 leftBandwidth = left.attributes.BANDWIDTH;
25 }
26 leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
27 if (right.attributes && right.attributes.BANDWIDTH) {
28 rightBandwidth = right.attributes.BANDWIDTH;
29 }
30 rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
31
32 return leftBandwidth - rightBandwidth;
33 },
34
17 /** 35 /**
18 * Constructs a new URI by interpreting a path relative to another 36 * Constructs a new URI by interpreting a path relative to another
19 * URI. 37 * URI.
...@@ -92,14 +110,49 @@ var ...@@ -92,14 +110,49 @@ var
92 return 1; // HAVE_METADATA 110 return 1; // HAVE_METADATA
93 }; 111 };
94 112
95
96 /** 113 /**
97 * Chooses the appropriate media playlist based on the current 114 * Chooses the appropriate media playlist based on the current
98 * bandwidth estimate and the player size. 115 * bandwidth estimate and the player size.
116 * @return the highest bitrate playlist less than the currently detected
117 * bandwidth, accounting for some amount of bandwidth variance
99 */ 118 */
100 player.hls.selectPlaylist = function() { 119 player.hls.selectPlaylist = function() {
101 player.hls.media = player.hls.master.playlists[0]; 120 var
102 player.hls.mediaIndex = 0; 121 bestVariant,
122 effectiveBitrate,
123 sortedPlaylists = player.hls.master.playlists.slice(),
124 i = sortedPlaylists.length,
125 variant;
126
127 sortedPlaylists.sort(playlistBandwidth);
128
129 while (i--) {
130 variant = sortedPlaylists[i];
131
132 // ignore playlists without bandwidth information
133 if (!variant.attributes || !variant.attributes.BANDWIDTH) {
134 continue;
135 }
136
137 effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
138
139 // since the playlists are sorted in ascending order by bandwidth, the
140 // current variant is the best as long as its effective bitrate is
141 // below the current bandwidth estimate
142 if (effectiveBitrate < player.hls.bandwidth) {
143 bestVariant = variant;
144 break;
145 }
146 }
147
148 // console.log('bandwidth:',
149 // player.hls.bandwidth,
150 // 'variant:',
151 // (bestVariant || sortedPlaylists[0]).attributes.BANDWIDTH);
152
153 // if no acceptable variant was found, fall back on the lowest
154 // bitrate playlist
155 return bestVariant || sortedPlaylists[0];
103 }; 156 };
104 157
105 onDurationUpdate = function(value) { 158 onDurationUpdate = function(value) {
...@@ -111,7 +164,7 @@ var ...@@ -111,7 +164,7 @@ var
111 * URL is a master playlist, the default variant will be downloaded and 164 * URL is a master playlist, the default variant will be downloaded and
112 * parsed as well. Triggers `loadedmanifest` once for each playlist that is 165 * parsed as well. Triggers `loadedmanifest` once for each playlist that is
113 * downloaded and `loadedmetadata` after at least one media playlist has 166 * downloaded and `loadedmetadata` after at least one media playlist has
114 * been parsed. Whether multiple playlists were downloaded or not, after 167 * been parsed. Whether multiple playlists were downloaded or not, when
115 * `loadedmetadata` fires a parsed or inferred master playlist object will 168 * `loadedmetadata` fires a parsed or inferred master playlist object will
116 * be available as `player.hls.master`. 169 * be available as `player.hls.master`.
117 * 170 *
...@@ -141,6 +194,7 @@ var ...@@ -141,6 +194,7 @@ var
141 if (player.hls.master) { 194 if (player.hls.master) {
142 // merge this playlist into the master 195 // merge this playlist into the master
143 i = player.hls.master.playlists.length; 196 i = player.hls.master.playlists.length;
197
144 while (i--) { 198 while (i--) {
145 playlist = player.hls.master.playlists[i]; 199 playlist = player.hls.master.playlists[i];
146 playlistUri = resolveUrl(srcUrl, playlist.uri); 200 playlistUri = resolveUrl(srcUrl, playlist.uri);
...@@ -156,9 +210,22 @@ var ...@@ -156,9 +210,22 @@ var
156 }; 210 };
157 } 211 }
158 212
159 player.hls.selectPlaylist(); 213 // always start playback with the default rendition
214 if (!player.hls.media) {
215 player.hls.media = player.hls.master.playlists[0];
216 player.trigger('loadedmanifest');
217 player.trigger('loadedmetadata');
218 return;
219 }
220
221 // select a playlist and download its metadata if necessary
222 playlist = player.hls.selectPlaylist();
223 if (!playlist.segments) {
224 downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
225 } else {
226 player.hls.media = playlist;
227 }
160 player.trigger('loadedmanifest'); 228 player.trigger('loadedmanifest');
161 player.trigger('loadedmetadata');
162 } 229 }
163 }; 230 };
164 xhr.send(null); 231 xhr.send(null);
...@@ -204,10 +271,12 @@ var ...@@ -204,10 +271,12 @@ var
204 segmentXhr.open('GET', segmentUri); 271 segmentXhr.open('GET', segmentUri);
205 segmentXhr.responseType = 'arraybuffer'; 272 segmentXhr.responseType = 'arraybuffer';
206 segmentXhr.onreadystatechange = function() { 273 segmentXhr.onreadystatechange = function() {
274 var playlist;
275
207 if (segmentXhr.readyState === 4) { 276 if (segmentXhr.readyState === 4) {
208 // calculate the download bandwidth 277 // calculate the download bandwidth
209 player.hls.segmentXhrTime = (+new Date()) - startTime; 278 player.hls.segmentXhrTime = (+new Date()) - startTime;
210 player.hls.bandwidth = segmentXhr.response.byteLength / player.hls.segmentXhrTime; 279 player.hls.bandwidth = (segmentXhr.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000;
211 280
212 // transmux the segment data from MP2T to FLV 281 // transmux the segment data from MP2T to FLV
213 segmentParser.parseSegmentBinaryData(new Uint8Array(segmentXhr.response)); 282 segmentParser.parseSegmentBinaryData(new Uint8Array(segmentXhr.response));
...@@ -218,6 +287,15 @@ var ...@@ -218,6 +287,15 @@ var
218 287
219 segmentXhr = null; 288 segmentXhr = null;
220 player.hls.mediaIndex++; 289 player.hls.mediaIndex++;
290
291 // figure out what stream the next segment should be downloaded from
292 // with the updated bandwidth information
293 playlist = player.hls.selectPlaylist();
294 if (!playlist.segments) {
295 downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
296 } else {
297 player.hls.media = playlist;
298 }
221 } 299 }
222 }; 300 };
223 startTime = +new Date(); 301 startTime = +new Date();
...@@ -234,6 +312,7 @@ var ...@@ -234,6 +312,7 @@ var
234 player.on('loadedmetadata', fillBuffer); 312 player.on('loadedmetadata', fillBuffer);
235 player.on('timeupdate', fillBuffer); 313 player.on('timeupdate', fillBuffer);
236 314
315 player.hls.mediaIndex = 0;
237 downloadPlaylist(srcUrl); 316 downloadPlaylist(srcUrl);
238 }); 317 });
239 player.src({ 318 player.src({
......
...@@ -162,7 +162,6 @@ test('downloads media playlists after loading the master', function() { ...@@ -162,7 +162,6 @@ test('downloads media playlists after loading the master', function() {
162 type: 'sourceopen' 162 type: 'sourceopen'
163 }); 163 });
164 164
165 strictEqual(xhrUrls.length, 3, 'three requests were made');
166 strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested'); 165 strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested');
167 strictEqual(xhrUrls[1], 166 strictEqual(xhrUrls[1],
168 window.location.origin + 167 window.location.origin +
...@@ -189,6 +188,124 @@ test('calculates the bandwidth after downloading a segment', function() { ...@@ -189,6 +188,124 @@ test('calculates the bandwidth after downloading a segment', function() {
189 'saves segment request time: ' + player.hls.segmentXhrTime + 's'); 188 'saves segment request time: ' + player.hls.segmentXhrTime + 's');
190 }); 189 });
191 190
191 test('selects a playlist after segment downloads', function() {
192 var calls = 0;
193 player.hls('manifest/master.m3u8');
194 player.hls.selectPlaylist = function() {
195 calls++;
196 return player.hls.master.playlists[0];
197 };
198 videojs.mediaSources[player.currentSrc()].trigger({
199 type: 'sourceopen'
200 });
201
202 strictEqual(calls, 1, 'selects after the initial segment');
203 player.currentTime = function() {
204 return 1;
205 };
206 player.buffered = function() {
207 return videojs.createTimeRange(0, 2);
208 };
209 player.trigger('timeupdate');
210 strictEqual(calls, 2, 'selects after additional segments');
211 });
212
213 test('downloads additional playlists if required', function() {
214 var
215 called = false,
216 playlist = {
217 uri: 'media3.m3u8'
218 };
219 player.hls('manifest/master.m3u8');
220 videojs.mediaSources[player.currentSrc()].trigger({
221 type: 'sourceopen'
222 });
223
224 // before an m3u8 is downloaded, no segments are available
225 player.hls.selectPlaylist = function() {
226 if (!called) {
227 called = true;
228 return playlist;
229 }
230 playlist.segments = [];
231 return playlist;
232 };
233 xhrUrls = [];
234
235 // the playlist selection is revisited after a new segment is downloaded
236 player.currentTime = function() {
237 return 1;
238 };
239 player.trigger('timeupdate');
240
241 strictEqual(2, xhrUrls.length, 'requests were made');
242 strictEqual(xhrUrls[1],
243 window.location.origin +
244 window.location.pathname.split('/').slice(0, -1).join('/') +
245 '/manifest/' +
246 playlist.uri,
247 'made playlist request');
248 strictEqual(playlist, player.hls.media, 'a new playlists was selected');
249 ok(player.hls.media.segments, 'segments are now available');
250 });
251
252 test('selects a playlist below the current bandwidth', function() {
253 var playlist;
254 player.hls('manifest/master.m3u8');
255 videojs.mediaSources[player.currentSrc()].trigger({
256 type: 'sourceopen'
257 });
258
259 // the default playlist has a really high bitrate
260 player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10;
261 // playlist 1 has a very low bitrate
262 player.hls.master.playlists[1].attributes.BANDWIDTH = 1;
263 // but the detected client bandwidth is really low
264 player.hls.bandwidth = 10;
265
266 playlist = player.hls.selectPlaylist();
267 strictEqual(playlist,
268 player.hls.master.playlists[1],
269 'the low bitrate stream is selected');
270 });
271
272 test('raises the minimum bitrate for a stream proportionially', function() {
273 var playlist;
274 player.hls('manifest/master.m3u8');
275 videojs.mediaSources[player.currentSrc()].trigger({
276 type: 'sourceopen'
277 });
278
279 // the default playlist's bandwidth + 10% is equal to the current bandwidth
280 player.hls.master.playlists[0].attributes.BANDWIDTH = 10;
281 player.hls.bandwidth = 11;
282
283 // 9.9 * 1.1 < 11
284 player.hls.master.playlists[1].attributes.BANDWIDTH = 9.9;
285 playlist = player.hls.selectPlaylist();
286
287 strictEqual(playlist,
288 player.hls.master.playlists[1],
289 'a lower bitrate stream is selected');
290 });
291
292 test('uses the lowest bitrate if no other is suitable', function() {
293 var playlist;
294 player.hls('manifest/master.m3u8');
295 videojs.mediaSources[player.currentSrc()].trigger({
296 type: 'sourceopen'
297 });
298
299 // the lowest bitrate playlist is much greater than 1b/s
300 player.hls.bandwidth = 1;
301 playlist = player.hls.selectPlaylist();
302
303 // playlist 1 has the lowest advertised bitrate
304 strictEqual(playlist,
305 player.hls.master.playlists[1],
306 'the lowest bitrate stream is selected');
307 });
308
192 test('does not download the next segment if the buffer is full', function() { 309 test('does not download the next segment if the buffer is full', function() {
193 player.hls('manifest/media.m3u8'); 310 player.hls('manifest/media.m3u8');
194 player.currentTime = function() { 311 player.currentTime = function() {
......