MediaCodec Video To Bitmap

一、MediaCodec 概述

1.1 数据流转

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

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

MediaCodec-Data.png

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

1.2 状态机

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

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 逻辑流程

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完成了视频帧提取的能力,这里我们是通过同步解码的方式取出视频帧,欢迎尝试异步解码的方式对此进行扩展。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在 Android 中使用 MediaCodec 添加水印,可以考虑以下步骤: 1. 创建一个带有水印的 Bitmap。 2. 获取原始视频的帧数据解码。 3. 将水印 Bitmap 绘制到视频帧上。 4. 在编码器中编码带有水印的帧数据。 5. 将编码后的数据写入输出文件或传输到网络。 以下是一些示例代码,用于在视频帧上添加水印: ```java // 创建带有水印的 Bitmap Bitmap watermark = BitmapFactory.decodeResource(getResources(), R.drawable.watermark); watermark = Bitmap.createScaledBitmap(watermark, 100, 100, true); // 获取原始视频的帧数据解码 MediaExtractor extractor = new MediaExtractor(); extractor.setDataSource(videoFilePath); int trackIndex = selectTrack(extractor, "video/"); extractor.selectTrack(trackIndex); MediaFormat format = extractor.getTrackFormat(trackIndex); int width = format.getInteger(MediaFormat.KEY_WIDTH); int height = format.getInteger(MediaFormat.KEY_HEIGHT); MediaCodec decoder = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME)); decoder.configure(format, null, null, 0); decoder.start(); ByteBuffer[] inputBuffers = decoder.getInputBuffers(); ByteBuffer[] outputBuffers = decoder.getOutputBuffers(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); boolean isEOS = false; // 逐帧处理视频 while (!isEOS) { int inputBufferIndex = decoder.dequeueInputBuffer(10000); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; int sampleSize = extractor.readSampleData(inputBuffer, 0); if (sampleSize < 0) { decoder.queueInputBuffer(inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); isEOS = true; } else { decoder.queueInputBuffer(inputBufferIndex, 0, sampleSize, extractor.getSampleTime(), 0); extractor.advance(); } } int outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000); switch (outputBufferIndex) { case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: outputBuffers = decoder.getOutputBuffers(); break; case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: format = decoder.getOutputFormat(); break; case MediaCodec.INFO_TRY_AGAIN_LATER: break; default: ByteBuffer outputBuffer = outputBuffers[outputBufferIndex]; // 绘制水印到视频帧上 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); outputBuffer.rewind(); bitmap.copyPixelsFromBuffer(outputBuffer); Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(watermark, 0, 0, null); // 将带有水印的视频帧编码并写入输出文件或传输到网络 byte[] data = convertBitmapToNV21(bitmap, width, height); ByteBuffer inputBuffer = encoder.getInputBuffer(inputBufferIndex); inputBuffer.clear(); inputBuffer.put(data); encoder.queueInputBuffer(inputBufferIndex, 0, data.length, bufferInfo.presentationTimeUs, 0); decoder.releaseOutputBuffer(outputBufferIndex, false); break; } if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { isEOS = true; } } // 释放资源 decoder.stop(); decoder.release(); extractor.release(); ``` 在上面的示例中,我们使用 `MediaExtractor` 获取原始视频的帧数据,并通过 `MediaCodec` 解码每一帧。在处理每一帧数据时,我们将水印 Bitmap 绘制到视频帧上,并将带有水印的帧数据编码并写入输出文件或传输到网络。最后,我们释放所有资源。 注意,上面的示例代码仅用于演示如何在 Android 中使用 MediaCodec 添加水印。实际应用中,你需要根据实际情况进行适当的修改和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值