Android音视频学习(五):MediaCodec

0.前言

前面我们介绍了Android音视频开发框架的几个主要的组件,今天我们正式介绍Android的编解码组件MediaCodec。所谓编码,就是将原始的音视频帧进行压缩,转换成压缩后的数据帧,这类压缩数据无法直接使用,但便于传输和存储,因此,在播放时需要对其进行解压缩,这一过程即是解码。

1.MediaCodec

1.1 状态转移

MediaCodec的生命周期如图所示,一个Codec会处在StoppedExecutingReleased三种基本状态之一。其中,Stopped状态又分为ConfiguredUninitializedError三个子状态,而Executing则分为FlushedRunningEnd of Stream,三种子状态。

当创建一个MediaCodec时,它就处于Uninitialized状态,此时需要调用configure()系列方法,设置对应的MediaFormat,输出的Surface等,调用configure之后就进入了Configured状态。

在配置完之后调用start()方法Codec进入到Executing状态,此时即处于Flushed子状态,并且所有的Buffer都由Codec掌控。等到应用层调用dequeueInputBuffer()将第一个input buffer出队,此时Codec进入Running子状态,之后持续地控制Buffer出入队进行编解码处理。直到将最后一个标记为EOS(end of stream)的buffer加入到队列后,此时Codec进入到End of Stream状态,此时仍然可以产生output buffer,直到输出流也到了EOS。对于解码器而言,可以通过调用flush()Executing的任意子状态跳转到Flushed状态(对于编码器,行为可能未定义的)。

Executing下调用stop()会返回到Uninitialized状态,随即可以重新配置和运行。当不再使用Cdec时,可以调用release()方法释放掉资源。

1.2 内存管理

MediaCodec的内存管理流程如上图所示,对于Buffer的创建和释放均由MediaCodec来管理。应用层使用MediaCodec的方式就是一个典型的"C-S"模式。

对于输入数据,应用层需要先向Codec申请一个空的inputBuffer,之后填充输入数据后,加入到Codec的输入队列中等待处理。之后Codec会异步地对数据进行处理。

对于输出数据,应用层可以通过同步请求/异步回调的方式,来获得一个已经填满输出数据的outputBuffer,之后使用buffer,并交由MediaCodec释放掉。

MediaCodec的buffer支持以下三种数据类型:压缩数据(Compressed Buffers),原始图像数据(Raw Video Buffers),原始音频数据(Raw Audio Buffers)。

一个具体的MediaCodec,要么是执行编码任务的Encoder,要么是执行解码任务的Decoder,对于这两种任务,输入输出数据是不同的类型。

1.2.1 压缩数据

压缩数据一般是作为解码器Decoder的输入,或者是Encoder的输出,也就是编码后的视频流/音频流数据,对应ffmpeg中的AVPacket结构。

根据输入的Codec所指定的format不同(可以由MediaExtractor得到),其中具体包含的数据类型也不同,如视频流的mime "video/avc",也就是H264流;如音频流的mime "audio/mp4a-latm",也是音频AAC流的编码的一种格式。

对于视频流,它一般是一帧单独的压缩后的视频帧,对于音频流,它一般包含几百毫秒的音频信号后的数据。

1.2.2 原始图像数据

原始的图像数据,一般是作为视频流Decoder的输出,以及视频流Encoder的输入,可以理解成ffmepg中的AVFrame结构,如H264编码时的YUV420P格式的图像数据。对于原始的图像数据,Codec也可以直接交给Surface进行渲染。

对于MediaCodec的图像数据参数,可以通过getCodecInfo()来拿到对应的MediaCodecInfo,继而获取到它对应的能力信息,这部分可以参考前面一篇文章的内容。

1.2.3 原始音频数据

原始音频数据,一般是作为音频流Decoder的输出,以及音频流Encoder的输入,在ffmpeg中也是用AVFrame进行承载,其数据内容往往是PCM(Pulse Code Modulation, 脉冲编码调制)。

1.3 同步/异步使用方式

MediaCodec支持同步/异步调用的方式。

1.3.1 同步调用方式

同步模式下是使用dequeueInputBuffer()dequeueOutputBuffer()来获得对应的输入和输出Buffer的索引。

java 复制代码
// 输入 ---------------------------------------
// 同步接口,获取inputBuffer的id
public int dequeueInputBuffer(long timeoutUs);

// 拿到对应的inputIndex对应的ByteBuffer
public ByteBuffer getInputBuffer(int index)

// 填充完ByteBuffer后,将buffer加入到Codec的队列中等待处理
public void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags);

// 输出 ---------------------------------------
// 同步接口,获取outputBuffer的id,info
public int dequeueOutputBuffer(@NonNull BufferInfo info, long timeoutUs)

// 拿到对应的outputIndex对应的ByteBuffer
public ByteBuffer getOutputBuffer(int index)

// 释放outputBuffer,如果有设置Surface,也可以设置render = true,渲染到Surface上
public void releaseOutputBuffer(int index, boolean render)

以下是官方文档上的一个同步接口的使用示例:

java 复制代码
 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();

1.3.2 异步调用方式

对于异步调用方式,需要在codec开始处理前设置对应的回调方法来获得buffer的index。

java 复制代码
// 设置异步回调
public void setCallback(@Nullable Callback cb);

// 异步回调的接口
public abstract static class Callback {
    public Callback();

    // 当inputBuffer就绪时回调,返回mediaCodec结构,和对应的inputBufferIndex
    public abstract void onInputBufferAvailable(MediaCodec mc, int inputBufferId);

    // 当outputBuffer就绪时回调
    public abstract void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, BufferInfo info);

    // 错误时回调
    public abstract void onError(@NonNull MediaCodec var1, @NonNull CodecException var2);

    // 如果buffer是有加密的,可以配置一个MediaCrypto解密,一般没用上。
    // 解密失败时回调
    public void onCryptoError(@NonNull MediaCodec codec, @NonNull CryptoException e);

    // 当输出的运行时format改变时回调
    public abstract void onOutputFormatChanged(@NonNull MediaCodec var1, @NonNull MediaFormat var2);
}

同样地,官方文档上对于异步模式的使用示例

java 复制代码
 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(...) {
    ...
  }
  @Override
  void onCryptoError(...) {
    ...
  }
 });
 codec.configure(format, ...);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

2. 例子

一个使用MediaCodec解码,并在Surface上显示的例子,包含同步和异步两个方式。

java 复制代码
public class MediaCodecActivity extends AppCompatActivity {

    final String TAG = "MediaCodecActivity";
    final int MAX_SIZE = -1;
    int frameIdx = 0;

    Button btnCodecList;

    Button btnDecodeAsync;

    Button btnDecodeSync;

    SurfaceView surfaceView;
    SurfaceHolder surfaceHolder;

    long lastFrameTimeMs = -1;

    // MediaCodec
    MediaCodec mediaCodec;
    MediaExtractor extractor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_media_codec);

        initView();
    }

    private void initView() {
        btnCodecList = findViewById(R.id.btn_show_codec_list);
        btnCodecList.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                showCodecList();
            }
        });

        btnDecodeAsync = findViewById(R.id.btn_decode_async);
        btnDecodeAsync.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                codecTestAsync();
            }
        });

        btnDecodeSync = findViewById(R.id.btn_decode_sync);
        btnDecodeSync.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Thread thread = new Thread() {
                    @Override
                    public void run() {
                        super.run();
                        codecTestSync();
                    }
                };
                thread.start();

            }
        });

        surfaceView = findViewById(R.id.surface_media_codec);
        surfaceHolder = surfaceView.getHolder();
    }

    // 展示所有的Codec
    protected void showCodecList() {
        MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
        MediaCodecInfo[] infos = list.getCodecInfos();

        for (int i = 0; i < infos.length; ++i) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                Log.i(TAG, i + " : " + infos[i].getName() + " , hw : " + infos[i].isHardwareAccelerated());
                String[] supportTypes = infos[i].getSupportedTypes();
                for(String type : supportTypes) {
                    MediaCodecInfo.CodecCapabilities capabilities = infos[i].getCapabilitiesForType(type);
                    String mime = capabilities.getMimeType();
                    Log.i(TAG, "  - mime : " + mime);
                    MediaCodecInfo.VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
                    if(videoCapabilities != null) {
                        Range<Integer> fpsRange = videoCapabilities.getSupportedFrameRates();
                        Log.i(TAG, "  - fps : [" + fpsRange.getLower() + ", " + fpsRange.getUpper() + "]");

                        Range<Integer> bitRateRange = videoCapabilities.getBitrateRange();
                        Log.i(TAG, "  - bitrate : [" + bitRateRange.getLower() + ", " + bitRateRange.getUpper() + "]");
                    }
                }
            }
        }
    }

    // 异步解码方式
    protected void codecTestAsync() {
        String filename = Environment.getExternalStorageDirectory().getPath() + "/" + getString(R.string.test_file);
        try {
            // demuxer
            extractor = new MediaExtractor();
            extractor.setDataSource(filename);
            int i = 0;
            String mime = "";
            MediaFormat format = null;
            for(; i < extractor.getTrackCount(); ++i) {
                format = extractor.getTrackFormat(i);
                mime = format.getString(MediaFormat.KEY_MIME);
                if(mime.startsWith("video")) {
                    break;
                }
            }
            extractor.selectTrack(i);
            int frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
            long frameIntervalMs = (long) (1000.0 / frameRate);

            // decoder
            mediaCodec = MediaCodec.createDecoderByType(mime);
            mediaCodec.configure(format, surfaceHolder.getSurface(), null, 0);
            mediaCodec.setCallback(new MediaCodec.Callback() {
                @Override
                public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int i) {
                    ByteBuffer buffer = mediaCodec.getInputBuffer(i);
                    int bufferSize = extractor.readSampleData(buffer, 0);
                    if(frameIdx == MAX_SIZE || bufferSize <= 0) {
                        mediaCodec.queueInputBuffer(i, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                        return;
                    }
                    mediaCodec.queueInputBuffer(i, 0, bufferSize, extractor.getSampleTime() * 1000, 0);
                    extractor.advance();
                    ++frameIdx;
                }

                @Override
                public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int i, @NonNull MediaCodec.BufferInfo bufferInfo) {
                    if(frameIdx == MAX_SIZE && ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0)) {
                        return;
                    }
                    long currentTimeMs = System.currentTimeMillis();
                    long delay = frameIntervalMs - (currentTimeMs - lastFrameTimeMs);
                    if(delay > 0) {
                        try {
                            Thread.sleep(delay);
                        } catch (InterruptedException e) {
                            Log.e(TAG, "Exception : " + e.toString());
                        }
                    }
                    lastFrameTimeMs = currentTimeMs;
                    mediaCodec.releaseOutputBuffer(i, true);
                }

                @Override
                public void onError(@NonNull MediaCodec mediaCodec, @NonNull MediaCodec.CodecException e) {

                }

                @Override
                public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec, @NonNull MediaFormat mediaFormat) {

                }
            });
            mediaCodec.start();


        } catch (Exception e) {
            Log.i(TAG, "Exception : " + e);
        }
    }

    // 同步解码方式
    protected void codecTestSync() {
        String filename = Environment.getExternalStorageDirectory().getPath() + "/" + getString(R.string.test_file);
        try {
            // demuxer
            extractor = new MediaExtractor();
            extractor.setDataSource(filename);
            int i = 0;
            String mime = "";
            MediaFormat format = null;
            for (; i < extractor.getTrackCount(); ++i) {
                format = extractor.getTrackFormat(i);
                mime = format.getString(MediaFormat.KEY_MIME);
                if (mime.startsWith("video")) {
                    break;
                }
            }
            extractor.selectTrack(i);
            int frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
            long frameIntervalMs = (long) (1000.0 / frameRate);

            // decoder
            mediaCodec = MediaCodec.createDecoderByType(mime);
            mediaCodec.configure(format, surfaceHolder.getSurface(), null, 0);
            mediaCodec.start();

            boolean isEof = false;
            while(!isEof) {
                // Input
                int inputIdx = -1;
                if((inputIdx = mediaCodec.dequeueInputBuffer(-1)) >= 0) {
                    ByteBuffer buffer = mediaCodec.getInputBuffer(inputIdx);
                    int sampleSize = extractor.readSampleData(buffer, 0);
                    if(sampleSize > 0) {
                        mediaCodec.queueInputBuffer(inputIdx, 0, sampleSize, extractor.getSampleTime() * 1000, extractor.getSampleFlags());
                        extractor.advance();
                    } else {
                        mediaCodec.queueInputBuffer(inputIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                        isEof = true;
                    }
                }

                // Output
                MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
                int outputIdx = -1;
                // 超时10ms
                outputIdx = mediaCodec.dequeueOutputBuffer(info, 10000);
                if(outputIdx >= 0) {
                    long currentTimeMs = System.currentTimeMillis();
                    long delay = frameIntervalMs - (currentTimeMs - lastFrameTimeMs);
                    if(delay > 0) {
                        Thread.sleep(delay);
                    }
                    lastFrameTimeMs = currentTimeMs;
                    mediaCodec.releaseOutputBuffer(outputIdx, true);
                }
            }

        } catch (Exception e) {
            Log.i(TAG, "Exception : " + e);
        }
    }

    @Override
    protected void onDestroy() {
        if(mediaCodec != null) {
            mediaCodec.stop();
            mediaCodec.release();
        }

        if(extractor != null) {
            extractor.release();
        }

        super.onDestroy();
    }
}

界面如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MediaCodecActivity"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_show_codec_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="CodecList"
        />

    <Button
        android:id="@+id/btn_decode_async"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Decode Async"
        />

    <Button
        android:id="@+id/btn_decode_sync"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Decode Sync"
        />

    <SurfaceView
        android:id="@+id/surface_media_codec"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        />

</LinearLayout>

显示效果:

相关推荐
在狂风暴雨中奔跑20 天前
Android+FFmpeg+x264重编码压缩你的视频
音视频开发
音视频牛哥25 天前
[2015~2024]SmartMediaKit音视频直播技术演进之路
音视频开发·视频编码·直播
音视频牛哥1 个月前
Windows平台Unity3D下RTMP播放器低延迟设计探讨
音视频开发·视频编码·直播
音视频牛哥1 个月前
Windows平台Unity3D下如何低延迟低资源占用播放RTMP或RTSP流?
音视频开发·视频编码·直播
音视频牛哥1 个月前
Android平台GB28181设备接入模块动态文字图片水印技术探究
音视频开发·视频编码·直播
陈年1 个月前
纯前端视频剪辑
音视频开发
声知视界1 个月前
音视频基础能力之 Android 音频篇 (三):高性能音频采集
android·音视频开发
音视频牛哥1 个月前
RTSP摄像头8K超高清使用场景探究和播放器要求
音视频开发·视频编码·直播
音视频牛哥1 个月前
RTMP如何实现毫秒级延迟体验?
音视频开发·视频编码·直播