给Android工程师的音视频教程之一文弄懂MediaCodec

更多音视频知识请关注公众号:进击的代码家
音视频学习项目:LearnVideo AndroidMediaCodecDemo

简介

MediaCodec是Android提供的用于对音视频进行编解码的类,是Android Media基础框架的一部分,一般和 MediaExtractor, MediaMuxer, Surface和AudioTrack 一起使用。

MediaCodec的编解码流程

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

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

流程如下图所示:
在这里插入图片描述

MediaCodec的生命周期

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

Stopped,包含三种子状态:Uninitialized、Configured、Error。
Executing,包含三种子状态:Flushed、Running、End-of-Stream。

在这里插入图片描述
Stopped的三种子状态
Uninitialized:当创建了一个MediaCodec对象,此时处于Uninitialized状态。可以在任何状态调用reset()方法使MediaCodec返回到Uninitialized状态。

Configured:使用configure(…)方法对MediaCodec进行配置转为Configured状态。

Error:MediaCodec遇到错误时进入Error状态。错误可能是在队列操作时返回的错误或者异常导致的。

Executing的三种子状态:
Flushed:在调用start()方法后MediaCodec立即进入Flushed子状态,此时MediaCodec会拥有所有的缓存。可以在Executing状态的任何时候通过调用flush()方法返回到Flushed子状态。

Running:一旦第一个输入缓存(input buffer)被移出队列,MediaCodec就转入Running子状态,这种状态占据了MediaCodec的大部分生命周期。通过调用stop()方法转移到Uninitialized状态。

End-of-Stream:将一个带有end-of-stream标记的输入buffer入队列时,MediaCodec将转入End-of-Stream子状态。在这种状态下,MediaCodec不再接收之后的输入buffer,但它仍然产生输出buffer直到end-of-stream标记输出。

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

主要API介绍

简介:

1.MediaCodec创建:
createDecoderByType/createEncoderByType:根据特定MIME类型(如"video/avc")创建codec。
createByCodecName:知道组件的确切名称(如OMX.google.mp3.decoder)的时候,根据组件名创建codec。使用MediaCodecList可以获取组件的名称。

2.configure:配置解码器或者编码器。
3.start:成功配置组件后调用start。

4.buffer处理的接口:
dequeueInputBuffer:从输入流队列中取数据进行编码操作。
queueInputBuffer:输入流入队列。
dequeueOutputBuffer:从输出队列中取出编码操作之后的数据。
releaseOutputBuffer:处理完成,释放ByteBuffer数据。

5.flush:清空的输入和输出端口。
6.stop:终止decode/encode会话
7.release:释放编解码器实例使用的资源。

MediaCodec创建

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

MediaCodec创建:
1.可以使用MediaCodecList为特定的媒体格式创建一个MediaCodec。
可以从MediaExtractor#getTrackFormat获得track的格式。
使用MediaFormat#setFeatureEnabled注入想要添加的任何特性。
然后调用MediaCodecList#findDecoderForFormat来获取能够处理该特定媒体格式的编解码器的名称。
最后,使用createByCodecName(字符串)创建编解码器。

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

configure

配置codec。

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

MediaFormat format:输入数据的格式(解码器)或输出数据的所需格式(编码器)。传null等同于传递MediaFormat#MediaFormat作为空的MediaFormat。

Surface surface:指定Surface,用于解码器输出的渲染。如果编解码器不生成原始视频输出(例如,不是视频解码器)和/或想配置解码器输出ByteBuffer,则传null。

MediaCrypto crypto:指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。

int flags:当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。

MediaFormat:封装描述媒体数据格式的信息(包括音频或视频),以及可选的特性元数据。媒体数据的格式指定为key/value对。key是字符串。值可以integer、long、float、String或ByteBuffer。
特性元数据被指定为string/boolean对。

dequeueInputBuffer

public final int dequeueInputBuffer(long timeoutUs)

返回用于填充有效数据的输入buffer的索引,如果当前没有可用的buffer,则返回-1。
long timeoutUs:等待可用的输入buffer的时间。
如果timeoutUs == 0,则立即返回。
如果timeoutUs < 0,则无限期等待可用的输入buffer。
如果timeoutUs > 0,则等待“timeoutUs”微秒。

如果应用程序需要尽快将数据提交到编码器进行编码,可以将timeoutUs设置为0,这样方法会立即返回,如果没有可用的缓冲区则会返回-1。

如果应用程序不需要立即提交数据进行编码,则可以设置一个适当的超时时间,例如1秒钟,等待MediaCodec中的编码器线程处理完之前的缓冲区,以便在缓冲区可用时获取它并将待编码的数据写入其中。

值得注意的是,如果将超时时间设置为-1,则该方法将一直等待,直到有可用的输入缓冲区。

一般而言,我们可以通过测试和实验得到一个适合当前应用场景的超时时间,以充分利用硬件编解码器的性能。同时,为了避免出现输入缓冲区的不足或者编码延迟等问题,我们还需要根据编解码器的编码速度以及输入数据的产生速率,对缓冲区的数量进行适当的设置。

getInputBuffer

MediaCodec的getInputBuffer方法用于获取一个输入缓冲区,以便应用程序可以将输入数据写入缓冲区中,从而进行编码操作。

调用getInputBuffer方法之前,需要先调用dequeueInputBuffer方法获取一个可用的输入缓冲区的索引。一旦获取到输入缓冲区的索引,就可以通过getInputBuffer方法获取相应的输入缓冲区对象。getInputBuffer方法的参数是输入缓冲区的索引,返回值是一个ByteBuffer对象,该对象表示输入缓冲区的数据存储空间。

需要注意的是,每个输入缓冲区只能被使用一次,一旦输入数据写入缓冲区中,并且调用了queueInputBuffer方法提交数据,该缓冲区就不能再次使用,需要重新获取一个可用的输入缓冲区。在获取输入缓冲区之后,应用程序需要清空缓冲区的内容,以确保输入数据不会受到之前的数据影响。

另外,由于ByteBuffer对象可能被多个线程同时使用,因此在操作ByteBuffer对象时需要进行同步处理,以避免线程安全问题。可以使用Java中的同步机制(如synchronized关键字)或者使用Android提供的线程安全的ByteBuffer类(如java.nio.ByteBuffer)来处理这些问题。

一般我们把这个ByteBuffer放入MediaExtractor中,获取解封装后的数据,再调用queueInputBuffer进行解码。

MediaCodec codec = MediaCodec.createEncoderByType("video/avc");
MediaFormat format = MediaFormat.createVideoFormat("video/avc", 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, iFrameInterval);
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
codec.start();

ByteBuffer inputBuffer = codec.getInputBuffer(index);
int sampleSize = extractor.readSampleData(inputBuffer, 0);

queueInputBuffer

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

特定于codec的数据

许多codec要求实际压缩的数据流之前必须有“特定于codec的数据”,即用于初始化codec的设置数据,如
AVC视频中的PPS/SPS。
vorbis音频中的code tables。

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

**int index:**以前调用dequeueInputBuffer(long)返回的输入buffer的索引。
**int offset:**数据开始时输入buffer中的字节偏移量。
offset参数是指输入数据缓冲区中的起始位置。一般情况下,可以将offset设置为0,表示从缓冲区的起始位置开始存储数据。

但是,在一些特殊情况下,需要从数据缓冲区中的指定位置开始存储数据,例如缓冲区前面的位置已经被其他数据占用。此时,可以将offset参数设置为需要存储数据的起始位置。

具体来说,假设我们有一个byte[]类型的输入数据数组data,需要存储到MediaCodec的输入数据缓冲区中。如果整个缓冲区都可用,可以将offset设置为0,如下所示:

int inputBufferIndex = mediaCodec.dequeueInputBuffer(timeoutUs);
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.put(data);
mediaCodec.queueInputBuffer(inputBufferIndex, 0, data.length, presentationTimeUs, flags);

如果需要从缓冲区的指定位置开始存储数据,可以将offset设置为指定位置的偏移量,如下所示:

int inputBufferIndex = mediaCodec.dequeueInputBuffer(timeoutUs);
ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex);
inputBuffer.clear();
inputBuffer.position(offset);
inputBuffer.put(data, offset, length);
mediaCodec.queueInputBuffer(inputBufferIndex, offset, length, presentationTimeUs, flags);

需要注意的是,offset的值必须小于或等于输入数据缓冲区的容量,并且数据存储的长度不能超过缓冲区的容量减去offset的值。

**int size:**有效输入数据的字节数。
size参数是指当前input buffer中有效数据的大小,单位是字节。一般来说,根据当前的输入数据,可以直接得知其大小,也可以通过计算得出。

对于音频数据,其大小可以通过采样率、采样位深、通道数和时长计算得出。例如,如果采样率为44.1kHz,采样位深为16位,双声道,时长为1秒,那么该段音频数据的大小就是44.1kHz * 16bit * 2 * 1s = 1,411,200bit = 176,400byte。

对于视频数据,其大小则与分辨率、帧率、色彩空间等相关。一般来说,对于编码器编码出的数据,可以通过MediaFormat中的关键参数获取,例如video/avc编码器编码出的数据,可以通过MediaFormat的KEY_WIDTH和KEY_HEIGHT参数获取视频分辨率,通过KEY_FRAME_RATE获取帧率。

在确定输入数据大小时,需要注意一些细节问题,例如如果当前帧为B帧,那么它的数据大小应该为0,只有参考帧才会有数据,这些细节问题需要根据具体的编码格式进行处理。

long presentationTimeUs:此buffer的PTS(以微秒为单位)。

int flags:
flags参数表示输入数据的标志位,可以控制编解码器的一些行为,通常情况下需要根据具体的场景来确定。

以下是一些常见的flags参数值及其含义:

BUFFER_FLAG_END_OF_STREAM:表示当前输入数据为结束流标记,用于告知编码器或解码器当前输入数据已经结束。当该标记设置时,queueInputBuffer方法的size参数可以为0。
BUFFER_FLAG_KEY_FRAME:表示当前输入数据为关键帧,对于视频编码器,该标记通常表示需要编码出一个完整的I帧。
BUFFER_FLAG_CODEC_CONFIG:表示当前输入数据为编解码器的配置信息,用于告知编解码器一些必要的参数。通常情况下只在第一帧时使用。
BUFFER_FLAG_SYNC_FRAME:表示当前输入数据为同步帧,通常用于音视频同步的场景中。

dequeueOutputBuffer

从MediaCodec获取输出buffer。

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

方法参数说明:

info: 一个MediaCodec.BufferInfo对象,用于存储输出缓冲区的元数据信息,如大小、时间戳等。
timeoutUs: 超时时间,单位为微秒。

返回值:已成功解码的输出buffer的索引或INFO_*常量之一(INFO_TRY_AGAIN_LATER, INFO_OUTPUT_FORMAT_CHANGED 或 INFO_OUTPUT_BUFFERS_CHANGED)。

返回INFO_TRY_AGAIN_LATER而timeoutUs指定为了非负值,表示超时了。
返回INFO_OUTPUT_FORMAT_CHANGED表示输出格式已更改,后续数据将遵循新格式。

该方法的调用流程一般如下:

应用程序调用MediaCodec的dequeueOutputBuffer方法,取出一个输出缓冲区。
应用程序对该输出缓冲区进行处理,如将其渲染到Surface上。
应用程序调用MediaCodec的releaseOutputBuffer方法将该输出缓冲区释放,以便编解码器继续使用。
需要注意的是,应用程序在处理输出缓冲区时,需要根据MediaCodec.BufferInfo对象中的offset和size字段,确定输出缓冲区的有效数据区域。同时,应用程序还需要根据MediaCodec.BufferInfo对象中的presentationTimeUs字段,将输出缓冲区的数据按照时间戳进行处理。

**BufferInfo info:**输出buffer的metadata。

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;
    };

BufferInfo参数,用于返回有关输出缓冲区的信息,包括:

**offset:**表示缓冲区中有效数据的偏移量,通常为0;
**size:**表示当前解码出来的视频帧数据大小,可以用于计算码率等相关指标。
**presentationTimeUs:**表示当前解码出来的视频帧的展示时间戳,单位是微秒。通过该参数可以帮助我们在渲染视频帧时控制视频的播放速度和时序。
**flags:**表示当前解码出来的视频帧的标志位,例如是否为关键帧、是否包含SEI信息等。flags包含如下取值:

BUFFER_FLAG_KEY_FRAME:buffer包含关键帧的数据。
BUFFER_FLAG_CODEC_CONFIG:buffer包含编解码器初始化/编解码器特定的数据,而不是媒体数据。
BUFFER_FLAG_END_OF_STREAM:标志着流的结束,即在此之后没有buffer可用,除非后面跟着flush。
BUFFER_FLAG_PARTIAL_FRAME:buffer只包含帧的一部分,解码器应该对数据进行批处理,直到在解码帧之前出现没有该标志的buffer为止。

    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;

在使用dequeueOutputBuffer方法时,需要注意BufferInfo中的presentationTimeUs参数,它表示当前输出缓冲区中数据的呈现时间。在使用时,可以通过与当前的播放时间比较来确定是否需要延迟播放。

在实际开发中,我们可以根据上述参数来实现一些高级功能,例如:

1)播放器快进/快退:通过修改presentationTimeUs的值可以让视频快进或快退。

2)实时修改视频码率:根据解码出来的视频帧大小,动态调整视频码率,以达到平衡视频质量和带宽占用的目的。

3)实时处理SEI信息:通过判断flags标志位中是否包含SEI信息,可以动态调整视频播放的效果,例如显示字幕、调整色彩等。

releaseOutputBuffer

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

public void releaseOutputBuffer (int index, 
                boolean render)

boolean render:如果在配置codec时指定了一个有效的surface,则传递true会将此输出buffer在surface上渲染。一旦不再使用buffer,该surface将把buffer释放回codec。

flush

flush()方法可以清空编解码器中的缓冲区,并重置其状态,以便重新开始一个新的编解码任务。调用flush()方法后,所有输入缓冲区和输出缓冲区都会被丢弃,并且编解码器的状态会回到初始状态。这在需要重新开始编解码时很有用。

具体来说,调用flush()方法后,以下几个操作会发生:

1.所有还没有输入到编解码器的数据都将被丢弃。
2.编解码器中的所有缓冲区都将被释放,并将其标记为可用状态。
3.编解码器的状态将被重置为未配置状态。
在编解码器被重置为未配置状态后,我们需要重新调用configure()方法重新配置编解码器。完成配置后,需要重新调用start()方法以启动编解码器。如果我们只想清空输出缓冲区而不是重置编解码器的状态,我们可以使用flush()方法的另一个重载版本,该版本接受一个boolean类型的参数,用于指定是否清空输出缓冲区。

需要注意的是,调用flush()方法可能会引起一些延迟。在调用flush()方法后,可能需要一些时间才能完成所有缓冲区的处理。因此,在实际应用中,我们应该尽量避免频繁地调用flush()方法,以避免对性能产生负面影响。

同步和异步API的使用流程

同步API的使用流程

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

异步API的使用流程

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

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

MediaCodec使用实践

通过MediaCodec进行编解码,播放视频和音频,具体示例见项目:
MediaCodecDemo

参考文章:
developer.android/reference//MediaCodec

更多音视频知识请关注公众号:进击的代码家

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的代码家

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值