MediaCodec

简介

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

MediaCodec_1.png

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

数据

数据载体模式:

  • ByteBuffer模式:3种数据类型都可使用ByteBuffer模式。这种模式下,通常可以使用Image类和getInput/OutputImage(int)获取原始视频帧,下面会具体列举到。

  • Surface模式:当处理原始视频数据时,应该考虑使用Surface,而不是ByteBuffer来作为数据载体,这样可以提高编解码器性能。因为,Surface使用的是更底层的视频缓冲区,而不是将数据映射或复制到ByteBuffer中,效率更高。这种模式下,可以使用ImageReader类来访问原始视频帧,并且它仍然比ByteBuffer模式高效。

数据类型:

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

  • 指定格式:通过MediaFormat.KEY_MIME来指定或获取格式,编解码器才知道如何处理这些数据。
  • 视频数据:一般情况下,输入给解码器或从编码器得到的一个ByteBuffer,都会是完整的一帧数据。除非设置了BUFFER_FLAG_PARTIAL_FRAME标记,它表示了缓冲区只包含帧的一部分,解码器会对数据进行批处理,直到没有该标志的缓冲区出现,才开始解码。比如H264解码,必须将分割符和NALU单元作为一个完整的数据帧,传给解码器才能正确解码。
  • 音频数据:音频的要求则要稍微宽松,一个ByteBuffer可能包含多个编码的音频访问单元。

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

  • 解码器输出:通过getOutputFormat()来获取MediaFormat
  • 编码器输入:通过getInputFormat()来获取MediaFormat

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

/**
  * 根据声道获取采样数据
  */
 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种颜色格式:

  • native raw video format: KEY_COLOR_FORMAT为COLOR_FormatSurface,表示该数据将是GraphicBuffer元数据的引用,在OMX中(即软编解码)中,被称为OMX_COLOR_FormatAndroidOpaque,这类格式的数据可以作为Surface模式的输入和输出。

  • flexible YUV buffers:比如CodecCapabilities.COLOR_FormatYUV420Flexible,Surface模式和ByteBuffer模式2中模式,都可以作为输入和输出。

  • other, specific formats: 除了以上2种,只支持ByteBuffer模式,在MediaCodecInfo.CodecCapabilities中可以看到许多不同的格式,只要后缀为flexible类型的,如,CodecCapabilities.COLOR_FormatRGBFlexible, 都可以使用Image类的getInput/OutputImage(int)来获取原始视频帧。

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

生命周期和状态

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


MediaCodec_4.png

1)Stopped

  • Uninitialized:当通过工厂方法了成功创建编解码器后,此时处于Uninitialized子状态
  • Configured:通过configure方法配置编解码器,此时处于Configured子状态,接着,需要调用start方法来启动,让编解码器进入Executing状态的Flushed子状态,才能输入数据给编解码器处理
  • Error

2)Executing

  • Flushed:执行start方法后,此时处于Flushed子状态
  • Running :当第一个输入缓冲区被出队,编解码器便进入Running子状态,这意味着,大部分时间编解码器都处于此状态
  • End-of-Stream:当给编解码器发送一个带有End-of-Stream标记的Buffer后,编解码器就切换为End-of-Stream子状态,此时,编解码器不再接收输入数据,但仍旧会继续输出,直到end-of-stream标记输出

3)Released

  • Released:Stopped和Executing都可切换至此状态,当使用编码器操作完成后,应该调用release方法,使编解码器进入此状态

状态重置:

  • flush:在Executing状态下,调用flush方法,来使编解码器回到Flushed子状态
  • stop:在Executing状态下,调用stop方法,使编解码器进入Uninitialized子状态,此时可以调用configure方法来重新配置,进入下一轮循环
  • reset:在某些情况下,编解码器会出现异常,此时应该使用reset而不是stop方法,使编解码进入Uninitialized状态,事实上,reset可以在任何时候被调用,如果异常发生后,不准备重新使用编解码器,那应该调用release进行释放

创建

通过编解码器类型和编解码器名字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)
  • MediaCrypto和MediaDescrambler:用于加解密处理,都是可空参数,如果不涉及加密和解密,它们没有区别,会执行同一个重载方法

  • flags:传入MediaCodec.CONFIGURE_FLAG_ENCODE时(它的值为1),用来指定创建编码器,奇怪的是MediaCodec没有定义一个类似MediaCodec.CONFIGURE_FLAG_DECODE的常量用于创建解码器,这里通常直接传入0来指定创建解码器

  • Surface:调用releaseOutputBuffer(int index, boolean render)方法将render设置为true,解码器会将输出数据渲染到此处指定的Surface,当Surface不再使用或显示时,缓冲区会自动释放给编解码器

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

//解码器配置
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

  • timeoutUs == 0,立即返回
  • timeoutUs < 0,无限期等待
  • timeoutUs > 0,等待“timeoutUs”微秒超时

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

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

  • BUFFER_FLAG_KEY_FRAME:是否关键帧
  • BUFFER_FLAG_CODEC_CONFIG:是否sps和pps等特殊数据,通常应该用MediaFormat做传递
  • BUFFER_FLAG_PARTIAL_FRAME:通常一个ByteBuffer包含一帧完整视频数据,除非指定该标志,出现该标记解码器会批量处理多个缓冲区,直到没有该标记出现,才进行解码,大多数情况下不会使用
  • BUFFER_FLAG_END_OF_STREAM:结束输入数据给解码器,除非调用flush方法,否则不要再向编解码器输入缓冲区,可以在最后一个带有有效数据的缓冲区上加上此标记,也可以用一个空的缓冲区
    来传递,此时的pts可为0

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

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返回缓冲区索引或常量值

  • INFO_TRY_AGAIN_LATER:获取超时
  • INFO_OUTPUT_BUFFERS_CHANGED:表明输出缓冲区数据已更改,在5.0前,此时用getOutputBuffers方法获取缓冲区数组,5.0后,此方式已过期,可以忽略
  • INFO_OUTPUT_FORMAT_CHANGED:表明输出格式已更改,后续数据将采用新的格式,此时可以通过getOutputFormat方法来获取新的格式,随后如果不出现超时,通常开始返回正确的输出缓冲区索引

步骤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模式作为数据输入源或输出源时,对应的缓冲区无法访问:

  • 输出源:使用输出Surface时,数据处理几乎与ByteBuffer模式相同。但是,输出缓冲区将不可访问,并表示为空值。例如,getOutputBuffer / Image(int)将返回null,getOutputBuffers将返回仅包含null的数组。

  • 输入源:使用输入Surface时,没有可访问的输入缓冲区,因为缓冲区会自动从输入表面传递到编解码器。调用dequeueInputBuffer会抛出IllegalStateException,并且getInputBuffers返回一个不可写入的伪造ByteBuffer数组。调用signalEndOfInputStream以信号流结束后,输入表面将立即停止向编解码器提交数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 前言 MediaCodec前面已经做了简介,那么这一篇就是使用了。 参考文章 官方MediaCodec Andro...
    yzzCool阅读 2,026评论 0 2
  • 本篇文章是对官方的文档MediaCodec[https://developer.android.google.cn...
    leilifengxingmw阅读 1,785评论 0 1
  • 前言 MediaCodec大坑绝对是大坑,坑的很直溜。本系列是参考 [奇卓社]的文章,喜欢的小伙伴可以直接去看[奇...
    yzzCool阅读 3,963评论 0 3
  • 简介 从 API 16开始,Android提供了MediaCodec类以便开发者更加灵活的处理音视频的编解码,较M...
    极客匠阅读 2,967评论 0 1
  • 推荐指数: 6.0 书籍主旨关键词:特权、焦点、注意力、语言联想、情景联想 观点: 1.统计学现在叫数据分析,社会...
    Jenaral阅读 5,700评论 0 5