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

2020-10-24  本文已影响0人  yabin小站

目录

  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,了了它们的关系,使用方法场景和优缺点。

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

欢迎讨论

上一篇下一篇

猜你喜欢

热点阅读