Merge pull request #438 from videojs/blacklist-incompatible-codecs
Blacklist incompatible codecs
Showing
3 changed files
with
291 additions
and
2 deletions
... | @@ -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({ | ... | ... |
-
Please register or sign in to post a comment