99a5aeb1 by David LaPalomento

Filter incompatbile streams from playlist selection

Media Source Extensions is very picky about the type of media data fed to it. If you stop passing video because you've switched to an audio-only rendition, for instance, MSE will stall until video data is provided. Since we don't have a good way of reconfiguring a Media Source once we've started adding SourceBuffers to it yet, filter out variant playlists that we know are incompatible with the current SourceBuffer configuration.
1 parent 764e1393
......@@ -294,6 +294,37 @@ videojs.Hls.bufferedAdditions_ = function(original, update) {
return result;
};
/**
* Blacklist playlists that are known to be codec or
* stream-incompatible with the SourceBuffer configuration. For
* instance, Media Source Extensions would cause the video element to
* stall waiting for video data if you switched from a variant with
* video and audio to an audio-only one.
*
* @param media {object} a media playlist compatible with the current
* set of SourceBuffers. Variants in the current master playlist that
* do not appear to have compatible codec or stream configurations
* will be excluded from the default playlist selection algorithm
* indefinitely.
*/
videojs.HlsHandler.prototype.excludeIncompatibleStreams_ = function(media) {
var master = this.playlists.master, codecs = '';
if (media.attributes && media.attributes.CODECS) {
codecs = media.attributes.CODECS;
}
master.playlists.forEach(function(variant) {
var variantCodecs = '';
if (variant.attributes && variant.attributes.CODECS) {
variantCodecs = variant.attributes.CODECS;
}
if (variantCodecs !== codecs) {
variant.excludeUntil = Infinity;
}
});
};
videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
var media = this.playlists.media(), mimeType;
......@@ -311,6 +342,10 @@ videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
}
this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
// exclude any incompatible variant streams from future playlist
// selection
this.excludeIncompatibleStreams_(media);
// transition the sourcebuffer to the ended state if we've hit the end of
// the playlist
this.sourceBuffer.addEventListener('updateend', function() {
......@@ -389,6 +424,7 @@ videojs.HlsHandler.prototype.setupFirstPlay = function() {
var seekable, media;
media = this.playlists.media();
// check that everything is ready to begin buffering
// 1) the video is a live stream of unknown duration
......@@ -585,7 +621,8 @@ videojs.HlsHandler.prototype.selectPlaylist = function () {
effectiveBitrate,
sortedPlaylists = this.playlists.master.playlists.slice(),
bandwidthPlaylists = [],
i = sortedPlaylists.length,
now = +new Date(),
i,
variant,
oldvariant,
bandwidthBestVariant,
......@@ -596,8 +633,18 @@ videojs.HlsHandler.prototype.selectPlaylist = function () {
sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth);
// filter out any playlists that have been excluded due to
// incompatible configurations or playback errors
sortedPlaylists = sortedPlaylists.filter(function(variant) {
if (variant.excludeUntil !== undefined) {
return now >= variant.excludeUntil;
}
return true;
});
// filter out any variant that has greater effective bitrate
// than the current estimated bandwidth
i = sortedPlaylists.length;
while (i--) {
variant = sortedPlaylists[i];
......
......@@ -999,7 +999,7 @@ test('uses the lowest bitrate if no other is suitable', function() {
'the lowest bitrate stream is selected');
});
test('selects the correct rendition by player dimensions', function() {
test('selects the correct rendition by player dimensions', function() {
var playlist;
player.src({
......@@ -1071,6 +1071,130 @@ test('selects the highest bitrate playlist when the player dimensions are ' +
'selected the highest bandwidth variant');
});
test('filters playlists that are currently excluded', function() {
var playlist;
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech_.hls.bandwidth = 1e10;
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
'media1.m3u8\n'); // master
standardXHRResponse(requests.shift()); // media
// exclude the current playlist
player.tech_.hls.playlists.master.playlists[0].excludeUntil = +new Date() + 1000;
playlist = player.tech_.hls.selectPlaylist();
equal(playlist, player.tech_.hls.playlists.master.playlists[1], 'respected exclusions');
// timeout the exclusion
clock.tick(1000);
playlist = player.tech_.hls.selectPlaylist();
equal(playlist, player.tech_.hls.playlists.master.playlists[0], 'expired the exclusion');
});
test('blacklists switching from video+audio playlists to audio only', function() {
var audioPlaylist;
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech_.hls.bandwidth = 1e10;
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
'media1.m3u8\n'); // master
standardXHRResponse(requests.shift()); // media1
equal(player.tech_.hls.playlists.media(),
player.tech_.hls.playlists.master.playlists[1],
'selected video+audio');
audioPlaylist = player.tech_.hls.playlists.master.playlists[0];
equal(audioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
});
test('blacklists switching from audio-only playlists to video+audio', function() {
var videoAudioPlaylist;
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech_.hls.bandwidth = 1;
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
'media1.m3u8\n'); // master
standardXHRResponse(requests.shift()); // media1
equal(player.tech_.hls.playlists.media(),
player.tech_.hls.playlists.master.playlists[0],
'selected audio only');
videoAudioPlaylist = player.tech_.hls.playlists.master.playlists[1];
equal(videoAudioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
});
test('blacklists switching from video-only playlists to video+audio', function() {
var videoAudioPlaylist;
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech_.hls.bandwidth = 1;
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d"\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
'media1.m3u8\n'); // master
standardXHRResponse(requests.shift()); // media
equal(player.tech_.hls.playlists.media(),
player.tech_.hls.playlists.master.playlists[0],
'selected video only');
videoAudioPlaylist = player.tech_.hls.playlists.master.playlists[1];
equal(videoAudioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
});
test('blacklists switching between playlists with incompatible audio codecs', function() {
var alternatePlaylist;
player.src({
src: 'manifest/master.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech_.hls.bandwidth = 1;
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.5"\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
'media1.m3u8\n'); // master
standardXHRResponse(requests.shift()); // media
equal(player.tech_.hls.playlists.media(),
player.tech_.hls.playlists.master.playlists[0],
'selected HE-AAC stream');
alternatePlaylist = player.tech_.hls.playlists.master.playlists[1];
equal(alternatePlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
});
test('does not download the next segment if the buffer is full', function() {
var currentTime = 15;
player.src({
......