Android MediaCodec

MediaCodec官方图解:
在这里插入图片描述

MediaCodec操作步骤图解:
在这里插入图片描述
从API 16开始,Android提供了MediaCodec类以便开发者更加灵活的处理音视频的编解码,与MediaPlayer/VideoView等high-level APIs相比,MediaCodec是low-level APIs,因此它提供了更加完善、灵活、丰富的接口,开发者可以实现更加灵活的功能。

MediaCodec类可用于访问Android底层的多媒体编解码器,例如编码器/解码器组件。它是Android底层多媒体支持基础架构的一部分(通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface以及AudioTrack一起使用)。

1 MediaCodec使用步骤

  • 创建并配置一个MediaCodec对象

  • 循环直到完成:

    • 通过请求一个空的输入缓存(ByteBuffer),向其中填充满数据并将它传递给编解码器MediaCodec处理

    • 编解码器MediaCodec处理完这些数据并将处理结果输出至一个空的输出缓存(ByteBuffer)中

    • 使用完输出缓存的数据之后,将输出缓存容器释放回编解码器MediaCodec再次使用

  • 释放MediaCodec
    上述的操作流程如下:
    数据读入空的输入缓冲区->
    MediaCodec从输入缓冲区异步读取数据解码->
    MediaCodec将解码后的数据重新编码后输入空的输出缓冲区->
    使用完输出的数据后将空的输出缓冲区释放回MediaCodec再次使用

2 MediaCodec可以处理的数据类型

MediaCodec编解码器可以处理的三种类型的数据均可以利用ByteBuffers进行处理:

  • 压缩数据(即经过H264、H265等编码的视频数据或AAC等编码的音频数据)

  • 原始音频数据(PCM)

  • 原始视频数据(YUV或RGB【Y控制亮度,U和V控制色度;YUV主要分为两大类:YUV xxx P和YUV xxxSP,例如原格式是NV21,就是YUV420SP】)

3 MediaCodec对原始视频数据的特殊处理:

对于原始视频数据编解码时应提供一个Surface以提高编解码器的性能。Surface直接使用本地视频数据缓存(native video buffers,而没有映射或复制数据到ByteBuffers,因此,这种方式会更加高效。在使用Surface的时候,通常不能直接访问原始视频数据,但是可以使用ImageReader类来访问非安全的解(原始)视频帧。这仍然比使用ByteBuffers更加高效,因为一些本地缓存(native buffer)可以被映射到direct ByteBuffers。当使用ByteBuffer模式,你可以利用Image类和getInput/OutputImage方法来访问到原始视频数据帧。

上述的原始视频数据处理简要如下:

  • 原始视频编解码要提供一个Surface;

  • Surface不能直接访问原始视频数据,要使用ImageReader获取视频帧

4 MediaCodec生命周期

MediaCodec生命周期:
在这里插入图片描述

在编解码器的生命周期内有三种理论状态:停止态(Stopped)、执行态(Executing)、释放态(Released)。

1、停止态包括了三种子状态:

  • 未初始化(Uninitialized)

  • 配置(Configured)

  • 错误(Error)

2、执行态包括了三种子状态:

  • 刷新(Flushed)

  • 运行(Running)

  • 流结束(End-Of-Stream)

MediaCodec生命周期状态切换时机:

任意工厂方法创建了MediaCodec未初始化状态(Uninitialized)
config(…)配置状态(Configured)
start()刷新状态(Flushed),此时编解码器会拥有所有的缓存
第一个输入缓存被移除队列运行状态(Running)
一个带有end-of-stream标记的输入缓存入队列时流结束状态(End-of-Stream),此时仍然产生输出缓存直到end-of-stream标记到达输出端
在执行状态(Executing)调用flush()刷新状态(Flushed)
stop()未初始化状态(Uninitialized),此时编解码器可以重新配置
队列操作时返回错误或异常错误状态(Error),可以通过调用reset()使编解码器再次可用
reset()未初始化状态(Uninitialized)
release()释放状态(Released)

以上状态切换简要如下:

正常生命周期切换:

Uninitialized->
Configured->
Flushed->
Running->
End-of-Stream->
Released

用户手动切换:

Uninitialized->
Configured->
Flushed->
Running->
调用stop()或reset()->
Uninitialized->
Configured->
Flushed->
Running->
End-of-Stream->
Released

发生错误:

Uninitialized->
Configured->
Flushed->
Running->
Error->
Uninitialized->
Configured->
Flushed->
Running->
End-of-Stream->
Released

MediaCodec数据读取相关方法:

  • MediaCodec.getInputBuffers():获取缓存输入缓冲区数组

  • MediaCodec.getOutputBuffers():获取缓存输出缓冲区数组

  • MediaCodec.dequeueInputBuffer():获取缓存数组的输入缓冲区索引

  • MediaCodec.queueInputBuffer():将输入读入输入缓冲区

  • MediaCodec.dequeueOutputBuffer():获取缓存数组的输出缓冲区索引

  • MediaCodec.releaseOutputBuffer():释放输出缓冲区回MediaCodec

5 案例:将音频文件解码为PCM后再重新编码为AAC输出

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn_pcm_codec_aac"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="PCM音频编码为AAC" />

    <TextView
        android:id="@+id/tv_target_file_length"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:id="@+id/tv_current_file_length"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

package com.example.media.codec;

import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.text.format.Formatter;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.example.media.R;

import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;


public class MediaCodecActivity extends AppCompatActivity {
    private static final String TAG = MediaCodecActivity.class.getSimpleName();
    private static final int TAG_CODEC_PROGRESS = 0x0001; // 正在编解码
    private static final int TAG_CODEC_COMPLETED = 0x0002; // 完成编解码

    private Button mBtnPCMcodecAAC;
    private TextView mTvTargetFileLength;
    private TextView mTvCurrentFileLength;

    private String mTargetAudioPath;
    private String mOutputAudioPath;
    // 解码
    private MediaExtractor mMediaDecoderExtractor; // 分离音频文件获取音频轨PCM数据
    private MediaCodec mMediaDecoder; // 解码器
    private ByteBuffer[] mDecoderInputBuffers; // 解码输入缓冲区数组
    private ByteBuffer[] mDecoderOutputBuffers; // 解码输出缓冲区数组
    private MediaCodec.BufferInfo mDecoderBufferInfo;
    // 编码
    private MediaCodec mMediaEncoder; // 编码器
    private ByteBuffer[] mEncoderInputBuffers; // 编码输入缓冲区数组
    private ByteBuffer[] mEncoderOutputBuffers; // 编码输出缓冲区数组
    private MediaCodec.BufferInfo mEncoderBufferInfo;
    private FileOutputStream fos;
    private BufferedOutputStream bos;
    private int mBitRate; //
    private int mChannelCount; // 声道数量
    private int mSampleRate; // 采样率
    private int mSampleRateType; // 采样率类型
    private int mTargetFileTotalLength;
    private int mCurrentFileLength;
    private ArrayBlockingQueue<byte[]> mPCMQueue; // 存放解码的PCM队列
    private boolean mIsDecodeOver; // 解码完成标志

    private Handler mHandler;

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

        mTvTargetFileLength = findViewById(R.id.tv_target_file_length);
        mTvCurrentFileLength = findViewById(R.id.tv_current_file_length);

        mBtnPCMcodecAAC = findViewById(R.id.btn_pcm_codec_aac);
        mBtnPCMcodecAAC.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mBtnPCMcodecAAC.setEnabled(false);
                mBtnPCMcodecAAC.setText("正在编解码中....");

                if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                    Log.v(TAG, "external storage no exists");
                    mBtnPCMcodecAAC.setEnabled(true);
                    return;
                }

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath() +
                            "/Android/data/" + getPackageName() + "/audio/";
                    File rootDir = new File(rootPath);
                    if (!rootDir.exists()) {
                        if (!rootDir.mkdirs()) {
                            Log.v(TAG, "root dir create failed");
                            mBtnPCMcodecAAC.setEnabled(true);
                            return;
                        }
                    }
                    mTargetAudioPath = rootPath + "heavy.mp3";
                    if (!new File(mTargetAudioPath).exists()) {
                        Log.v(TAG, "target audio file no exists");
                        return;
                    }
                    mTargetFileTotalLength = (int) new File(mTargetAudioPath).length();
                    mOutputAudioPath = rootPath + "audio_aac.aac";

                    mPCMQueue = new ArrayBlockingQueue<>(10);
                    try {
                        fos = new FileOutputStream(new File(mOutputAudioPath));
                        bos = new BufferedOutputStream(fos, 200 * 1024);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                        Toast.makeText(MediaCodecActivity.this, "FileNotFoundException", Toast.LENGTH_SHORT).show();
                        return;
                    }

                    initMediaDecoder();
                    initMediaEncoder();
                    startCodec();
                }
            }
        });

        mHandler = new Handler(getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case TAG_CODEC_PROGRESS:
                        String targetFileLength = Formatter.formatFileSize(MediaCodecActivity.this, mTargetFileTotalLength);
                        String currentFileLength = Formatter.formatFileSize(MediaCodecActivity.this, msg.arg1);
                        mTvTargetFileLength.setText(targetFileLength);
                        mTvCurrentFileLength.setText(currentFileLength);
                        break;
                    case TAG_CODEC_COMPLETED:
                        mBtnPCMcodecAAC.setEnabled(true);
                        mBtnPCMcodecAAC.setText("编码完成");
                        break;
                    default:
                        break;
                }
            }
        };
        mOnCodecProgressListener = new OnCodecProgressListener() {

            @Override
            public void onProgress(long currentFileLength, long targetFileTotalLength) {
                Log.v(TAG, "currentFileLength = " + currentFileLength
                        + ", targetFileTotalLength = " + targetFileTotalLength);
            }

            @Override
            public void onCompleted() {
                release();
            }
        };
    }

    @Override
    protected void onDestroy() {
        mHandler.removeCallbacksAndMessages(null);
        release();
        super.onDestroy();
    }

    /**
     * 开启线程进行编解码
     */
    private void startCodec() {
        new Thread(new DecoderRunnable()).start();
        new Thread(new EncoderRunnable()).start();
    }

    /**
     * 编码线程
     */
    private class EncoderRunnable implements Runnable {

        @Override
        public void run() {
            while (!mIsDecodeOver || !mPCMQueue.isEmpty()) {
                PCMtoAAC();
            }

            Message msg = Message.obtain();
            msg.what = TAG_CODEC_COMPLETED;
            mHandler.sendMessage(msg);
            if (mOnCodecProgressListener != null) {
                mOnCodecProgressListener.onCompleted();
            }
        }
    }

    /**
     * 解码线程
     */
    private class DecoderRunnable implements Runnable {

        @Override
        public void run() {
            while (!mIsDecodeOver) {
                decodeToPcm();
            }
        }
    }

    /**
     * 初始化编码
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void initMediaEncoder() {
        MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, mSampleRate,
                mChannelCount);
        format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
        format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100 * 1024);
        try {
            mMediaEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); // Uninitialized
            mMediaEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // Configured
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (mMediaEncoder == null) {
            Log.v(TAG, "create encoder failed");
            return;
        }
        mMediaEncoder.start(); // Executing,Flushed
        mEncoderInputBuffers = mMediaEncoder.getInputBuffers();
        mEncoderOutputBuffers = mMediaEncoder.getOutputBuffers();
        mEncoderBufferInfo = new MediaCodec.BufferInfo();
    }

    /**
     * 初始化解码
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void initMediaDecoder() {
        try {
            mMediaDecoderExtractor = new MediaExtractor();
            mMediaDecoderExtractor.setDataSource(mTargetAudioPath);
            int trackCount = mMediaDecoderExtractor.getTrackCount();
            for (int i = 0; i < trackCount; i++) {
                MediaFormat format = mMediaDecoderExtractor.getTrackFormat(i);
                String mimeType = format.getString(MediaFormat.KEY_MIME);
                if (mimeType.startsWith("audio")) {
                    mMediaDecoderExtractor.selectTrack(i);
                    mBitRate = format.getInteger(MediaFormat.KEY_BIT_RATE);
                    mChannelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                    mSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                    mSampleRateType = ADTSUtils.getSampleRateType(mSampleRate);
                    mMediaDecoder = MediaCodec.createDecoderByType(mimeType); // Uninitialized
                    mMediaDecoder.configure(format, null, null, 0); // Configured
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (mMediaDecoder == null) {
            Log.v(TAG, "create decoder failed");
            return;
        }
        mMediaDecoder.start(); // Executing,Flushed
        mDecoderInputBuffers = mMediaDecoder.getInputBuffers();
        mDecoderOutputBuffers = mMediaDecoder.getOutputBuffers();
        mDecoderBufferInfo = new MediaCodec.BufferInfo();
    }

    /**
     * 将音频文件解码为PCM
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void decodeToPcm() {
        // dequeueInputBuffer():获取输入缓冲区数组索引
        // queueInputBuffer():将数据读入输入缓冲区
        // dequeueOutputBuffer():获取输出缓冲区数组索引
        // releaseOutputBuffer():释放输出缓冲区给MediaCodec

        // 解码流程:
        // dequeueInputBuffer()获取输入缓冲区索引->根据获取获取输入缓冲区->MediaExtractor获取数据->
        // queueInputBuffer()将数据读入输入缓冲区解码->dequeueOutputBuffer()获取输出缓冲区索引->
        // 将解码后的数据另外写入队列中->releaseOutputBuffer()释放输出缓冲区回MediaCodec

        for (int i = 0; i < mDecoderInputBuffers.length - 1; i++) {
            // 获取输入缓存数组索引
            // 传入参数为等待时间,单位微秒,-1一直等待,0不等待,建议-1避免丢帧
            int inputBufferIndex = mMediaDecoder.dequeueInputBuffer(-1);
            if (inputBufferIndex < 0) {
                mIsDecodeOver = true;
                return;
            }
            ByteBuffer inputBuffer = mDecoderInputBuffers[inputBufferIndex]; // 根据索引从缓存数组获取到输入缓冲区
            inputBuffer.clear(); // 清空一下之前传入inputBuffer的数据
            int sampleSize = mMediaDecoderExtractor.readSampleData(inputBuffer, 0); // 读取数据
            if (sampleSize < 0) {
                mIsDecodeOver = true;
            } else {
                // 通知MediaCodec解码刚才传入的数据sampleSize
                mMediaDecoder.queueInputBuffer(inputBufferIndex, 0, sampleSize, 0, 0);
                mMediaDecoderExtractor.advance();

                mCurrentFileLength += sampleSize;
                Message msg = Message.obtain();
                msg.what = TAG_CODEC_PROGRESS;
                msg.arg1 = mCurrentFileLength;
                mHandler.sendMessage(msg);
                if (mOnCodecProgressListener != null) {
                    mOnCodecProgressListener.onProgress(mCurrentFileLength, mTargetFileTotalLength);
                }
            }
        }

        // 获取输出缓存数组索引
        // 传入参数为等待时间,单位微秒,-1一直等待,0不等待,建议不要传入-1,有些时候没有数据输出那么就会一直卡住等待
        int outputBufferIndex = mMediaDecoder.dequeueOutputBuffer(mDecoderBufferInfo, 10000);
        while (outputBufferIndex >= 0) {
            ByteBuffer outputBuffer = mDecoderOutputBuffers[outputBufferIndex];
            byte[] pcmBuffer = new byte[mDecoderBufferInfo.size];
            outputBuffer.get(pcmBuffer); // 将输出缓冲区的数据写入pcmBuffer
            outputBuffer.clear(); // 要清空缓冲区,否则下次会拿到相同的数据
            putPCMData(pcmBuffer);
            mMediaDecoder.releaseOutputBuffer(outputBufferIndex, false); // 释放输出缓冲区给MediaCodec,没有该操作将不能向外输出数据
            outputBufferIndex = mMediaDecoder.dequeueOutputBuffer(mDecoderBufferInfo, 10000); // 重新获取
        }
    }

    /**
     * 将PCM编码为AAC
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void PCMtoAAC() {
        for (int i = 0; i < mEncoderInputBuffers.length - 1; i++) {
            byte[] pcm = getPCMData();
            if (pcm == null) {
                break;
            }
            int inputBufferIndex = mMediaEncoder.dequeueInputBuffer(-1);
            ByteBuffer inputBuffer = mEncoderInputBuffers[inputBufferIndex];
            inputBuffer.clear();
            inputBuffer.limit(pcm.length);
            inputBuffer.put(pcm); // PCM数据写入inputBuffer
            // 通知MediaCodec编码
            mMediaEncoder.queueInputBuffer(inputBufferIndex, 0, pcm.length, 0, 0);
        }

        int outputBufferIndex = mMediaEncoder.dequeueOutputBuffer(mEncoderBufferInfo, 10000);
        while (outputBufferIndex >= 0) {
            int outBitSize = mEncoderBufferInfo.size;
            int outPacketSize = outBitSize + 7; // 7为ADTS头部的大小
            ByteBuffer outputBuffer = mEncoderOutputBuffers[outputBufferIndex];
            outputBuffer.position(mEncoderBufferInfo.offset);
            outputBuffer.limit(mEncoderBufferInfo.offset + outBitSize);
            byte[] aacBuffer = new byte[outPacketSize];
            addADTStoPacket(aacBuffer, outPacketSize); // 添加ADTS
            outputBuffer.get(aacBuffer, 7, outBitSize); // 将编码得到的AAC数据写入字节数组aacBuffer
            outputBuffer.position(mEncoderBufferInfo.offset);
            try {
                bos.write(aacBuffer, 0, aacBuffer.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
            mMediaEncoder.releaseOutputBuffer(outputBufferIndex, false);
            outputBufferIndex = mMediaEncoder.dequeueOutputBuffer(mEncoderBufferInfo, 10000);
        }
    }

    private void putPCMData(byte[] pcm) {
        try {
            Log.v(TAG, "pcm size = " + pcm.length);
            mPCMQueue.put(pcm);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private byte[] getPCMData() {
        try {
            if (mPCMQueue.isEmpty()) {
                return null;
            }
            return mPCMQueue.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        }
    }

    private void addADTStoPacket(byte[] packet, int packetLength) {
        int profile = 2;
        int freqIdx = mSampleRateType;
        int chanCfg = 2;

        packet[0] = (byte) 0xFF;
        packet[1] = (byte) 0xF9;
        packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
        packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLength >> 11));
        packet[4] = (byte) ((packetLength & 0x7FF) >> 3);
        packet[5] = (byte) (((packetLength & 7) << 5) + 0x1F);
        packet[6] = (byte) 0xFC;
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void release() {
        if (mMediaDecoder != null) {
            mMediaDecoder.stop();
            mMediaDecoder.release();
            mMediaDecoder = null;
        }
        if (mMediaEncoder != null) {
            mMediaEncoder.stop();
            mMediaEncoder.release();
            mMediaEncoder = null;
        }
        if (mMediaDecoderExtractor != null) {
            mMediaDecoderExtractor.release();
            mMediaDecoderExtractor = null;
        }

        closeSilently(fos);
        closeSilently(bos);
    }

    private void closeSilently(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static final class ADTSUtils {
        private static Map<String, Integer> sSampleRateTypes;

        static {
            sSampleRateTypes = new HashMap<>();
            sSampleRateTypes.put("96000", 0);
            sSampleRateTypes.put("88200", 1);
            sSampleRateTypes.put("64000", 2);
            sSampleRateTypes.put("48000", 3);
            sSampleRateTypes.put("44100", 4);
            sSampleRateTypes.put("32000", 5);
            sSampleRateTypes.put("24000", 6);
            sSampleRateTypes.put("22050", 7);
            sSampleRateTypes.put("16000", 8);
            sSampleRateTypes.put("12000", 9);
            sSampleRateTypes.put("11025", 10);
            sSampleRateTypes.put("8000", 11);
            sSampleRateTypes.put("7350", 12);
        }

        static int getSampleRateType(int sampleRate) {
            return sSampleRateTypes.get(sampleRate + "");
        }
    }

    private OnCodecProgressListener mOnCodecProgressListener;
    public interface OnCodecProgressListener {
        void onProgress(long currentFileLength, long targetFileTotalLength);
        void onCompleted();
    }
    public void setOnCodecProgressListener(OnCodecProgressListener listener) {
        this.mOnCodecProgressListener = listener;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值