当前位置: 首页>编程语言>正文

MediaCodec Video To Bitmap

一、MediaCodec 概述

1.1 数据流转

MediaCodec 类可用于访问低级媒体编解码器,即编码器/解码器组件。它是 Android 低级多媒体支持基础结构的一部分,通常与 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface 和 AudioTrack 一起使用。

下图是官方提供的MediaCodec数据流转图:

MediaCodec Video To Bitmap,第1张
MediaCodec-Data.png

客户端通过输入队列向Codec输入源数据,Codec编解码完成后,通过Output队列向客户端输出加工后数据。

1.2 状态机

当然MediaCodec在使用过程中需要遵循一定的规范,以下是官方提供到的MediaCodec相关的状态机:

MediaCodec Video To Bitmap,第2张
MediaCodec-State.png

MediaCodec只有在start之后才可以配合之前所介绍到的数据流转图进行使用,一旦stop/reset后,想要继续使用需要重新configure、start确保Codec对象进入可用状态。

二、关键 API 介绍

2.1 MediaExtractor

1)void setDataSource(String path)
设置解封装器的文件来源,解封装器会从视频原件中解析音频、视频、字幕等数据轨道索引;

2)void selectTrack(int index)
设定Extractor接下来读取数据的轨道索引,后续读取数据皆从此轨道中按序读取;

3)int readSampleData(ByteBuffer byteBuf, int offset)
从轨道中读取数据,偏移默认为0,返回值>0则是读取的数据大小,返回值<0则代表已到轨道数据末尾,已读完;

4)boolean advance()
移动到下一帧,readSampleData读取下一帧数据,return true表示还没读完,false表示已经到文件末尾,已读完;

2.2 MediaCodec

1)configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
surface传入null时,Codec加工后的数据会输出到指定的输出队列上,否则会默认输出到surface上、getOutputByteBuffer为null;

2)int dequeueInputBuffer(long timeoutUs)
3)ByteBuffer getInputBuffer(int index)
申请1个输入队列,申请成功时返回队列索引,通过getInputBuffer得到一个空闲的输入Buffer,此时向ByteBuffer输入原始数据即可,原始数据则是通过MediaExtractor读取;

4)int dequeueOutputBuffer(BufferInfo info, long timeoutUs)
5)ByteBuffer getOutputBuffer(int index)
6)void releaseOutputBuffer(int index, boolean render)
等待输出队列,有输出数据时返回输出队列索引(>=0),通过getOutputBuffer得到输出Buffer,通过Buffer读出编解码数据即可,注意数据取出后,相应的OutputBuffer要通过releaseOutputBuffer及时释放,否则Codec内部会状态异常,这是因为Codec内Buffer资源受限,一直不释放会导致Codec编解码数据无处可输出,因此下一个dequeueOutputBuffer调用也会一直得不到有效值。

2.3 YuvImage

MediaCodec解码视频数据时,我们一般指定解码后的数据类型为YUV数据,同时Android平台默认提供了YuvImage,它可以将YUV NV21的原始数据转化为JPEG的图片数据,而JPEG的图片数据我们则可以通过Bitmap在ImageView上展示或者导出为图片文件等等,YuvImage相关Api如下:

1)YuvImage(byte[] yuv, int format, int width, int height, int[] strides)
构造函数,format需要指定为ImageFormat.NV21,因此yuv原始数据的类型也应该与NV21排列方式对照上,width、height则是该YUV对应帧图的宽、高;

2)boolean compressToJpeg(Rect rectangle, int quality, OutputStream stream)
将YuvImage数据压缩为jpeg的类型数据并输出到指定的输出流上,我们后续可以通过该输出流创建Bitmap或者直接导出到文件上。

三、YUV转换

YUV相关的数据格式介绍网上有详细的资料说明,可以在其他博客上详细了解;这里我们只关注常见的几个YUV类型的数据排列方式,便于我们能够从数据流中取出正确的y、u、v分量数据:

YUV i420:
YYYY UU VV

YUV NV21:
YYYY VU VU

YUV NV12:
YYYY UV UV

关注以上的数据排列方式,我们将在接下来的数据转换中通过以上的数据排列方式从原始数据中读出正确的y、u、v分量数据,并将y、u、v分量数据合并为NV21排列方式的YUV原始数据

四、Demo 实现

4.1 逻辑流程

MediaCodec Video To Bitmap,第3张
Video2Jpeg.jpg

4.2 代码示例

1)MediaExtractor 找到视频轨道 并创建 MediaCodec

// mOutputColorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible;

for (int i = 0; i < mExtractor.getTrackCount(); i++) {
    MediaFormat format = mExtractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if (mime != null && mime.startsWith("video/")) {
        mTrackIndexVideo = i;
        mVideoFormat = format;
        mCodec = MediaCodec.createDecoderByType(mime);
        if (!isColorFormatSupport(mOutputColorFormat,
                mCodec.getCodecInfo().getCapabilitiesForType(mime))) {
            throw new Exception("Color Format [" + mOutputColorFormat + "] Not Support By " + mime);
        }

        // 设置颜色格式
        mVideoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mOutputColorFormat);
        break;
    }
}

2)选择视频轨道 + 配置Codec + Start 进入解码流

// 解封装器选择读取数据的轨道(音频轨道 or 视频轨道 or ...)
mExtractor.selectTrack(mTrackIndexVideo);
// 配置解码器
mCodec.configure(mVideoFormat, null, null, 0);
// 启动解码器
mCodec.start();

3)向 MediaCodec 输入视频数据

// 申请1个解码器输入队列,用于填充输入的视频压缩数据
int inputIndex = mCodec.dequeueInputBuffer(10000);
if (inputIndex < 0) {
    continue;
}

ByteBuffer input = mCodec.getInputBuffer(inputIndex);
// MediaExtractor 从视频轨道中读取一帧
int sampleSize = mExtractor.readSampleData(input, 0);
if (sampleSize >= 0) {
    mCodec.queueInputBuffer(inputIndex,
         0, sampleSize,
         mExtractor.getSampleTime(),
         mExtractor.getSampleFlags());
    // 调整偏移,移动到下一帧
    mExtractor.advance();
} else {
    // 数据已读完
    mCodec.queueInputBuffer(inputIndex,
        0, 0, 0,
        MediaCodec.BUFFER_FLAG_END_OF_STREAM);
    inputEnd = true;
}

4)从 MediaCodec 获取解码后数据

int outputIndex = mCodec.dequeueOutputBuffer(bufferInfo, 10000);
if (outputIndex >= 0) {
    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
        // 输出结束了
        outputEnd = true;
    }

    try {
        // 帧索引(第X帧)
        index++;
        if (index == targetFrameIndex || outputEnd) {
            ByteBuffer outputBuffer = mCodec.getOutputBuffer(outputIndex);
            MediaFormat outputFormat = mCodec.getOutputFormat();
            int w = outputFormat.getInteger(MediaFormat.KEY_WIDTH);
            int h = outputFormat.getInteger(MediaFormat.KEY_HEIGHT);
            int color = outputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);

            return YuvToBitmap.convert(convertColorFormat(color), outputBuffer, w, h);
        }
    } finally {
        // 释放output buffer,否则解码器数据无处可放,状态异常
        mCodec.releaseOutputBuffer(outputIndex, true);
    }
}

5)从解码数据 ByteBuffer 解析y、u、v分量数据

byte[] y = new byte[w * h];
byte[] u = new byte[(w / 2) * (h / 2)];
byte[] v = new byte[u.length];

switch (type) {
    case YUVType.YUV_420P: {
        // yyyy uu vv
        src.get(y, 0, y.length);
        src.get(u, 0, u.length);
        src.get(v, 0, v.length);
    } break;
    case YUVType.NV21: {
        // yyyy vu vu
        src.get(y, 0, y.length);
        for (int i = 0; i < u.length * 2; i += 2) {
            src.get(v, i / 2, 1);
            src.get(u, i / 2, 1);
        }
    } break;
    case YUVType.NV12: {
        // yyyy uv uv
        src.get(y, 0, y.length);
        for (int i = 0; i < u.length * 2; i += 2) {
            src.get(u, i / 2, 1);
            src.get(v, i / 2, 1);
        }
    } break;
    default: {
        return null;
    } break;
}

6)y、u、v分量数据组合为 yuv nv21 数据

byte[] data = new byte[y.length + u.length + v.length];
System.arraycopy(y, 0, data, 0, y.length);

// yyyy vu vu
for (int i = 0; i < u.length * 2; i += 2) {
    data[y.length + i] = v[i / 2];
    data[y.length + i + 1] = u[i / 2];
}

return data;

7)yuv nv21 送入 YuvImage 并转化为 Bitmap

// yuv image only support ImageFormat.NV21
byte[] data = yuv2nv21(y, u, v);
try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
    YuvImage image = new YuvImage(data, ImageFormat.NV21, w, h, null);
    Rect rect = new Rect(0, 0, w, h);
    image.compressToJpeg(rect, 100, outStream);
    return BitmapFactory.decodeByteArray(outStream.toByteArray(), 0, outStream.size());
}

总结

至此我们通过MediaExtractor配合MediaCodec与YuvImage完成了视频帧提取的能力,这里我们是通过同步解码的方式取出视频帧,欢迎尝试异步解码的方式对此进行扩展。


https://www.xamrdz.com/lan/5hr2016199.html

相关文章: