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) { ...@@ -294,6 +294,37 @@ videojs.Hls.bufferedAdditions_ = function(original, update) {
294 return result; 294 return result;
295 }; 295 };
296 296
297 /**
298 * Blacklist playlists that are known to be codec or
299 * stream-incompatible with the SourceBuffer configuration. For
300 * instance, Media Source Extensions would cause the video element to
301 * stall waiting for video data if you switched from a variant with
302 * video and audio to an audio-only one.
303 *
304 * @param media {object} a media playlist compatible with the current
305 * set of SourceBuffers. Variants in the current master playlist that
306 * do not appear to have compatible codec or stream configurations
307 * will be excluded from the default playlist selection algorithm
308 * indefinitely.
309 */
310 videojs.HlsHandler.prototype.excludeIncompatibleStreams_ = function(media) {
311 var master = this.playlists.master, codecs = '';
312
313 if (media.attributes && media.attributes.CODECS) {
314 codecs = media.attributes.CODECS;
315 }
316 master.playlists.forEach(function(variant) {
317 var variantCodecs = '';
318 if (variant.attributes && variant.attributes.CODECS) {
319 variantCodecs = variant.attributes.CODECS;
320 }
321
322 if (variantCodecs !== codecs) {
323 variant.excludeUntil = Infinity;
324 }
325 });
326 };
327
297 videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { 328 videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
298 var media = this.playlists.media(), mimeType; 329 var media = this.playlists.media(), mimeType;
299 330
...@@ -311,6 +342,10 @@ videojs.HlsHandler.prototype.setupSourceBuffer_ = function() { ...@@ -311,6 +342,10 @@ videojs.HlsHandler.prototype.setupSourceBuffer_ = function() {
311 } 342 }
312 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType); 343 this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
313 344
345 // exclude any incompatible variant streams from future playlist
346 // selection
347 this.excludeIncompatibleStreams_(media);
348
314 // transition the sourcebuffer to the ended state if we've hit the end of 349 // transition the sourcebuffer to the ended state if we've hit the end of
315 // the playlist 350 // the playlist
316 this.sourceBuffer.addEventListener('updateend', function() { 351 this.sourceBuffer.addEventListener('updateend', function() {
...@@ -389,6 +424,7 @@ videojs.HlsHandler.prototype.setupFirstPlay = function() { ...@@ -389,6 +424,7 @@ videojs.HlsHandler.prototype.setupFirstPlay = function() {
389 var seekable, media; 424 var seekable, media;
390 media = this.playlists.media(); 425 media = this.playlists.media();
391 426
427
392 // check that everything is ready to begin buffering 428 // check that everything is ready to begin buffering
393 429
394 // 1) the video is a live stream of unknown duration 430 // 1) the video is a live stream of unknown duration
...@@ -585,7 +621,8 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { ...@@ -585,7 +621,8 @@ videojs.HlsHandler.prototype.selectPlaylist = function () {
585 effectiveBitrate, 621 effectiveBitrate,
586 sortedPlaylists = this.playlists.master.playlists.slice(), 622 sortedPlaylists = this.playlists.master.playlists.slice(),
587 bandwidthPlaylists = [], 623 bandwidthPlaylists = [],
588 i = sortedPlaylists.length, 624 now = +new Date(),
625 i,
589 variant, 626 variant,
590 oldvariant, 627 oldvariant,
591 bandwidthBestVariant, 628 bandwidthBestVariant,
...@@ -596,8 +633,18 @@ videojs.HlsHandler.prototype.selectPlaylist = function () { ...@@ -596,8 +633,18 @@ videojs.HlsHandler.prototype.selectPlaylist = function () {
596 633
597 sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth); 634 sortedPlaylists.sort(videojs.Hls.comparePlaylistBandwidth);
598 635
636 // filter out any playlists that have been excluded due to
637 // incompatible configurations or playback errors
638 sortedPlaylists = sortedPlaylists.filter(function(variant) {
639 if (variant.excludeUntil !== undefined) {
640 return now >= variant.excludeUntil;
641 }
642 return true;
643 });
644
599 // filter out any variant that has greater effective bitrate 645 // filter out any variant that has greater effective bitrate
600 // than the current estimated bandwidth 646 // than the current estimated bandwidth
647 i = sortedPlaylists.length;
601 while (i--) { 648 while (i--) {
602 variant = sortedPlaylists[i]; 649 variant = sortedPlaylists[i];
603 650
......
...@@ -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,130 @@ test('selects the highest bitrate playlist when the player dimensions are ' + ...@@ -1071,6 +1071,130 @@ 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('blacklists switching between playlists with incompatible audio codecs', function() {
1175 var alternatePlaylist;
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.4d400d,mp4a.40.2"\n' +
1188 'media1.m3u8\n'); // master
1189
1190 standardXHRResponse(requests.shift()); // media
1191 equal(player.tech_.hls.playlists.media(),
1192 player.tech_.hls.playlists.master.playlists[0],
1193 'selected HE-AAC stream');
1194 alternatePlaylist = player.tech_.hls.playlists.master.playlists[1];
1195 equal(alternatePlaylist.excludeUntil, Infinity, 'excluded incompatible playlist');
1196 });
1197
1074 test('does not download the next segment if the buffer is full', function() { 1198 test('does not download the next segment if the buffer is full', function() {
1075 var currentTime = 15; 1199 var currentTime = 15;
1076 player.src({ 1200 player.src({
......