无需配置Surface,如何通过ByteBuffer获取MediaCodec解码的正确RGB Bitmap?
我SD卡里有一段MP4视频,对应的MediaFormat参数如下:
MediaFormat = { repeat-previous-frame-after=66666, mime=video/avc, frame-rate=15, color-format=2130708361, height=720, width=1280, bitrate=1000000, i-frame-interval=1 }
当给MediaCodec解码器配置Surface时,图像能正常渲染;但如果不配置Surface,通过ByteBuffer创建Bitmap时,得到的图像颜色完全异常。我试过用YuvImage、YUV420转RGB的工具类,甚至ScriptIntrinsicYuvToRGB,都没法得到正确的Bitmap。
我的核心需求是:必须创建Bitmap,且不能给MediaCodec解码器配置Surface!
以下是我的解码线程代码:
private class PlayerThread extends Thread { private MediaExtractor extractor; private MediaCodec decoder; private Surface surface; private boolean needStop = false; final int TIMEOUT_USEC = 10000; PlayerThread(Surface surface) { this.surface = surface; } @Override public void run() { extractor = new MediaExtractor(); try { extractor.setDataSource(SAMPLE); } catch (IOException e) { e.printStackTrace(); } for (int i = 0; i < extractor.getTrackCount(); i++) { MediaFormat format = extractor.getTrackFormat(i); String mime = format.getString(MediaFormat.KEY_MIME); if (mime.startsWith("video/")) { extractor.selectTrack(i); try { decoder = MediaCodec.createDecoderByType(mime); } catch (IOException e) { e.printStackTrace(); } decoder.configure(format, /*surface*/ null, null, 0); break; } } if (decoder == null) { return; } decoder.start(); ByteBuffer[] inputBuffers = decoder.getInputBuffers(); ByteBuffer[] outputBuffers = decoder.getOutputBuffers(); MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); long startMs = System.currentTimeMillis(); boolean isEOS = false; while (!Thread.interrupted() && !needStop) { if (!isEOS) { int inIndex = -1; try { inIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC); } catch (IllegalStateException e) { e.printStackTrace(); } if (inIndex >= 0) { ByteBuffer buffer = inputBuffers[inIndex]; int sampleSize = extractor.readSampleData(buffer, 0); if (sampleSize < 0) { if (!needStop) { decoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); isEOS = true; } } else { try { if (!needStop) { decoder.queueInputBuffer(inIndex, 0, sampleSize, extractor.getSampleTime(), 0); extractor.advance(); } } catch (IllegalStateException e) { e.printStackTrace(); } } } } int outIndex = MediaCodec.INFO_TRY_AGAIN_LATER; try { if (!needStop) { outIndex = decoder.dequeueOutputBuffer(info, TIMEOUT_USEC); } } catch (IllegalStateException e) { e.printStackTrace(); } switch (outIndex) { case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: outputBuffers = decoder.getOutputBuffers(); break; case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: break; case MediaCodec.INFO_TRY_AGAIN_LATER: break; default: ByteBuffer buffer = outputBuffers[outIndex]; buffer.position(info.offset); buffer.limit(info.offset + info.size); byte[] ba = new byte[buffer.remaining()]; buffer.get(ba); // 这里我试过很多转换算法来获取Bitmap YuvImage yuvimage = new YuvImage(ba, ImageFormat.NV21, 1280, 720, null); ByteArrayOutputStream baos = new ByteArrayOutputStream(); yuvimage.compressToJpeg(new Rect(0, 0, 1280, 720), 80, baos); byte[] jdata = baos.toByteArray(); final Bitmap bmp = BitmapFactory.decodeByteArray(jdata, 0, jdata.length); if (bmp != null) { srcRect.left = 0; srcRect.top = 0; srcRect.bottom = 720; srcRect.right = 1280; Canvas canvas = surface.lockCanvas(dstRect); try { if (canvas != null) { canvas.drawBitmap(bmp, srcRect, dstRect, null); } } finally { if (canvas != null) { surface.unlockCanvasAndPost(canvas); } } } else { Log.e(TAG, "bmp = BAD"); } while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs && !needStop) { try { sleep(10); } catch (InterruptedException e) { PlayerThread.this.interrupt(); e.printStackTrace(); break; } } decoder.releaseOutputBuffer(outIndex, false); break; } if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } } decoder.stop(); decoder.release(); extractor.release(); } }
正常渲染的图像:
颜色异常的Bitmap:
请问怎么把解码器输出的ByteBuffer正确转换成Bitmap?
兄弟,问题出在你忽略了MediaCodec输出的YUV格式细节!你看到的color-format=2130708361对应的是MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible,这是Android 5.0之后引入的灵活格式,它的YUV数据排列和你直接用的NV21(固定格式)完全不一样,这就是颜色乱掉的核心原因。
直接从ByteBuffer拿数据转NV21的思路走不通,因为Flexible格式的YUV可能是平面、半平面甚至交错排列,不同设备的实现还可能有差异。给你两个靠谱的解决方案:
方案一:用ImageReader间接获取Bitmap(推荐)
ImageReader可以接收MediaCodec的输出,而且它会帮你处理格式转换,不用自己折腾ByteBuffer。步骤如下:
- 创建ImageReader,指定和视频一致的宽高、格式(用
ImageFormat.YUV_420_888对应Flexible格式); - 把ImageReader的Surface传给MediaCodec.configure;
- 从ImageReader获取Image对象,再转换成Bitmap。
修改后的核心代码片段:
// 初始化ImageReader ImageReader imageReader = ImageReader.newInstance(1280, 720, ImageFormat.YUV_420_888, 2); // 配置解码器时传入ImageReader的Surface decoder.configure(format, imageReader.getSurface(), null, 0); // 解码循环中,替换原来的ByteBuffer处理逻辑 Image image = imageReader.acquireLatestImage(); if (image != null) { // 把Image转成Bitmap,这里用ScriptIntrinsicYuvToRGB来处理 Bitmap bitmap = imageToBitmap(image, 1280, 720); // 接下来就可以用这个bitmap做你要的操作了 image.close(); } // 实现imageToBitmap方法 private Bitmap imageToBitmap(Image image, int width, int height) { Image.Plane[] planes = image.getPlanes(); ByteBuffer yBuffer = planes[0].getBuffer(); ByteBuffer uBuffer = planes[1].getBuffer(); ByteBuffer vBuffer = planes[2].getBuffer(); int ySize = yBuffer.remaining(); int uSize = uBuffer.remaining(); int vSize = vBuffer.remaining(); byte[] nv21 = new byte[ySize + uSize + vSize]; // 复制Y通道 yBuffer.get(nv21, 0, ySize); // 复制UV通道,处理成NV21格式 vBuffer.get(nv21, ySize, vSize); uBuffer.get(nv21, ySize + vSize, uSize); // 用ScriptIntrinsicYuvToRGB转成Bitmap RenderScript rs = RenderScript.create(this); ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)); Type.Builder yuvType = new Type.Builder(rs, Element.U8(rs)).setX(nv21.length); Allocation in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT); Type.Builder rgbaType = new Type.Builder(rs, Element.U8_4(rs)).setX(width).setY(height); Allocation out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); in.copyFrom(nv21); yuvToRgbIntrinsic.setInput(in); yuvToRgbIntrinsic.forEach(out); out.copyTo(bitmap); rs.destroy(); return bitmap; }
方案二:直接处理Flexible格式的ByteBuffer(不推荐)
如果你一定要坚持用ByteBuffer的方式,那得先把Flexible格式的YUV数据转换成标准的NV21或YUV420P。但这个方法兼容性差,因为不同设备的Flexible格式布局可能不同,需要先获取格式的详细信息:
// 在decoder.start()之后,获取输出格式 MediaFormat outputFormat = decoder.getOutputFormat(); int colorFormat = outputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT); // 然后根据colorFormat的具体类型(比如COLOR_FormatYUV420Planar、COLOR_FormatYUV420SemiPlanar等)来解析ByteBuffer中的Y、U、V分量,再重新排列成NV21
不过这个方法太繁琐,适配成本高,还是用ImageReader更省心。
另外,你原来的代码里用YuvImage直接转JPEG,因为格式不匹配,所以颜色才会异常——YuvImage只支持NV21和YUY2格式,而Flexible格式的YUV根本不是这两种,所以肯定转不对。
最后提醒一下,用完Image一定要调用close(),不然会导致内存泄漏哦!
内容的提问来源于stack exchange,提问作者Roman Irtov




