aecdda0d by David LaPalomento

Merge pull request #92 from videojs/hotfix/bw-scenarios

Adaptive switching enhancements
2 parents 64b2d4f3 ba1507ee
...@@ -11,9 +11,6 @@ ...@@ -11,9 +11,6 @@
11 11
12 var 12 var
13 13
14 // the desired length of video to maintain in the buffer, in seconds
15 goalBufferLength = 5,
16
17 // a fudge factor to apply to advertised playlist bitrates to account for 14 // a fudge factor to apply to advertised playlist bitrates to account for
18 // temporary flucations in client bandwidth 15 // temporary flucations in client bandwidth
19 bandwidthVariance = 1.1, 16 bandwidthVariance = 1.1,
...@@ -333,6 +330,20 @@ var ...@@ -333,6 +330,20 @@ var
333 }; 330 };
334 331
335 /** 332 /**
333 * Abort all outstanding work and cleanup.
334 */
335 player.hls.dispose = function() {
336 if (segmentXhr) {
337 segmentXhr.onreadystatechange = null;
338 segmentXhr.abort();
339 }
340 if (this.playlists) {
341 this.playlists.dispose();
342 }
343 videojs.Flash.prototype.dispose.call(this);
344 };
345
346 /**
336 * Determines whether there is enough video data currently in the buffer 347 * Determines whether there is enough video data currently in the buffer
337 * and downloads a new segment if the buffered time is less than the goal. 348 * and downloads a new segment if the buffered time is less than the goal.
338 * @param offset (optional) {number} the offset into the downloaded segment 349 * @param offset (optional) {number} the offset into the downloaded segment
...@@ -370,7 +381,8 @@ var ...@@ -370,7 +381,8 @@ var
370 381
371 // if there is plenty of content in the buffer and we're not 382 // if there is plenty of content in the buffer and we're not
372 // seeking, relax for awhile 383 // seeking, relax for awhile
373 if (typeof offset !== 'number' && bufferedTime >= goalBufferLength) { 384 if (typeof offset !== 'number' &&
385 bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) {
374 return; 386 return;
375 } 387 }
376 388
...@@ -396,6 +408,12 @@ var ...@@ -396,6 +408,12 @@ var
396 segmentXhr = null; 408 segmentXhr = null;
397 409
398 if (error) { 410 if (error) {
411 // if a segment request times out, we may have better luck with another playlist
412 if (error === 'timeout') {
413 player.hls.bandwidth = 1;
414 return player.hls.playlists.media(player.hls.selectPlaylist());
415 }
416 // otherwise, try jumping ahead to the next segment
399 player.hls.error = { 417 player.hls.error = {
400 status: this.status, 418 status: this.status,
401 message: 'HLS segment request error at URL: ' + url, 419 message: 'HLS segment request error at URL: ' + url,
...@@ -577,6 +595,9 @@ videojs.Hls = videojs.Flash.extend({ ...@@ -577,6 +595,9 @@ videojs.Hls = videojs.Flash.extend({
577 } 595 }
578 }); 596 });
579 597
598 // the desired length of video to maintain in the buffer, in seconds
599 videojs.Hls.GOAL_BUFFER_LENGTH = 30;
600
580 videojs.Hls.prototype.src = function(src) { 601 videojs.Hls.prototype.src = function(src) {
581 var 602 var
582 player = this.player(), 603 player = this.player(),
...@@ -606,13 +627,6 @@ videojs.Hls.prototype.duration = function() { ...@@ -606,13 +627,6 @@ videojs.Hls.prototype.duration = function() {
606 return 0; 627 return 0;
607 }; 628 };
608 629
609 videojs.Hls.prototype.dispose = function() {
610 if (this.playlists) {
611 this.playlists.dispose();
612 }
613 videojs.Flash.prototype.dispose.call(this);
614 };
615
616 videojs.Hls.isSupported = function() { 630 videojs.Hls.isSupported = function() {
617 return videojs.Flash.isSupported() && videojs.MediaSource; 631 return videojs.Flash.isSupported() && videojs.MediaSource;
618 }; 632 };
...@@ -664,10 +678,14 @@ xhr = videojs.Hls.xhr = function(url, callback) { ...@@ -664,10 +678,14 @@ xhr = videojs.Hls.xhr = function(url, callback) {
664 if (options.timeout) { 678 if (options.timeout) {
665 if (request.timeout === 0) { 679 if (request.timeout === 0) {
666 request.timeout = options.timeout; 680 request.timeout = options.timeout;
681 request.ontimeout = function() {
682 request.timedout = true;
683 };
667 } else { 684 } else {
668 // polyfill XHR2 by aborting after the timeout 685 // polyfill XHR2 by aborting after the timeout
669 abortTimeout = window.setTimeout(function() { 686 abortTimeout = window.setTimeout(function() {
670 if (request.readystate !== 4) { 687 if (request.readystate !== 4) {
688 request.timedout = true;
671 request.abort(); 689 request.abort();
672 } 690 }
673 }, options.timeout); 691 }, options.timeout);
...@@ -683,7 +701,12 @@ xhr = videojs.Hls.xhr = function(url, callback) { ...@@ -683,7 +701,12 @@ xhr = videojs.Hls.xhr = function(url, callback) {
683 // clear outstanding timeouts 701 // clear outstanding timeouts
684 window.clearTimeout(abortTimeout); 702 window.clearTimeout(abortTimeout);
685 703
686 // request error 704 // request timeout
705 if (request.timedout) {
706 return callback.call(this, 'timeout', url);
707 }
708
709 // request aborted or errored
687 if (this.status >= 400 || this.status === 0) { 710 if (this.status >= 400 || this.status === 0) {
688 return callback.call(this, true, url); 711 return callback.call(this, true, url);
689 } 712 }
......
...@@ -432,6 +432,11 @@ form label { ...@@ -432,6 +432,11 @@ form label {
432 stroke-dasharray: 5, 5; 432 stroke-dasharray: 5, 5;
433 } 433 }
434 434
435 .buffer-empty {
436 fill: #e44d26;
437 opacity: 0.4;
438 }
439
435 .timeline { 440 .timeline {
436 color: #888; 441 color: #888;
437 height: 500px; 442 height: 500px;
......
...@@ -85,6 +85,13 @@ ...@@ -85,6 +85,13 @@
85 capacity. 85 capacity.
86 <button type=button class=add-time-period>Add time period</button> 86 <button type=button class=add-time-period>Add time period</button>
87 </p> 87 </p>
88 <p>
89 If you've created a complex scenario you'd like to retry
90 later, run the simulation and then save the URL of this
91 page. All of the network conditions you specify are
92 saved into the URL fragment after the results of a
93 simulation run are displayed.
94 </p>
88 The video is available at 95 The video is available at
89 <ul> 96 <ul>
90 <li><input class=bitrate type=number min=1 value=65536> bits per second</li> 97 <li><input class=bitrate type=number min=1 value=65536> bits per second</li>
......
...@@ -10,9 +10,6 @@ ...@@ -10,9 +10,6 @@
10 player, 10 player,
11 runButton, 11 runButton,
12 parameters, 12 parameters,
13 addTimePeriod,
14 networkTimeline,
15 timePeriod,
16 timeline, 13 timeline,
17 14
18 displayTimeline; 15 displayTimeline;
...@@ -31,24 +28,47 @@ ...@@ -31,24 +28,47 @@
31 }; 28 };
32 29
33 // a dynamic number of time-bandwidth pairs may be defined to drive the simulation 30 // a dynamic number of time-bandwidth pairs may be defined to drive the simulation
34 addTimePeriod = document.querySelector('.add-time-period'); 31 (function() {
35 networkTimeline = document.querySelector('.network-timeline'); 32 var params,
36 timePeriod = networkTimeline.cloneNode(true); 33 networkTimeline = document.querySelector('.network-timeline'),
37 addTimePeriod.addEventListener('click', function() { 34 timePeriod = networkTimeline.querySelector('li:last-child').cloneNode(true),
35 appendTimePeriod = function() {
38 var clone = timePeriod.cloneNode(true), 36 var clone = timePeriod.cloneNode(true),
39 fragment = document.createDocumentFragment(),
40 count = networkTimeline.querySelectorAll('input.bandwidth').length, 37 count = networkTimeline.querySelectorAll('input.bandwidth').length,
41 time = clone.querySelector('.time'), 38 time = clone.querySelector('.time'),
42 bandwidth = clone.querySelector('input.bandwidth'); 39 bandwidth = clone.querySelector('input.bandwidth');
43 40
44 time.name = 'time' + count; 41 time.name = 'time' + count;
45 bandwidth.name = 'bandwidth' + count; 42 bandwidth.name = 'bandwidth' + count;
46 while (clone.childNodes.length) { 43 networkTimeline.appendChild(clone);
47 fragment.appendChild(clone.childNodes[0]); 44 };
45 document.querySelector('.add-time-period')
46 .addEventListener('click', appendTimePeriod);
47
48 // apply any simulation parameters that were set in the fragment identifier
49 if (!window.location.hash) {
50 return;
48 } 51 }
49 networkTimeline.appendChild(fragment); 52
53 // time periods are specified as t<seconds>=<bitrate>
54 // e.g. #t15=450560&t150=65530
55 params = window.location.hash.substring(1)
56 .split('&')
57 .map(function(param) {
58 return ((/t(\d+)=(\d+)/i).exec(param) || [])
59 .map(window.parseFloat).slice(1);
60 }).filter(function(pair) {
61 return pair.length === 2;
50 }); 62 });
51 63
64 networkTimeline.innerHTML = '';
65 params.forEach(function(param) {
66 appendTimePeriod();
67 networkTimeline.querySelector('li:last-child .time').value = param[0];
68 networkTimeline.querySelector('li:last-child input.bandwidth').value = param[1];
69 });
70 })();
71
52 // collect the simulation parameters 72 // collect the simulation parameters
53 parameters = function() { 73 parameters = function() {
54 var times = Array.prototype.slice.call(document.querySelectorAll('.time')), 74 var times = Array.prototype.slice.call(document.querySelectorAll('.time')),
...@@ -143,7 +163,7 @@ ...@@ -143,7 +163,7 @@
143 buffered = 0, 163 buffered = 0,
144 currentTime = 0; 164 currentTime = 0;
145 165
146 // mock out buffered and currentTime 166 // simulate buffered and currentTime during playback
147 player.buffered = function() { 167 player.buffered = function() {
148 return videojs.createTimeRange(0, currentTime + buffered); 168 return videojs.createTimeRange(0, currentTime + buffered);
149 }; 169 };
...@@ -191,10 +211,6 @@ ...@@ -191,10 +211,6 @@
191 // segment response headers arrive after the propogation delay 211 // segment response headers arrive after the propogation delay
192 setTimeout(function() { 212 setTimeout(function() {
193 var arrival = Math.ceil(+new Date() * 0.001); 213 var arrival = Math.ceil(+new Date() * 0.001);
194 results.playlists.push({
195 time: arrival,
196 bitrate: +request.url.match(/(\d+)-\d+$/)[1]
197 });
198 request.setResponseHeaders({ 214 request.setResponseHeaders({
199 'Content-Type': 'video/mp2t' 215 'Content-Type': 'video/mp2t'
200 }); 216 });
...@@ -204,12 +220,23 @@ ...@@ -204,12 +220,23 @@
204 if (remaining - value.bandwidth <= 0) { 220 if (remaining - value.bandwidth <= 0) {
205 // send the response body once all bytes have been delivered 221 // send the response body once all bytes have been delivered
206 setTimeout(function() { 222 setTimeout(function() {
207 buffered += segmentDuration; 223 var time = Math.ceil(+new Date() * 0.001);
224 if (request.aborted) {
225 return;
226 }
208 request.status = 200; 227 request.status = 200;
209 request.response = new Uint8Array(segmentSize * 0.125); 228 request.response = new Uint8Array(segmentSize * 0.125);
210 request.setResponseBody(''); 229 request.setResponseBody('');
230
231 results.playlists.push({
232 time: time,
233 bitrate: +request.url.match(/(\d+)-\d+$/)[1]
234 });
235 // update the buffered value
236 buffered += segmentDuration;
237 results.buffered[results.buffered.length - 1].buffered = buffered;
211 results.effectiveBandwidth.push({ 238 results.effectiveBandwidth.push({
212 time: Math.ceil(+new Date() * 0.001), 239 time: time,
213 bandwidth: player.hls.bandwidth 240 bandwidth: player.hls.bandwidth
214 }); 241 });
215 }, ((remaining / value.bandwidth) + i) * 1000); 242 }, ((remaining / value.bandwidth) + i) * 1000);
...@@ -240,6 +267,11 @@ ...@@ -240,6 +267,11 @@
240 clock.restore(); 267 clock.restore();
241 fakeXhr.restore(); 268 fakeXhr.restore();
242 269
270 // update the fragment identifier so this scenario can be re-run easily
271 window.location.hash = '#' + options.bandwidths.map(function(interval) {
272 return 't' + interval.time + '=' + interval.bandwidth;
273 }).join('&');
274
243 done(null, results); 275 done(null, results);
244 }, 0); 276 }, 0);
245 }); 277 });
...@@ -303,7 +335,9 @@ ...@@ -303,7 +335,9 @@
303 })); 335 }));
304 y.domain([0, Math.max(d3.max(data.bandwidth, function(data) { 336 y.domain([0, Math.max(d3.max(data.bandwidth, function(data) {
305 return data.bandwidth; 337 return data.bandwidth;
306 }), d3.max(data.options.playlists))]); 338 }), d3.max(data.options.playlists), d3.max(data.playlists, function(data) {
339 return data.bitrate;
340 }))]);
307 341
308 // time axis 342 // time axis
309 svg.selectAll('.axis').remove(); 343 svg.selectAll('.axis').remove();
...@@ -324,6 +358,7 @@ ...@@ -324,6 +358,7 @@
324 .text('Bitrate (kb/s)'); 358 .text('Bitrate (kb/s)');
325 359
326 // playlist bitrate lines 360 // playlist bitrate lines
361 svg.selectAll('.line.bitrate').remove();
327 svg.selectAll('.line.bitrate') 362 svg.selectAll('.line.bitrate')
328 .data(data.options.playlists) 363 .data(data.options.playlists)
329 .enter().append('path') 364 .enter().append('path')
...@@ -368,6 +403,40 @@ ...@@ -368,6 +403,40 @@
368 .attr('cy', function(playlist) { 403 .attr('cy', function(playlist) {
369 return y(playlist.bitrate); 404 return y(playlist.bitrate);
370 }); 405 });
406
407 // highlight intervals when the buffer is empty
408 svg.selectAll('.buffer-empty').remove();
409 svg.selectAll('.buffer-empty')
410 .data(data.buffered.reduce(function(result, sample) {
411 var last = result[result.length - 1];
412 if (sample.buffered === 0) {
413 if (last && sample.time === last.end + 1) {
414 // add this sample to the interval we're accumulating
415 return result.slice(0, result.length - 1).concat({
416 start: last.start,
417 end: sample.time
418 });
419 } else {
420 // this sample starts a new interval
421 return result.concat({
422 start: sample.time,
423 end: sample.time
424 });
425 }
426 }
427 // filter out time periods where the buffer isn't empty
428 return result;
429 }, []))
430 .enter().append('rect')
431 .attr('class', 'buffer-empty')
432 .attr('x', function(data) {
433 return x(data.start);
434 })
435 .attr('width', function(data) {
436 return x(1 + data.end - data.start);
437 })
438 .attr('y', 0)
439 .attr('height', y(height));
371 }; 440 };
372 })(); 441 })();
373 442
......
...@@ -420,7 +420,6 @@ test('selects a playlist after segment downloads', function() { ...@@ -420,7 +420,6 @@ test('selects a playlist after segment downloads', function() {
420 player.trigger('timeupdate'); 420 player.trigger('timeupdate');
421 421
422 standardXHRResponse(requests[3]); 422 standardXHRResponse(requests[3]);
423 console.log(requests.map(function(i) { return i.url; }));
424 strictEqual(calls, 2, 'selects after additional segments'); 423 strictEqual(calls, 2, 'selects after additional segments');
425 }); 424 });
426 425
...@@ -631,15 +630,16 @@ test('selects the correct rendition by player dimensions', function() { ...@@ -631,15 +630,16 @@ test('selects the correct rendition by player dimensions', function() {
631 630
632 631
633 test('does not download the next segment if the buffer is full', function() { 632 test('does not download the next segment if the buffer is full', function() {
633 var currentTime = 15;
634 player.src({ 634 player.src({
635 src: 'manifest/media.m3u8', 635 src: 'manifest/media.m3u8',
636 type: 'application/vnd.apple.mpegurl' 636 type: 'application/vnd.apple.mpegurl'
637 }); 637 });
638 player.currentTime = function() { 638 player.currentTime = function() {
639 return 15; 639 return currentTime;
640 }; 640 };
641 player.buffered = function() { 641 player.buffered = function() {
642 return videojs.createTimeRange(0, 20); 642 return videojs.createTimeRange(0, currentTime + videojs.Hls.GOAL_BUFFER_LENGTH);
643 }; 643 };
644 player.hls.mediaSource.trigger({ 644 player.hls.mediaSource.trigger({
645 type: 'sourceopen' 645 type: 'sourceopen'
...@@ -1150,6 +1150,27 @@ test('clears the segment buffer on seek', function() { ...@@ -1150,6 +1150,27 @@ test('clears the segment buffer on seek', function() {
1150 strictEqual(aborts, 1, 'cleared the segment buffer on a seek'); 1150 strictEqual(aborts, 1, 'cleared the segment buffer on a seek');
1151 }); 1151 });
1152 1152
1153 test('resets the switching algorithm if a request times out', function() {
1154 player.src({
1155 src: 'master.m3u8',
1156 type: 'application/vnd.apple.mpegurl'
1157 });
1158 player.hls.mediaSource.trigger({
1159 type: 'sourceopen'
1160 });
1161 standardXHRResponse(requests.shift()); // master
1162 standardXHRResponse(requests.shift()); // media.m3u8
1163 // simulate a segment timeout
1164 requests[0].timedout = true;
1165 requests.shift().abort();
1166
1167 standardXHRResponse(requests.shift());
1168
1169 strictEqual(player.hls.playlists.media(),
1170 player.hls.playlists.master.playlists[1],
1171 'reset to the lowest bitrate playlist');
1172 });
1173
1153 test('disposes the playlist loader', function() { 1174 test('disposes the playlist loader', function() {
1154 var disposes = 0, player, loaderDispose; 1175 var disposes = 0, player, loaderDispose;
1155 player = createPlayer(); 1176 player = createPlayer();
......