Android原生编解码接口 MediaCodec 之——完全解

2020-06-21  本文已影响0人  小口锅

Android 官方的 MediaCodec API

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

MediaCodec 基本介绍

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);
    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(...)把缓存数据入队;

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();
}
//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 会在首次输出时专门输出编码配置数据,后面的关键帧里是不携带这些数据的,所以需要我们手动做一个拼接;

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

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

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 层有另一套名字和它们的值一一对应:

动态调整目标码率:

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

Android 流控策略选择

编码栗子

下面展示使用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);
            }
        }
    }
}
上一篇 下一篇

猜你喜欢

热点阅读