aecdda0d by David LaPalomento

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

Adaptive switching enhancements
2 parents 64b2d4f3 ba1507ee
......@@ -11,9 +11,6 @@
var
// the desired length of video to maintain in the buffer, in seconds
goalBufferLength = 5,
// a fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
bandwidthVariance = 1.1,
......@@ -333,6 +330,20 @@ var
};
/**
* Abort all outstanding work and cleanup.
*/
player.hls.dispose = function() {
if (segmentXhr) {
segmentXhr.onreadystatechange = null;
segmentXhr.abort();
}
if (this.playlists) {
this.playlists.dispose();
}
videojs.Flash.prototype.dispose.call(this);
};
/**
* Determines whether there is enough video data currently in the buffer
* and downloads a new segment if the buffered time is less than the goal.
* @param offset (optional) {number} the offset into the downloaded segment
......@@ -370,7 +381,8 @@ var
// if there is plenty of content in the buffer and we're not
// seeking, relax for awhile
if (typeof offset !== 'number' && bufferedTime >= goalBufferLength) {
if (typeof offset !== 'number' &&
bufferedTime >= videojs.Hls.GOAL_BUFFER_LENGTH) {
return;
}
......@@ -396,6 +408,12 @@ var
segmentXhr = null;
if (error) {
// if a segment request times out, we may have better luck with another playlist
if (error === 'timeout') {
player.hls.bandwidth = 1;
return player.hls.playlists.media(player.hls.selectPlaylist());
}
// otherwise, try jumping ahead to the next segment
player.hls.error = {
status: this.status,
message: 'HLS segment request error at URL: ' + url,
......@@ -577,6 +595,9 @@ videojs.Hls = videojs.Flash.extend({
}
});
// the desired length of video to maintain in the buffer, in seconds
videojs.Hls.GOAL_BUFFER_LENGTH = 30;
videojs.Hls.prototype.src = function(src) {
var
player = this.player(),
......@@ -606,13 +627,6 @@ videojs.Hls.prototype.duration = function() {
return 0;
};
videojs.Hls.prototype.dispose = function() {
if (this.playlists) {
this.playlists.dispose();
}
videojs.Flash.prototype.dispose.call(this);
};
videojs.Hls.isSupported = function() {
return videojs.Flash.isSupported() && videojs.MediaSource;
};
......@@ -664,10 +678,14 @@ xhr = videojs.Hls.xhr = function(url, callback) {
if (options.timeout) {
if (request.timeout === 0) {
request.timeout = options.timeout;
request.ontimeout = function() {
request.timedout = true;
};
} else {
// polyfill XHR2 by aborting after the timeout
abortTimeout = window.setTimeout(function() {
if (request.readystate !== 4) {
request.timedout = true;
request.abort();
}
}, options.timeout);
......@@ -683,7 +701,12 @@ xhr = videojs.Hls.xhr = function(url, callback) {
// clear outstanding timeouts
window.clearTimeout(abortTimeout);
// request error
// request timeout
if (request.timedout) {
return callback.call(this, 'timeout', url);
}
// request aborted or errored
if (this.status >= 400 || this.status === 0) {
return callback.call(this, true, url);
}
......
......@@ -432,6 +432,11 @@ form label {
stroke-dasharray: 5, 5;
}
.buffer-empty {
fill: #e44d26;
opacity: 0.4;
}
.timeline {
color: #888;
height: 500px;
......
......@@ -85,6 +85,13 @@
capacity.
<button type=button class=add-time-period>Add time period</button>
</p>
<p>
If you've created a complex scenario you'd like to retry
later, run the simulation and then save the URL of this
page. All of the network conditions you specify are
saved into the URL fragment after the results of a
simulation run are displayed.
</p>
The video is available at
<ul>
<li><input class=bitrate type=number min=1 value=65536> bits per second</li>
......
......@@ -10,9 +10,6 @@
player,
runButton,
parameters,
addTimePeriod,
networkTimeline,
timePeriod,
timeline,
displayTimeline;
......@@ -31,23 +28,46 @@
};
// a dynamic number of time-bandwidth pairs may be defined to drive the simulation
addTimePeriod = document.querySelector('.add-time-period');
networkTimeline = document.querySelector('.network-timeline');
timePeriod = networkTimeline.cloneNode(true);
addTimePeriod.addEventListener('click', function() {
var clone = timePeriod.cloneNode(true),
fragment = document.createDocumentFragment(),
count = networkTimeline.querySelectorAll('input.bandwidth').length,
time = clone.querySelector('.time'),
bandwidth = clone.querySelector('input.bandwidth');
time.name = 'time' + count;
bandwidth.name = 'bandwidth' + count;
while (clone.childNodes.length) {
fragment.appendChild(clone.childNodes[0]);
(function() {
var params,
networkTimeline = document.querySelector('.network-timeline'),
timePeriod = networkTimeline.querySelector('li:last-child').cloneNode(true),
appendTimePeriod = function() {
var clone = timePeriod.cloneNode(true),
count = networkTimeline.querySelectorAll('input.bandwidth').length,
time = clone.querySelector('.time'),
bandwidth = clone.querySelector('input.bandwidth');
time.name = 'time' + count;
bandwidth.name = 'bandwidth' + count;
networkTimeline.appendChild(clone);
};
document.querySelector('.add-time-period')
.addEventListener('click', appendTimePeriod);
// apply any simulation parameters that were set in the fragment identifier
if (!window.location.hash) {
return;
}
networkTimeline.appendChild(fragment);
});
// time periods are specified as t<seconds>=<bitrate>
// e.g. #t15=450560&t150=65530
params = window.location.hash.substring(1)
.split('&')
.map(function(param) {
return ((/t(\d+)=(\d+)/i).exec(param) || [])
.map(window.parseFloat).slice(1);
}).filter(function(pair) {
return pair.length === 2;
});
networkTimeline.innerHTML = '';
params.forEach(function(param) {
appendTimePeriod();
networkTimeline.querySelector('li:last-child .time').value = param[0];
networkTimeline.querySelector('li:last-child input.bandwidth').value = param[1];
});
})();
// collect the simulation parameters
parameters = function() {
......@@ -143,7 +163,7 @@
buffered = 0,
currentTime = 0;
// mock out buffered and currentTime
// simulate buffered and currentTime during playback
player.buffered = function() {
return videojs.createTimeRange(0, currentTime + buffered);
};
......@@ -191,10 +211,6 @@
// segment response headers arrive after the propogation delay
setTimeout(function() {
var arrival = Math.ceil(+new Date() * 0.001);
results.playlists.push({
time: arrival,
bitrate: +request.url.match(/(\d+)-\d+$/)[1]
});
request.setResponseHeaders({
'Content-Type': 'video/mp2t'
});
......@@ -204,12 +220,23 @@
if (remaining - value.bandwidth <= 0) {
// send the response body once all bytes have been delivered
setTimeout(function() {
buffered += segmentDuration;
var time = Math.ceil(+new Date() * 0.001);
if (request.aborted) {
return;
}
request.status = 200;
request.response = new Uint8Array(segmentSize * 0.125);
request.setResponseBody('');
results.playlists.push({
time: time,
bitrate: +request.url.match(/(\d+)-\d+$/)[1]
});
// update the buffered value
buffered += segmentDuration;
results.buffered[results.buffered.length - 1].buffered = buffered;
results.effectiveBandwidth.push({
time: Math.ceil(+new Date() * 0.001),
time: time,
bandwidth: player.hls.bandwidth
});
}, ((remaining / value.bandwidth) + i) * 1000);
......@@ -240,6 +267,11 @@
clock.restore();
fakeXhr.restore();
// update the fragment identifier so this scenario can be re-run easily
window.location.hash = '#' + options.bandwidths.map(function(interval) {
return 't' + interval.time + '=' + interval.bandwidth;
}).join('&');
done(null, results);
}, 0);
});
......@@ -303,7 +335,9 @@
}));
y.domain([0, Math.max(d3.max(data.bandwidth, function(data) {
return data.bandwidth;
}), d3.max(data.options.playlists))]);
}), d3.max(data.options.playlists), d3.max(data.playlists, function(data) {
return data.bitrate;
}))]);
// time axis
svg.selectAll('.axis').remove();
......@@ -324,6 +358,7 @@
.text('Bitrate (kb/s)');
// playlist bitrate lines
svg.selectAll('.line.bitrate').remove();
svg.selectAll('.line.bitrate')
.data(data.options.playlists)
.enter().append('path')
......@@ -368,6 +403,40 @@
.attr('cy', function(playlist) {
return y(playlist.bitrate);
});
// highlight intervals when the buffer is empty
svg.selectAll('.buffer-empty').remove();
svg.selectAll('.buffer-empty')
.data(data.buffered.reduce(function(result, sample) {
var last = result[result.length - 1];
if (sample.buffered === 0) {
if (last && sample.time === last.end + 1) {
// add this sample to the interval we're accumulating
return result.slice(0, result.length - 1).concat({
start: last.start,
end: sample.time
});
} else {
// this sample starts a new interval
return result.concat({
start: sample.time,
end: sample.time
});
}
}
// filter out time periods where the buffer isn't empty
return result;
}, []))
.enter().append('rect')
.attr('class', 'buffer-empty')
.attr('x', function(data) {
return x(data.start);
})
.attr('width', function(data) {
return x(1 + data.end - data.start);
})
.attr('y', 0)
.attr('height', y(height));
};
})();
......
......@@ -420,7 +420,6 @@ test('selects a playlist after segment downloads', function() {
player.trigger('timeupdate');
standardXHRResponse(requests[3]);
console.log(requests.map(function(i) { return i.url; }));
strictEqual(calls, 2, 'selects after additional segments');
});
......@@ -631,15 +630,16 @@ test('selects the correct rendition by player dimensions', function() {
test('does not download the next segment if the buffer is full', function() {
var currentTime = 15;
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.currentTime = function() {
return 15;
return currentTime;
};
player.buffered = function() {
return videojs.createTimeRange(0, 20);
return videojs.createTimeRange(0, currentTime + videojs.Hls.GOAL_BUFFER_LENGTH);
};
player.hls.mediaSource.trigger({
type: 'sourceopen'
......@@ -1150,6 +1150,27 @@ test('clears the segment buffer on seek', function() {
strictEqual(aborts, 1, 'cleared the segment buffer on a seek');
});
test('resets the switching algorithm if a request times out', function() {
player.src({
src: 'master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
player.hls.mediaSource.trigger({
type: 'sourceopen'
});
standardXHRResponse(requests.shift()); // master
standardXHRResponse(requests.shift()); // media.m3u8
// simulate a segment timeout
requests[0].timedout = true;
requests.shift().abort();
standardXHRResponse(requests.shift());
strictEqual(player.hls.playlists.media(),
player.hls.playlists.master.playlists[1],
'reset to the lowest bitrate playlist');
});
test('disposes the playlist loader', function() {
var disposes = 0, player, loaderDispose;
player = createPlayer();
......