Android技术讨论Android 音视频开发Android 复习学习使用

MediaCodec的使用介绍

2019-06-06  本文已影响0人  smallest_one

目录

  1. 概述
  2. 支持的数据类型
  3. 使用MediaCodec的编解码流程
  4. MediaCodec生命周期
  5. MediaCodec API简介
  6. 同步和异步API的使用流程
  7. 示例程序

参考

1. 概述

推荐以官方文档[1]作为主要的参考,其中有详细的介绍。

MediaCodec是Android提供的用于对音视频进行编解码的类,它通过访问底层的codec来实现编解码的功能。是Android media基础框架的一部分,通常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, SurfaceAudioTrack 一起使用。

历史:

强烈建议从示例代码开始了解MediaCodec,而不是试图从文档把它搞清楚。

2. 支持的数据类型

编解码器支持的数据类型: 压缩的音视频据、原始音频数据和原始视频数据。

压缩数据

原始音频buffer

原始音频buffer包含PCM音频数据的整个帧,这是每个通道按通道顺序的一个样本。每个样本都是一个 AudioFormat#ENCODING_PCM_16BIT

原始视频buffer

在ByteBuffer模式下,视频buffer根据它们的MediaFormat#KEY_COLOR_FORMAT进行布局。可以从getCodecInfo(). MediaCodecInfo.getCapabilitiesForType.CodecCapability.colorFormats获取支持的颜色格式。视频编解码器可以支持三种颜色格式:

从Build.VERSION_CODES.LOLLIPOP_MR1.开始,所有视频编解码器都支持flexible的YUV 4:2:0 buffer。

3. 使用MediaCodec的编解码流程

一些编解码器对于它们的buffer要求是比较特殊的,比如内存对齐或是有特定的最小最大限制,为了适应广泛的可能性,buffer分配是由编解码器实现的。

MediaCodec采用异步方式处理数据,并且使用了一组输入输出buffer(ByteBuffer)。

  1. 使用者从MediaCodec请求一个空的输入buffer(ByteBuffer),填充满数据后将它传递给MediaCodec处理。
  2. MediaCodec处理完这些数据并将处理结果输出至一个空的输出buffer(ByteBuffer)中。
  3. 使用者从MediaCodec获取输出buffer的数据,消耗掉里面的数据,使用完输出buffer的数据之后,将其释放回编解码器。

流程如下图所示:


image.png

4. MediaCodec的生命周期

MediaCodec的生命周期有三种状态:Stopped、Executing、Released。

Stopped的三种子状态:

  1. Uninitialized:当创建了一个MediaCodec对象,此时处于Uninitialized状态。可以在任何状态调用reset()方法使MediaCodec返回到Uninitialized状态。
  2. Configured:使用configure(…)方法对MediaCodec进行配置转为Configured状态。
  3. Error:MediaCodec遇到错误时进入Error状态。错误可能是在队列操作时返回的错误或者异常导致的。

Executing的三种子状态:

  1. Flushed:在调用start()方法后MediaCodec立即进入Flushed子状态,此时MediaCodec会拥有所有的缓存。可以在Executing状态的任何时候通过调用flush()方法返回到Flushed子状态。
  2. Running:一旦第一个输入缓存(input buffer)被移出队列,MediaCodec就转入Running子状态,这种状态占据了MediaCodec的大部分生命周期。通过调用stop()方法转移到Uninitialized状态。
  3. End-of-Stream:将一个带有end-of-stream标记的输入buffer入队列时,MediaCodec将转入End-of-Stream子状态。在这种状态下,MediaCodec不再接收之后的输入buffer,但它仍然产生输出buffer直到end-of-stream标记输出。

Released

  1. 当使用完MediaCodec后,必须调用release()方法释放其资源。调用 release()方法进入最终的Released状态。

5. MediaCodec API简介

上面MediaCodec的生命周期的图中包含了MediaCodec一些主要的方法,下面对
MediaCodec 主要的API做一个介绍:

5.1 MediaCodec创建

MediaCodec的一个实例处理一种特定类型的数据(例如MP3音频或H.264视频),进行编码或解码操作。

MediaCodec创建:

  1. 可以使用MediaCodecList为特定的媒体格式创建一个MediaCodec。
  1. 还可以使用createDecoder/EncoderByType(java.lang.String)为特定MIME类型创建首选的编解码器。但是,这不能用于注入特性,并且可能会创建一个不能处理特定媒体格式的编解码器。

5.2 configure

配置codec。

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

MediaFormat:封装描述媒体数据格式的信息(包括音频或视频),以及可选的特性元数据。

5.3 dequeueInputBuffer

public final int dequeueInputBuffer(long timeoutUs)

5.4 queueInputBuffer

在指定索引处填充输入buffer后,使用queueInputBuffer将buffer提交给组件。

特定于codec的数据

    public native final void queueInputBuffer(
            int index,
            int offset, int size, long presentationTimeUs, int flags)

5.5 dequeueOutputBuffer

从MediaCodec获取输出buffer。

    public final int dequeueOutputBuffer(
            @NonNull BufferInfo info, long timeoutUs) 

BufferInfo

    public final static class BufferInfo {
        public void set(
                int newOffset, int newSize, long newTimeUs, int newFlags);
        public int offset;
        public int size;
        public long presentationTimeUs;
        public int flags;
    };
    public static final int BUFFER_FLAG_KEY_FRAME = 1;
    public static final int BUFFER_FLAG_CODEC_CONFIG = 2;
    public static final int BUFFER_FLAG_END_OF_STREAM = 4;
    public static final int BUFFER_FLAG_PARTIAL_FRAME = 8;

5.6 releaseOutputBuffer

使用此方法将输出buffer返回给codec或将其渲染在输出surface。

public void releaseOutputBuffer (int index, 
                boolean render)

6. 同步和异步API的使用流程

6.1 同步API的使用流程

- 创建并配置MediaCodec对象。
- 循环直到完成:
  - 如果输入buffer准备好了:
    - 读取一段输入,将其填充到输入buffer中
  - 如果输出buffer准备好了:
    - 从输出buffer中获取数据进行处理。
- 处理完毕后,release MediaCodec 对象。

官方文档中给出的同步API的代码示例

 MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
  int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
  if (inputBufferId >= 0) {
    ByteBuffer inputBuffer = codec.getInputBuffer(…);
    // fill inputBuffer with valid data
    …
    codec.queueInputBuffer(inputBufferId, …);
  }
  int outputBufferId = codec.dequeueOutputBuffer(…);
  if (outputBufferId >= 0) {
    ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
    MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
    // bufferFormat is identical to outputFormat
    // outputBuffer is ready to be processed or rendered.
    …
    codec.releaseOutputBuffer(outputBufferId, …);
  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    // Subsequent data will conform to new format.
    // Can ignore if using getOutputFormat(outputBufferId)
    outputFormat = codec.getOutputFormat(); // option B
  }
 }
 codec.stop();
 codec.release();

6.2 异步API的使用流程

在Android 5.0, API21,引入了“异步模式”。

- 创建并配置MediaCodec对象。
- 给MediaCodec对象设置回调MediaCodec.Callback
- 在onInputBufferAvailable回调中:
    - 读取一段输入,将其填充到输入buffer中
- 在onOutputBufferAvailable回调中:
    - 从输出buffer中获取数据进行处理。
- 处理完毕后,release MediaCodec 对象。

官方文档中给出的异步API的代码示例

 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();

7. 示例程序

Android源码中的CTS部分给出了很多可以关于Media编解码的Demo[2],可以去参考和学习。

7.1 设备支持的解码器

MediaCodecList可用于获取设备支持的编解码器的名字、能力,以查找合适的编解码器。

以下是获取编解码器组件名称的示例代码:

        MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);//REGULAR_CODECS参考api说明
        MediaCodecInfo[] codecs = list.getCodecInfos();
        Log.d(TAG, "Decoders: ");
        for (MediaCodecInfo codec : codecs) {
            if (codec.isEncoder())
                continue;
            Log.d(TAG, codec.getName());
        }
        Log.d(TAG, "Encoders: ");
        for (MediaCodecInfo codec : codecs) {
            if (codec.isEncoder())
                Log.d(TAG, codec.getName());
        }

不同设备支持的类型不同,下面本机获取到编解码器组件的名称:

Decoders: 
OMX.google.mp3.decoder
OMX.google.amrnb.decoder
OMX.google.amrwb.decoder
OMX.google.aac.decoder
OMX.google.g711.alaw.decoder
OMX.google.g711.mlaw.decoder
OMX.google.vorbis.decoder
OMX.google.opus.decoder
OMX.google.raw.decoder
OMX.google.gsm.decoder
OMX.qcom.video.decoder.avc
OMX.qcom.video.decoder.mpeg4
OMX.qcom.video.decoder.mpeg2
OMX.qcom.video.decoder.h263
OMX.qcom.video.decoder.vc1
OMX.qcom.video.decoder.divx
OMX.qcom.video.decoder.divx311
OMX.qcom.video.decoder.divx4
OMX.qcom.video.decoder.vp8
OMX.qcom.video.decoder.vp9
OMX.qcom.video.decoder.hevc
OMX.ffmpeg.dsd.decoder
OMX.ffmpeg.dts.decoder
OMX.ffmpeg.adpcm.decoder
OMX.qti.audio.decoder.flac
OMX.google.mpeg4.decoder
OMX.google.h263.decoder
OMX.google.h264.decoder
OMX.google.hevc.decoder
OMX.google.vp8.decoder
OMX.google.vp9.decoder
Encoders: 
OMX.google.aac.encoder
OMX.google.amrnb.encoder
OMX.google.amrwb.encoder
OMX.google.flac.encoder
OMX.qcom.video.encoder.avc
OMX.qcom.video.encoder.mpeg4
OMX.qcom.video.encoder.h263
OMX.qcom.video.encoder.vp8
OMX.qcom.video.encoder.hevc
OMX.google.h263.encoder
OMX.google.h264.encoder
OMX.google.mpeg4.encoder
OMX.google.vp8.encoder

5.2 AAC解码为PCM的示例

public class AACToPCM {
    private static final String TAG = "AACToPCM";
    public static final int ERROR_INPUT_INVALID = 100;
    public static final int ERROR_OUTPUT_FAILED = 200;
    public static final int ERROR_OPEN_CODEC = 300;
    public static final int OK = 0;
    private static final int TIMEOUT_USEC = 0;
    private MediaExtractor mExtractor;
    private MediaFormat mFormat;
    private MediaCodec mDecoder;
    private FileOutputStream mFos;
    private ByteBuffer[] mInputBuffers;
    private ByteBuffer[] mOutputBuffers;
    private boolean mDecodeEnd;
    public AACToPCM() {}

     private int checkPath(String path) {
        if (path == null || path.isEmpty()) {
            Log.d(TAG, "invalid path, path is empty");
            return ERROR_INPUT_INVALID;
        }
        File file = new File(path);
        if (!file.isFile()) {
            Log.d(TAG, "path is not a file, path:" + path);
            return ERROR_INPUT_INVALID;
        } else if (!file.exists()) {
            Log.d(TAG, "file not exists, path:" + path);
            return ERROR_INPUT_INVALID;
        } else {
            Log.d(TAG, "path is a file, path:" + path);
        }
        return OK;
    }
    public int decodeAACToPCM(String audioPath, String pcmPath) {
        int ret;
        if (OK != (ret = openInput(audioPath))) {
            return ret;
        }
        if (OK != (ret = openOutput(pcmPath))) {
            return ret;
        }
        if (OK != (ret = openCodec(mFormat))) {
            return ret;
        }
        mDecodeEnd = false;
        while (!mDecodeEnd) {
            if (OK != (ret = decode(mDecoder, mExtractor))) {
                Log.d(TAG, "decode failed, ret=" + ret);
                break;
            }
        }
        close();
        return ret;
    }

     private int decode(MediaCodec codec, MediaExtractor extractor) {
        Log.d(TAG, "decode");
        int inputIndex = codec.dequeueInputBuffer(TIMEOUT_USEC);
        if (inputIndex >= 0) {
            ByteBuffer inputBuffer;
            if (Build.VERSION.SDK_INT >= 21) {
                inputBuffer = codec.getInputBuffer(inputIndex);
            } else {
                inputBuffer = mInputBuffers[inputIndex];
            }
            inputBuffer.clear();
            int sampleSize = extractor.readSampleData(inputBuffer, 0);
            if (sampleSize < 0) {//read end
                codec.queueInputBuffer(inputIndex, 0, 0, 0L,
                        MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            } else {
                codec.queueInputBuffer(inputIndex, 0, sampleSize, extractor.getSampleTime(), 0);
                extractor.advance();
            }
        } 

         MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
        if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {//TIMEOUT
            Log.d(TAG, "INFO_TRY_AGAIN_LATER");//TODO how to declare this info
            return OK;
        } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            Log.d(TAG, "output format changed");
            return OK;
        } else if (outputIndex < 0) {
            Log.d(TAG, "outputIndex=" + outputIndex);
            return OK;
        } else {
            ByteBuffer outputBuffer;
            if (Build.VERSION.SDK_INT >= 21) {
                outputBuffer = codec.getOutputBuffer(outputIndex);
            } else {
                outputBuffer = mOutputBuffers[outputIndex];
            }
            byte[] buffer = new byte[bufferInfo.size];
            outputBuffer.get(buffer);
            try {
                Log.d(TAG, "output write, size="+ bufferInfo.size);
                mFos.write(buffer);
                mFos.flush();
            } catch (IOException e) {
                e.printStackTrace();
                return ERROR_OUTPUT_FAILED;
            }
            codec.releaseOutputBuffer(outputIndex, false);
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                mDecodeEnd = true;
            }
        }
        return OK;
    }

     private int openCodec(MediaFormat format) {
        Log.d(TAG, "openCodec, format mime:" + format.getString(MediaFormat.KEY_MIME));
        try {
            mDecoder = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
        } catch (IOException e) {
            e.printStackTrace();
            return ERROR_OPEN_CODEC;
        }
        mDecoder.configure(format, null, null, 0);
        mDecoder.start();
        if (Build.VERSION.SDK_INT < 21) {
            mInputBuffers = mDecoder.getInputBuffers();
            mOutputBuffers = mDecoder.getOutputBuffers();
        }
        return OK;
    }

     private int openInput(String audioPath) {
        Log.d(TAG, "openInput audioPath:" + audioPath);
        int ret;
        if (OK != (ret = checkPath(audioPath))) {
            return ret;
        }
        mExtractor = new MediaExtractor();
        int audioTrack = -1;
        boolean hasAudio = false;
        try {
            mExtractor.setDataSource(audioPath);
            for (int i = 0; i < mExtractor.getTrackCount(); ++i) {
                MediaFormat format = mExtractor.getTrackFormat(i);
                String mime = format.getString(MediaFormat.KEY_MIME);
                Log.d(TAG, "mime=" + mime);
                if (mime.startsWith("audio/")) {
                    audioTrack = i;
                    hasAudio = true;
                    mFormat = format;
                    break;
                }
            }
            if (!hasAudio) {
                Log.d(TAG, "input contain no audio");
                return ERROR_INPUT_INVALID;
            }
            mExtractor.selectTrack(audioTrack);
        } catch (IOException e) {
            return ERROR_INPUT_INVALID;
        }
        return OK;
    }

     private int openOutput(String outputPath) {
        Log.d(TAG, "openOutput outputPath:" + outputPath);
        try {
            mFos = new FileOutputStream(outputPath);
        } catch (IOException e) {
            return ERROR_OUTPUT_FAILED;
        }
        return OK;
    }

     private void close() {
        mExtractor.release();
        mDecoder.stop();
        mDecoder.release();
        try {
            mFos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读