美文网首页
音视频开发之旅(六)MediaCodec工作原理、流程与实践

音视频开发之旅(六)MediaCodec工作原理、流程与实践

作者: yabin小站 | 来源:发表于2020-10-24 22:29 被阅读0次

    目录

    1. MediaCodec介绍
    2. 工作原理和基本流程
    3. 数据格式
    4. 生命周期
    5. 同步和异步模式
    6. 流控
    7. AAC解码为PCM同步和异步的两种实践
    8. 遇到的问题
    9. 参考
    10. 收获

    一、介绍

    Android底层多媒体模块采用的是OpenMax框架,实现方都要遵循OpenMax标准。Google默认提供了一系列的软编软解的实现,而硬编硬解则由芯片厂商完成,所以不同芯片的手机,硬编硬解的实现和性能是会有差异的。比如我手机的编解码实现部分如下


    MediaCodec提供了一套访问底层多媒体模块的接口共应用层实现编解码功能。

    二、工作原理和基本流程

    MediaCodec使用的基本流程如下:

    - createByCodeName/createEncoderByType/createDecoderByType: (静态工厂构造MediaCodec对象)--Uninitialized状态
    - configure: (配置) -- configure状态
    - start        (启动)--进入Running状态
    - while(1) {
        try{
           - dequeueInputBuffer    (从编解码器获取输入缓冲区buffer)
           - queueInputBuffer      (buffer被生成方client填满之后提交给编解码器)
           - dequeueOutputBuffer   (从编解码器获取输出缓冲区buffer)
           - releaseOutputBuffer   (消费方client消费之后释放给编解器)
        } catch(Error e){
           - error                   (出现异常 进入error状态)
        }
        
    }
    - stop                          (编解码完成后,释放codec)
    - release
    

    基本流程结合代码两张Buffer队列示意图和生命周期图一起看,整个流程就会很清晰。


    下面我们重点看下核心的部分Buffer队列的操作。
    MediaCodec采用了2个缓冲区队列(inputBuffer和outputBuffer),异步处理数据,

    1. 数据生成方(左侧Client)从input缓冲队列申请empty buffer—》dequeueinputBuffer
    2. 数据生产方(左侧Client)把需要编解码的数据copy到empty buffer,然后繁缛到input缓冲队列 —》queueInputBuffer
    3. MediaCodec从input缓冲区队列中取一帧进行编解码处理
    4. 编解码处理结束后,MediaCodec将原始inputbuffer置为empty后放回左侧的input缓冲队列,将编解码后的数据放入到右侧output缓冲区队列
    5. 消费方Client(右侧Client)从output缓冲区队列申请编解码后的buffer —》dequeueOutputBuffer
    6. 消费方client(右侧Client)对编解码后的buffer进行渲染或者播放
    7. 渲染/播放完成后,消费方Client再将该buffer放回到output缓冲区队列 —》releaseOutputBuffer
    

    三、数据格式

    Mediacodec接受三种数据格式和两种载体 分别如下:
    压缩数据、原始音频数据和原始视频数据
    以Surface作为载体或者ByteBuffer作为载体

    1. 压缩数据
      压缩数据可以作为解码器的输入、编码器的输出,需要指定数据的格式,这样codec才知道如何处理这些压缩数据
    2. 原始音频数据 — PCM音频数据帧
    3. 原始视频数据
      视频看解码支持的常用的色彩格式有 native raw video format和 flexible YUV buffers
      native raw video format : COLOR_FormatSurface,可以用来处理surface模式的数据输入输出。
      flexible YUV buffers : 例如COLOR_FormatYUV420Flexible,可以用来处理surface模式的输出输出,在使用ByteBuffer模式的时候可以用getInput/OutputImage(int)方法来获取image数据。

    四、生命周期

    MediaCodec生命周期状态分为三种 Stopped、Executing和Released
    在上面第一部分工作原理和基本流程中我们也简单提到了,下面我们详细说下
    其中Stopped包含三种子状态 Uninitialized(为初始化状态)、Configured(已配置状态)、Error(异常状态)
    Executing也包含三个子状态 Flushed(刷新状态)、Running(运行状态)和EOS(流结束状态)
    我们重点看下Executing状态
    在调用mediacodec.start()方法后编解码器立即进入Executing状态的Flush子状态,此时编解码器会拥有所有的inputBuffer
    一旦第一个输入缓存inputbuffer被移出队列(即:queueInputBuffer),编解码器转为Running状态,编解码器的大部分生命周期会在此状态下。
    当带有end-of-stream标记的inputBuffer入队列时(queueInputBuffer),编解码器将转入EOS状态。在这种状态下,编解码器不再接收新的inputBuffer,但是仍然产生outputBuffer,知道end-of-stream标记到达输出端。
    可以在Executiong下的任何时候调用flush()使编解码器重新回到Flushed状态。

    五、同步异步模式

    Buffer的生产消费有种模式,一种是同步模式,即本文第一部分介绍的流程方式。从android5.0 google推出了异步模式,通过给codec设置回调setCalback进行buffer的生产消费操作。
    (img)
    官方给出的典型代码如下:

    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 流控

    编码器可以设置一个目标码率,但编码器的实际输出码率不会完全符合设置,在编码过程中实际可以控制的并不是最终的输出码率,而是编码过程中的一个量化参数(Quantiaztion Parameter QP),它和码率并没有固定的关系,而是取决于图像内容。
    android码率控制有两种模式
    一种是设置cofigure时设定目标码率和码率控制模式,

    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
    mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE,
    MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
    mVideoCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    

    码率控制模式有三种:
    CQ 表示完全不控制码率,尽最大可能保证图像质量, 质量要求高、不在乎带宽、解码器支持码率剧烈波动的情况下,可以选择这种策略;
    CBR 表示编码器会尽量把输出码率控制为设定值,输出码率会在一定范围内波动,对于小幅晃动,方块效应会有所改善,但对剧烈晃动仍无能为力;连续调低码率则会导致码率急剧下降,如果无法接受这个问题,那 VBR 就不是好的选择;
    VBR 表示编码器会根据图像内容的复杂度(实际上是帧间变化量的大小)来动态调整输出码率,图像复杂则码率高,图像简单则码率低,优点是稳定可控,这样对实时性的保证有帮助。所以 WebRTC 开发中一般使用的是CBR;

    另一种是动态的调整目标码率。

    Bundle param = new Bundle();
    param.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate);
    mediaCodec.setParameters(param);
    

    七、把AAC转码成PCM (音频解码)

    目的:通过该功能的实践,熟悉mediaCodec的流程,以及同步和异步两种方式实现
    具体实现如下:

    package com.av.mediajourney.mediacodec;
    
    import android.content.Context;
    import android.media.MediaCodec;
    import android.media.MediaExtractor;
    import android.media.MediaFormat;
    import android.os.Build;
    import android.os.Environment;
    import android.text.TextUtils;
    import android.util.Log;
    import android.widget.Toast;
    
    import androidx.annotation.NonNull;
    
    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    
    public class AACToPCMDelegate {
    
        private static final String TAG = "AACToPCMDelegate";
        private static final long TIMEOUT_US = 0;
        private Context mContext;
    
        private ByteBuffer[] inputBuffers;
        private ByteBuffer[] outputBuffers;
        private MediaExtractor mediaExtractor;
        private MediaCodec decodec;
    
        private FileOutputStream fileOutputStream;
    
        public AACToPCMDelegate(MediaCodecActivity context) {
            this.mContext = context;
        }
    
        /**
         * 通过aacToPCM 熟悉mediaCodec的流程,以及通过同步和异步两种方式实现
         */
        void aacToPCM() {
            boolean isAsync = true;
            if (isAsync && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                isAsync = false;
            }
    
            //1. initFile
            File file = initFile(isAsync);
            if (file == null) {
                return;
            }
    
            //2. 初始化mediaCodec
            initMediaCodec(file.getAbsolutePath(), isAsync);
            if (decodec == null) {
                Toast.makeText(mContext, "decodec is null", Toast.LENGTH_SHORT).show();
                return;
            }
    
            if (!isAsync) {
                //同步处理
                decodecAacToPCMSync();
            }
        }
    
        private File initFile(boolean isAsync) {
            String child = isAsync ? "aacToPcmAsync.pcm" : "aacToPcmSync.pcm";
            File outputfile = new File(mContext.getExternalFilesDir(Environment.DIRECTORY_MUSIC), child);
            if (outputfile.exists()) {
                outputfile.delete();
            }
            try {
                fileOutputStream = new FileOutputStream(outputfile.getAbsolutePath());
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            File file = new File(mContext.getExternalFilesDir(Environment.DIRECTORY_MUSIC), "sanguo.aac");
            if (!file.exists()) {
                Toast.makeText(mContext, "文件不存在", Toast.LENGTH_SHORT).show();
                return null;
            }
            return file;
        }
    
        private void initMediaCodec(String path, boolean isASync) {
            mediaExtractor = new MediaExtractor();
            try {
                mediaExtractor.setDataSource(path);
                int trackCount = mediaExtractor.getTrackCount();
                for (int i = 0; i < trackCount; i++) {
    
                    MediaFormat trackFormat = mediaExtractor.getTrackFormat(i);
                    String mime = trackFormat.getString(MediaFormat.KEY_MIME);
                    if (TextUtils.isEmpty(mime)) {
                        continue;
                    }
                    Log.i(TAG, "initMediaCodec: mime=" + mime);
                    if (mime.startsWith("audio/")) {
                        mediaExtractor.selectTrack(i);
                    }
                    //生成MediaCodec,此时处于Uninitialized状态
                    decodec = MediaCodec.createDecoderByType(mime);
                    //configure 处于Configured状态
                    decodec.configure(trackFormat, null, null, 0);
    
                    if (isASync) {
                        setAsyncCallBack();
                    }
                    //处于Excuting状态 Flushed子状态
                    decodec.start();
                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                        inputBuffers = decodec.getInputBuffers();
                        outputBuffers = decodec.getOutputBuffers();
                        Log.i(TAG, "initMediaCodec: inputBuffersSize=" + inputBuffers.length + " outputBuffersSize=" + outputBuffers.length);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    
        /**
         * 成功输出到目标文件
         * 到处生成的pcm,用ffplay播放pcm文件 发现和之前的aac是一样的
         * ffplay -ar 44100 -channels 2 -f s16le -i /Users/yabin/Desktop/tmp/aacToPcm.pcm
         */
        private void decodecAacToPCMSync() {
            boolean isInputBufferEOS = false;
            boolean isOutPutBufferEOS = false;
    
            while (!isOutPutBufferEOS) {
                if (!isInputBufferEOS) {
                    //1. 从codecInputBuffer中拿到empty input buffer的index
                    int index = decodec.dequeueInputBuffer(TIMEOUT_US);
                    if (index >= 0) {
                        ByteBuffer inputBuffer;
                        //2. 通过index获取到inputBuffer
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                            inputBuffer = decodec.getInputBuffer(index);
                        } else {
                            inputBuffer = inputBuffers[index];
                        }
                        if (inputBuffer != null) {
                            Log.i(TAG, "decodecAacToPCMSync: " + "  index=" + index);
    
                            inputBuffer.clear();
                        }
                        //extractor读取sampleData
                        int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
                        //3. 如果读取不到数据,则认为是EOS。把数据生产端的buffer 送回到code的inputbuffer
                        Log.i(TAG, "decodecAacToPCMSync: sampleSize=" + sampleSize);
                        if (sampleSize < 0) {
                            isInputBufferEOS = true;
                            decodec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                        } else {
                            decodec.queueInputBuffer(index, 0, sampleSize, mediaExtractor.getSampleTime(), 0);
                            //读取下一帧
                            mediaExtractor.advance();
                        }
                    }
                }
    
                MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
                //4. 数据消费端Client 拿到一个有数据的outputbuffer的index
                int index = decodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
                if (index < 0) {
                    continue;
                }
                ByteBuffer outputBuffer;
                //5. 通过index获取到inputBuffer
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    outputBuffer = decodec.getOutputBuffer(index);
                } else {
                    outputBuffer = outputBuffers[index];
                }
    
                Log.i(TAG, "decodecAacToPCMSync: outputbuffer index=" + index + " size=" + bufferInfo.size + " flags=" + bufferInfo.flags);
                //把数据写入到FileOutputStream
                byte[] bytes = new byte[bufferInfo.size];
                outputBuffer.get(bytes);
                try {
                    fileOutputStream.write(bytes);
                    fileOutputStream.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
                //6. 然后清空outputbuffer,再释放给codec的outputbuffer
                decodec.releaseOutputBuffer(index, false);
                if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    isOutPutBufferEOS = true;
                }
            }
    
            close();
        }
    
        private void close() {
            mediaExtractor.release();
            decodec.stop();
            decodec.release();
            try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private void setAsyncCallBack() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                decodec.setCallback(new MediaCodec.Callback() {
                    @Override
                    public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
                        Log.i(TAG, "setAsyncCallBack - onInputBufferAvailable: index=" + index);
                        //1. 从codecInputBuffer中拿到empty input buffer的index
                        if (index >= 0) {
                            ByteBuffer inputBuffer;
                            //2. 通过index获取到inputBuffer
                            inputBuffer = decodec.getInputBuffer(index);
                            if (inputBuffer != null) {
                                Log.i(TAG, "setAsyncCallBack- onInputBufferAvailable: " + "  index=" + index);
                                inputBuffer.clear();
                            }
                            //extractor读取sampleData
                            int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
                            //3. 如果读取不到数据,则认为是EOS。把数据生产端的buffer 送回到code的inputbuffer
                            Log.i(TAG, "setAsyncCallBack- onInputBufferAvailable: sampleSize=" + sampleSize);
                            if (sampleSize < 0) {
                                decodec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                            } else {
                                decodec.queueInputBuffer(index, 0, sampleSize, mediaExtractor.getSampleTime(), 0);
                                //读取下一帧
                                mediaExtractor.advance();
                            }
                        }
                    }
    
                    @Override
                    public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo bufferInfo) {
                        Log.i(TAG, "setAsyncCallBack - onOutputBufferAvailable: index=" + index + " size=" + bufferInfo.size + " flags=" + bufferInfo.flags);
                        //4. 数据消费端Client 拿到一个有数据的outputbuffer的index
                        if (index >= 0) {
                            ByteBuffer outputBuffer;
                            //5. 通过index获取到inputBuffer
                            outputBuffer = decodec.getOutputBuffer(index);
    
                            Log.i(TAG, "setAsyncCallBack - onOutputBufferAvailable: outputbuffer index=" + index + " size=" + bufferInfo.size + " flags=" + bufferInfo.flags);
                            //把数据写入到FileOutputStream
                            byte[] bytes = new byte[bufferInfo.size];
                            outputBuffer.get(bytes);
                            try {
                                fileOutputStream.write(bytes);
                                fileOutputStream.flush();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
    
                            //6. 然后清空outputbuffer,再释放给codec的outputbuffer
                            decodec.releaseOutputBuffer(index, false);
                            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                                close();
                            }
                        }
    
                    }
    
                    @Override
                    public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
                        Log.e(TAG, "setAsyncCallBack - onError: ");
                    }
    
                    @Override
                    public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
                        Log.i(TAG, "setAsyncCallBack - onOutputFormatChanged: ");
    
                    }
                });
            }
    
        }
    
    }
    

    八、遇到的问题

    java.lang.IllegalStateException
            at android.media.MediaCodec.getBuffer(Native Method)
            at android.media.MediaCodec.getInputBuffer(MediaCodec.java:3246)
     
    int index = decodec.dequeueInputBuffer(TIMEOUT_US); _
    之后直接进行了inputbuffer的处理,而没有判断index是否有效(index>=0)
    

    九、参考

    1. MeidaCodec官方文档
    2. Android音频开发(5):音频数据的编解码
    3. MediaCodec进行AAC编解码(文件格式转换)
    4. Android MediaCodec stuff

    十、收获

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

    1. 了解了MediaCodec工作原理和基本流程
    2. 生命周期的应用了解
    3. codec的同步和异步的使用和场景
    4. 流控的设置
    5. 通过AAC解码为PCM,同步和异步两种实现逐步理解原理流程
    6. 遇到的问题总结复盘

    感谢你的阅读。
    下一篇我们接学习实践SurfaceView 、GLSurfaceView、TextureView 、SurfaceTexture、Surface,了了它们的关系,使用方法场景和优缺点。

    欢迎关注“音视频开发之旅”,你的“再看”、“点赞”、“分享”都是莫大的支持。我们下篇见。

    欢迎讨论

    相关文章

      网友评论

          本文标题:音视频开发之旅(六)MediaCodec工作原理、流程与实践

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