Android视频编解码 - Go语言中文社区

Android视频编解码


简介

在这里插入图片描述
  从广义上讲,编解码器就是处理输入数据来产生输出数据。MediaCode采用异步方式处理数据,并且使用了一组输入输出缓存(input and output buffers)。简单来讲,你请求或接收到一个空的输入缓存(input buffer),向其中填充满数据并将它传递给编解码器处理。编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(output buffer)中。最终,你请求或接收到一个填充了结果数据的输出缓存(output buffer),使用完其中的数据,并将其释放给编解码器再次使用。

状态(States)

在编解码器的生命周期内有三种理论状态:停止态-Stopped、执行态-Executing、释放态-Released,停止状态(Stopped)包括了三种子状态:未初始化(Uninitialized)、配置(Configured)、错误(Error)。执行状态(Executing)在概念上会经历三种子状态:刷新(Flushed)、运行(Running)、流结束(End-of-Stream)。 在这里插入图片描述

  • 当你使用任意一种工厂方法(factory methods)创建了一个编解码器,此时编解码器处于未初始化状态(Uninitialized)。首先,你需要使用configure(…)方法对编解码器进行配置,这将使编解码器转为配置状态(Configured)。然后调用start()方法使其转入执行状态(Executing)。在这种状态下你可以通过上述的缓存队列操作处理数据。
  • 执行状态(Executing)包含三个子状态: 刷新(Flushed)、运行( Running) 以及流结束(End-of-Stream)。在调用start()方法后编解码器立即进入刷新子状态(Flushed),此时编解码器会拥有所有的缓存。一旦第一个输入缓存(input buffer)被移出队列,编解码器就转入运行子状态(Running),编解码器的大部分生命周期会在此状态下度过。当你将一个带有end-of-stream 标记的输入缓存入队列时,编解码器将转入流结束子状态(End-of-Stream)。在这种状态下,编解码器不再接收新的输入缓存,但它仍然产生输出缓存(output buffers)直到end-of- stream标记到达输出端。你可以在执行状态(Executing)下的任何时候通过调用flush()方法使编解码器重新返回到刷新子状态(Flushed)。
  • 通过调用stop()方法使编解码器返回到未初始化状态(Uninitialized),此时这个编解码器可以再次重新配置 。当你使用完编解码器后,你必须调用release()方法释放其资源。
  • 在极少情况下编解码器会遇到错误并进入错误状态(Error)。这个错误可能是在队列操作时返回一个错误的值或者有时候产生了一个异常导致的。通过调用 reset()方法使编解码器再次可用。你可以在任何状态调用reset()方法使编解码器返回到未初始化状态(Uninitialized)。否则,调用 release()方法进入最终的Released状态。

一、编码

初始化编码器

	public void prepare(int width, int height) throws IOException {
		// MIME_TYPE:"video/avc" -> H264  "video/hevc" -> H265
        MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);

        mWidth = width;
        mHeight = height;

        // Set some properties.  Failing to specify some of these can cause the MediaCodec
        // configure() call to throw an unhelpful exception.
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
        format.setInteger(MediaFormat.KEY_BIT_RATE, VideoConfig.BIT_RATE);
        // FPS 每秒传输帧数(Frames Per Second)
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
        // I-frame 关键帧时间间隔,单位min
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        Log.d(TAG, "format: " + format);
        
        // Create a MediaCodec encoder, and configure it with our format.  Get a Surface
        // we can use for input and wrap it with a class that handles the EGL work.
        mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
        mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        mMediaFormat = mEncoder.getOutputFormat();
        mEncoder.start();
    }

向InputBuffer输入编码数据

如果从Camera拿到的数据为NV21/NV12格式,可以先通过 YUV库 将NV21/NV12转码为I420格式,再将数据送入编码器编码

	/**
	 * 向编码器InputBuffer中填入数据
	 *
	 * @param data       NV21数据
	 * @param timeSptamp 时间戳 ms
	 */
	private void putDataToInputBuffer(byte[] data, long timeSptamp) {
        int index = mEncoder.dequeueInputBuffer(-1);
        if (index >= 0) {
            ByteBuffer buffer = mEncoder.getInputBuffer(index);
            if (buffer == null) {
                Log.d(TAG, "InputBuffer is null point");
                return;
            }
            if (yuv == null) {
            	// YUV数据存储空间大小为 Y分量->width * height U、V分量->width * height / 4
                yuv = new byte[mWidth * mHeight * 3 / 2];
            }
            // NV21格式数据转为I420P
            nv21ToYuv420p(data, timeSptamp);
            buffer.clear();
            buffer.put(yuv);

            mEncoder.queueInputBuffer(index, 0, data.length, timeSptamp * 1000, 0);

        }
        drainEncoder(false);
    }

说明:视频添加文字/图片水印,可以在将YUV数据送入编码器前,将文字转为Bitmap,通过YUV库将ARGB转码为I420P,再使用YUV图片合成技术合成,这样编码后的H264/H265视频码流就添加上了水印。

处理OutputBuffer

	/**
     * 读取编码后的H264/H265数据
     *
     * @param endOfStream 标识是否结束
     */
    public void drainEncoder(boolean endOfStream) {
        final int TIMEOUT_USEC = 10000;

        if (endOfStream) {
            Log.d(TAG, "sending EOS to encoder");
            return;
        }
        while (true) {
            int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet
                if (!endOfStream) {
                    break;      // out of while
                } else {
                    Log.d(TAG, "no output available, spinning to await EOS");
                }
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // should happen before receiving buffers, and should only happen once
                mMediaFormat = mEncoder.getOutputFormat();
                Log.d(TAG, "encoder output format changed: " + mMediaFormat);
            } else if (encoderStatus < 0) {
                Log.d(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
                        encoderStatus);
                // let's ignore it
            } else {
                ByteBuffer encodedData = mEncoder.getOutputBuffer(encoderStatus);
                if (encodedData == null) {
                    Log.w(TAG, "encoderOutputBuffer " + encoderStatus +
                            " was null");
                    break;
                }

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    // The codec config data was pulled out and fed to the muxer when we got
                    // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
                    Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
                    mBufferInfo.size = 0;
                }

                if (mBufferInfo.size != 0) {
                    // adjust the ByteBuffer values to match BufferInfo (not needed?)
                    encodedData.position(mBufferInfo.offset);
                    encodedData.limit(mBufferInfo.offset + mBufferInfo.size);

                    // 取出编码好的H264数据
                    byte[] data = new byte[mBufferInfo.size];
                    encodedData.get(data);

                    // todo 将编码好的H264/H265数据存储到缓冲区或者传递给MediaMuxer生成视频文件(MP4)
					// mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME 表示该帧数据为关键帧(I帧)
                    Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
                            mBufferInfo.presentationTimeUs);
                }

				// 释放输出缓冲区
                mEncoder.releaseOutputBuffer(encoderStatus, false);

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (!endOfStream) {
                        Log.d(TAG, "reached end of stream unexpectedly");
                    } else {
                        Log.d(TAG, "end of stream reached");
                    }
                    break;      // out of while
                }
            }
        }
    }

二、解码

基础知识

1、Codec-specific数据

有些格式,特别是ACC音频和MPEG4、H.264和H.265视频格式要求实际数据以若干个包含配置数据或编解码器指定数据的缓存为前缀。当处理这种压缩格式的数据时,这些数据必须在调用start()方法后且在处理任何帧数据之前提交给编解码器。这些数据必须在调用queueInputBuffer方法时使用BUFFER_FLAG_CODEC_CONFIG进行标记。

Codec-specific数据也可以被包含在传递给configure方法的格式信息(MediaFormat)中,在ByteBuffer条目中以"csd-0", "csd-1"等key标记。这些keys一直包含在通过MediaExtractor获得的Audio Track or Video Track的MediaFormat中。一旦调用start()方法,MediaFormat中的Codec-specific数据会自动提交给编解码器;你不能显示的提交这些数据。如果MediaFormat中不包含编解码器指定的数据,你可以根据格式要求,按照正确的顺序使用指定数目的缓存来提交codec-specific数据。在H264 AVC编码格式下,你也可以连接所有的codec-specific数据并作为一个单独的codec-config buffer提交。

Android 使用下列的codec-specific data buffers。对于适当的MediaMuxer轨道配置,这些也要在轨道格式中进行设置。每一个参数集以及被标记为(*)的codec-specific-data段必须以"x00x00x00x01"字符开头。
在这里插入图片描述
注意:当编解码器被立即刷新或start之后不久刷新,并且在任何输出buffer或输出格式变化被返回前需要特别地小心,因为编解码器的codec specific data可能会在flush过程中丢失。为保证编解码器的正常运行,你必须在刷新后使用标记为BUFFER_FLAG_CODEC_CONFIG的buffers再次提交这些数据。

2、流域界与关键帧(Stream Boundary and Key Frames)

调用start()或flush()方法后,输入数据在合适的流边界开始是非常重要的:其第一帧必须是关键帧(key-frame)。一个关键帧能够独立地完全解码(对于大多数编解码器它意味着I-frame),关键帧之后显示的帧不会引用关键帧之前的帧。

下面的表格针对不同的视频格式总结了合适的关键帧:
在这里插入图片描述

核心代码

初始化解码器
	public void prepare(int width, int height, int fps, byte[] sps, byte[] pps) throws IOException {
        String mimeType = "video/avc";
        MediaFormat format = MediaFormat.createVideoFormat(mimeType, width, height);

        mWidth = width;
        mHeight = height;

		// 参见Codec-specific数据说明,H264数据格式需要 csd-0(sps)、csd-1(pps);
		// H265数据格式需要 csd-0(vps+sps+pps)
        if (sps != null) {
            format.setByteBuffer("csd-0", ByteBuffer.wrap(sps));
        }
        if (pps != null) {
            format.setByteBuffer("csd-1", ByteBuffer.wrap(pps));
        }

        format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
        format.setInteger(MediaFormat.KEY_PUSH_BLANK_BUFFERS_ON_STOP, 1);
        
        Log.i(TAG, String.format("config codec:%s", format));
		// 创建解码器
        mDecoder = MediaCodec.createDecoderByType(mimeType);
        // 配置解码器 format
        mDecoder.configure(format, null, null, 0);
        mDecoder.start();
    }

注意:在解码H264/H265数据时,传递码流数据前一定要先配置好csd-*,参见Codec-specific说明。

向InputBuffer输入解码数据
    /**
     * 向解码器InputBuffer中填入数据
     *
     * @param data       H264/H265数据
     * @param timeSptamp 时间戳 us
     */
    private void putDataToInputBuffer(byte[] data, long timeSptamp) {
        int index = mDecoder.dequeueInputBuffer(-1);
        if (index >= 0) {
            ByteBuffer buffer = mDecoder.getInputBuffer(index);
            if (buffer == null) {
                LogUtils.d(TAG, "InputBuffer is null point");
                return;
            }
            buffer.clear();
            buffer.put(data);

            Log.d(TAG, "queueInputBuffer data length: " + data.length + "  timeSptamp: " + timeSptamp);
            mDecoder.queueInputBuffer(index, 0, data.length, timeSptamp, 0);

        }
        drainDecoder(false, timeSptamp);
    }

注意:传递给解码器的第一帧数据必须是关键帧(I-帧),参见流域界与关键帧说明。

处理OutputBuffer
    /**
     * 读取解码后的H264/H265数据
     *
     * @param endOfStream 标识是否结束
     * @param timeSptamp  当前解码的数据的时间戳
     */
    private void drainDecoder(boolean endOfStream, long timeSptamp) {
        final int TIMEOUT_USEC = 10000;

        if (endOfStream) {
            Log.d(TAG, "sending EOS to encoder");
            return;
        }
        while (true) {
            int decoderStatus = mDecoder.dequeueOutputBuffer(mBufferInfo, 0);
            if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // no output available yet  输出为空
                if (!endOfStream) {
                    break;      // out of while
                } else {
                    Log.d(TAG, "no output available, spinning to await EOS");
                }
            } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // should happen before receiving buffers, and should only happen once
                mMediaFormat = mDecoder.getOutputFormat();
                Log.d(TAG, "encoder output format changed: " + mMediaFormat);
            } else if (decoderStatus < 0) {
                Log.d(TAG, "unexpected result from encoder.dequeueOutputBuffer: " +
                        decoderStatus);
                // let's ignore it
            } else {

                ByteBuffer decodedData = mDecoder.getOutputBuffer(decoderStatus);
                if (decodedData == null) {
                    Log.w(TAG, "decoderOutputBuffer " + decoderStatus +
                            " was null");
                    break;
                }

                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    // The codec config data was pulled out and fed to the muxer when we got
                    // the INFO_OUTPUT_FORMAT_CHANGED status.  Ignore it.
                    Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
                    mBufferInfo.size = 0;
                }

                if (mBufferInfo.size != 0) {
                    // adjust the ByteBuffer values to match BufferInfo (not needed?)
                    decodedData.position(mBufferInfo.offset);
                    decodedData.limit(mBufferInfo.offset + mBufferInfo.size);

                    // 取出解码好的NV12数据
                    byte[] data = new byte[mBufferInfo.size];
                    decodedData.get(data);

					// todo 可以将解码后的NV12数据转码为ARGB8888,保存为jpg图片
                    Log.d(TAG, "sent " + mBufferInfo.size + " bytes to muxer, ts=" +
                            mBufferInfo.presentationTimeUs);
                }

                mDecoder.releaseOutputBuffer(decoderStatus, false);
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (!endOfStream) {
                        Log.d(TAG, "reached end of stream unexpectedly");
                    } else {
                        Log.d(TAG, "end of stream reached");
                    }
                    break;      // out of while
                }
            }
        }
    }

说明:解码后的NV12数据可以通过YUV库转码为ARGB8888格式,再将ARGB8888转为Bitmap对象,从而保存为jpeg格式的图片文件。

// 将ARGB8888原始数据转为Bitmap对象
Bitmap bitmap= Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(argb));

参考文献

https://developer.android.google.cn/reference/android/media/MediaCodec
https://github.com/google/grafika
Android Camera预览时输出的帧率控制
https://chromium.googlesource.com/libyuv/libyuv/
使用libyuv对YUV数据进行缩放,旋转,镜像,裁剪等操作
YUV图像的水印的添加
EasyPlayer一款精炼、高效、稳定的流媒体播放器
H264(NAL简介与I帧判断)

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/lkl22/article/details/87855642
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-01 20:47:20
  • 阅读 ( 1115 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢