958f2b8c by David LaPalomento

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

Blacklist incompatible codecs
2 parents 6dcb32ce 6c574219
...@@ -39,6 +39,11 @@ ...@@ -39,6 +39,11 @@
39 padding: 0 5px; 39 padding: 0 5px;
40 margin: 20px 0; 40 margin: 20px 0;
41 } 41 }
42 input {
43 margin-top: 15px;
44 min-width: 450px;
45 padding: 5px;
46 }
42 </style> 47 </style>
43 48
44 </head> 49 </head>
...@@ -56,10 +61,31 @@ ...@@ -56,10 +61,31 @@
56 src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8" 61 src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"
57 type="application/x-mpegURL"> 62 type="application/x-mpegURL">
58 </video> 63 </video>
64
65 <form id=load-url>
66 <label>
67 Video URL:
68 <input id=url type=url value="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8">
69 </label>
70 <button type=submit>Load</button>
71 </form>
72
59 <script> 73 <script>
60 videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf'; 74 videojs.options.flash.swf = 'node_modules/videojs-swf/dist/video-js.swf';
61 // initialize the player 75 // initialize the player
62 var player = videojs('video'); 76 var player = videojs('video');
77
78 // hook up the video switcher
79 var loadUrl = document.getElementById('load-url');
80 var url = document.getElementById('url');
81 loadUrl.addEventListener('submit', function(event) {
82 event.preventDefault();
83 player.src({
84 src: url.value,
85 type: 'application/x-mpegURL'
86 });
87 return false;
88 });
63 </script> 89 </script>
64 </body> 90 </body>
65 </html> 91 </html>
......
...@@ -294,6 +294,85 @@ videojs.Hls.bufferedAdditions_ = function(original, update) { ...@@ -294,6 +294,85 @@ videojs.Hls.bufferedAdditions_ = function(original, update) {
294 return result; 294 return result;
295 }; 295 };
296 296
297
298 var parseCodecs = function(codecs) {
299 var result = {
300 codecCount: 0,
301 videoCodec: null,
302 audioProfile: null
303 };
304
305 result.codecCount = codecs.split(',').length;
306 result.codecCount = result.codecCount || 2;
307
308 // parse the video codec but ignore the version
309 result.videoCodec = /(^|\s|,)+(avc1)[^ ,]*/i.exec(codecs);
310 result.videoCodec = result.videoCodec && result.videoCodec[2];
311
312 // parse the last field of the audio codec
313 result.audioProfile = /(^|\s|,)+mp4a.\d+\.(\d+)/i.exec(codecs);
314 result.audioProfile = result.audioProfile && result.audioProfile[2];
315
316 return result;
317 };
318 /**
319 * Blacklist playlists that are known to be codec or
320 * stream-incompatible with the SourceBuffer configuration. For
321 * instance, Media Source Extensions would cause the video element to
322 * stall waiting for video data if you switched from a variant with
323 * video and audio to an audio-only one.
324 *
325 * @param media {object} a media playlist compatible with the current
326 * set of SourceBuffers. Variants in the current master playlist that
327 * do not appear to have compatible codec or stream configurations
328 * will be excluded from the default playlist selection algorithm
329 * indefinitely.
330 */
331 videojs.HlsHandler.prototype.excludeIncompatibleVariants_ = function(media) {
332 var
333 master = this.playlists.master,
334 codecCount = 2,
335 videoCodec = null,
336 audioProfile = null,
337 codecs;
338
339 if (media.attributes && media.attributes.CODECS) {
340 codecs = parseCodecs(media.attributes.CODECS);
341 videoCodec = codecs.videoCodec;
342 audioProfile = codecs.audioProfile;
343 codecCount = codecs.codecCount;
344 }
345 master.playlists.forEach(function(variant) {
346 var variantCodecs = {
347 codecCount: 2,
348 videoCodec: null,
349 audioProfile: null
350 };
351
352 if (variant.attributes && variant.attributes.CODECS) {
353 variantCodecs = parseCodecs(variant.attributes.CODECS);
354 }
355
356 // if the streams differ in the presence or absence of audio or
357 // video, they are incompatible
358 if (variantCodecs.codecCount !== codecCount) {
359 variant.excludeUntil = Infinity;
360 }
361
362 // if h.264 is specified on the current playlist, some flavor of
363 // it must be specified on all compatible variants
364 if (variantCodecs.videoCodec !== videoCodec) {
365 variant.excludeUntil = Infinity;
366 }
367 // HE-AAC ("mp4a.40.5") is incompatible with all other versions of
368 // AAC audio in Chrome 46. Don't mix the two.
369 if ((variantCodecs.audioProfile === '5' && audioProfile !== '5') ||
370 (audioProfile === '5' && variantCodecs.audioProfile !== '5')) {
371 variant.excludeUntil = Infinity;
372 }
373 });
374 };
375
297 videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { 376 videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
298 var media = this.playlists.media(), mimeType; 377 var media = this.playlists.media(), mimeType;
299 378
...@@ -311,6 +390,10 @@ videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { ...@@ -311,6 +390,10 @@ videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
311 } 390 }
312 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType); 391 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
313 392
393 // exclude any incompatible variant streams from future playlist
394 // selection
395 this.excludeIncompatibleVariants_(media);
396
314 // transition the sourcebuffer to the ended state if we've hit the end of 397 // transition the sourcebuffer to the ended state if we've hit the end of
315 // the playlist 398 // the playlist
316 this.sourceBuffer.addEventListener('updateend', function() { 399 this.sourceBuffer.addEventListener('updateend', function() {
...@@ -389,6 +472,7 @@ videojs.HlsHandler.prototype.setupFirstPlay = function() { ...@@ -389,6 +472,7 @@ videojs.HlsHandler.prototype.setupFirstPlay = function() {
389 var seekable, media; 472 var seekable, media;
390 media = this.playlists.media(); 473 media = this.playlists.media();
391 474
475
392 // check that everything is ready to begin buffering 476 // check that everything is ready to begin buffering
393 477
394 // 1) the video is a live stream of unknown duration 478 // 1) the video is a live stream of unknown duration
...@@ -585,7 +669,8 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { ...@@ -585,7 +669,8 @@ videojs.HlsHandler.prototype.selectPlaylist = function () {
585 effectiveBitrate, 669 effectiveBitrate,
586 sortedPlaylists = this.playlists.master.playlists.slice(), 670 sortedPlaylists = this.playlists.master.playlists.slice(),
587 bandwidthPlaylists = [], 671 bandwidthPlaylists = [],
588 i = sortedPlaylists.length, 672 now = +new Date(),
673 i,
589 variant, 674 variant,
590 oldvariant, 675 oldvariant,
591 bandwidthBestVariant, 676 bandwidthBestVariant,
...@@ -596,8 +681,18 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { ...@@ -596,8 +681,18 @@ videojs.HlsHandler.prototype.selectPlaylist = function () {
596 681
597 sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth); 682 sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth);
598 683
684 // filter out any playlists that have been excluded due to
685 // incompatible configurations or playback errors
686 sortedPlaylists = sortedPlaylists.filter(function(variant) {
687 if (variant.excludeUntil !== undefined) {
688 return now >= variant.excludeUntil;
689 }
690 return true;
691 });
692
599 // filter out any variant that has greater effective bitrate 693 // filter out any variant that has greater effective bitrate
600 // than the current estimated bandwidth 694 // than the current estimated bandwidth
695 i = sortedPlaylists.length;
601 while (i--) { 696 while (i--) {
602 variant = sortedPlaylists[i]; 697 variant = sortedPlaylists[i];
603 698
......
...@@ -999,7 +999,7 @@ test('uses the lowest bitrate if no other is suitable', function() { ...@@ -999,7 +999,7 @@ test('uses the lowest bitrate if no other is suitable', function() {
999 'the lowest bitrate stream is selected'); 999 'the lowest bitrate stream is selected');
1000 }); 1000 });
1001 1001
1002 test('selects the correct rendition by player dimensions', function() { 1002 test('selects the correct rendition by player dimensions', function() {
1003 var playlist; 1003 var playlist;
1004 1004
1005 player.src({ 1005 player.src({
...@@ -1071,6 +1071,174 @@ test('selects the highest bitrate playlist when the player dimensions are ' + ...@@ -1071,6 +1071,174 @@ test('selects the highest bitrate playlist when the player dimensions are ' +
1071 'selected the highest bandwidth variant'); 1071 'selected the highest bandwidth variant');
1072 }); 1072 });
1073 1073
1074 test('filters playlists that are currently excluded', function() {
1075 var playlist;
1076 player.src({
1077 src: 'manifest/master.m3u8',
1078 type: 'application/vnd.apple.mpegurl'
1079 });
1080 openMediaSource(player);
1081
1082 player.tech_.hls.bandwidth = 1e10;
1083 requests.shift().respond(200, null,
1084 '#EXTM3U\n' +
1085 '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
1086 'media.m3u8\n' +
1087 '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
1088 'media1.m3u8\n'); // master
1089 standardXHRResponse(requests.shift()); // media
1090
1091 // exclude the current playlist
1092 player.tech_.hls.playlists.master.playlists[0].excludeUntil = +new Date() + 1000;
1093 playlist = player.tech_.hls.selectPlaylist();
1094 equal(playlist, player.tech_.hls.playlists.master.playlists[1], 'respected exclusions');
1095
1096 // timeout the exclusion
1097 clock.tick(1000);
1098 playlist = player.tech_.hls.selectPlaylist();
1099 equal(playlist, player.tech_.hls.playlists.master.playlists[0], 'expired the exclusion');
1100 });
1101
1102 test('blacklists switching from video+audio playlists to audio only', function() {
1103 var audioPlaylist;
1104 player.src({
1105 src: 'manifest/master.m3u8',
1106 type: 'application/vnd.apple.mpegurl'
1107 });
1108 openMediaSource(player);
1109
1110 player.tech_.hls.bandwidth = 1e10;
1111 requests.shift().respond(200, null,
1112 '#EXTM3U\n' +
1113 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
1114 'media.m3u8\n' +
1115 '#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
1116 'media1.m3u8\n'); // master
1117
1118 standardXHRResponse(requests.shift()); // media1
1119 equal(player.tech_.hls.playlists.media(),
1120 player.tech_.hls.playlists.master.playlists[1],
1121 'selected video+audio');
1122 audioPlaylist = player.tech_.hls.playlists.master.playlists[0];
1123 equal(audioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
1124 });
1125
1126 test('blacklists switching from audio-only playlists to video+audio', function() {
1127 var videoAudioPlaylist;
1128 player.src({
1129 src: 'manifest/master.m3u8',
1130 type: 'application/vnd.apple.mpegurl'
1131 });
1132 openMediaSource(player);
1133
1134 player.tech_.hls.bandwidth = 1;
1135 requests.shift().respond(200, null,
1136 '#EXTM3U\n' +
1137 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="mp4a.40.2"\n' +
1138 'media.m3u8\n' +
1139 '#EXT-X-STREAM-INF:BANDWIDTH=10,RESOLUTION=1x1\n' +
1140 'media1.m3u8\n'); // master
1141
1142 standardXHRResponse(requests.shift()); // media1
1143 equal(player.tech_.hls.playlists.media(),
1144 player.tech_.hls.playlists.master.playlists[0],
1145 'selected audio only');
1146 videoAudioPlaylist = player.tech_.hls.playlists.master.playlists[1];
1147 equal(videoAudioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
1148 });
1149
1150 test('blacklists switching from video-only playlists to video+audio', function() {
1151 var videoAudioPlaylist;
1152 player.src({
1153 src: 'manifest/master.m3u8',
1154 type: 'application/vnd.apple.mpegurl'
1155 });
1156 openMediaSource(player);
1157
1158 player.tech_.hls.bandwidth = 1;
1159 requests.shift().respond(200, null,
1160 '#EXTM3U\n' +
1161 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d"\n' +
1162 'media.m3u8\n' +
1163 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
1164 'media1.m3u8\n'); // master
1165
1166 standardXHRResponse(requests.shift()); // media
1167 equal(player.tech_.hls.playlists.media(),
1168 player.tech_.hls.playlists.master.playlists[0],
1169 'selected video only');
1170 videoAudioPlaylist = player.tech_.hls.playlists.master.playlists[1];
1171 equal(videoAudioPlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
1172 });
1173
1174 test('does not blacklist compatible H.264 codec strings', function() {
1175 var master;
1176 player.src({
1177 src: 'manifest/master.m3u8',
1178 type: 'application/vnd.apple.mpegurl'
1179 });
1180 openMediaSource(player);
1181
1182 player.tech_.hls.bandwidth = 1;
1183 requests.shift().respond(200, null,
1184 '#EXTM3U\n' +
1185 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.5"\n' +
1186 'media.m3u8\n' +
1187 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400f,mp4a.40.5"\n' +
1188 'media1.m3u8\n'); // master
1189
1190 standardXHRResponse(requests.shift()); // media
1191 master = player.tech_.hls.playlists.master;
1192 strictEqual(master.playlists[0].excludeUntil, undefined, 'did not blacklist');
1193 strictEqual(master.playlists[1].excludeUntil, undefined, 'did not blacklist');
1194 });
1195
1196 test('does not blacklist compatible AAC codec strings', function() {
1197 var master;
1198 player.src({
1199 src: 'manifest/master.m3u8',
1200 type: 'application/vnd.apple.mpegurl'
1201 });
1202 openMediaSource(player);
1203
1204 player.tech_.hls.bandwidth = 1;
1205 requests.shift().respond(200, null,
1206 '#EXTM3U\n' +
1207 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.2"\n' +
1208 'media.m3u8\n' +
1209 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.3"\n' +
1210 'media1.m3u8\n'); // master
1211
1212 standardXHRResponse(requests.shift()); // media
1213 master = player.tech_.hls.playlists.master;
1214 strictEqual(master.playlists[0].excludeUntil, undefined, 'did not blacklist');
1215 strictEqual(master.playlists[1].excludeUntil, undefined, 'did not blacklist');
1216 });
1217
1218 test('blacklists switching between playlists with incompatible audio codecs', function() {
1219 var alternatePlaylist;
1220 player.src({
1221 src: 'manifest/master.m3u8',
1222 type: 'application/vnd.apple.mpegurl'
1223 });
1224 openMediaSource(player);
1225
1226 player.tech_.hls.bandwidth = 1;
1227 requests.shift().respond(200, null,
1228 '#EXTM3U\n' +
1229 '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.5"\n' +
1230 'media.m3u8\n' +
1231 '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,mp4a.40.2"\n' +
1232 'media1.m3u8\n'); // master
1233
1234 standardXHRResponse(requests.shift()); // media
1235 equal(player.tech_.hls.playlists.media(),
1236 player.tech_.hls.playlists.master.playlists[0],
1237 'selected HE-AAC stream');
1238 alternatePlaylist = player.tech_.hls.playlists.master.playlists[1];
1239 equal(alternatePlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
1240 });
1241
1074 test('does not download the next segment if the buffer is full', function() { 1242 test('does not download the next segment if the buffer is full', function() {
1075 var currentTime = 15; 1243 var currentTime = 15;
1076 player.src({ 1244 player.src({
......