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
// 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,
playlistBandwidth = function(left, right) {
var leftBandwidth, rightBandwidth;
if (left.attributes && left.attributes.BANDWIDTH) {
leftBandwidth = left.attributes.BANDWIDTH;
}
leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.BANDWIDTH) {
rightBandwidth = right.attributes.BANDWIDTH;
}
rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
return leftBandwidth - rightBandwidth;
},
/**
* Constructs a new URI by interpreting a path relative to another
* URI.
......@@ -92,14 +110,49 @@ var
return 1; // HAVE_METADATA
};
/**
* Chooses the appropriate media playlist based on the current
* bandwidth estimate and the player size.
* @return the highest bitrate playlist less than the currently detected
* bandwidth, accounting for some amount of bandwidth variance
*/
player.hls.selectPlaylist = function() {
player.hls.media = player.hls.master.playlists[0];
player.hls.mediaIndex = 0;
var
bestVariant,
effectiveBitrate,
sortedPlaylists = player.hls.master.playlists.slice(),
i = sortedPlaylists.length,
variant;
sortedPlaylists.sort(playlistBandwidth);
while (i--) {
variant = sortedPlaylists[i];
// ignore playlists without bandwidth information
if (!variant.attributes || !variant.attributes.BANDWIDTH) {
continue;
}
effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
// since the playlists are sorted in ascending order by bandwidth, the
// current variant is the best as long as its effective bitrate is
// below the current bandwidth estimate
if (effectiveBitrate < player.hls.bandwidth) {
bestVariant = variant;
break;
}
}
// console.log('bandwidth:',
// player.hls.bandwidth,
// 'variant:',
// (bestVariant || sortedPlaylists[0]).attributes.BANDWIDTH);
// if no acceptable variant was found, fall back on the lowest
// bitrate playlist
return bestVariant || sortedPlaylists[0];
};
onDurationUpdate = function(value) {
......@@ -111,7 +164,7 @@ var
* URL is a master playlist, the default variant will be downloaded and
* parsed as well. Triggers `loadedmanifest` once for each playlist that is
* downloaded and `loadedmetadata` after at least one media playlist has
* been parsed. Whether multiple playlists were downloaded or not, after
* been parsed. Whether multiple playlists were downloaded or not, when
* `loadedmetadata` fires a parsed or inferred master playlist object will
* be available as `player.hls.master`.
*
......@@ -141,6 +194,7 @@ var
if (player.hls.master) {
// merge this playlist into the master
i = player.hls.master.playlists.length;
while (i--) {
playlist = player.hls.master.playlists[i];
playlistUri = resolveUrl(srcUrl, playlist.uri);
......@@ -156,9 +210,22 @@ var
};
}
player.hls.selectPlaylist();
// always start playback with the default rendition
if (!player.hls.media) {
player.hls.media = player.hls.master.playlists[0];
player.trigger('loadedmanifest');
player.trigger('loadedmetadata');
return;
}
// select a playlist and download its metadata if necessary
playlist = player.hls.selectPlaylist();
if (!playlist.segments) {
downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
} else {
player.hls.media = playlist;
}
player.trigger('loadedmanifest');
}
};
xhr.send(null);
......@@ -204,10 +271,12 @@ var
segmentXhr.open('GET', segmentUri);
segmentXhr.responseType = 'arraybuffer';
segmentXhr.onreadystatechange = function() {
var playlist;
if (segmentXhr.readyState === 4) {
// calculate the download bandwidth
player.hls.segmentXhrTime = (+new Date()) - startTime;
player.hls.bandwidth = segmentXhr.response.byteLength / player.hls.segmentXhrTime;
player.hls.bandwidth = (segmentXhr.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000;
// transmux the segment data from MP2T to FLV
segmentParser.parseSegmentBinaryData(new Uint8Array(segmentXhr.response));
......@@ -218,6 +287,15 @@ var
segmentXhr = null;
player.hls.mediaIndex++;
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
playlist = player.hls.selectPlaylist();
if (!playlist.segments) {
downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
} else {
player.hls.media = playlist;
}
}
};
startTime = +new Date();
......@@ -234,6 +312,7 @@ var
player.on('loadedmetadata', fillBuffer);
player.on('timeupdate', fillBuffer);
player.hls.mediaIndex = 0;
downloadPlaylist(srcUrl);
});
player.src({
......
......@@ -162,7 +162,6 @@ test('downloads media playlists after loading the master', function() {
type: 'sourceopen'
});
strictEqual(xhrUrls.length, 3, 'three requests were made');
strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested');
strictEqual(xhrUrls[1],
window.location.origin +
......@@ -189,6 +188,124 @@ test('calculates the bandwidth after downloading a segment', function() {
'saves segment request time: ' + player.hls.segmentXhrTime + 's');
});
test('selects a playlist after segment downloads', function() {
var calls = 0;
player.hls('manifest/master.m3u8');
player.hls.selectPlaylist = function() {
calls++;
return player.hls.master.playlists[0];
};
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(calls, 1, 'selects after the initial segment');
player.currentTime = function() {
return 1;
};
player.buffered = function() {
return videojs.createTimeRange(0, 2);
};
player.trigger('timeupdate');
strictEqual(calls, 2, 'selects after additional segments');
});
test('downloads additional playlists if required', function() {
var
called = false,
playlist = {
uri: 'media3.m3u8'
};
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// before an m3u8 is downloaded, no segments are available
player.hls.selectPlaylist = function() {
if (!called) {
called = true;
return playlist;
}
playlist.segments = [];
return playlist;
};
xhrUrls = [];
// the playlist selection is revisited after a new segment is downloaded
player.currentTime = function() {
return 1;
};
player.trigger('timeupdate');
strictEqual(2, xhrUrls.length, 'requests were made');
strictEqual(xhrUrls[1],
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/' +
playlist.uri,
'made playlist request');
strictEqual(playlist, player.hls.media, 'a new playlists was selected');
ok(player.hls.media.segments, 'segments are now available');
});
test('selects a playlist below the current bandwidth', function() {
var playlist;
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// the default playlist has a really high bitrate
player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10;
// playlist 1 has a very low bitrate
player.hls.master.playlists[1].attributes.BANDWIDTH = 1;
// but the detected client bandwidth is really low
player.hls.bandwidth = 10;
playlist = player.hls.selectPlaylist();
strictEqual(playlist,
player.hls.master.playlists[1],
'the low bitrate stream is selected');
});
test('raises the minimum bitrate for a stream proportionially', function() {
var playlist;
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// the default playlist's bandwidth + 10% is equal to the current bandwidth
player.hls.master.playlists[0].attributes.BANDWIDTH = 10;
player.hls.bandwidth = 11;
// 9.9 * 1.1 < 11
player.hls.master.playlists[1].attributes.BANDWIDTH = 9.9;
playlist = player.hls.selectPlaylist();
strictEqual(playlist,
player.hls.master.playlists[1],
'a lower bitrate stream is selected');
});
test('uses the lowest bitrate if no other is suitable', function() {
var playlist;
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// the lowest bitrate playlist is much greater than 1b/s
player.hls.bandwidth = 1;
playlist = player.hls.selectPlaylist();
// playlist 1 has the lowest advertised bitrate
strictEqual(playlist,
player.hls.master.playlists[1],
'the lowest bitrate stream is selected');
});
test('does not download the next segment if the buffer is full', function() {
player.hls('manifest/media.m3u8');
player.currentTime = function() {
......