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.
Showing
2 changed files
with
173 additions
and
2 deletions
... | @@ -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({ | ... | ... |
-
Please register or sign in to post a comment