2019-02-22 08:36:23 lkl22 阅读数 1170

简介

在这里插入图片描述
  从广义上讲,编解码器就是处理输入数据来产生输出数据。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段必须以"\x00\x00\x00\x01"字符开头。
在这里插入图片描述
注意:当编解码器被立即刷新或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帧判断)

2019-07-31 15:33:49 qq_15827013 阅读数 368

一.字符集

1.字符集的由来

  1. 计算机识别、处理、传递、存储数据,都是基于一个个的字节,一个字节有8为,每位都是0或1,即计算机是通过01字节流来处理数据的,而我们使用的各种字符,包括数字、符号、字母、中文等,都是需要通过字节来编码的,也就是用一些特定的字节标示一个唯一的字符。

  2. 计算机最早是按英语单字节字符设计的,即一个字节(8位能标示256个字符)就可以表示英文中所有的字符(包括字符、大小写字母、各种英文字符),也就是我们常说的ASCII字符集,该集合包括了上述的所有字符,并为每个字符指定唯一的号码,如:

    i.数字0:十进制号码是48,对应的二进制编码就是0011 0000

    ii.大写字母A:十进制号码是65,对应的二进制编码就是0100 0001

    iii.字符+:十进制号码是43,对应的二进制编码就是0010 1011

    这样一来,每一个英文字符,都会有唯一对应的二进制编码,无论数据(二进制流)传递到哪个系统上,它们都以ASCII字符集的编码来解码字节流,得到的就是正确的原始字符串数据;这也就是字符集的作用—包括一组字符,并为每个字符指定唯一的编码

  3. 如果全世界只有ASCII这一种字符集的话,其实编码解码这个概念就很简单了,但是全世界有很多很多语言,相应的就有很多很多的字符(远远大于256个),显然,这么多的字符,是一个字节无法表示全的,也就是说ASCII字符集不能涵盖全世界的字符,那么怎么办呢?扩展字符集呗,于是,就诞生了多种多样的字符集。。。

2.字符集的演变

(1)ISO-8859-1字符集

ISO-8859-1字符集采用单字节,能够表示256个字符,兼容ASCII,可以支持大多数国家语言

(2)GB2312/GBK字符集

这些字符集是为了支持中文而产生的,使用单双字节变长编码对字符进行编码,英文字符使用单字节编码,兼容ASCII编码,对于中文使用双字节编码

GBK是兼容GB2312的字符集,GB2312只支持简体中文,而GBK支持繁体中文

(3)Unicode字符集

ISO-8859-1不支持中文字符,GBK也不支持其他一些国家的字符,这就导致了使用GBK编码的程序到了其他一些国家系统上就出现乱码的情况,所以,世界需要一个通用完整的字符集来避免这些情况

Unicode为目前最统一最全的字符集,它收录了世界上所有的字符,并为其每个字符指定了一个编码,其编码与上述字符集不兼容,是自己的一套编码系统,该字符集也是目前使用最广泛的一个字符集

对于Unicode字符集来说,最重要的是它的编码实现方式:Unicode字符集只是容纳了所有字符并为其提供唯一的编码,但是不与其他编码兼容,而且对于英文也会占用多字节,对内存不友好,所以,Unicode有许多不同编码实现方式,所谓编码实现方式,就是将一些自定义的编码,能够映射到唯一的Unicode字符编码上,从而可以知道唯一的字符,而这些自定义的编码,可以想办法做成省内存的,兼容其他编码的;其中最常用的编码方式就是UTF-8了

(4)UTF-8编码方式

UTF(Unicode Transformation Format),顾名思义,UTF就是将Unicode编码转换成另一种编码的编码方式,常见的有UTF-8、UTF-7、UTF-16等,最常用的就是UTF-8了

UTF-8是一种可变长字节的编码方式,它使用1-6个字节来对所有Unicode字符进行编码:因为对于ASCII英文字符,只需要单字节即可,如果使用Unicode的话就会使用两个字节,对内存不太友好,所以UTF-8相比Unicode的编码方式来说,可以大大的节省内存开销;对于其他字符比如中文,UTF-8通常是3个字节就可以表示一个中文字符

UTF-8可变长字节的实现原理简介:如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的字节数,其余各字节均以10开头。
在这里插入图片描述

二.编码解码

1.编码和解码

上面介绍了字符集的概念,知道字符集是用来唯一标识每个字符的二进制字节的, 那么为什么要这么做呢?其实上面也已经说过了,因为计算机处理的数据实质上都是01bit流,8个bits就是一个字节,即计算机处理的的数据本质上都是字节流,也就是说,无论是字符还是字符串还是,最终都会以二进制字节流的形式进行处理,了解了这个,也就明白为什么要编码解码了吧?因为每个字符最终都是一串字节(一串01bits),也就是说我们传递数据前,应该将数据转换成一个一个的字节,这就叫编码;而接收方接收到的数据也就是一个一个的字节,也需要将这一串字节,转换成相应的字符(毕竟字符才是我们真正想要的数据),这就叫解码;再结合上面说的字符集:不同的程序可能使用不同的字符集,不同的字符集对于字符的编码方式不同,所以,我们不光要编码解码,还要指定编码解码所用的字符集,还要保证其编码解码用的字符集一样,否则,不同解析字节的规则解析相同的字节流,结果当然就可能出错了呗,变成了我们未知的字符了,这也就是乱码的形成原因!

2.Java中的编码解码

说了这么多,有些人可能会想:"我写程序使用、传递数据的时候,并没有过多的涉及到编码解码这块的处理,好像也并没有提示过我要编码解码。"下面我们来看看java中,我们通常在哪些地方使用到了编码解码,以及如何使用的:
在这里插入图片描述
来看输出:
在这里插入图片描述
由代码可知,字符串数据最终传递时,都需要转换成byte字节数组,而转换时,需要指定字符集编码方式(否则使用系统默认的),我们的网络传输等等,最终在底层传递是都是这样的

  1. 从输出可以看出,“我是test"这6个字符被转化成了10个字节的字节数组,由UTF-8编码规则可知,中文字符通常被表示为三个字节,所以[-26,-130,-111]代表"我”,[-26,-104,-81]代表"是",而英文字符符ASCII编码,所以116代表"t",101代表"e",115代表"t"

  2. 从输出可以看出,解码时也是需要指定字符集编码方式的,即utf-8根据自己的规则(上面说过),确定前三个字节为一个字符,并通过计算规则映射到Unicode中"我"的编码,于是前三个字节就解码为"我"字符,后面同理

  3. 是不是有人要问,Java也可以使用字符流读取,即InputStreamReader,可以一次性读取n个字符,那它是怎么知道几个字节为一个字符的呢?我们来看一看
    在这里插入图片描述
    再来看输出:
    在这里插入图片描述
    可见,我们构造Reader时,会指定编码方式的,而每个编码方式可以通过自己的编码规则来确定哪几个字节作为一个字符,比如ISO-8859-1为定长单字节编码,那么它解码时每次获取一个字节进行解码即可;而UTF-8为可变长编码,但是有规则(上面有介绍):每个字符的第一个字节连续前n位为1,那么这个字符就占n个字节。据此规则也可以顺利解码

  4. 以上就是我们日常使用java时容易忽视的编码解码,有人会问:"那解码编码规则使用不一样时,真的会导致乱码么?"这里就举一个例子,来解释一下:

    还是原来的输入"我是test"
    在这里插入图片描述
    再来看输出:
    在这里插入图片描述
    这结果是为什么呢?其实很简单,以utf-8进行编码,解码时,以iso-8859-1进行解码,那么iso-8859-1是定长单字节编码,所以取每一个字节进行解码,这些由utf-8生成的码在iso-8859-1的字符集编码中找到的字符肯定和utf-8不一样啊;而对于英文字符来说,这些字符集都满足ASCII编码规则,所以即使是不同的字符集对于这些字符的编码都是一致的;所以最后的"test"四个字符是没有问题的,而前面的两个中文字符(6个字节)就被解码成iso-8859-1字符集里对应的6个字符了

3.URL的解码编码

对于这个概念,其实很多人都不是很清楚,认为URL的编码解码和字符的编码解码是一回事,或者说是字符集编码方式之一,其实并不是这样的,字符集编码是计算机对于数据传递的一种统一方式,而Url的编码解码是针对于Url这种特殊数据传递所定义的一种规则而已。

Url(统一资源定位符),大家都再熟悉不过了,是URI(统一资源标识符)的一种,有形如此类的格式:scheme://host:port/path?key1=value1&key2=value2,是用于干什么的呢?当然是定位到某个设备某个路径上的标识,再说通用点吧,就是我们请求后端的地址,并可以传递多个键值对参数的字符串,那么问题来了,它也是一个字符串,最终还是要编码成字节流传递到远端的,而这种格式中有些起着特殊作用的字符呢?比如/、?、=、&等等,这些字符也属于ASCII没问题,被编码成字节也没问题,但是,他们的作用是起分隔作用的,比如&分隔了每个键值对,=分隔了键和值,那如果我们的正常数据中(实际要传递的数据)有这些特殊字符呢,比如value1是“a=b”,value2是"a&b",那么这个Url的后半部分就变成了?key1=“a=b”&key2=“a&b”,如果按照原样进行编码解码,那么后端拿到的url字符串还是这样的,在取出参数和分隔键值对的时候就会有错了:按&分隔成的键值对就为key1=“a=b”,key2=“a,b”;再按=分隔成的键值就为<key1,"a>,<key2,"a>,这显然就酿成大祸了。。。

所以,为了使Url访问时,能够保留这些特殊字符,并且Url希望使用安全字符(即不会造成混乱的字符,也就是使用通用的ASCII字符集的符号),就为Url设置了一套"编码解码"规则来达到这些要求:

  1. 对于字母、数字、一些ASCII中的字符不做处理

  2. 对于空格,容易造成歧义,转换为+(+会做(3)处理,不会影响)

  3. 对于其他字符,都使用&hex1hex2来转义,hex1hex2是该字符在对应的字符集编码方式中的编码对应的16进制数

这样一来,整个Url就都是ASCII字符集里的安全字符,并且实际数据中的那些特殊字符都被转义了,我们来举个例子看的更明白些:

原始Url:?kw="a=b"&exp="a&b"

Url编码后的Url:?kw=%22a%3Db%22&exp=%22a%26b%22

服务端在拿到Url是,就是编码后的Url,此时可以通过=&来取出键值对(因为特殊字符没有被编码),取出后,将每个key-value再进行Url的解码,就生成这样的键值对:

<kw,"a=b">,<exp,"a&b">

所以,Url的编码解码只是因为Url的一些可能造成歧义和错误的字符,而定义的一套转义规则,并不是和utf-8一样的一种编码方式,只是将字符转换成另一种字符而已。

三.android中的编码解码

(1)在Android中,数据的编码解码和java中其实一样(毕竟用的java。。。),如果相对Url应用编码解码,可以使用提供的URLEncoder和URLDecoder的相关方法,流程就是和上述介绍的URL解码编码规则类似

URLEncoder

private void appendEncoded(StringBuilder builder, String s, Charset charset, boolean isPartiallyEncoded) { 
        int escapeStart = -1; 
        for (int i = 0; i < s.length(); i++) { 
            char c = s.charAt(i); 
            if ((c >= 'a' && c <= 'z') 
                    || (c >= 'A' && c <= 'Z') 
                    || (c >= '0' && c <= '9') 
                    || isRetained(c) 
                    || (c == '%' && isPartiallyEncoded)) { 
                if (escapeStart != -1) {
                    appendHex(builder, s.substring(escapeStart, i), charset);  //转义
                    escapeStart = -1; 
                } 
                if (c == '%' && isPartiallyEncoded) { 
                    // this is an encoded 3-character sequence like "%20" 
                    builder.append(s, i, i + 3); 
                    i += 2; 
                } else if (c == ' ') { //空格换成+
                    builder.append('+'); 
                } else { //原样保留
                    builder.append(c); 
                } 
            } else if (escapeStart == -1) { 
                escapeStart = i; 
            } 
        }
}
private static void appendHex(StringBuilder builder, String s, Charset charset) {         
		for (byte b : s.getBytes(charset)) { //指定字符集编码
            appendHex(builder, b); 
        } 
    } 
 
private static void appendHex(StringBuilder sb, byte b) { 
        sb.append('%'); //转换为%hex1hex2
        sb.append(Byte.toHexString(b, true)); 
} 

URLDecoder

public static String decode(String s, boolean convertPlus, Charset charset, boolean throwOnFailure) { 
        if (s.indexOf('%') == -1 && (!convertPlus || s.indexOf('+') == -1)) { 
            return s; 
        } 
        StringBuilder result = new StringBuilder(s.length()); 
        ByteArrayOutputStream out = new ByteArrayOutputStream(); 
        for (int i = 0; i < s.length();) { 
            char c = s.charAt(i); 
            if (c == '%') { 
                do { 
                    int d1, d2; 
                    if (i + 2 < s.length() 
                            && (d1 = hexToInt(s.charAt(i + 1))) != -1 
                            && (d2 = hexToInt(s.charAt(i + 2))) != -1) { 
                        out.write((byte) ((d1 << 4) + d2)); //%连同后两位16进制数字转换为字节写入字节数组(恢复)
                    } else if (throwOnFailure) { 
                        throw new IllegalArgumentException("Invalid % sequence at " + i + ": " + s); 
                    } else { 
                        byte[] replacement = "\ufffd".getBytes(charset); 
                        out.write(replacement, 0, replacement.length); 
                    } 
                    i += 3; 
                } while (i < s.length() && s.charAt(i) == '%'); 
                result.append(new String(out.toByteArray(), charset)); //按指定字符集解码为字符
                out.reset(); 
            } else { 
                if (convertPlus && c == '+') { //+恢复为空格
                    c = ' '; 
                } 
                result.append(c); 
                i++; 
            } 
        } 
        return result.toString(); 
    } 

(2)Android中另一种处理Url的方式就是使用Uri类,这也是重点要说的类
在这里插入图片描述
这是构建Uri的Builder类,我们可以看到,可以设置Uri的scheme,authority,path和query,而且这些属性大多都提供了两种方法,一种是encodedXxx,一种是xxx,顾名思义,前者是设置已经编码过得值,后者就是未编码过的值,而appendQueryParameter方法最常用,它设置的就是未编码过得值,那么为什么要区分呢?是基于Uri的实现机制的,Uri内部几乎所有的值都有编码未编码状态,即encoded和decoded状态,在使用时,也有两种方法,一种是getXxx,一种是getEncodedXxx,前者就是获取到decoded的相应属性,后者是获取encoded的相应属性
在这里插入图片描述
也就是说,Uri内部的属性,要么是编码过的,要么是未编码过的,取出时,也会进行相应的编码和解码,清楚这个很重要,否则可能会导致编码过的再编码,解码过的再解码的情况

(3)下面就举一个容易掉进坑里的例子吧:

i.隐式Uri跳转到一个页面,url为:mypro://www.mypro.com/mainAct?name=“鲜果100%店”,只需要使用Uri.parse(url)即可构建Uri

public static Intent buildIntent(Context context, String url) {
    Intent intent = new Intent();
    Uri uri = Uri.parse(url);
    ...
    return intent;//后续startActivity(intent)即可
}

ii.我们来看Uri.parse方法

public static Uri parse(String uriString) {
    return new StringUri(uriString);
}
private StringUri(String uriString) {
    this.uriString = uriString;//直接保存在stringUri变量中
}

iii.再来看我们如果使用uri.getQueryParameter(key)方法会发生什么

public String getQueryParameter(String key) {
    ...
    final String query = getEncodedQuery();//获取的是encodedQuery
    ...
    final String encodedKey = encode(key, null);
    final int length = query.length();
    int start = 0;
    do {
        int nextAmpersand = query.indexOf('&', start);
        int end = nextAmpersand != -1 ? nextAmpersand : length;

        int separator = query.indexOf('=', start);
        if (separator > end || separator == -1) {
            separator = end;
        }

        if (separator - start == encodedKey.length()
                && query.regionMatches(start, encodedKey, 0, encodedKey.length())) {
            if (separator == end) {
                return "";
            } else {
                String encodedValue = query.substring(separator + 1, end);//value现在是鲜果100%店,被当做了encoded状态了
                return UriCodec.decode(encodedValue, true, StandardCharsets.UTF_8, false);//所以要decode:%hex1hex2解码,而100%后面不是16进制数,所以错误(崩溃与否看参数)
            }
        }

        // Move start to end of name.
        if (nextAmpersand != -1) {
            start = nextAmpersand + 1;
        } else {
            break;
        }
    } while (true);
    return null;
}
 
public String getEncodedQuery() {
    return getQueryPart().getEncoded();//获取queryPart对象
}
 
private Part getQueryPart() {
    return query == null
            ? query = Part.fromEncoded(parseQuery()) : query;//由url直接parse构建,queryPart为null,所以根据stringUri(传入的url)创建
}
 
private String parseQuery() {
    int qsi = uriString.indexOf('?', findSchemeSeparator());
    if (qsi == NOT_FOUND) {
        return null;
    }
	...
    return uriString.substring(qsi + 1, fsi);//找到?后面的返回
}
 
static Part fromEncoded(String encoded) {
    return from(encoded, NOT_CACHED);//!!!将此queryString当成了encoded的
}

如上所述,问题就在于Uri.parse传入的url参数,被Uri默认当成的是encoded的了,而我们通常用于构建Uri的url,是decoded的;

当getQueryParameter()时,Uri是拿到encoded的queryString然后进行decode解码,这就造成了对"鲜果100%店"这个已经decode过的字符串再度decode了,就导致了错误,所以在使用Uri的时候,要注意这点。

2016-10-13 20:11:53 sz66cm 阅读数 3015

Android 采集过程注意

  1. Camera.addCallbackBuffer(byte[] data)其中data的大小要紧密与采集数据的格式相关.
  2. 如采集到的数据格式yuv422i,那么data的大小应该为width * height * 2.
  3. Camera.setPreviewCallback(Camera.PreviewCallback cb)
  4. 每次调用onPreviewFrame(...)的末尾在添加一次Camera.addCallbackBuffer(byte[] data)

Android H264硬编码过程

  1. 生成编码器并且设置相关参数
    public synchronized void open() {
        //YUV420P的大小关系
        byte[] yuv420 = new byte[mWidth * mHeight * 3 / 2];
        //生成编码器并且设置相关参数
        mediaCodec = MediaCodec.createEncoderByType("video/avc");
        //编码格式参数设置,如果对yuv数据进行旋转以后,注意mWidth,
        //mHeight的在90度或270度会颠倒,不设置正确的话对端会花屏
        MediaFormat mediaFormat = MediaFormat.createVideoFormat(
            "video/avc", mWidth, mHeight);
        //设置码率,码率越低,失真越厉害
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
        /**设置编码输入缓存大小,默认输入数据为yuv420大小,即1.5倍的宽高积,
        如果使用的是其他格式,如yuv422那么就要手动设置大小,不然塞数据时
        会报BufferOverflowException异常*/
        mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,
                 mWidth * mHeight * 3 / 2);
        //设置帧率
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
        /**设置颜色格式(I420,YV12,NV21等)
            如:MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar*/
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
        //设置发送I帧的时间间隔
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);//单位:s(秒)
        //完成配置,启动
        mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAT_ENCODE);
        mediaCodec.start();
    }
  1. 进行编码
    public synchronized int encode(byte[] in, int offset, byte[] out, int length) {
        int pos = 0;
        byte[] inBuf = in;
        int l = length;
        /**由于Android 摄像头默认采集的数据是NV21格式,所以要
           转成MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar
           让编码器支持.*/
        NV21toYUV420SemiPlannr(in, offset, yuv420, mWidth, mHeight);
        try {
            /**
                    注意此处获取inputBuffer和outputBuffer的方法,在android LOLLIPOP之后
                    的版本要修改inputBuffer = MediaCodec.getInputBuffer(index);
            */
            ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
            ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
            /**
                解码的时候,如果此处TIME_OUT非0会有个大坑,很多机子在这句卡死
            */
            int inputBufferIndex = mediaCodec.dequeueInputBuffer(TIME_OUT);
            if(inputBufferIndex >= 0){
                ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
                inputBuffer.clear();
                inputBuffer.put(inBuf, offset, l);
                /**此处getMyTime()函数维护一个递增的时间戳
                    据说此处的第四个参数不传,第一个I帧以后,
                    mediaCodec.dequeueOutputBuffer()一直返回-1,
                    -1对应原生代码的再试一遍的意思*/
                mediaCodec.queueInputBuffer(inputBufferIndex, 0, l, getMyTime(), 0);
            }
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            /**
                此处的TIME_OUT是否要传,待研究.
             */
            int outBufferIndex = mediaCodec.dequeueInputBuffer(bufferInfo, TIME_OUT);
            //待理解继续
        }
    }

(待补充)

Android H264硬解码过程

  1. 生成MediaCodec对象并且设置好参数
    //H264解码器
    codec = MediaCodec.createDecoderByType("video/avc");
    MeidaFormat mediaFormat = MediaFormat.createVideoFormat(
        "video/avc", width, height);
    mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, width * height);
    codec.configure(mediaFormat, surface, null, 0);
    codec.start();
  1. 进行解码并且播放
    public void decodeAndPlayBack(byte[] in, int offset, int length) {
        //获取喂数据的ByteBuffer数组
        ByteBuffer[] inputBuffers = codec.getInputBuffers();
        /**以下特别注意,TIME_OUT建议设置成0,设置非0很多机子
            出现卡死在此句代码,设置成0的代价只是丢帧*/
        int inputBuffersIndex = codec.dequeueInputBuffer(TIME_OUT);
        if(inputBuffersIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBuffersIndex];
            inputBuffer.clear();
            inputBuffer.put(in, offset, length);
            //填充好数据以后,提交通知解码器解码,这几个参数待研究
            codec.queueInputBuffer(inputBuffersIndex, 0, length, 0, 0);
        }
        //释放缓存空间
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0);
        while (outputBufferIndex >=0) {
            codec.releaseOutputBuffer(outputBufferIndex, true);
            outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0);
        }
    }
2018-08-14 16:08:48 gb702250823 阅读数 10431

希望我们尊重每个人的成果,转载请标明出处:
https://blog.csdn.net/gb702250823/article/details/81627503
本文出自小口锅的博客

Android 官方的 MediaCodec API

MediaCodec 是Android 4.1(api 16)版本引入的编解码接口,Developer 官网上描述的已经很清楚了。可以配合中文翻译一起看。理解更深刻。

MediaCodec 基本介绍

  • MediaCodec类可用于访问Android底层的多媒体编解码器,例如,编码器/解码器组件。它是Android底层多媒体支持基础架构的一部分(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, 以及AudioTrack一起使用)。
  • Android 底层多媒体模块采用的是 OpenMax 框架,任何 Android 底层编解码模块的实现,都必须遵循 OpenMax 标准。Google 官方默认提供了一系列的软件编解码器:包括:OMX.google.h264.encoder,OMX.google.h264.encoder, OMX.google.aac.encoder, OMX.google.aac.decoder 等等,而硬件编解码功能,则需要由芯片厂商依照 OpenMax 框架标准来完成,所以,一般采用不同芯片型号的手机,硬件编解码的实现和性能是不同的

  • Android 应用层统一由 MediaCodec API 来提供各种音视频编解码功能,由参数配置来决定采用何种编解码算法、是否采用硬件编解码加速等

MediaCodec的工作流程:
这里写图片描述
从上图可以看出 MediaCodec 架构上采用了2个缓冲区队列,异步处理数据,并且使用了一组输入输出缓存。
你请求或接收到一个空的输入缓存(input buffer),向其中填充满数据并将它传递给编解码器处理。编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(output buffer)中。最终,你请求或接收到一个填充了结果数据的输出缓存(output buffer),使用完其中的数据,并将其释放给编解码器再次使用。
具体工作如下:
1. Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer]
2. Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer]
3. MediaCodec 模块从 input 缓冲区队列取一帧数据进行编解码处理
4. 编解码处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列
5. Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer]
6. Client 对编解码后的 buffer 进行渲染/播放
7. 渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer]

MediaCodec的基本调用流程是:

createEncoderByType/createDecoderByType
configure
start
while(true) {
     dequeueInputBuffer  //从输入流队列中取数据进行编码操作 
     getInputBuffers     //获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组 
     queueInputBuffer    //输入流入队列 
     dequeueOutputBuffer //从输出队列中取出编码操作之后的数据
     getOutPutBuffers    // 获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
     releaseOutputBuffer //处理完成,释放ByteBuffer数据
}
stop
release

1.初始化MediaCodec,方法有两种,分别是通过名称和类型来创建,对应的方法为:

MediaCodec createByCodecName (String name);
MediaCodec createDecoderByType (String type);
  • 选择第一种创建方式
    根据 mineType 以及是否为编码器,选择出一个 MediaCodecInfo,然后使用第一种方式初始化MediaCodec;
    private MediaCodecInfo selectSupportCodec(String mimeType) {
        int numCodecs = MediaCodecList.getCodecCount();
        for (int i = 0; i < numCodecs; i++) {
            MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
            // 判断是否为编码器,否则直接进入下一次循环
            if (!codecInfo.isEncoder()) {
                continue;
            }
            // 如果是编码器,判断是否支持Mime类型
            String[] types = codecInfo.getSupportedTypes();
            for (int j = 0; j < types.length; j++) {
                if (types[j].equalsIgnoreCase(mimeType)) {
                    return codecInfo;
                }
            }
        }
        return null;
    }

  MediaCodecInfo codecInfo = selectSupportCodec(config.mMime);
  if (codecInfo == null) return;
  mMediaCodec = MediaCodec.createByCodecName(codecInfo.getName());
  • 第二种方式比较简单
mMediaCodec = MediaCodec.createDecoderByType (MIME_TYPE);

2.配置编码器,设置各种编码器参数(MediaFormat),这个类包含了比特率、帧率、关键帧间隔时间等。然后再调用 mMediaCodec .configure,对于 API 19 以上的系统,我们可以选择 Surface 输入:mMediaCodec .createInputSurface,

format= MediaFormat.createVideoFormat(MIME_TYPE, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);  
format.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);     
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); //关键帧间隔时间 单位s
mMediaCodec .configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mMediaCodec.createInputSurface();

3.打开编码器,获取输入输出缓冲区

mMediaCodec .start();
mInputBuffers = mMediaCodec .getInputBuffers();
mOutputBuffers = mMediaCodec .getOutputBuffers();

获取输入输出缓冲区在api19 上是以上方式获取,api21以后 可以使用直接获取ByteBuffer

ByteBuffer intputBuffer = mMediaCodec.getOutputBuffer(inputBufferIndex);
ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);

4.输入数据,有2种方式,一种是普通输入,一种是Surface 输入
普通输入又可区分为两种情况,一种是配合MediaExtractor ,一种是取原数据;

  • 获取可使用的缓冲区索引
 int outputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMES_OUT);

返回一个填充了有效数据的input buffer的索引,如果没有可用的buffer则返回-1,参数为超时时间(TIMES_OUT),单位是微秒,当timeoutUs==0时,该方法立即返回;当timeoutUs<0时,无限期地等待一个可用的input buffer,当timeoutUs>0时,
等待时间为传入的微秒值。

  • 普通输入之获取原数据方式
ByteBuffer inputBuffer = mInputBuffers[inputbufferindex];
inputBuffer.clear();//清除原来的内容以接收新的内容
inputBuffer.put(bytes, 0, len);//len是传进来的有效数据长度
mMediaCodec .queueInputBuffer(inputbufferindex, 0, len, timestamp, 0);

上面输入缓存的index,通过getInputBuffers()得到的是输入缓存数组,通过index和输入缓存数组可以得到当前请求的输入缓存,在使用之前要clear一下,避免之前的缓存数据影响当前数据,接着就是把数据添加到输入缓存中,并调用queueInputBuffer(…)把缓存数据入队;

  • 普通输入之配合MediaExtractor 解码其他的音视频数据
ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
int chunkSize = SDecoder.extractor.readSampleData(inputBuf, 0);
if (chunkSize < 0) {
   SDecoder.decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
   SDecoder.decoder.queueInputBuffer(inputBufIndex, 0, chunkSize, SDecoder.extractor.getSampleTime(), 0);                
   SDecoder.extractor.advance();
}
  • 使用Surface输入
    Surface输入是Android 4.3(api 18)引入。但用在某些 API 18 的机型上会导致编码器输出数据量特别小,画面是黑屏,所以 Surface 输入模式从 API 19 启用比较好。
//Requests a Surface to use as the input to an encoder, in place of input buffers. This may only be 
//called after configure(MediaFormat, Surface, MediaCrypto, int) and before start().
//调用此方法,官方有这么一段话,意思是必须在configure之后 start()之前调用。
mInputSurface =  mMediaCodec.createInputSurface();

5.输出数据
通常编码传输时每个关键帧头部都需要带上编码配置数据(PPS,SPS),但 MediaCodec 会在首次输出时专门输出编码配置数据,后面的关键帧里是不携带这些数据的,所以需要我们手动做一个拼接;

  • 获取可使用的缓冲区
    获取输出缓存和获取输入缓存类似,首先通过dequeueOutputBuffer(BufferInfo info, long timeoutUs)来请求一个输出缓存,这里需要传入一个BufferInfo对象,用于存储ByteBuffer的信息,TIMES_OUT为超时时间。TIMES_OUT传的是 0,表示不会等待,由于这里并没有一个单独的线程不停调用,所以这样没什么问题,反倒可以防止阻塞,但如果我们单独起了一个线程专门取输出数据,那这就会导致 CPU 资源的浪费了,可以加上一个合适的值,例如 3~10ms;
BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo,TIMES_OUT);
  • 获取数据
ByteBuffer outputBuffer = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
    outputBuffer = outputBuffers[outputBufferIndex];
} else {
    outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
    MediaFormat format = mMediaCodec.getOutputFormat();
    format.setByteBuffer("csd-0",outputBuffer);
    mBufferInfo.size = 0;
}

// 如果API<=19,需要根据BufferInfo的offset偏移量调整ByteBuffer的位置
// 并且限定将要读取缓存区数据的长度,否则输出数据会混乱
if (mBufferInfo.size != 0) {
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
        outputBuffer.position(mBufferInfo.offset);
        outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
    }
    // mMuxer.writeSampleData(mTrackIndex, encodedData, bufferInfo);
}
  • 释放缓冲区
mMediaCodec.releaseOutputBuffer(outputBufferIndex,false);

6.使用完MediaCodec后释放资源
要告知编码器我们要结束编码,Surface 输入的话调用 mMediaCodec .signalEndOfInputStream,普通输入则可以为在 queueInputBuffer 时指定 MediaCodec.BUFFER_FLAG_END_OF_STREAM 这个 flag;告知编码器后我们就可以等到编码器输出的 buffer 带着 MediaCodec.BUFFER_FLAG_END_OF_STREAM 这个 flag 了,等到之后我们调用 mMediaCodec .release 销毁编码器

if (mMediaCodec != null) {
    mMediaCodec.stop();
    mMediaCodec.release();
    mMediaCodec = null;
}

MediaCodec 流控

流控就是流量控制。为什么要控制,就是为了在一定的限制条件下,收益最大化!
涉及到了 TCP 和视频编码:
对 TCP 来说就是控制单位时间内发送数据包的数据量,对编码来说就是控制单位时间内输出数据的数据量。

TCP 的限制条件是网络带宽,流控就是在避免造成或者加剧网络拥塞的前提下,尽可能利用网络带宽。带宽够、网络好,我们就加快速度发送数据包,出现了延迟增大、丢包之后,就放慢发包的速度(因为继续高速发包,可能会加剧网络拥塞,反而发得更慢)。

视频编码的限制条件最初是解码器的能力,码率太高就会无法解码,后来随着 codec 的发展,解码能力不再是瓶颈,限制条件变成了传输带宽/文件大小,我们希望在控制数据量的前提下,画面质量尽可能高。
一般编码器都可以设置一个目标码率,但编码器的实际输出码率不会完全符合设置,因为在编码过程中实际可以控制的并不是最终输出的码率,而是编码过程中的一个量化参数(Quantization Parameter,QP),它和码率并没有固定的关系,而是取决于图像内容。 这一点不在这里展开,感兴趣的朋友可以阅读视频压缩编码和音频压缩编码的基本原理。

无论是要发送的 TCP 数据包,还是要编码的图像,都可能出现“尖峰”,也就是短时间内出现较大的数据量。TCP 面对尖峰,可以选择不为所动(尤其是网络已经拥塞的时候),这没有太大的问题,但如果视频编码也对尖峰不为所动,那图像质量就会大打折扣了。如果有几帧数据量特别大,但仍要把码率控制在原来的水平,那势必要损失更多的信息,因此图像失真就会更严重。这种情况通常的表现是画面出现很多小方块,看上去像是打了马赛克一样,导致画面的局部或者整体看不清楚的情况

  • Android 硬编码流控
    MediaCodec 流控相关的接口并不多,一是配置时设置目标码率和码率控制模式,二是动态调整目标码率(Android 19+)。

配置时指定目标码率和码率控制模式:

mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

码率控制模式有三种:
码率控制模式在 MediaCodecInfo.EncoderCapabilities类中定义了三种,在 framework 层有另一套名字和它们的值一一对应:

  • CQ 对应于 OMX_Video_ControlRateDisable,它表示完全不控制码率,尽最大可能保证图像质量;
  • CBR 对应于 OMX_Video_ControlRateConstant,它表示编码器会尽量把输出码率控制为设定值,即我们前面提到的“不为所动”;
  • VBR 对应于 OMX_Video_ControlRateVariable,它表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低;

动态调整目标码率:

Bundle param = new Bundle();
param.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate);
mediaCodec.setParameters(param);

Android 流控策略选择

  • 质量要求高、不在乎带宽、解码器支持码率剧烈波动的情况下,可以选择 CQ 码率控制策略。
  • VBR 输出码率会在一定范围内波动,对于小幅晃动,方块效应会有所改善,但对剧烈晃动仍无能为力;连续调低码率则会导致码率急剧下降,如果无法接受这个问题,那 VBR 就不是好的选择。

编码栗子

下面展示使用MediaExtractor获取数据后,用MediaMuxer重新写成一个MP4文件的简单栗子

private void doExtract() throws IOException {
    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    boolean outputDone = false;
    boolean inputDone = false;
    while (!outputDone) {
        if (!inputDone) {
            int inputBufIndex = mMediaCodec.dequeueInputBuffer(10000);
            if (inputBufIndex >= 0) {
                ByteBuffer inputBuf = mMediaCodec.getInputBuffers()[inputBufIndex];
                int chunkSize = mMediaExtractor.readSampleData(inputBuf, 0);
                if (chunkSize < 0) {
                    mMediaCodec.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    inputDone = true;
                } else {
                    mMediaCodec.queueInputBuffer(inputBufIndex, 0, chunkSize, mMediaExtractor.getSampleTime(), 0);
                    mMediaExtractor.advance();
                }
            }
        }

        if (!outputDone) {
            int decoderStatus =mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000);
            if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                 Log.d(TAG, "no output from decoder available");
            } else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                Log.d(TAG, "decoder output buffers changed");
            } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                MediaFormat newFormat = SDecoder.decoder.getOutputFormat();
                Log.d(TAG, "decoder output format changed: " + newFormat);
            } else {
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    mMediaCodec.releaseOutputBuffer(outputBufferIndex,false);
                    outputDone = true;
                    break;
                }
                // 获取一个只读的输出缓存区inputBuffer ,它包含被编码好的数据
                ByteBuffer outputBuffer = null;
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                    outputBuffer = mMediaCodec.getOutputBuffers()[outputBufferIndex];
                } else {
                    outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
                }
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    MediaFormat format = mMediaCodec.getOutputFormat();
                    format.setByteBuffer("csd-0",outputBuffer);
                    mBufferInfo.size = 0;
                }

                // 如果API<=19,需要根据BufferInfo的offset偏移量调整ByteBuffer的位置
                // 并且限定将要读取缓存区数据的长度,否则输出数据会混乱
                if (mBufferInfo.size != 0) {
                    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
                        outputBuffer.position(mBufferInfo.offset);
                        outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
                    }
                    // mMuxer.writeSampleData(mTrackIndex, encodedData, bufferInfo);
                }
                mMediaCodec.releaseOutputBuffer(decoderStatus, false);
            }
        }
    }
}
2018-03-02 13:44:37 fword 阅读数 5384

1.请求关键帧:

if (Build.VERSION.SDK_INT >= 23) {
Bundle params = new Bundle();
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
mMediaCodec.setParameters(params);
}