美文网首页
MediaCodec

MediaCodec

作者: JackyWu15 | 来源:发表于2021-03-04 17:25 被阅读0次

    简介

    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以信号流结束后,输入表面将立即停止向编解码器提交数据。

    相关文章

      网友评论

          本文标题:MediaCodec

          本文链接:https://www.haomeiwen.com/subject/jttidktx.html