前言
MediaCodec是谷歌原生编解码接口,可以说只要涉及到音视频开发的学习,这个接口都是无法躲避,属于必学知识。我刚开始接触这个接口的时候,多多少少有点抵触这个接口,因为我觉得这个接口的使用流程超级复杂,但是后来看多了几个demo,发现也就那么一回事,流程都是一致的,只是在MediaCodec的配置上面,不同的格式编解码会有不同的编码配置,仅此而已。
今天我将以我自己实现的一个demo给大家展示一下这个接口的解码和编码的基本流程(MP3->PCM->AAC),同时也为我日后复习做好复习文档。
MediaCodec编解码的流程
MediaCodec 首先获取一个空的输入缓冲区,填充要编码或解码的数据,再将填充数据的输入缓冲区送到 MediaCodec 进行处理,处理完数据后会释放这个填充数据的输入缓冲区,最后获取已经编码或解码的输出缓冲区,使用完毕后释放输出缓冲区,其编解码的流程示意图如下:
各个阶段对应的 API 如下:
// 获取可用的输入缓冲区的索引
public int dequeueInputBuffer (long timeoutUs)
// 获取输入缓冲区
public ByteBuffer getInputBuffer(int index)
// 将填满数据的inputBuffer提交到编码队列
public final void queueInputBuffer(int index,int offset, int size, long presentationTimeUs, int flags)
// 获取已成功编解码的输出缓冲区的索引
public final int dequeueOutputBuffer(BufferInfo info, long timeoutUs)
// 获取输出缓冲区
public ByteBuffer getOutputBuffer(int index)
// 释放输出缓冲区
public final void releaseOutputBuffer(int index, boolean render)
我们使用MediaCodec接口,其实就是围绕这几个接口方法进行使用,大家可以先重点留意一下这几个方法,后续代码中会重点使用。
MediaCodec生命周期
MediaCodec 简单来说只有三种状态,分别是执行(Executing)、停止(Stopped)和释放(Released)。很多文章都会给出一些状态转换的图解,不可否认,超级直观好理解。但是其实我老是记不熟,所以我这次打算直接罗列一些每种状态所对应的接口方法。
- 停止状态(Stopped)
// 创建MediaCodec进入Uninitialized子状态
public static MediaCodec createByCodecName (String name)
public static MediaCodec createEncoderByType (String type)
public static MediaCodec createDecoderByType (String type)
// 配置MediaCodec进入Configured子状态,crypto和descrambler会在后文中进行说明
public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
public void configure(MediaFormat format, @Nullable Surface surface,int flags,MediaDescrambler descrambler)
// Error
// 编解码过程中遇到错误进入Error子状态
- 执行状态(Executing)
// start之后立即进入Flushed子状态
public final void start()
// 第一个输入缓冲区出队的时候进入Running子状态
public int dequeueInputBuffer (long timeoutUs)
// 输入缓冲区与流结束标记排队时,编解码器将转换为End-of-Stream子状态
// 此时MediaCodec将不接受其他输入缓冲区,但会生成输出缓冲区
public void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)
//输出缓冲区的操作也是执行状态,基本重复,这边就不进行重复叙述
- 释放状态(Released)
// 编解码完成结束后释放MediaCodec进入释放状态(Released)
public void release ()
Demo实现基本流程
了解完MediaCodec的基本流程和生命周期之后,我们便可以开始实现我们的编码解码demo,在开始之前我们需要先理清楚一下我们实现该功能的流程。
1、获取mp3文件
2、创建解码MediaCodec,进行解码操作,生成pcm文件
3、解码结束后,获取pcm文件
4、创建编码MediaCodec,进行编码操作,生成AAC文件
看上去那是非常简单,接下来我们来看看具体的实现。
代码实现
我们先来看看最后我们需要实现的一个效果。
效果非常简单,点击按钮之后,便会开始mp3的解码生成pcm文件,解码之后,便会自动获取pcm文件进行编码生成AAC文件。
Layout.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>
MainActivity
package cn.yjs.mediacodecaccapplication;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import java.io.File;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button mAudioChangeBtn;
private Handler mMainHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
verifyStoragePermissions(this);
bindView();
mMainHandler = new Handler(getMainLooper());
}
private void bindView() {
mAudioChangeBtn = findViewById(R.id.audio_change);
mAudioChangeBtn.setOnClickListener(this);
}
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.audio_change:
Log.d("yjs", "i has click the button");
//初始aac文件
File filesDir = getFilesDir();
String inputAbsolutePath = filesDir.getAbsolutePath();
final String mInputAACPath = inputAbsolutePath + "/AudioVideoLearning/test.mp3";
Log.d("yjs", "mInputAACPath:" + mInputAACPath);
File file = new File(mInputAACPath);
if (file.exists()) {
Log.d("yjs", "inputfile is exists");
}
//解码的pcm文件路径
final String mPcmPath = inputAbsolutePath + "/AudioVideoLearning/test.pcm";
Log.d("yjs", "mPcmPath:" + mPcmPath);
//pcm文件编码成的aac文件
final String mOutputAACPath = inputAbsolutePath + "/AudioVideoLearning/outputtest.aac";
AudioCodec.getPCMFromAudio(mInputAACPath, mPcmPath, new AudioCodec.AudioCodecCallback() {
@Override
public void decodeOver() {
Log.d("yjs", "IS DECODE OVER");
//解码之后,我们再将test.pcm编码成AAC格式
AudioCodec.PCMToAudio(mPcmPath, mOutputAACPath, new AudioCodec.AudioCodecCallback() {
@Override
public void decodeOver() {
Log.d("YJS", "AudioCodec.PCMToAudio OVER");
mMainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "pcm to aac success", Toast.LENGTH_LONG).show();
}
});
}
@Override
public void decodeFail() {
mMainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "pcm to aac failed", Toast.LENGTH_LONG).show();
}
});
}
});
}
@Override
public void decodeFail() {
Log.d("yjs", "IS DECODE FAIL");
}
});
break;
}
}
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
Manifest.permission.MANAGE_EXTERNAL_STORAGE};
public static void verifyStoragePermissions(Activity activity) {
try {
// 检测是否有写的权限
for (String s : PERMISSIONS_STORAGE) {
int permission = ActivityCompat.checkSelfPermission(activity,
s);
if (permission != PackageManager.PERMISSION_GRANTED) {
// 没有写的权限,去申请写的权限,会弹出对话框
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
关于上面这段代码还是比较容易看懂的,我这边集中讲解一下其中的一个语句
AudioCodec.getPCMFromAudio(String audioPath, String audioDecodePath, AudioCodecCallback audioCodecCallback)
AudioCodec可以理解成一个工具类,getPCMFromAudio传入mp3文件路径,pcm文件输出路径,以及解码的状态回调函数。这个方法内部是重新开一个线程进行解码处理,该方法体后续也会详细的展示出来。
AudioCodec.getPCMFromAudio(mInputAACPath, mPcmPath, new AudioCodec.AudioCodecCallback() {
@Override
public void decodeOver() {
Log.d("yjs", "IS DECODE OVER");
//解码之后,我们再将test.pcm编码成AAC格式
AudioCodec.PCMToAudio(mPcmPath, mOutputAACPath, new AudioCodec.AudioCodecCallback() {
可以看到在pcm文件生成之后,我们便开始了进行pcm文件的编码操作。
自此MainActivity就没有什么好说的了,我们接下来可以着重来看看AudioCodec这个工具类。
AudioCodec
首先我们来看看这个类的一个方法结构
可以看到这个工具类创建了三个回调接口,这么设计主要是为了在解码抑或是编码结束之后对于状态的回调,让开发者可以根据状态执行接下来的各种操作。
我们着重看看getPCMFromAudio方法以及PCMToAudio这两个核心方法,顾名思义,前者是将MP3格式文件转换成PCM文件的方法,后者是将PCM文件编码成为AAC文件的方法。我们先来看看getPCMFromAudio方法,做了什么事情!
getPCMFromAudio
public static void getPCMFromAudio(String audioPath, String audioDecodePath, AudioCodecCallback audioCodecCallback) {
Log.d(TAG, "getPCMFromAudio");
MediaExtractor extractor = new MediaExtractor();
int audioTrack = -1;// 音频MP3文件其实只有一个音轨
boolean hasAudio = false;// 判断音频文件是否有音频音轨
try {
extractor.setDataSource(audioPath);
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat trackFormat = extractor.getTrackFormat(i);
String trackFormatString = trackFormat.getString(MediaFormat.KEY_MIME);
Log.d("yjs", "track format:" + trackFormatString);
if (trackFormatString.startsWith("audio")) {
audioTrack = i;
hasAudio = true;
break;
}
}
if (hasAudio) {
Log.d("yjs", "begin to get pcm");
extractor.selectTrack(audioTrack);
//开始解码
new Thread(new AudioDecodeRunnable(extractor, audioDecodePath, audioTrack, new DecodeOverListener() {
@Override
public void decodeOver() {
Log.d(TAG, "IS DECODE OVER");
audioCodecCallback.decodeOver();
}
@Override
public void decodeFail() {
Log.d(TAG, "IS DECODE FAIL");
audioCodecCallback.decodeFail();
}
})).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
需要注意的是,在这里我们使用了MediaExtractor类,这个类可以让我们将流媒体文件的轨道分开。MediaExtractor从api16开始添加,可用于分离视频文件的音轨和视频轨道,如果你只想要视频,那么用selectTrack方法选中视频轨道,然后用readSampleData读出数据,这样你就得到了一个没有声音的视频,想得到音频也可以用同样的方法。
不太了解这个类的同学可以先去看看这个类,还挺有用的。
new Thread(new AudioDecodeRunnable(extractor, audioDecodePath, audioTrack, new DecodeOverListener() {
@Override
public void decodeOver() {
Log.d(TAG, "IS DECODE OVER");
audioCodecCallback.decodeOver();
}
@Override
public void decodeFail() {
Log.d(TAG, "IS DECODE FAIL");
audioCodecCallback.decodeFail();
}
})).start();
在该方法中,核心逻辑是创建一个子线程去进行解码操作,需要注意的是,这是很有必要的,因为解码是一个比较耗时的操作,如果放在主线程可能会导致应用报不响应错误。
AudioDecodeRunnable
package cn.yjs.mediacodecaccapplication;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.PrimitiveIterator;
public class AudioDecodeRunnable implements Runnable{
private static final String TAG ="YJS,AudioDecodeRunnable";
private MediaExtractor mediaExtractor;
private String mSavePath;
private int mTrackIndex;
private AudioCodec.DecodeOverListener listener;
final static int TIMEOUT_USEC = 1000;
MediaCodec audioCodec=null;
public AudioDecodeRunnable(MediaExtractor mediaExtractor,String mSavePath,int mTrackIndex,AudioCodec.DecodeOverListener listener){
this.mediaExtractor=mediaExtractor;
this.mTrackIndex=mTrackIndex;
this.mSavePath=mSavePath;
this.listener=listener;
}
@Override
public void run() {
MediaFormat trackFormat = mediaExtractor.getTrackFormat(mTrackIndex);
// 初始化音频解码器,并配置解码器属性
audioCodec = null;
try {
// 初始化音频解码器,并配置解码器属性
audioCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME));
audioCodec.configure(trackFormat, null, null, 0);
Log.d(TAG,"audioCodec.start();");
//启动MeidaCodec,等待传入数据
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;// 整体输入结束标记
File pcmSaveFile = new File(mSavePath);
FileOutputStream outputStream = new FileOutputStream(pcmSaveFile);
BufferedOutputStream bufferedOutputStream=new BufferedOutputStream(outputStream);
//输入
while (!codeOver){
if (!inputDone){
for (int i=0;i<inputBuffers.length;i++){
Log.d(TAG,"audioCodec.dequeueInputBuffer(TIMEOUT_USEC) BEGIN");
int mInputIndex = audioCodec.dequeueInputBuffer(TIMEOUT_USEC);
Log.d(TAG,"audioCodec.dequeueInputBuffer(TIMEOUT_USEC) FINISH");
if (mInputIndex>=0) {
ByteBuffer mInputBuffer = inputBuffers[mInputIndex];
mInputBuffer.clear();
int mSampleSize = mediaExtractor.readSampleData(mInputBuffer, 0);
if (mSampleSize < 0) {
Log.d(TAG, "结束输入");
audioCodec.queueInputBuffer(mInputIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone=true;
} else {
Log.d(TAG, "mSampleSize:"+mSampleSize);
inputInfo.offset = 0;
inputInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
inputInfo.size = mSampleSize;
inputInfo.presentationTimeUs = mediaExtractor.getSampleTime();
Log.d(TAG, "往解码器写入数据,当前时间戳:" + inputInfo.presentationTimeUs);
//inputInfo.offset,inputInfo.size一起描述了要写入缓冲区的具体数据内容
audioCodec.queueInputBuffer(mInputIndex, inputInfo.offset, inputInfo.size, inputInfo.presentationTimeUs, inputInfo.flags);
mediaExtractor.advance();
}
}
}
}
//输出
boolean decodeOutputDone = false;// 整体解码结束标记
while (!decodeOutputDone){
Log.d(TAG,"audioCodec.dequeueOutputBuffer(decodeBufferInfo, TIMEOUT_USEC) BEGIN");
int dequeueOutputIndex = audioCodec.dequeueOutputBuffer(decodeBufferInfo, TIMEOUT_USEC);
Log.d(TAG,"dequeueOutputIndex:"+dequeueOutputIndex);
Log.d(TAG,"audioCodec.dequeueOutputBuffer(decodeBufferInfo, TIMEOUT_USEC) FINISH");
if (dequeueOutputIndex==MediaCodec.INFO_TRY_AGAIN_LATER){
Log.d(TAG,"dequeueOutputIndex==MediaCodec.INFO_TRY_AGAIN_LATER");
decodeOutputDone=true;
}else if (dequeueOutputIndex==MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED){
Log.d(TAG,"dequeueOutputIndex==MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED");
outputBuffers=audioCodec.getOutputBuffers();
}else if (dequeueOutputIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED){
Log.d(TAG,"dequeueOutputIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED");
MediaFormat outputFormat = audioCodec.getOutputFormat();
}else if (dequeueOutputIndex<0){
Log.d(TAG,"dequeueOutputIndex<0");
}else {
ByteBuffer outputBuffer = outputBuffers[dequeueOutputIndex];
byte[] chunkpcm = new byte[decodeBufferInfo.size];
outputBuffer.get(chunkpcm);
outputBuffer.clear();
Log.d(TAG,"outputStream.write,time:"+decodeBufferInfo.presentationTimeUs);
bufferedOutputStream.write(chunkpcm);
Log.d(TAG,"outputStream.write,FINISH");
bufferedOutputStream.flush();
//有surface可以设置成true,让这些outputbuffer直接到surface
Log.d(TAG,"releaseOutputBuffer");
audioCodec.releaseOutputBuffer(dequeueOutputIndex,false);
if ((decodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {// 编解码结束
Log.d(TAG,"编解码结束");
mediaExtractor.release();
audioCodec.stop();
audioCodec.release();
codeOver = true;
decodeOutputDone = true;
}
}
}
}
outputStream.close();
listener.decodeOver();
} catch (IOException e) {
e.printStackTrace();
audioCodec.stop();
audioCodec.release();
audioCodec=null;
listener.decodeFail();
}
}
}
代码量也不算特别多,大家可以仔细看看,应该没有什么阅读门槛,我把核心的逻辑标注一下,进行简单的解释。
创建配置启动 MediaCodec
//创建MediaCodec
audioCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME));
//配置MediaCodec
audioCodec.configure(trackFormat, null, null, 0);
//启动MediaCodec
audioCodec.start();
将数据写入输入缓冲区,这里采用的是直接通过遍历将所有输入缓存区写入数据。
int mInputIndex = audioCodec.dequeueInputBuffer(TIMEOUT_USEC);
//只有获取的index大于0的时候,才表示有可用的输入缓冲区位置
if (mInputIndex>=0) {
ByteBuffer mInputBuffer = inputBuffers[mInputIndex];
mInputBuffer.clear();
//通过MediaExtractor读取mp3文件数据,注意这里是不能直接读取mp3的输入流放入到输入缓冲区中进行处理,mp3中有其他的格式,直接丢入会转换失败;
int mSampleSize = mediaExtractor.readSampleData(mInputBuffer, 0);
if (mSampleSize < 0) {
Log.d(TAG, "结束输入");
//当读取结束之后,记得给mediacodec加多一个flage表示该流结束
audioCodec.queueInputBuffer(mInputIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone=true;
} else {
Log.d(TAG, "mSampleSize:"+mSampleSize);
inputInfo.offset = 0;
inputInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
inputInfo.size = mSampleSize;
inputInfo.presentationTimeUs = mediaExtractor.getSampleTime();
Log.d(TAG, "往解码器写入数据,当前时间戳:" + inputInfo.presentationTimeUs);
//inputInfo.offset,inputInfo.size一起描述了要写入缓冲区的具体数据内容
audioCodec.queueInputBuffer(mInputIndex, inputInfo.offset, inputInfo.size, inputInfo.presentationTimeUs, inputInfo.flags);
//使用 advance() 方法来移动到下一个样本的位置,以便后续进行数据的解析或处理。
mediaExtractor.advance();
}
这里指的注意的是,将最后读到的buffer写入到输入缓冲区的时候要特别设置一个结束的flag,这样也方便后续我们获取输出缓冲区数据作为停止获取的标识。
数据写入输入缓冲区之后,MediaCodec便会开始解码数据,解码结束之后,会将解码之后的数据传递到输出缓冲区中,这时候我们将其从输出缓冲区中取出来,通过IO流写入到文件中,便得到了我们需要的PCM文件。
接下来我会抽取出获取输出缓冲区数据的核心代码逻辑进行一个简单的注释讲解。
//获取输出缓冲区索引
int dequeueOutputIndex = audioCodec.dequeueOutputBuffer(decodeBufferInfo, TIMEOUT_USEC);
//通过索引获得解码之后的数据
ByteBuffer outputBuffer = outputBuffers[dequeueOutputIndex];
byte[] chunkpcm = new byte[decodeBufferInfo.size];
outputBuffer.get(chunkpcm);
outputBuffer.clear();
Log.d(TAG,"outputStream.write,time:"+decodeBufferInfo.presentationTimeUs);
bufferedOutputStream.write(chunkpcm);
Log.d(TAG,"outputStream.write,FINISH");
bufferedOutputStream.flush();
//有surface可以设置成true,让这些outputbuffer直接到surface
Log.d(TAG,"releaseOutputBuffer");
audioCodec.releaseOutputBuffer(dequeueOutputIndex,false);
if ((decodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {// 编解码结束,这个flag是在输入的时候设置的结束标识
Log.d(TAG,"编解码结束");
mediaExtractor.release();
audioCodec.stop();
audioCodec.release();
codeOver = true;
decodeOutputDone = true;
}
大家在使用IO流的时候可以主动的去使用缓冲流,这样可以加快IO流的速度,使用也是比较简单的。
但是需要注意的是缓冲I/O流适用于大多数情况,可以提高读取和写入数据的效率。然而,有一些特定情况下,使用缓冲I/O流可能不太适合:
-
对实时性要求较高的场景:缓冲I/O流会将数据存储在内存缓冲区中,当缓冲区满了或达到一定条件时才进行实际的I/O操作。这个过程会引入一定的延迟。如果你的应用对实时性要求很高,例如需要立即响应输入或输出的变化,那么可能不适合使用缓冲I/O流。
-
处理大型二进制文件的场景:缓冲I/O流适用于处理文本数据和小型文件。但是,如果你需要处理非常大的二进制文件,例如几个GB或更大的文件,将整个文件加载到内存缓冲区可能会导致内存不足或性能问题。在这种情况下,你可能需要考虑使用流式处理,逐块读取或写入数据。
-
需要精确控制I/O操作的场景:缓冲I/O流对于大多数常见的I/O操作来说是足够的,但是如果你需要对每个字节进行细粒度的控制,例如实现某些协议或算法,那么可能需要直接使用底层的非缓冲I/O流,以便更好地掌控每个字节的读写过程。
自此将MP3格式解码成PCM的流程大概就完成了,其实核心还是对MediaCodec的使用,输入缓冲区的放入数据,输出缓冲区的拿出数据。
PCM文件我们获取之后,我们就可以将这个PCM文件进行编码成AAC文件。其实代码流程跟解码PCM文件差不多。我将这个过程封装成了一个方法——PCMToAudio,接下来我们来集中解释一下这个方法的逻辑。
PCMToAudio
public static void PCMToAudio(String mPcmInputPath, String mOutputAACPath, AudioCodecCallback audioCodecCallback) {
Log.d(TAG, "PCMToAudio");
new Thread(new AudioEncodeRunnable(mPcmInputPath, mOutputAACPath, new EnCodeOverListener() {
@Override
public void devodeOver() {
Log.d(TAG, "PCMToAudio,decodeOver");
audioCodecCallback.decodeOver();
}
@Override
public void decodeFail() {
Log.d(TAG,"PCMToAudio,decodeFail");
audioCodecCallback.decodeFail();
}
})).start();
}
可以看到这个方法也是通过创建一个子线程进行PCM文件的编码,防止直接在主线程中进行编码操作,导致应用ANR。
AudioEncodeRunnable
package cn.yjs.mediacodecaccapplication;
import android.hardware.TriggerEventListener;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.util.Log;
import java.io.BufferedInputStream;
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 {
public final String TAG = "yjs,AudioEncodeRunnable";
private String mInputPcmPath;
private String mOutputAACPath;
private AudioCodec.EnCodeOverListener enCodeOverListener;
private MediaCodec mediaEncode;
private FileInputStream fileInputStream;
private BufferedInputStream bufferedInputStream;
private FileOutputStream fileOutputStream;
private BufferedOutputStream bufferedOutputStream;
public AudioEncodeRunnable(String mInputPcmPath, String mOutputAACPath, AudioCodec.EnCodeOverListener enCodeOverListener) {
this.mInputPcmPath = mInputPcmPath;
this.mOutputAACPath = mOutputAACPath;
this.enCodeOverListener = enCodeOverListener;
}
@Override
public void run() {
Log.d(TAG, "begin to encode pcm");
try {
if (!new File(mInputPcmPath).exists()) {// pcm文件目录不存在
if (enCodeOverListener != null) {
enCodeOverListener.decodeFail();
}
return;
}
// 初始化编码格式 mimetype 采样率 声道数
//创建了一个音频格式为 AAC 的 MediaFormat 对象,采样率为 44100 Hz,声道数为 2(立体声)。
MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 2);
//每秒传输的比特数为 96000,用于控制编码后音频的质量和文件大小。
encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
//其中 AACObjectLC 表示使用低复杂度配置(Low Complexity),这是 AAC 编码的一种配置模式,适用于较低的计算资源和功耗。
encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
//设置编码器输入缓冲区的最大大小为 500 KB
encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 500 * 1024);
// 初始化编码器
mediaEncode = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaEncode.start();
fileInputStream = new FileInputStream(new File(mInputPcmPath));
bufferedInputStream = new BufferedInputStream(fileInputStream);
fileOutputStream = new FileOutputStream(new File(mOutputAACPath));
bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
//通过mediacodec获取输入输出的容器
ByteBuffer[] inputBuffers = mediaEncode.getInputBuffers();
ByteBuffer[] outputBuffers = mediaEncode.getOutputBuffers();
MediaCodec.BufferInfo encodeBufferInfo = new MediaCodec.BufferInfo();
byte[] buffer = new byte[8 * 1024];
byte[] allAudioBytes=null;
boolean isReadDone = false;
while (true) {
if (!isReadDone) {
Log.d(TAG, "get mediaEncode.dequeueInputBuffer(-1) begin");
int dequeueInputBufferIndex = mediaEncode.dequeueInputBuffer(0);
Log.d(TAG, "mediaEncode.dequeueInputBuffer(-1) end");
if (dequeueInputBufferIndex >= 0) {
int read = bufferedInputStream.read(buffer);
if (read != -1) {
Log.d(TAG, "读取到pcm文件");
allAudioBytes = Arrays.copyOf(buffer, read);
ByteBuffer inputBuffer = inputBuffers[dequeueInputBufferIndex];
inputBuffer.clear();
inputBuffer.put(allAudioBytes);
mediaEncode.queueInputBuffer(dequeueInputBufferIndex, 0, allAudioBytes.length, 0, 0);
} else {
Log.d(TAG, "读取结束");
mediaEncode.queueInputBuffer(dequeueInputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isReadDone = true;
}
} else {
Log.d(TAG, "没有可用的input 小推车");
}
}
Log.d(TAG, "encode begin to dequeueoutputbuffer");
//dequeueBuffer
int dequeueOutputBufferIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 0);
Log.d(TAG, "dequeueOutputBufferIndex:" + dequeueOutputBufferIndex);
switch (dequeueOutputBufferIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.i(TAG, "输出的format已更改" + mediaEncode.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.i(TAG, "超时,没获取到");
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.i(TAG, "输出缓冲区已更改");
break;
default:
// 从解码器中取出数据
int outBitSize = encodeBufferInfo.size;
int outPacketSize = outBitSize + 7;// 7为adts头部大小
ByteBuffer outputBuffer = outputBuffers[dequeueOutputBufferIndex];// 拿到输出的buffer
//
outputBuffer.position(encodeBufferInfo.offset);
outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
byte[] chunkAudio = new byte[outPacketSize];
addADTStoPacket(chunkAudio, outPacketSize);
outputBuffer.get(chunkAudio, 7, outBitSize);
outputBuffer.position(encodeBufferInfo.offset);
bufferedOutputStream.write(chunkAudio);
bufferedOutputStream.flush();
mediaEncode.releaseOutputBuffer(dequeueOutputBufferIndex, false);
Log.d(TAG, "mediaEncode.releaseOutputBuffer(dequeueOutputBufferIndex, false)");
break;
}
if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(TAG, "表示当前编解码已经完事了");
break;
}
}
if (enCodeOverListener != null) {
enCodeOverListener.devodeOver();
}
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
mediaEncode.stop();
mediaEncode.release();
try {
bufferedOutputStream.close();
fileOutputStream.close();
bufferedInputStream.close();
fileOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
/**
* 添加ADTS头
*
* @param packet
* @param packetLen
*/
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; // AAC LC
int freqIdx = 4; // 44.1KHz
int chanCfg = 2; // CPE
// fill in ADTS data
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;
}
}
还是一样的讲解方式,上面是完整的代码逻辑,可以自己仔细看看,我这边挑一些比较核心的语句进行讲解。
配置并启动编码器
// 初始化编码格式 mimetype 采样率 声道数
//创建了一个音频格式为 AAC 的 MediaFormat 对象,采样率为 44100 Hz,声道数为 2(立体声)。
MediaFormat encodeFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 2);
//每秒传输的比特数为 96000,用于控制编码后音频的质量和文件大小。
encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000);
//其中 AACObjectLC 表示使用低复杂度配置(Low Complexity),这是 AAC 编码的一种配置模式,适用于较低的计算资源和功耗。
encodeFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
//设置编码器输入缓冲区的最大大小为 500 KB
encodeFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 500 * 1024);
mediaEncode = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
mediaEncode.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaEncode.start();
紧接着便是想办法将PCM文件读取传入输入缓冲区
Log.d(TAG, "get mediaEncode.dequeueInputBuffer(-1) begin");
int dequeueInputBufferIndex = mediaEncode.dequeueInputBuffer(0);
Log.d(TAG, "mediaEncode.dequeueInputBuffer(-1) end");
if (dequeueInputBufferIndex >= 0) {
int read = bufferedInputStream.read(buffer);
if (read != -1) {
Log.d(TAG, "读取到pcm文件");
allAudioBytes = Arrays.copyOf(buffer, read);
ByteBuffer inputBuffer = inputBuffers[dequeueInputBufferIndex];
inputBuffer.clear();
inputBuffer.put(allAudioBytes);
mediaEncode.queueInputBuffer(dequeueInputBufferIndex, 0, allAudioBytes.length, 0, 0);
} else {
Log.d(TAG, "读取结束");
mediaEncode.queueInputBuffer(dequeueInputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isReadDone = true;
}
} else {
Log.d(TAG, "没有可用的input 小推车");
}
跟之前的解码流程差不多,读取数据传入到输入缓冲区中,然后注意在结束的时候,传入输入缓冲区一个结束flag。
紧接着我们再去获取输出缓冲区的数据
int dequeueOutputBufferIndex = mediaEncode.dequeueOutputBuffer(encodeBufferInfo, 0);
Log.d(TAG, "dequeueOutputBufferIndex:" + dequeueOutputBufferIndex);
switch (dequeueOutputBufferIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.i(TAG, "输出的format已更改" + mediaEncode.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.i(TAG, "超时,没获取到");
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.i(TAG, "输出缓冲区已更改");
break;
default:
// 从解码器中取出数据
int outBitSize = encodeBufferInfo.size;
int outPacketSize = outBitSize + 7;// 7为adts头部大小
ByteBuffer outputBuffer = outputBuffers[dequeueOutputBufferIndex];// 拿到输出的buffer
//
outputBuffer.position(encodeBufferInfo.offset);
outputBuffer.limit(encodeBufferInfo.offset + outBitSize);
byte[] chunkAudio = new byte[outPacketSize];
addADTStoPacket(chunkAudio, outPacketSize);
outputBuffer.get(chunkAudio, 7, outBitSize);
outputBuffer.position(encodeBufferInfo.offset);
bufferedOutputStream.write(chunkAudio);
bufferedOutputStream.flush();
mediaEncode.releaseOutputBuffer(dequeueOutputBufferIndex, false);
Log.d(TAG, "mediaEncode.releaseOutputBuffer(dequeueOutputBufferIndex, false)");
break;
}
if ((encodeBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i(TAG, "表示当前编解码已经完事了");
break;
}
}
这里有两个地方需要注意的,首先就是我们这边是不断去获取输出缓冲区数据,知道我们读取到结束flag停止获取输出缓冲区数据,因为这时候已经获取完了。
其次就是对于我们获取出来数据,如果直接写入到文件中其实是没有编码完成的,我们还需要再每次读取的数据前面加上ADTS才是完整的。ATDS需要七位的存储空间,所以我们需要预留七个位子在字节数组中。同时可以看到我们这里使用了bytebuffer的position,get,limit等方法,这些都是NIO典型的方法使用,我们需要另外学习一下才能更好的消化上面的代码。我这边给大家附上学习的链接,我这边就不进行详细讲解了。
玩转 ByteBuffer - 知乎 (zhihu.com)
同时获取完输出缓冲区数据之后也一定要记得执行 mediaEncode.releaseOutputBuffer方法,否则会导致非常多问题,不断可能输出缓冲区一直获取不到可用索引,也可能导致输入缓冲区也一直获取不到可用索引,所以释放这一步至关重要。
自此PCM编码为AAC的流程就差不多导通了,将填充好的数据通过IO流写入文件便可以得到AAC文件,该文件是可以直接在播放器中进行播放。大家可以通过是否可以在播放器中播放判断自己是否编码成功。
总结
其实MediaCodec的使用步骤还是非常简单的,主要是第一次使用会觉得有点生疏,所以觉得有点难以接受。其实也就是那些东西,大家认真写个demo基本就能摸透他的使用。我最后将一般使用的逻辑顺序罗列出来,大家好好消化一下即可。
1.通过IO流获取文件
2.配置启动MediaCodec
3.获取输入缓冲区可用索引
4.根据索引写入数据
5.获取输出缓冲区可用索引
6.根据索引获取数据
7.通过IO流写入到文件中