Android音视频开发学习MediaCodec API,完成音频AAC硬编硬解

Posted 小陈乱敲代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android音视频开发学习MediaCodec API,完成音频AAC硬编硬解相关的知识,希望对你有一定的参考价值。

前言

android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》。本文是Android音视频任务列表的其中一个, 对应的要学习的内容是:学习MediaCodec API,完成音频AAC硬编、硬解。

目录


(一)什么是编码、解码?

音视频领域,我们常说的编码就是压缩,解码就是解压缩。 编码的目的是减小数据的体积,减少存储空间和传输已存储文件所需的带宽。 编码后的数据是不能直接使用的,必须先解码成原来的样子。就像 zip 压缩文件里面有张图片,我们用图片查看器是无法打开的,必须先解压文件,恢复图片原来的数据,这样才能查看。音视频编解码也是同样的道理。


(二)MediaCodec简单介绍

(1)MediaCodec简单介绍

MediaCodec类是Android 官方提供的音频编解码的 API。 MediaCodec采用了基于环形缓冲区的「生产者-消费者」模型,异步处理数据。在 input 端,Client 是这个环形缓冲区「生产者」,MediaCodec 是「消费者」。在 output 端,MediaCodec 是这个环形缓冲区「生产者」,而 Client 则变成了「消费者」。

(2)MediaCodec工作流程

(1)Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer]
(2)Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer]
(3)MediaCodec 从 input 缓冲区队列取一帧数据进行编解码处理 (4)处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列 (5)Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer]
(6)Client 对编解码后的 buffer 进行渲染/播放 (7)渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JUPsZEAZ-1657540637224)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ec4027588fa6460abb53667bab239c56~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image)]

(3)MediaCodec的生命周期

MediaCodec的生命周期有三种状态:停止态-Stopped、执行态-Executing、释放态-Released。 停止状态(Stopped)包括了三种子状态:未初始化(Uninitialized)、配置(Configured)、错误(Error)。 执行状态(Executing)会经历三种子状态:刷新(Flushed)、运行(Running)、流结束(End-of-Stream)

(1)当创建编解码器的时候处于未初始化状态。首先你需要调用 configure(…) 方法让它处于 Configured 状态,然后调用 start() 方法让其处于 Executing 状态。在 Executing 状态下,你就可以使用上面提到的缓冲区来处理数据。 (2)Executing 的状态下也分为三种子状态:Flushed, Running、End-of-Stream。在 start() 调用后,编解码器处于 Flushed 状态,这个状态下它保存着所有的缓冲区。一旦第一个输入 buffer 出现了,编解码器就会自动运行到 Running 的状态。当带有 end-of-stream 标志的 buffer 进去后,编解码器会进入 End-of-Stream 状态,这种状态下编解码器不在接受输入 buffer,但是仍然在产生输出的 buffer。此时你可以调用 flush() 方法,将编解码器重置于 Flushed 状态。 (3)调用 stop() 将编解码器返回到未初始化状态,然后可以重新配置。 完成使用编解码器后,您必须通过调用 release() 来释放它。 (4)在极少数情况下,编解码器可能会遇到错误并转到错误状态。 这是使用来自排队操作的无效返回值或有时通过异常来传达的。 调用 reset() 使编解码器再次可用。 您可以从任何状态调用它来将编解码器移回未初始化状态。 否则,调用 release() 动到终端释放状态。

(4)MediaCodec编解码器支持的数据类型

压缩数据、原始音频数据和原始视频数据 你可以通过ByteBuffers能够处理这三种数据,但是需要你提供一个Surface,用于对原始的视频数据进行展示,这样也能提高编解码的性能。 Surface使用的是本地的视频缓冲区,这个缓冲区不映射或拷贝到ByteBuffers,这样的机制让编解码器的效率更高。 通常在使用Surface的时候,无法访问原始的视频数据,但是你可以使用ImageReader访问解码后的原始视频帧。在使用ByteBuffer的模式下,您可以使用Image类和getInput/OutputImage(int)访问原始视频帧。


(三)MediaCodec API简介

上面MediaCodec的生命周期的图中包含了MediaCodec一些主要的方法,下面对 MediaCodec 主要的API做一个介绍: MediaCodec创建:

  • createDecoderByType/createEncoderByType:根据特定MIME类型(如"video/avc")创建codec。
  • createByCodecName:知道组件的确切名称(如OMX.google.mp3.decoder)的时候,根据组件名创建codec。使用MediaCodecList可以获取组件的名称。

configure:配置解码器或者编码器。 start:成功配置组件后调用start。 buffer:处理的接口:

  • dequeueInputBuffer:从输入流队列中取数据进行编码操作。
  • queueInputBuffer:输入流入队列。
  • dequeueOutputBuffer:从输出队列中取出编码操作之后的数据。
  • releaseOutputBuffer:处理完成,释放ByteBuffer数据。
  • getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数
  • getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组。

flush:清空的输入和输出端口。 stop:终止decode/encode会话 release:释放编解码器实例使用的资源。

3.1 MediaCodec创建

MediaCodec的一个实例处理一种特定类型的数据(例如MP3音频或H.264视频),进行编码或解码操作。 MediaCodec创建: (1)可以使用MediaCodecList为特定的媒体格式创建一个MediaCodec。

  • 可以从MediaExtractor#getTrackFormat获得track的格式。
  • 使用MediaFormat#setFeatureEnabled注入想要添加的任何特性。
  • 然后调用MediaCodecList#findDecoderForFormat来获取能够处理该特定媒体格式的编解码器的名称。
  • 最后,使用createByCodecName(字符串)创建编解码器。

(2)还可以使用createDecoder/EncoderByType(java.lang.String)为特定MIME类型创建首选的编解码器。但是,这不能用于注入特性,并且可能会创建一个不能处理特定媒体格式的编解码器。

3.2 configure

配置codec

public void configure (
            MediaFormat format,
            Surface surface, MediaCrypto crypto, int flags); 
  • MediaFormat format:输入数据的格式(解码器)或输出数据的所需格式(编码器)。传null等同于传递MediaFormat#MediaFormat作为空的MediaFormat。
  • Surface surface:指定Surface,用于解码器输出的渲染。如果编解码器不生成原始视频输出(例如,不是视频解码器)和/或想配置解码器输出ByteBuffer,则传null。
  • MediaCrypto crypto:指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。
  • int flags:当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。

MediaFormat:封装描述媒体数据格式的信息(包括音频或视频),以及可选的特性元数据。

  • 媒体数据的格式指定为key/value对。key是字符串。值可以integer、long、float、String或ByteBuffer。
  • 特性元数据被指定为string/boolean对。

3.3 dequeueInputBuffer

public final int dequeueInputBuffer(long timeoutUs) 

返回用于填充有效数据的输入buffer的索引,如果当前没有可用的buffer,则返回-1。 long timeoutUs:等待可用的输入buffer的时间。

  • 如果timeoutUs == 0,则立即返回。
  • 如果timeoutUs < 0,则无限期等待可用的输入buffer。
  • 如果timeoutUs > 0,则等待“timeoutUs”微秒。

3.4 queueInputBuffer

在指定索引处填充输入buffer后,使用queueInputBuffer将buffer提交给组件。 特定于codec的数据

  • 许多codec要求实际压缩的数据流之前必须有“特定于codec的数据”,即用于初始化codec的设置数据
  • AVC视频中的PPS/SPS。
  • vorbis音频中的code tables。
 public native final void queueInputBuffer(
            int index,
            int offset, int size, long presentationTimeUs, int flags) 
  • int index:以前调用dequeueInputBuffer(long)返回的输入buffer的索引。
  • int offset:数据开始时输入buffer中的字节偏移量。
  • int size:有效输入数据的字节数。
  • long presentationTimeUs:此buffer的PTS(以微秒为单位)。
  • int flags:一个由BUFFER_FLAG_CODEC_CONFIG和BUFFER_FLAG_END_OF_STREAM标志组成的位掩码。虽然没有被禁止,但是大多数codec并不对输入buffer使用BUFFER_FLAG_KEY_FRAME标志。

(1)BUFFER_FLAG_END_OF_STREAM:用于指示这是输入数据的最后一部分。 (2)BUFFER_FLAG_CODEC_CONFIG:通过指定这个标志,可以在start()或flush()之后直接提交特定于codec的数据buffer。但是,如果您使用包含这些密钥的媒体格式配置编解码器,它们将在启动后由MediaCodec直接自动提交。因此,不建议使用BUFFER_FLAG_CODEC_CONFIG标志,只建议高级用户使用。

3.5 dequeueOutputBuffer

从MediaCodec获取输出buffer。

public final int dequeueOutputBuffer(
            @NonNull BufferInfo info, long timeoutUs) 

返回值:已成功解码的输出buffer的索引或INFO_*常量之一(INFO_TRY_AGAIN_LATER, INFO_OUTPUT_FORMAT_CHANGED 或 INFO_OUTPUT_BUFFERS_CHANGED)。 返回INFO_TRY_AGAIN_LATER而timeoutUs指定为了非负值,表示超时了。 返回INFO_OUTPUT_FORMAT_CHANGED表示输出格式已更改,后续数据将遵循新格式。 BufferInfo info:输出buffer的metadata。 long timeoutUs:含义同dequeueInputBuffer中的timeoutUs参数。

BufferInfo

public final static class BufferInfo 
        public void set(
                int newOffset, int newSize, long newTimeUs, int newFlags);
        public int offset;
        public int size;
        public long presentationTimeUs;
        public int flags;
; 

offset:buffer中数据的起始偏移量。 注意设备之间的offset是不一致的。在一些设备上,offset是相对裁剪矩形的左上角像素,而在大多数设备上,offset是相对整个帧的左上角像素。 size:buffer中的数据量(以字节为单位)。如果是0则表示buffer中没有数据,可以丢弃。0大小的buffer的唯一用途是携带流结束标记。 presentationTimeUs:buffer的PTS(以微秒为单位)。来源于相应输入buffer一起传入的PTS。对于大小为0的buffer,应该忽略这个值。 flags:与buffer关联的标识信息,flags包含如下取值:

  • BUFFER_FLAG_KEY_FRAME:buffer包含关键帧的数据。
  • BUFFER_FLAG_CODEC_CONFIG:buffer包含编解码器初始化/编解码器特定的数据,而不是媒体数据。
  • BUFFER_FLAG_END_OF_STREAM:标志着流的结束,即在此之后没有buffer可用,除非后面跟着flush。
  • BUFFER_FLAG_PARTIAL_FRAME:buffer只包含帧的一部分,解码器应该对数据进行批处理,直到在解码帧之前出现没有该标志的buffer为止。
public static final int BUFFER_FLAG_KEY_FRAME = 1;
public static final int BUFFER_FLAG_CODEC_CONFIG = 2;
public static final int BUFFER_FLAG_END_OF_STREAM = 4;
public static final int BUFFER_FLAG_PARTIAL_FRAME = 8; 

3.6 releaseOutputBuffer

使用此方法将输出buffer返回给codec或将其渲染在输出surface。

public void releaseOutputBuffer (int index, boolean render) 

boolean render:如果在配置codec时指定了一个有效的surface,则传递true会将此输出buffer在surface上渲染。一旦不再使用buffer,该surface将把buffer释放回codec。


(四)同步和异步API的使用流程

4.1 同步API的使用流程

- 创建并配置MediaCodec对象。
- 循环直到完成:
  - 如果输入buffer准备好了:
    - 读取一段输入,将其填充到输入buffer中
  - 如果输出buffer准备好了:
    - 从输出buffer中获取数据进行处理。
- 处理完毕后,release MediaCodec 对象。 

官方文档中给出的同步API的代码示例

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(); 

4.2 异步API的使用流程

在Android 5.0, API21,引入了“异步模式”。

- 创建并配置MediaCodec对象。
- 给MediaCodec对象设置回调MediaCodec.Callback
- 在onInputBufferAvailable回调中:
    - 读取一段输入,将其填充到输入buffer中
- 在onOutputBufferAvailable回调中:
    - 从输出buffer中获取数据进行处理。
- 处理完毕后,release MediaCodec 对象。 

官方文档中给出的异步API的代码示例

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(); 

(五)完整代码

首先将aac解码成PCM,再将PCM编码成aac格式的音频文件

(1)布局:

activity_main.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/audio_change"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="音频转换"/>

</LinearLayout> 

(2)代码:

MainActivity

package com.lzacking.mediacodecaac;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener 

    private static final String TAG = "MainActivity";
    private Button btnAudioChange;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 获取权限
        verifyStoragePermissions(this);

        btnAudioChange = (Button)findViewById(R.id.audio_change);
        btnAudioChange.setOnClickListener(this);
    

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void onClick(View v) 
        switch (v.getId()) 
            case R.id.audio_change:
                Log.e(TAG,"点击了按钮");
                // 首先将aac解码成PCM,再将PCM编码成aac格式的音频文件
                // aac文件(初始文件)
                final String aacPath = Environment.getExternalStorageDirectory().getPath() + "/The Dawn_clip.aac";
                // pcm文件
                final String pcmPath = Environment.getExternalStorageDirectory().getPath() + "/The Dawn_clip.pcm";
                // aac文件(结果文件)
                final String aacResultPath = Environment.getExternalStorageDirectory().getPath() + "/The Dawn_clip1.aac";

                AudioCodec.getPCMFromAudio(aacPath, pcmPath, new AudioCodec.AudioDecodeListener() 
                    @Override
                    public void decodeOver() 
                        Log.e(TAG,"音频解码完成" + pcmPath);

                        // 解码成功之后,开始编码
                        AudioCodec.PcmToAudio(pcmPath, aacResultPath, new AudioCodec.AudioDecodeListener() 
                            @Override
                            public void decodeOver() 
                                Log.e(TAG,"音频编码完成");
                            

                            @Override
                            public void decodeFail() 
                                Log.e(TAG,"音频编码失败");
                            
                        );
                    

                    @Override
                    public void decodeFail() 
                        Log.e(TAG,"音频解码失败");
                    

                );
                break;

            default:
                break;
        
    

    private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private static String[] PERMISSIONS_STORAGE = 
            "android.permission.READ_EXTERNAL_STORAGE",
            "android.permission.WRITE_EXTERNAL_STORAGE" ;

    public static void verifyStoragePermissions(Activity activity) 
        try 
            // 检测是否有写的权限
            int permission = ActivityCompat.checkSelfPermission(activity,
                    "android.permission.WRITE_EXTERNAL_STORAGE");
            if (permission != PackageManager.PERMISSION_GRANTED) 
                // 没有写的权限,去申请写的权限,会弹出对话框
                ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
            
         catch (Exception e) 
            e.printStackTrace();
        
    

 

AudioCodec:

package com.lzacking.mediacodecaac;

import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import androidx.annotation.RequiresApi;

import java.io.IOException;

/**
* 音频相关的操作类
*/
public class AudioCodec 

    private static final String TAG = "AudioCodec";
    private static Handler handler = new Handler(Looper.getMainLooper());

    /**
     * 将音频文件解码成原始的PCM数据
     * @param audioPath         音频文件目录
     * @param audiosavePath     pcm文件保存位置
     * @param listener
     */
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    public static void getPCMFromAudio(String audioPath, String audioSavePath, final AudioDecodeListener listener) 
        MediaExtractor extractor = new MediaExtractor();// 此类可分离视频文件的音轨和视频轨道
        int audioTrack = -1;// 音频MP3文件其实只有一个音轨
        boolean hasAudio = false;// 判断音频文件是否有音频音轨

        try 
            extractor.setDataSource(audioPath);
            for (int i = 0; i < extractor.getTrackCount(); i++) 
                MediaFormat format = extractor.getTrackFormat(i);
                String mime = format.getString(MediaFormat.KEY_MIME);
                if (mime.startsWith("audio/")) 
                    audioTrack = i;
                    hasAudio = true;
                    break;
                
            

            if (hasAudio) 
                extractor.selectTrack(audioTrack);
                // 原始音频解码
                new Thread(new AudioDecodeRunnable(extractor, audioTrack, audioSavePath, new DecodeOverListener() 
                    @Override
                    public void decodeIsOver() 
                        handler.post(new Runnable() 
                            @Override
                            public void run() 
                                if (listener != null) 
                                    listener.decodeOver();
                                
                            
                        );
                    

                    @Override
                    public void decodeFail() 
                        handler.post(new Runnable() 
                            @Override
                            public void run() 
                                if (listener != null) 
                                    listener.decodeFail();
                                
                            
                        );
                    
                )).start();

             else // 如果音频文件没有音频音轨
                Log.e(TAG,"音频文件没有音频音轨");
                if (listener != null) 
                    listener.decodeFail();
                
            
         catch (IOException e) 
            e.printStackTrace();
            Log.e(TAG,"解码失败");
            if (listener != null) 
                listener.decodeFail();
            
        

    



    /**
     * pcm文件转音频
     * @param pcmPath       pcm文件目录
     * @param audioPath     音频文件目录
     * @param listener
     */
    public static void PcmToAudio(String pcmPath,String audioPath,final AudioDecodeListener listener) 

        new Thread(new AudioEncodeRunnable(pcmPath, audioPath, new AudioDecodeListener() 
            @Override
            public void decodeOver() 
                if (listener != null) 
                    handler.post(new Runnable() 
                        @Override
                        public void run() 
                            listener.decodeOver();
                        
                    );
                
            

            @Override
            public void decodeFail() 
                if (listener != null)
                    handler.post(new Runnable() 
                        @Override
                        public void run() 
                            listener.decodeFail();
                        
                    );
                
            
        )).start();
    

    /**
     * 写入ADTS头部数据
     * @param packet
     * @param packetLen
     */
    public static void addADTStoPacket(byte[] packet, int packetLen) 
        int profile = 2; // AAC LC
        int freqIdx = 4; // 44.1KHz
        int chanCfg = 2; // CPE

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

    public interface DecodeOverListener 
        void decodeIsOver();
        void decodeFail();
    


    /**
     * 音频解码监听器:监听是否解码成功
     */
    public interface AudioDecodeListener 
        void decodeOver();
        void decodeFail();
    

 

AudioDecodeRunnable:

package com.lzacking.mediacodecaac;

import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Build;
import android.util.Log;

import androidx.annotation.RequiresApi;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

/**
* 音频解码过程
*/
public class AudioDecodeRunnable implements Runnable 

    private static final String TAG = "AudioDecodeRunnable";
    final static int TIMEOUT_USEC = 0;
    private MediaExtractor extractor;
    private int audioTrack;
    private AudioCodec.DecodeOverListener mListener;
    private String mPcmFilePath;

    public AudioDecodeRunnable(MediaExtractor extractor, int trackIndex, String savePath, AudioCodec.DecodeOverListener listener) 
        this.extractor = extractor;
        audioTrack = trackIndex;
        mListener = listener;
        mPcmFilePath = savePath;
    

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void run() 
        try 
            // 直接从MP3音频文件中得到音轨的MediaFormat
            MediaFormat format = extractor.getTrackFormat(audioTrack);
            // 初始化音频解码器,并配置解码器属性
            MediaCodec audioCodec = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
            audioCodec.configure(format, null, null, 0);

            // 启动MediaCodec,等待传入数据
            audioCodec.start();

            ByteBuffer[] inputBuffers = audioCodec.getInputBuffers();// 获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组
            ByteBuffer[] outputBuffers = audioCodec.getOutputBuffers();// 获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组
            MediaCodec.BufferInfo decodeBufferInfo = new MediaCodec.BufferInfo();// 用于描述解码得到的byte[]数据的相关信息
            MediaCodec.BufferInfo inputInfo = new MediaCodec.BufferInfo();// 用于描述输入数据的byte[]数据的相关信息
            boolean codeOver = false;
            boolean inputDone = false;// 整体输入结束标记

            FileOutputStream fos = new FileOutputStream(mPcmFilePath);

            while (!codeOver) 
                if (!inputDone) 
                    for (int i = 0; i < inputBuffers.length; i++) 
                        // 从输入流队列中取数据进行操作
                        // 返回用于填充有效数据的输入buffer的索引,如果当前没有可用的buffer,则返回-1
                        int inputIndex = audioCodec.dequeueInputBuffer(TIMEOUT_USEC);
                        if (inputIndex >= 0) 
                            // 从分离器拿出输入,写入解码器
                            // 拿到inputBuffer
                            ByteBuffer inputBuffer = inputBuffers[inputIndex];
                            // 将position置为0,并不清除buffer内容
                            inputBuffer.clear();
                            int sampleSize = extractor.readSampleData(inputBuffer,0);// 将MediaExtractor读取数据到inputBuffer
                            if (sampleSize < 0)// 表示所有数据已经读取完毕
                                audioCodec.queueInputBuffer(inputIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                             else 
                                inputInfo.offset = 0;
                                inputInfo.size = sampleSize;
                                inputInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
                                inputInfo.presentationTimeUs = extractor.getSampleTime();

                                Log.e(TAG,"往解码器写入数据,当前时间戳:" + inputInfo.presentationTimeUs);
                                // 通知MediaCodec解码刚刚传入的数据
                                audioCodec.queueInputBuffer(inputIndex, inputInfo.offset, sampleSize, inputInfo.presentationTimeUs, 0);
                                // 读取下一帧数据
                                extractor.advance();
                            
                        
                    
                


                // dequeueInputBuffer dequeueOutputBuffer 返回值解释
                // INFO_TRY_AGAIN_LATER=-1 等待超时
                // INFO_OUTPUT_FORMAT_CHANGED=-2 媒体格式更改
                // INFO_OUTPUT_BUFFERS_CHANGED=-3 缓冲区已更改(过时)
                // 大于等于0的为缓冲区数据下标

                boolean decodeOutputDone = false;// 整体解码结束标记
                byte[] chunkPCM;
                while (!decodeOutputDone) 
                    int outputIndex = audioCodec.dequeueOutputBuffer(decodeBufferInfo, TIMEOUT_USEC);
                    if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) 
                        // 没有可用的解码器
                        decodeOutputDone = true;
                     else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) 
                        outputBuffers = audioCodec.getOutputBuffers();
                     else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) 
                        MediaFormat newFormat = audioCodec.getOutputFormat();
                     else if (outputIndex < 0) 

                     else 
                        ByteBuffer outputBuffer;
                        if (Build.VERSION.SDK_INT >= 21) 
                            outputBuffer = audioCodec.getOutputBuffer(outputIndex);
                         else 
                            outputBuffer = outputBuffers[outputIndex];
                        

                        chunkPCM = new byte[decodeBufferInfo.size];
                        outputBuffer.get(chunkPCM);
                        outputBuffer.clear();

                        fos.write(chunkPCM);//数据写入文件中
                        fos.flush();
                        Log.e(TAG,"释放输出流缓冲区:" + outputIndex);
                        audioCodec.releaseOutputBuffer(outputIndex,false);

                        if ((decodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) // 编解码结束
                            extractor.release();
                            audioCodec.stop();
                            audioCodec.release();
                            codeOver = true;
                            decodeOutputDone = true;
                        
                    
                
            

            fos.close();
            mListener.decodeIsOver();
            if (mListener != null) 
                mListener.decodeIsOver();
            

         catch (IOException e) 
            e.printStackTrace();
            if (mListener != null) 
                mListener.decodeFail();
            
        

    
 

AudioEncodeRunnable:

package com.lzacking.mediacodecaac;

import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.util.Log;

import androidx.annotation.RequiresApi;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
* 音频编码过程
*/
public class AudioEncodeRunnable implements Runnable 

    private static final String TAG = "AudioEncodeRunnable";
    private String pcmPath;
    private String audioPath;
    private AudioCodec.AudioDecodeListener mListener;

    public AudioEncodeRunnable(String pcmPath, String audioPath, final AudioCodec.AudioDecodeListener listener) 
        this.pcmPath = pcmPath;
        this.audioPath = audioPath;
        mListener = listener;
    

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void run() 
        try 
            if (!new File(pcmPath).exists()) // pcm文件目录不存在
                if (mListener != null) 
                    mListener.decodeFail();
                
                return;
            

            FileInputStream fis = new FileInputStream(pcmPath);
            byte[] buffer = new byte[8 * 1024];
            byte[] allAudioBytes;

            int inputIndex;
            ByteBuffer inputBuffer;
            int outputIndex;
            ByteBuffer outputBuffer;

            byte[] chunkAudio;
            int outBitSize;
            int outPacketSize;

            // 初始化编码格式   mimetype  采样率  声道数
            MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 2);
            encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
            encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 500 * 1024);

            // 初始化编码器
            MediaCodec mediaEncode = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
            mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mediaEncode.start();

            ByteBuffer[] encodeInputBuffers = mediaEncode.getInputBuffers();
            ByteBuffer[] encodeOutputBuffers = mediaEncode.getOutputBuffers();
            MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo();

            // 初始化文件写入流
            FileOutputStream fos = new FileOutputStream(new File(audioPath));
            BufferedOutputStream bos = new BufferedOutputStream(fos, 500 * 1024);
            boolean isReadEnd = false;
            while (!isReadEnd) 
                for (int i = 0; i < encodeInputBuffers.length - 1; i++) // 减掉1很重要,不要忘记
                    if (fis.read(buffer) != -1) 
                        allAudioBytes = Arrays.copyOf(buffer, buffer.length);
                     else 
                        Log.e(TAG,"文件读取完成");
                        isReadEnd = true;
                        break;
                    

                    Log.e(TAG,"读取文件并写入编码器" + allAudioBytes.length);
                    // 从输入流队列中取数据进行编码操作
                    inputIndex = mediaEncode.dequeueInputBuffer(-1);

                    inputBuffer = encodeInputBuffers[inputIndex];
                    inputBuffer.clear();
                    inputBuffer.limit(allAudioBytes.length);
                    inputBuffer.put(allAudioBytes);// 将pcm数据填充给inputBuffer
                    // 在指定索引处填充输入buffer后,使用queueInputBuffer将buffer提交给组件。
                    mediaEncode.queueInputBuffer(inputIndex, 0, allAudioBytes.length, 0, 0);// 开始编码
                

                // 从输入流队列中取数据进行编码操作
                outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo,10000);
                while (outputIndex >= 0) 
                    // 从解码器中取出数据
                    outBitSize = encodeBufferInfo.size;
                    outPacketSize = outBitSize + 7;// 7为adts头部大小
                    outputBuffer = encodeOutputBuffers[outputIndex];// 拿到输出的buffer
                    outputBuffer.position(encodeBufferInfo.offset);
                    outputBuffer.limit(encodeBufferInfo.offset + outBitSize);

                    chunkAudio = new byte[outPacketSize];
                    AudioCodec.addADTStoPacket(chunkAudio, outPacketSize);// 添加ADTS
                    outputBuffer.get(chunkAudio, 7, outBitSize);// 将编码得到的AAC数据取出到byte[]中,偏移量为7
                    outputBuffer.position(encodeBufferInfo.offset);
                    Log.e(TAG, "编码成功并写入文件" + chunkAudio.length);
                    bos.write(chunkAudio,0,chunkAudio.length);// 将文件保存在sdcard中
                    bos.flush();

                    mediaEncode.releaseOutputBuffer(outputIndex,false);
                    outputIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo,10000);
                
            

            mediaEncode.stop();
            mediaEncode.release();
            fos.close();

            if (mListener != null)
                mListener.decodeOver();
            
         catch (IOException e) 
            e.printStackTrace();
            if (mListener != null)
                mListener.decodeFail();
            
        
    

 

(3)权限

<uses-permission 
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission 
android:name="android.permission.READ_EXTERNAL_STORAGE" /> 

(4)结果

最后

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长。而不成体系的学习效果低效漫长且无助。时间久了,付出巨大的时间成本和努力,没有看到应有的效果,会气馁是再正常不过的。

所以学习一定要找到最适合自己的方式,有一个思路方法,不然不止浪费时间,更可能把未来发展都一起耽误了。

如果你是卡在缺少学习资源的瓶颈上,那么刚刚好我能帮到你。以上知识笔记全部免费分享,**如有需要获取知识笔记的朋友,可以点击下方二维码费领取。

以上是关于Android音视频开发学习MediaCodec API,完成音频AAC硬编硬解的主要内容,如果未能解决你的问题,请参考以下文章

给Android工程师的音视频教程之一文弄懂MediaCodec

Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜

Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜

Android 音视频编解码 -- 视频编码和H264格式原理讲解

Android 音视频编解码 -- 视频编码和H264格式原理讲解

Android媒体解码MediaCodec MediaExtractor学习