当前位置: 首页 > article >正文

Android Camera系列(六):MediaCodec视频编码上-编码YUV

己所不欲勿施于人

本系列主要讲述Android开发中Camera的相关操作、预览方式、视频录制等。项目结构简单、代码耦合性低,旨在帮助大家能从中有所收获(方便copy :) ),对于个人来说也是一个总结的好机会。

Alt

引言

前面我们已经花费了大量的章节来讲解Camera配合各种View用来预览、拍照等,接下来我们来讲解如何进行视频录制。Android中要进行录制视频有很多种方式,如:使用系统相机录制、MediaRecorder+Camera自定义录制等。如果上面的方式能够满足你的需求,那本章你可以划走了。

假如有这么一种需求,我们要获取Camera的帧数据流YUV进行实时检测识别,同时我又要将检测到的数据进行保存,那么MediaCodec就很合适,MediaCodec(硬编解码器)主要对帧数据进行编解码处理。实际的例子就好比抖音拍摄视频,人实时美颜或贴图后将视频保存。

除了可以使用MediaCodec对数据流进行编解码,还有ffmpeg同样也可以。

  • MediaCodec属于硬解码,处理数据使用GPU效率高,功能受硬件限制兼容性略低。
  • ‌FFmpeg属于软解码,处理数据使用CPU效率低,功能强大兼容性好。当然该方式不在我们本章讨论范围

MediaCodec介绍

MediaCodec是Android平台上的一个多媒体编解码器,它可以用于对音频和视频进行编解码。通过MediaCodec,开发者可以直接访问底层的编解码器,实现更高效的音视频处理。同时,MediaCodec也支持硬件加速,可以利用设备的硬件资源来提高编解码的性能。MediaCodec主要应用于以下几个方面:

  1. 音视频编解码:MediaCodec可以对音频和视频进行硬件加速的编解码处理,可以实现高效的音视频处理和播放。
  2. 多媒体格式支持:支持常见的音视频格式,包括H.264、AAC、MP3等,可以进行解码和编码操作。
  3. 硬件加速:利用设备的硬件加速功能,可以提高音视频处理的效率和性能。
  4. 实时处理:支持实时的音视频处理,适用于实时通信、直播等场景。
  5. 自定义处理:可以通过MediaCodec进行自定义的音视频处理,如滤镜、特效等操作。

MediaCodec在Android平台上提供了强大的音视频编解码功能,可以用于多媒体应用的开发和优化。

硬编码流程

  1. 使用MediaCodec,将yuv数据编码获得h264数据
  2. MediaMuxer用来封装h264获得一个mp4文件

MediaCodec使用

1. 创建MediaCodec

通过静态方法创建:createDecoderByTypecreateEncoderByTypecreateByCodecName

使用 createDecoderByTypecreateEncoderByType两个,创建用于解码和编码的实体,传入参数为编解码器的mime type名称。createByCodecName需要传入具体编解码器的名称。

官方建议:最好使用MediaCodecList.findEncoderForFormat和createByCodecName来确保生成的编解码器可以处理给定的格式。

mime可用的类型:

*   "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm)
*   "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm)
*   "video/avc" - H.264/AVC video
*   "video/hevc" - H.265/HEVC video
*   "video/mp4v-es" - MPEG4 video
*   "video/3gpp" - H.263 video
*   "audio/3gpp" - AMR narrowband audio
*   "audio/amr-wb" - AMR wideband audio
*   "audio/mpeg" - MPEG1/2 audio layer III
*   "audio/mp4a-latm" - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
*   "audio/vorbis" - vorbis audio
*   "audio/g711-alaw" - G.711 alaw audio
*   "audio/g711-mlaw" - G.711 ulaw audio

最常用的是 : “video/avc” - H.264/AVC video,现在网络上流传的短视频的视轨格式基本上都是H264。

private static final String MIME_TYPE = "video/avc";

// 创建MediaCodec
MediaCodec mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);

2. 配置MediaCodec

实例化MediaCodec后,我们需要配置一些参数如:视频录制的宽高、码率、帧率等信息。配置使用的类是MediaFormat

调用MediaCodecconfigure方法:

public void configure(
            MediaFormat format,
            Surface surface,
            MediaCrypto crypto,
            int flags
);
  • MediaFormat format:输入数据的格式(解码器)或输出数据的所需格式(编码器)。
  • Surface surface:指定surface,一般用于解码器,设置后解码的内容会被渲染到所指定的surface上。无需要则传null
  • MediaCrypto crypto:指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。
  • int flags:当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。

后面三个参数基本是固定的,基本不需要变化。

第一个参数MediaFormat是需要我们深入去了解下,解码的情况下,该参数大多通过MediaExtractor从视频中获取,这里先不讨论。编码的情况实体需要由我们来进行创建,常用的方法是它的静态方法:createVideoFormatcreateAudioFormat
以视频为例:

MediaFormat format = MediaFormat.createVideoFormat(MIME,width, height);
  • MIME:第一步中的mine_type
  • width,height:视频的分辨率/高宽

MediaFormat 必须设置的参数:YUV编码格式、比特率、帧率、关键帧间隔,不设置会报异常。

// 设置编码yuv格式
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
// 设置比特率
format.setInteger(MediaFormat.KEY_BIT_RATE, 1 * 1024 * 1024);
// 设置帧率
format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
// 设置关键帧间隔,单位:秒
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);

需要特別说明的是MediaFormat.KEY_COLOR_FORMAT :该属性用于指明video编码器的颜色格式,具体选择哪种颜色格式与输入的视频数据源颜色格式有关。
比如:Camera预览采集的图像流通常为NV21或YV12,那么编码器需要指定相应的颜色格式,否则编码得到的数据可能会出现花屏、叠影、颜色失真等现象。

MediaCodecInfo.CodecCapabilities.存储了编码器所有支持的颜色格式,常见颜色格式映射如下:

// 最常用的
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, // 19, I420,测试通过
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar, // 21, NV12,测试通过
// 其他不常用
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar, // 20, I420,测试通过
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar, // 39, NV12, 测试通过

YUV各种格式还不熟悉的,请看下面的说明:

  • YUV420p: I420、YV12
  • YUV420sp: NV12、NV21

同样,对于一个6*4的图像,这四种像素格式的存储方式如下:

	Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y
	Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y
	Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y
	Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y      Y Y Y Y Y Y
	U U U U U U      V V V V V V      U V U V U V      V U V U V U
	V V V V V V      U U U U U U      U V U V U V      V U V U V U
     - I420 -          - YV12 -         - NV12 -         - NV21 -

虽然MediaCodec支持了多种YUV编码格式,而且代码中也有对应的值,但是正如我们引言中说的MediaCodec兼容性不好。支持哪种硬编码格式需要根据硬件来定,这也是大多数初入MediaCodec的人来说最头疼的地方。

3. 选择合适的编码器

MediaCodec都知道是硬编解码器,其实不然。他也是支持软编解码的,软编解码由Google实现,硬编解码由厂商自定义。这也就是兼容性的根源,因为我们不知道厂商是否真的支持硬编解码。我们只能自己查找合适的编解码器。而每种编解码器支持编码的YUV格式又不尽相同,我们是优先选择编码器还是优先选择YUV格式这是我们需要抉择的。

看过大部分的博文讲解都是优先选择编码器,而一旦我们选择的编码器后,就只能使用编码器中支持的YUV格式进行编码,如果发现YUV格式不是我们想要的,那么就会编码失败。而我们这里优先根据YUV格式来选择编码器,这么做的理由是有更好的兼容性,但是可能会牺牲一部分性能。

public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncoder {

    private static final String MIME_TYPE = "video/avc";

    protected int mColorFormat;

	...
    
    /**
     * select the first codec that match a specific MIME type
     *
     * @param mimeType
     * @return null if no codec matched
     */
    @SuppressWarnings("deprecation")
    protected final MediaCodecInfo selectVideoCodec(final String mimeType) {
        if (DEBUG) Log.v(TAG, "selectVideoCodec:");

        // get the list of available codecs
        final int numCodecs = MediaCodecList.getCodecCount();
        // 硬编码器列表
        List<MediaCodecInfo> hardwareCodecInfoList = new ArrayList<>();
        // 软编码器列表
        List<MediaCodecInfo> softwareCodecInfoList = new ArrayList<>();

        for (int i = 0; i < numCodecs; i++) {
            final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);

            if (!codecInfo.isEncoder()) {  // skipp decoder
                continue;
            }

            /**
             * 硬件编解码器
             * OMX.qcom.video.encoder.heic
             * OMX.qcom.video.decoder.avc
             * OMX.qcom.video.decoder.avc.secure
             * OMX.qcom.video.decoder.mpeg2
             * OMX.google.gsm.decoder
             * OMX.qti.video.decoder.h263sw
             * c2.qti.avc.decoder
             *
             * 软件编解码器,通常以OMX.google或者c2.android开头
             * OMX.google.h264.encoder
             * c2.android.aac.decoder
             * c2.android.aac.decoder
             * c2.android.aac.encoder
             * c2.android.aac.encoder
             * c2.android.amrnb.decoder
             * c2.android.amrnb.decoder
             */
            // select first codec that match a specific MIME type and color format
            final String[] types = codecInfo.getSupportedTypes();
            for (int j = 0; j < types.length; j++) {
                if (types[j].equalsIgnoreCase(mimeType)) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//                        if (codecInfo.getName().contains("qcom") || codecInfo.getName().contains("c2.qti")|| codecInfo.getName().contains("c2.android")) continue;
                        if (codecInfo.isVendor()) { // 硬解码
                            hardwareCodecInfoList.add(codecInfo);
                        } else { // 软解码
                            softwareCodecInfoList.add(codecInfo);
                        }
                    } else {
                        if (codecInfo.getName().contains("google") || codecInfo.getName().startsWith("c2.android")) { // 软解码
                            softwareCodecInfoList.add(codecInfo);
                        } else { // 硬解码
                            hardwareCodecInfoList.add(codecInfo);
                        }
                    }

                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        Logs.i(TAG, "codec:" + codecInfo.getName() + ", " + codecInfo.isVendor() + ", " + getSupportColorFormatStr(codecInfo, mimeType));
                    } else {
                        Logs.i(TAG, "codec:" + codecInfo.getName() + ", " + getSupportColorFormatStr(codecInfo, mimeType));
                    }
                }
            }
        }

        for (int i = 0; i < recognizedFormats.length; i++) {
            int colorFormat = recognizedFormats[i];
            for (MediaCodecInfo codecInfo : hardwareCodecInfoList) {
                if (containsColorFormatByCodec(colorFormat, codecInfo, mimeType)) {
                    mColorFormat = colorFormat;
                    return codecInfo;
                }
            }
        }
        for (int i = 0; i < recognizedFormats.length; i++) {
            int colorFormat = recognizedFormats[i];
            for (MediaCodecInfo codecInfo : softwareCodecInfoList) {
                if (containsColorFormatByCodec(colorFormat, codecInfo, mimeType)) {
                    mColorFormat = colorFormat;
                    return codecInfo;
                }
            }
        }

        return null;
    }

    protected static final boolean containsColorFormatByCodec(int requestColorFormat, final MediaCodecInfo codecInfo, final String mimeType) {
        final MediaCodecInfo.CodecCapabilities caps;
        try {
            Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
            caps = codecInfo.getCapabilitiesForType(mimeType);
        } finally {
            Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
        }
        for (int i = 0; i < caps.colorFormats.length; i++) {
            if (requestColorFormat == caps.colorFormats[i]) {
                return true;
            }
        }
        return false;
    }

    protected static final String getSupportColorFormatStr(final MediaCodecInfo codecInfo, final String mimeType) {
        final MediaCodecInfo.CodecCapabilities caps;
        try {
            Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
            caps = codecInfo.getCapabilitiesForType(mimeType);
        } finally {
            Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
        }
        StringBuffer sb = new StringBuffer();
        sb.append("[");
        for (int i = 0; i < caps.colorFormats.length; i++) {
            sb.append(caps.colorFormats[i]);
            if (i == caps.colorFormats.length - 1) {
                sb.append("]");
            } else {
                sb.append(", ");
            }
        }
        return sb.toString();
    }

    /**
     * color formats that we can use in this class
     */
    protected static int[] recognizedFormats;

    /**
     A5. The color formats for the camera output and the MediaCodec encoder input are different. Camera supports YV12 (planar YUV 4:2:0) and NV21 (semi-planar YUV 4:2:0). The MediaCodec encoders support one or more of:

     #19 COLOR_FormatYUV420Planar (I420)
     #20 COLOR_FormatYUV420PackedPlanar (also I420)
     #21 COLOR_FormatYUV420SemiPlanar (NV12)
     #39 COLOR_FormatYUV420PackedSemiPlanar (also NV12)
     #0x7f000100 COLOR_TI_FormatYUV420PackedSemiPlanar (also also NV12)
     */
    static {
        recognizedFormats = new int[]{
                // 最常用的
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar, // 19, I420,测试通过
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar, // 21, NV12,测试通过
                // 其他不常用
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar, // 20, I420,测试通过
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar, // 39, NV12, 测试通过

//                MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar, //  2141391872, NV21, 测试通过,该格式部分设备录制绿屏(不支持部分分辨率的编解码)
//                MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar, // 2130706688, NV12, 未测试
        };
    }
}

上面代码的逻辑是优先从最常用的YUV格式COLOR_FormatYUV420PlanarCOLOR_FormatYUV420SemiPlanarCOLOR_FormatYUV420PackedPlanarCOLOR_FormatYUV420PackedSemiPlanar选择,如果硬编码器中有这些格式我们选择硬编码器,如果硬编码器没有这几种格式,那么我就从软编码器中选择。

如果你发现这四种YUV编码格式兼容性还不足够好,那么我们就选择最常用的两种

4. 启动编码器

当我们选择好合适的编码器,配置好视频参数后,我们就可以启动MediaCodec了,核心代码如下:

public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncoder {
    private static final boolean DEBUG = Logs.issIsLogEnabled();  // TODO set false on release
    private static final String TAG = "MediaVideoBufferEncoder";

    private static final String MIME_TYPE = "video/avc";

    // parameters for recording
    private static final int FRAME_RATE = 15;
    private static float BPP = 1.0f;

    private final int mWidth, mHeight;
    protected int mColorFormat;

    @Override
    protected void prepare() throws IOException {
        if (DEBUG) Log.i(TAG, "prepare: ");
        mTrackIndex = -1;
        mMuxerStarted = mIsEOS = false;

        final MediaCodecInfo videoCodecInfo = selectVideoCodec(MIME_TYPE);
        if (videoCodecInfo == null) {
            Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE);
            return;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            Logs.i(TAG, "codec:" + videoCodecInfo.getName() + ", " + videoCodecInfo.isVendor() + ", " + getSupportColorFormatStr(videoCodecInfo, MIME_TYPE));
        } else {
            Logs.i(TAG, "codec:" + videoCodecInfo.getName() + ", " + getSupportColorFormatStr(videoCodecInfo, MIME_TYPE));
        }
        if (DEBUG) Log.i(TAG, "selected codec: " + videoCodecInfo.getName());
        final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat);
        format.setInteger(MediaFormat.KEY_BIT_RATE, calcBitRate());
        format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
        format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            // 描述编码器要使用的所需比特率模式的键
            // BITRATE_MODE_CQ: 表示完全不控制码率,尽最大可能保证图像质量
            // BITRATE_MODE_CBR: 表示编码器会尽量把输出码率控制为设定值
            // BITRATE_MODE_VBR: 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低;
            boolean isBitrateModeSupported = videoCodecInfo.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC).getEncoderCapabilities().isBitrateModeSupported(MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
            Logs.i(TAG, "isBitrateModeSupported:" + isBitrateModeSupported);
            if (isBitrateModeSupported) {
                format.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
            }
        }
        if (DEBUG) Log.i(TAG, "format: " + format);

        mMediaCodec = MediaCodec.createByCodecName(videoCodecInfo.getName());
        mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mMediaCodec.start();
        if (DEBUG) Log.i(TAG, "prepare finishing");
        if (mListener != null) {
            mListener.onPrepared(this);
        }
    }
}

5. 编码数据

在这里插入图片描述
数据编码流程如上图,input 为输入端,output 为输出端,输入和输出端各有若干个 buffer,输入端不断拿到一个空 buffer,装上数据,再传入 MediaCodec 直到所有数据输入为止,输出端不断从 MediaCodec 获取到 buffer,每次得到处理好的数据后,再将 buffer 交还给 MediaCodec。

关键步骤:

  1. 使用者从MediaCodec请求一个空的输入buffer(ByteBuffer),填充满数据后将它传递给MediaCodec处理。
 int inputBufferIndex = mMediaCodec.dequeueInputBuffer(0);//获取输入缓存区授权
ByteBuffer inputBuffer = mMediaCodec.getInputBuffer();//获取实际buffer
// 填充
...
mMediaCodec.queueInputBuffer(inputBufferId,);//加入编码队列

核心代码如下:

    protected void encode(final ByteBuffer buffer, final int length, final long presentationTimeUs) {
        if (!mIsCapturing) return;
        int ix = 0, sz;
        final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
        while (mIsCapturing && ix < length) {
            final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
            if (inputBufferIndex >= 0) {
                final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                inputBuffer.clear();
                sz = inputBuffer.remaining();
                sz = (ix + sz < length) ? sz : length - ix;
                if (sz > 0 && (buffer != null)) {
                    buffer.position(ix + sz);
                    buffer.flip();
                    inputBuffer.put(buffer);
                }
                ix += sz;
                if (length <= 0) {
                    // send EOS
                    mIsEOS = true;
                    if (DEBUG) Log.i(TAG, "send BUFFER_FLAG_END_OF_STREAM");
                    mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0,
                            presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    break;
                } else {
                    mMediaCodec.queueInputBuffer(inputBufferIndex, 0, sz,
                            presentationTimeUs, 0);
                }
            } else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // wait for MediaCodec encoder is ready to encode
                // nothing to do here because MediaCodec#dequeueInputBuffer(TIMEOUT_USEC)
                // will wait for maximum TIMEOUT_USEC(10msec) on each call
            }
        }
    }
  1. MediaCodec处理完这些数据并将处理结果输出至一个空的输出buffer(ByteBuffer)中。
    使用者从MediaCodec获取输出buffer的数据,消耗掉里面的数据,使用完输出buffer的数据之后,将其释放回编解码器。
 int encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 0);//获取输出缓存区授权
 ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);//获取实际buffer取得编码后的数据
  。。。
 codec.releaseOutputBuffer(outputBufferId,);//释放使缓冲buffer回到队列中
  1. 将编码好的h264数据使用MediaMuxer封装成mp4文件

核心代码如下:

    protected void drain() {
        if (mMediaCodec == null) return;
        ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers();
        int encoderStatus, count = 0;
        final MediaMuxerWrapper muxer = mWeakMuxer.get();
        if (muxer == null) {
//        	throw new NullPointerException("muxer is unexpectedly null");
            Log.w(TAG, "muxer is unexpectedly null");
            return;
        }
        LOOP:
        while (mIsCapturing) {
            // get encoded data with maximum timeout duration of TIMEOUT_USEC(=10[msec])
            encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // wait 5 counts(=TIMEOUT_USEC x 5 = 50msec) until data/EOS come
                if (!mIsEOS) {
                    if (++count > 5)
                        break LOOP;    // out of while
                }
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                if (DEBUG) Log.v(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
                // this shoud not come when encoding
                encoderOutputBuffers = mMediaCodec.getOutputBuffers();
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                if (DEBUG) Log.v(TAG, "INFO_OUTPUT_FORMAT_CHANGED");
                // this status indicate the output format of codec is changed
                // this should come only once before actual encoded data
                // but this status never come on Android4.3 or less
                // and in that case, you should treat when MediaCodec.BUFFER_FLAG_CODEC_CONFIG come.
                if (mMuxerStarted) {  // second time request is error
                    throw new RuntimeException("format changed twice");
                }
                // get output format from codec and pass them to muxer
                // getOutputFormat should be called after INFO_OUTPUT_FORMAT_CHANGED otherwise crash.
                final MediaFormat format = mMediaCodec.getOutputFormat(); // API >= 16
                mTrackIndex = muxer.addTrack(format);
                mMuxerStarted = true;
                if (!muxer.start()) {
                    // we should wait until muxer is ready
                    synchronized (muxer) {
                        while (!muxer.isStarted())
                            try {
                                muxer.wait(100);
                            } catch (final InterruptedException e) {
                                break LOOP;
                            }
                    }
                }
            } else if (encoderStatus < 0) {
                // unexpected status
                if (DEBUG)
                    Log.w(TAG, "drain:unexpected result from encoder#dequeueOutputBuffer: " + encoderStatus);
            } else {
                final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
                if (encodedData == null) {
                    // this never should come...may be a MediaCodec internal error
                    throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
                }
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    // You shoud set output format to muxer here when you target Android4.3 or less
                    // but MediaCodec#getOutputFormat can not call here(because INFO_OUTPUT_FORMAT_CHANGED don't come yet)
                    // therefor we should expand and prepare output format from buffer data.
                    // This sample is for API>=18(>=Android 4.3), just ignore this flag here
                    if (DEBUG) Log.d(TAG, "drain:BUFFER_FLAG_CODEC_CONFIG");
                    mBufferInfo.size = 0;
                }

                if (mBufferInfo.size != 0) {
                    // encoded data is ready, clear waiting counter
                    count = 0;
                    if (!mMuxerStarted) {
                        // muxer is not ready...this will prrograming failure.
                        throw new RuntimeException("drain:muxer hasn't started");
                    }
                    // write encoded data to muxer(need to adjust presentationTimeUs.
                    mBufferInfo.presentationTimeUs = getPTSUs();
                    muxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
                    prevOutputPTSUs = mBufferInfo.presentationTimeUs;
                }
                // return buffer to encoder
                mMediaCodec.releaseOutputBuffer(encoderStatus, false);
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    // when EOS come.
                    mMuxerStarted = mIsCapturing = false;
                    break;      // out of while
                }
            }
        }
    }

dequeueOutputBuffer的 mBufferInfo 为入参(传入一个实体,函数内会为这个实体的字段赋值),其中有buffer的size,offsize信息,帧的时间戳信息和用于表示帧属性的flag字段(最常用的就是用来判断是否是关键帧)。

  1. 循环上面的步骤,当编码完成时,在步骤1传入MediaCodec.BUFFER_FLAG_END_OF_STREAM标识,用以标志整个流程的结束。
mMediaCodec.queueInputBuffer(  videoInputBufferIndex,  0,  0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);

在步骤2中读取到MediaCodec.BUFFER_FLAG_END_OF_STREAM时就可以停止整个编码任务。

  1. 对YUV数据编码,我们使用前面的篇章从PreviewBufferCallback中获取NV21数据就可以进行编码生成MP4了

从回调中获取NV21数据

private PreviewBufferCallback mPreviewBufferCallback = new PreviewBufferCallback() {
    @Override
    public void onPreviewBufferFrame(byte[] data, int width, int height, YUVFormat format) {
        if (mEncoder != null) {
            mEncoder.encode(data);
        }
    }
};

根据编码器支持的格式对Camera数据进行转换后编码

    /**
     * 编码数据
     *
     * @param data nv21
     */
    public void encode(byte[] data) {
        encode(data, YUVFormat.NV21);
    }

    /**
     * 编码数据
     *
     * @param data YUV420
     */
    public void encode(byte[] data, YUVFormat yuvFormat) {
        if (mEncoder != null) {
            int mColorFormat = mEncoder.getColorFormat();
            byte[] encodeData = null;
            long start = System.currentTimeMillis();
            if (mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
                    || mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar) { // 19, 20:I420
                if (yuvFormat == YUVFormat.NV21) {
                    YUVUtils.nativeNV21ToI420(data, mSize.getWidth(), mSize.getHeight(), mTempData);
                    encodeData = mTempData;
                } else {
                    encodeData = data;
                }
            } else if (mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
                    || mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar
                    || mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar) { // 21, 39:NV12
                // 使用C层转换最快
                if (yuvFormat == YUVFormat.NV21) {
                    YUVUtils.nativeNV21ToNV12(data, mSize.getWidth(), mSize.getHeight(), mTempData);
                } else {
                    YUVUtils.nativeI420ToNV12(data, mSize.getWidth(), mSize.getHeight(), mTempData);
                }
                encodeData = mTempData;
            } else if (mColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar) {// 2141391872:NV21
                if (yuvFormat == YUVFormat.NV21) {
                    encodeData = data;
                } else {
                    YUVUtils.nativeI420ToNV21(data, mSize.getWidth(), mSize.getHeight(), mTempData);
                    encodeData = mTempData;
                }
            }

            mEncoder.frameAvailableSoon();
            ByteBuffer buffer = ByteBuffer.wrap(encodeData);
            mEncoder.encode(buffer);
        }
    }

以上只是关键代码,看详细代码去git中查看MediaEncoder类即可

6. 释放编码器

调用MediaCodec的stoprelease方法

mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;

再谈兼容性

文章处理了MediaCodec一些兼容性问题,但是还不够。事物的运行总是不按我们预想的来,COLOR_FormatYUV420SemiPlanar我们测试了好多机型都是NV12格式,但还是有少部分机型需要NV21,这让我们的适配变得困难。如果这种问题上了生产,大量用户使用,那么这个小问题就会被放大。

如果我们的硬件设备固定,MediaCodec是可以使用。如果是ToC,我这里还是建议慎重。

我们大篇幅讲解了MediaCodec难道真的只能用来纸上谈兵了吗???

答案是否定的,MediaCodec除了可以使用YUV数据进行编码,还支持COLOR_FormatSurface的方式进行编码。该方式通过MediaCodec创建Surface,然后Camera通过OpenGL把数据渲染到该Surface上,就可以进行编码。前面的篇章我们已经通过OpenGL将Camera数据渲染到视图Surface中,那现在要渲染到视频Surface中其实也是很简单的事情。该种方式兼容性好(Android > 4.3即可),使用OpenGL效率更高,我们会在接下来的篇章中详细介绍。

最后

lib-camera库包结构如下:

说明
cameracamera相关操作功能包,包括Camera和Camera2。以及各种预览视图
encoderMediaCdoec录制视频相关,包括对ByteBuffer和Surface的录制
glesopengles操作相关
permission权限相关
util工具类

每个包都可独立使用做到最低的耦合,方便白嫖

github地址:https://github.com/xiaozhi003/AndroidCamera

参考:

  1. https://github.com/saki4510t/UVCCamera
  2. https://github.com/google/grafika

http://www.kler.cn/a/394633.html

相关文章:

  • k8s 1.28.2 集群部署 docker registry 接入 MinIO 存储
  • ESLint 使用教程(五):ESLint 和 Prettier 的结合使用与冲突解决
  • SpringBoot(十八)SpringBoot集成Minio
  • 亲测有效:Maven3.8.1使用Tomcat8插件启动项目
  • Servlet入门 Servlet生命周期 Servlet体系结构
  • 学习记录:js算法(九十二):克隆图
  • Asp.NET Core Mvc中一个视图怎么设置多个强数据类型
  • PyQt入门指南五十二 版本控制与协作开发
  • Linux git-bash配置
  • 《深度学习》AlexNet网络
  • 11.14日志
  • vue中重置对象的好使方式(封装好的函数,可直接食用)
  • MATLAB中round函数用法
  • 用接地气的例子趣谈 WWDC 24 全新的 Swift Testing 入门(三)
  • 工程化实战内功修炼测试题
  • 深度学习笔记14-卷积神经网络2
  • C语言实现3D动态爱心图形的绘制与动画效果
  • 抖音小程序蓝海冷门玩法,前期搭建好后期自动变现模式解析!
  • 【IT人物系列】之Spring创始人
  • 计算机网络 (1)互联网的组成
  • AI赋能电商:提升销售效率与用户体验的新引擎
  • 飞腾平台Arm NN软件栈安装使用指南
  • 钉钉小程序 - - - - - overflow无效?
  • APEX高性能减速机MG/MGH系列 高负载应用下的精准动力传输
  • Linux sed 的多个用法
  • 微信小程序 — 农产品供销系统