求助:MediaCodec调用getOutputImage真机返回null,模拟器正常
我之前也碰到过几乎一模一样的情况——用ExoPlayer绕开SurfaceView,想通过MediaCodec.getOutputImage()拿视频帧,模拟器跑得顺风顺水,但到了Android 6/7的老真机上就一直返回null,声音正常就是拿不到图像,绑个SurfaceView又一切正常。这里给你分析下原因和可行的解决办法:
核心原因
早期Android(6.0-7.1)的硬件MediaCodec实现有兼容性问题:当你没有给Codec绑定任何Surface时,部分厂商的硬件解码器不会分配用于getOutputImage()的Image缓冲区,只会默认支持ByteBuffer输出(甚至有些连ByteBuffer都不给,必须绑定Surface才干活)。而模拟器用的是软件Codec,对无Surface场景的Image输出支持更完善,所以没这个问题。
可行解决方案
1. 用ImageReader替代直接调用getOutputImage(推荐)
ImageReader是系统提供的更兼容的图像捕获方式,老设备对它的支持比直接调用MediaCodec的getOutputImage好得多。思路是给MediaCodec绑定一个ImageReader生成的Surface,让Codec把帧输出到这个Surface,再从ImageReader里取Image:
// 先根据视频分辨率创建ImageReader,格式选YUV_420_888(和大多数视频Codec兼容) ImageReader imageReader = ImageReader.newInstance(videoWidth, videoHeight, ImageFormat.YUV_420_888, 2); // 设置Image可用的回调 imageReader.setOnImageAvailableListener(reader -> { try (Image image = reader.acquireNextImage()) { if (image != null) { // 在这里处理你的Image帧,try-with-resources会自动帮你close processVideoImage(image); } } catch (Exception e) { e.printStackTrace(); } }, null); // 把ImageReader的Surface传给ExoPlayer的解码器 // 可以通过自定义MediaCodecSelector或者修改RenderersFactory配置实现,比如: DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(context) .setMediaCodecAdapterFactory(new MediaCodecAdapterFactory() { @Override public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configuration) throws IOException { // 给Codec绑定ImageReader的Surface configuration.codec.configure(configuration.format, imageReader.getSurface(), null, 0); return DefaultMediaCodecAdapter.create(configuration.codec); } }); ExoPlayer player = new ExoPlayer.Builder(context, renderersFactory).build();
这样做既不用显示SurfaceView,又能让硬件Codec正常输出帧,兼容性覆盖绝大多数老设备。
2. 强制检查Codec的格式支持
有些硬件Codec虽然不支持无Surface的Image输出,但如果指定了正确的颜色格式(比如COLOR_FormatYUV420Flexible),可能会正常工作。你可以在ExoPlayer的MediaCodecSelector里过滤出支持该格式的Codec:
MediaCodecSelector customSelector = new MediaCodecSelector() { @Override public List<MediaCodecInfo> getDecoderInfos(String mimeType, boolean requiresSecureDecoder) throws MediaCodecNotFoundException { List<MediaCodecInfo> infos = MediaCodecSelector.DEFAULT.getDecoderInfos(mimeType, requiresSecureDecoder); Iterator<MediaCodecInfo> iterator = infos.iterator(); while (iterator.hasNext()) { MediaCodecInfo info = iterator.next(); boolean supportsFlexibleFormat = false; for (MediaCodecInfo.CodecCapabilities caps : info.getCapabilitiesForType(mimeType)) { for (int format : caps.colorFormats) { if (format == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible) { supportsFlexibleFormat = true; break; } } if (supportsFlexibleFormat) break; } if (!supportsFlexibleFormat) { iterator.remove(); // 移除不支持灵活格式的Codec } } return infos; } }; // 把自定义Selector传给ExoPlayer DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(context) .setMediaCodecSelector(customSelector); ExoPlayer player = new ExoPlayer.Builder(context, renderersFactory).build();
不过这个方法的兼容性不如ImageReader,有些老设备的硬件Codec可能根本不支持COLOR_FormatYUV420Flexible,只能作为备选方案。
3. fallback到软件解码(最后选项)
如果上面两种方法都不行,可以强制ExoPlayer使用软件解码器,软件Codec的实现更统一,对getOutputImage()的支持更好。缺点是性能差,高分辨率视频可能会卡顿:
MediaCodecSelector softwareSelector = new MediaCodecSelector() { @Override public List<MediaCodecInfo> getDecoderInfos(String mimeType, boolean requiresSecureDecoder) throws MediaCodecNotFoundException { List<MediaCodecInfo> infos = MediaCodecSelector.DEFAULT.getDecoderInfos(mimeType, requiresSecureDecoder); List<MediaCodecInfo> softwareInfos = new ArrayList<>(); for (MediaCodecInfo info : infos) { // 判断是否为软件Codec:名称不含硬件标识且带sw标记 if (!info.getName().startsWith("OMX.") || info.getName().contains(".sw.")) { softwareInfos.add(info); } } if (softwareInfos.isEmpty()) { throw new MediaCodecNotFoundException("No software decoder found for " + mimeType); } return softwareInfos; } };
额外注意事项
- 不管用哪种方法,拿到Image后一定要及时释放,用try-with-resources或者手动调用
image.close(),否则会导致缓冲区耗尽,后续帧无法输出。 - 如果用ImageReader,要根据视频帧率设置合适的缓冲区数量(newInstance的第三个参数),避免丢帧。
内容的提问来源于stack exchange,提问作者Oliver Metz

