MediaCodec

2021-03-04  本文已影响0人  JackyWu15

简介

MediaCodec是 Android media 基础框架的一部分,通常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, SurfaceAudioTrack 一起使用。

MediaCodec_1.png

这张图描述了MediaCodec的整体工作流程,一般情况下,由客户端向MediaCodec申请一个空的输入ByteBuffer,进行数据填充,再将ByteBuffer发送给MediaCodec,MediaCodec会采用异步的方式处理这些输入的数据,并将处理后的数据填充到输出Buffer中,消费者取到输出Buffer进行消费后,需将缓冲区释放,返还给MediaCodec。

数据

数据载体模式:

数据类型:

1)压缩数据:作为解码器的输入数据或者编码器的输出数据

2) 原始音频数据:ByteBuffer包含PCM音频数据的整个帧,这是每个声道按声道顺序的一个样本,每个PCM音频样本都是16位带符号整数或浮点数。格式为AudioFormat.ENCODING_PCM_16BIT才能做处理。

获取音频采样数据的示例代码如下:

/**
  * 根据声道获取采样数据
  */
 short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
  //获取输出缓冲区
  ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
  //获取音频编码格式
  MediaFormat format = codec.getOutputFormat(bufferId);
  //转换字节顺序和Short类型
  ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
  //获取声道数
  int numChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
  if (channelIx < 0 || channelIx >= numChannels) {
    return null;
  }
  //获取存储采样数据
  short[] res = new short[samples.remaining() / numChannels];
  for (int i = 0; i < res.length; ++i) {
    res[i] = samples.get(i * numChannels + channelIx);
  }
  return res;
 }

3) 原始视频数据:在ByteBuffer模式下,视频缓冲区根据MediaFormat.KEY_COLOR_FORMAT来进行布局。通过getCodecInfo().getCapabilitiesForType().colorFormats可以获取到支持的颜色格式数组,它包含3种颜色格式:

Build.VERSION_CODES.LOLLIPOP_MR1(5.1)以后,编解码器都支持YUV420P

生命周期和状态

MediaCodec有3种状态:Stopped,Executing和Released,其中Stopped和Released又各自细分成3种子状态,如下图所示:


MediaCodec_4.png

1)Stopped

2)Executing

3)Released

状态重置:

创建

通过编解码器类型和编解码器名字2种方式,可以创建编解码器,它们分别对应以下工厂方法:

//根据编码器类型创建解码器
public static MediaCodec createEncoderByType(@NonNull String type)
//根据解码器类型创建解码器
public static MediaCodec createDecoderByType(@NonNull String type)
//根据编解码器名字创建编解码器
public static MediaCodec createByCodecName(@NonNull String name)

MediaFormat包含了许多类型,如下:

public final class MediaFormat {
      。。。。。
    //视频类型
    public static final String MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8";
    public static final String MIMETYPE_VIDEO_VP9 = "video/x-vnd.on2.vp9";
    public static final String MIMETYPE_VIDEO_AV1 = "video/av01";
    public static final String MIMETYPE_VIDEO_AVC = "video/avc";
    public static final String MIMETYPE_VIDEO_HEVC = "video/hevc";
  
    //音频类型
    public static final String MIMETYPE_AUDIO_AMR_NB = "audio/3gpp";
    public static final String MIMETYPE_AUDIO_AMR_WB = "audio/amr-wb";
    public static final String MIMETYPE_AUDIO_MPEG = "audio/mpeg";
    public static final String MIMETYPE_AUDIO_AAC = "audio/mp4a-latm";
    public static final String MIMETYPE_AUDIO_QCELP = "audio/qcelp";
    public static final String MIMETYPE_AUDIO_VORBIS = "audio/vorbis";
    public static final String MIMETYPE_AUDIO_OPUS = "audio/opus";
      。。。。。
}

在创建解码器时,如果是本地文件或者网络流,可以配合使用MediaExtractor解封装器来做格式提取,如下:

//mTrackIndex为视频轨索引获取格式
MediaFormat decoderFormat = mediaExtractor.getTrackFormat(mTrackIndex);
//创建解码器
MediaCodec decoder = MediaCodec.createDecoderByType(decoderFormat.getString(MediaFormat.KEY_MIME));

在创建编码器时,可以直接指定期望的编码类型,如下:

//指定格式
MediaFormat encoderFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
//创建编码器
MediaCodec encoder =  MediaCodec.createEncoderByType(encoderFormat.getString(MediaFormat.KEY_MIME));

以上是硬编解码器的创建,如果创建失败,可以通过指定软解码器名字来创建,如下:

//创建软解码器
MediaCodec decoder = MediaCodec.createByCodecName("OMX.google.h264.decoder");
//创建软编码器
MediaCodec encoder = MediaCodec.createByCodecName("OMX.google.h264.encoder");

通常,以"OMX.google."为前缀的名字,即为软编解码类型

初始化配置

void configure (MediaFormat format, Surface surface, MediaCrypto crypto, int flags)

void configure (MediaFormat format, Surface surface, int flags, MediaDescrambler descrambler)

以下为编解码配置的示例代码:

//解码器配置
decoder.configure(decoderFormat, new Surface(new SurfaceTexture(getTextureId())), null, 0);
decoder.start()

这里指定了解码器最终输出渲染的表面,用来输出到开辟好的纹理空间

//编码器配置
encoder.configure(encoderFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface surface = encoder.createInputSurface()
encoder.start()

编码器创建的Surface只能用于硬件加速api的渲染输入,如OpenGL,这就是Surface模式的用法,比如编码器创建了自己的Surface,可以用来关联EGL做为编码器的数据输入,替代ByteBuffer输入方式。
还有另一种方式,也可以使用createPersistentInputSurface ()来创建Surface,其他的编码器随后可以调用setInputSurface(Surface)方法来继续使用这个Surface,但同一时间内,只能一个编解码器使用。

createInputSurface和setInputSurface方法必须在configure方法之后,start方法之前调用,否则抛IllegalStateException异常。
持久化表面必须使用createPersistentInputSurface创建,否则抛IllegalArgumentException异常。

特殊数据

某些格式如,AAC和MPEG4,H.264和H.265要求帧数据的前缀,包含设置数据或编解码器特定数据的缓冲区,如sps和pps。处理此类压缩格式时,必须在start方法之后且任何帧数据之前,将这些数据输送给编解码器。这类数据用BUFFER_FLAG_CODEC_CONFIG来做标记。

通常不使用ByteBuffer来直接提交,而是在configure方法时通过MediaFormat进行设置,它在start方法调用后,会直接提交给编解码器。编解码器同样会输出到输出缓冲区中,因此,在进行编码往Muxer写入数据时,携带BUFFER_FLAG_CODEC_CONFIG标记的数据不用再次写入,它应该通过MediaFormat传递给Muxer。

数据处理

1)同步方式:

系统版本5.0之前只能使用同步方式来处理,整个过程如开头的流程图所示,下面以解码流程为例:

  /**
   * 解封装
   */
 private int drainExtractor(long timeoutUs) {
        if (mIsExtractorEOS) return DRAIN_STATE_NONE;
        int trackIndex = mExtractor.getSampleTrackIndex();
        if (trackIndex >= 0 && trackIndex != mTrackIndex) {
            return DRAIN_STATE_NONE;
        }
        //步骤1
        int result = mDecoder.dequeueInputBuffer(timeoutUs);
        if (result < 0) return DRAIN_STATE_NONE;
        if (trackIndex < 0) {
            mIsExtractorEOS = true;
            mDecoder.queueInputBuffer(result, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            return DRAIN_STATE_NONE;
        }
        //步骤2
        ByteBuffer buffer = mDecoder.getInputBuffer(result);
        int sampleSize = mExtractor.readSampleData(buffer, 0);
        boolean isKeyFrame = (mExtractor.getSampleFlags() & MediaExtractor.SAMPLE_FLAG_SYNC) != 0;
        //步骤3
        mDecoder.queueInputBuffer(result, 0, sampleSize, mExtractor.getSampleTime(), isKeyFrame ? MediaCodec.BUFFER_FLAG_KEY_FRAME : 0);
        mExtractor.advance();

        return DRAIN_STATE_CONSUMED;
}

步骤1:从解码器获取输入缓冲区的id

步骤2:根据缓冲id获取输入缓冲区

步骤3:将充满数据的输入缓冲求传递给解码器,最后的flags标记有几种类型

下面从解码器获取输出数据:

private int drainDecoder(long timeoutUs) throws InterruptedException {
        if (mIsDecoderEOS) return DRAIN_STATE_NONE;
        //1
        int result = mDecoder.dequeueOutputBuffer(mBufferInfo, timeoutUs);
        switch (result) {
            case MediaCodec.INFO_TRY_AGAIN_LATER:
                return DRAIN_STATE_NONE;
            case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
            case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                return DRAIN_STATE_SHOULD_RETRY_IMMEDIATELY;
        }
        //2
        if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            mEncoder.signalEndOfInputStream();
            mIsDecoderEOS = true;
            mBufferInfo.size = 0;
        }
        //3
        boolean doRender = (mBufferInfo.size > 0);
        mDecoder.releaseOutputBuffer(result, doRender);
        ......
        return DRAIN_STATE_CONSUMED;
}

步骤1:从解码器获取输出缓冲区和缓冲区信息,result返回缓冲区索引或常量值

步骤2:接受到带有BUFFER_FLAG_END_OF_STREAM标记的缓冲区,表明所有解码数据已输出完成,不会再有输出,signalEndOfInputStream方法通知编码器输入结束

步骤3:使用完输出缓冲区后,调用releaseOutputBuffer释放回给解码器,doRender为true,则缓冲区的数据会渲染到在configure方法中配置的Surface

可以不立即queueinputbuffer/releaseOutputBuffer到编解码器,但持有input/outputbuffer可能会使编解码器停止工作,并且此行为取决于设备。 编解码器有可能在产生输出缓冲区之前暂停,直到所有未完成的缓冲区queueinputbuffer/releaseOutputBuffer。 因此,用户最好每次获得缓冲区后执行释放操作。

2)异步方式:

在系统版本5.0及以上增加了异步处理方式,同步方式仍可以使用,但官方推荐首选异步方式,它的流程状态和同步方式稍微有些不同,再调用start方法后,状态自动切换为Running子状态,并且在调用flush方法后,必须再次调用start使状态流转为Running状态,才能开始输入数据。


image.png

异步方式的代码示例如下:

 MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
  @Override
  void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
    // fill inputBuffer with valid data
    …
    codec.queueInputBuffer(inputBufferId, …);
  }
 
  @Override
  void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
    // bufferFormat is equivalent to mOutputFormat
    // outputBuffer is ready to be processed or rendered.
    …
    codec.releaseOutputBuffer(outputBufferId, …);
  }
 
  @Override
  void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
    // Subsequent data will conform to new format.
    // Can ignore if using getOutputFormat(outputBufferId)
    mOutputFormat = format; // option B
  }
 
  @Override
  void onError(…) {
    …
  }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

setCallback方法必须在configure方法之前被调用,并且不应该再使用getInputBuffers,getOutputBuffers,dequeueInputBuffer和dequeueOutputBuffer方法。

最后

当编解码器使用Surface模式作为数据输入源或输出源时,对应的缓冲区无法访问:

上一篇下一篇

猜你喜欢

热点阅读