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格式原理讲解