You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

无需配置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();
    }
}

正常渲染的图像
Auto decoder render in surface

颜色异常的Bitmap
No correct color 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。步骤如下:

  1. 创建ImageReader,指定和视频一致的宽高、格式(用ImageFormat.YUV_420_888对应Flexible格式);
  2. 把ImageReader的Surface传给MediaCodec.configure;
  3. 从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

火山引擎 最新活动