958f2b8c by David LaPalomento

Merge pull request #438 from videojs/blacklist-incompatible-codecs

Blacklist incompatible codecs
2 parents 6dcb32ce 6c574219
......@@ -39,6 +39,11 @@
padding: 0 5px;
margin: 20px 0;
}
input {
margin-top: 15px;
min-width: 450px;
padding: 5px;
}
</style>
</head>
......@@ -56,10 +61,31 @@
src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"
type="application/x-mpegURL">
</video>
<form id=load-url>
<label>
Video URL:
<input id=url type=url value="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8">
</label>
<button type=submit>Load</button>
</form>
<script>
videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf';
// initialize the player
var player = videojs('video');
// hook up the video switcher
var loadUrl = document.getElementById('load-url');
var url = document.getElementById('url');
loadUrl.addEventListener('submit', function(event) {
event.preventDefault();
player.src({
src: url.value,
type: 'application/x-mpegURL'
});
return false;
});
</script>
</body>
</html>
......
......@@ -294,6 +294,85 @@ videojs.Hls.bufferedAdditions_ = function(original, update) {
return result;
};
var parseCodecs = function(codecs) {
var result = {
codecCount: 0,
videoCodec: null,
audioProfile: null
};
result.codecCount = codecs.split(',').length;
result.codecCount = result.codecCount || 2;
// parse the video codec but ignore the version
result.videoCodec = /(^|\s|,)+(avc1)[^ ,]*/i.exec(codecs);
result.videoCodec = result.videoCodec && result.videoCodec[2];
// parse the last field of the audio codec
result.audioProfile = /(^|\s|,)+mp4a.\d+\.(\d+)/i.exec(codecs);
result.audioProfile = result.audioProfile && result.audioProfile[2];
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.excludeIncompatibleVariants_ = function(media) {
var
master = this.playlists.master,
codecCount = 2,
videoCodec = null,
audioProfile = null,
codecs;
if (media.attributes && media.attributes.CODECS) {
codecs = parseCodecs(media.attributes.CODECS);
videoCodec = codecs.videoCodec;
audioProfile = codecs.audioProfile;
codecCount = codecs.codecCount;
}
master.playlists.forEach(function(variant) {
var variantCodecs = {
codecCount: 2,
videoCodec: null,
audioProfile: null
};
if (variant.attributes && variant.attributes.CODECS) {
variantCodecs = parseCodecs(variant.attributes.CODECS);
}
// if the streams differ in the presence or absence of audio or
// video, they are incompatible
if (variantCodecs.codecCount !== codecCount) {
variant.excludeUntil = Infinity;
}
// if h.264 is specified on the current playlist, some flavor of
// it must be specified on all compatible variants
if (variantCodecs.videoCodec !== videoCodec) {
variant.excludeUntil = Infinity;
}
// HE-AAC ("mp4a.40.5") is incompatible with all other versions of
// AAC audio in Chrome 46. Don't mix the two.
if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
(audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
variant.excludeUntil = Infinity;
}
});
};
videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
var media = this.playlists.media(), mimeType;
......@@ -311,6 +390,10 @@ videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
}
this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
// exclude any incompatible variant streams from future playlist
// selection
this.excludeIncompatibleVariants_(media);
// transition the sourcebuffer to the ended state if we've hit the end of
// the playlist
this.sourceBuffer.addEventListener('updateend', function() {
......@@ -389,6 +472,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 +669,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 +681,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,174 @@ 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('does not blacklist compatible H.264 codec strings', function() {
var master;
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.4d400f,mp4a.40.5"\n' +
'media1.m3u8\n'); // master
standardXHRResponse(requests.shift()); // media
master = player.tech_.hls.playlists.master;
strictEqual(master.playlists[0].excludeUntil, undefined, 'did not blacklist');
strictEqual(master.playlists[1].excludeUntil, undefined, 'did not blacklist');
});
test('does not blacklist compatible AAC codec strings', function() {
var master;
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.2"\n' +
'media.m3u8\n' +
'#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.3"\n' +
'media1.m3u8\n'); // master
standardXHRResponse(requests.shift()); // media
master = player.tech_.hls.playlists.master;
strictEqual(master.playlists[0].excludeUntil, undefined, 'did not blacklist');
strictEqual(master.playlists[1].excludeUntil, undefined, 'did not blacklist');
});
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({
......