MediaCodec的使用介绍
目录
- 概述
- 支持的数据类型
- 使用MediaCodec的编解码流程
- MediaCodec生命周期
- MediaCodec API简介
- 同步和异步API的使用流程
- 示例程序
参考
- [1] developer.android/reference//MediaCodec
- [2] bigflake/Android MediaCodec stuff
- [3] 瞎猫/初识MediaCodec
1. 概述
推荐以官方文档[1]作为主要的参考,其中有详细的介绍。
MediaCodec是Android提供的用于对音视频进行编解码的类,它通过访问底层的codec来实现编解码的功能。是Android media基础框架的一部分,通常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface和AudioTrack 一起使用。
历史:
- Android 4.1, API16,MediaCodec的首个版本。
- Android 4.3, API18,扩展了一种通过 Surface 提供输入的方法(createInputSurface),这样允许输入来自于摄像头的预览或者是经过OpenGL ES渲染。
- Android 5.0, API21,引入了“异步模式”。
强烈建议从示例代码开始了解MediaCodec,而不是试图从文档把它搞清楚。
2. 支持的数据类型
编解码器支持的数据类型: 压缩的音视频据、原始音频数据和原始视频数据。
- 数据通过ByteBuffers类来表示。
- 可以设置Surface来获取/呈现原始的视频数据,Surface使用本地的视频buffer,不需要进行ByteBuffers拷贝。可以让编解码器的效率更高。
- 通常在使用Surface的时候,无法访问原始的视频数据,但是可以使用ImageReader访问解码后的原始视频帧。在使用ByteBuffer的模式下,可以使用Image类和getInput/OutputImage(int)获取原始视频帧。
压缩数据:
- MediaFormat#KEY_MIME格式类型。
- 对于视频类型,通常是一个单独的压缩视频帧。
- 对于音频数据,通常是一个单独的访问单元(一个编码的音频段通常包含由格式类型决定的几毫秒的音频),但是这个要求稍微宽松一些,因为一个buffer可能包含多个编码的音频访问单元。
- 在这两种情况下,buffer都不会在任意字节边界上开始或结束,而是在帧/访问单元边界上开始或结束,除非它们被BUFFER_FLAG_PARTIAL_FRAME标记。
原始音频buffer
原始音频buffer包含PCM音频数据的整个帧,这是每个通道按通道顺序的一个样本。每个样本都是一个 AudioFormat#ENCODING_PCM_16BIT。
原始视频buffer
在ByteBuffer模式下,视频buffer根据它们的MediaFormat#KEY_COLOR_FORMAT进行布局。可以从getCodecInfo(). MediaCodecInfo.getCapabilitiesForType.CodecCapability.colorFormats获取支持的颜色格式。视频编解码器可以支持三种颜色格式:
- native raw video format: CodecCapabilities.COLOR_FormatSurface,可以与输入/输出的Surface一起使用。
- flexible YUV buffers 例如CodecCapabilities.COLOR_FormatYUV420Flexible, 可以使用getInput/OutputImage(int)与输入/输出Surface一起使用,也可以在ByteBuffer模式下使用。
- other, specific formats: 通常只支持ByteBuffer模式。有些颜色格式是厂商特有的,其他定义在CodecCapabilities。对于等价于flexible格式的颜色格式,可以使用getInput/OutputImage(int)。
从Build.VERSION_CODES.LOLLIPOP_MR1.开始,所有视频编解码器都支持flexible的YUV 4:2:0 buffer。
3. 使用MediaCodec的编解码流程
一些编解码器对于它们的buffer要求是比较特殊的,比如内存对齐或是有特定的最小最大限制,为了适应广泛的可能性,buffer分配是由编解码器实现的。
- 这看起来和“零拷贝”原则是相悖的,但大部分情况发生拷贝的几率是比较小的,因为编解码器并不需要复制或调整这些数据来满足要求,而且大多数情况可以直接使用buffer,比如直接从磁盘或网络读取数据到buffer中,不需要复制。
MediaCodec采用异步方式处理数据,并且使用了一组输入输出buffer(ByteBuffer)。
- 使用者从MediaCodec请求一个空的输入buffer(ByteBuffer),填充满数据后将它传递给MediaCodec处理。
- MediaCodec处理完这些数据并将处理结果输出至一个空的输出buffer(ByteBuffer)中。
- 使用者从MediaCodec获取输出buffer的数据,消耗掉里面的数据,使用完输出buffer的数据之后,将其释放回编解码器。
流程如下图所示:
image.png
4. MediaCodec的生命周期
MediaCodec的生命周期有三种状态:Stopped、Executing、Released。
- Stopped,包含三种子状态:Uninitialized、Configured、Error。
-
Executing,包含三种子状态:Flushed、Running、End-of-Stream。
image.png
Stopped的三种子状态:
- Uninitialized:当创建了一个MediaCodec对象,此时处于Uninitialized状态。可以在任何状态调用reset()方法使MediaCodec返回到Uninitialized状态。
- Configured:使用configure(…)方法对MediaCodec进行配置转为Configured状态。
- Error:MediaCodec遇到错误时进入Error状态。错误可能是在队列操作时返回的错误或者异常导致的。
Executing的三种子状态:
- Flushed:在调用start()方法后MediaCodec立即进入Flushed子状态,此时MediaCodec会拥有所有的缓存。可以在Executing状态的任何时候通过调用flush()方法返回到Flushed子状态。
- Running:一旦第一个输入缓存(input buffer)被移出队列,MediaCodec就转入Running子状态,这种状态占据了MediaCodec的大部分生命周期。通过调用stop()方法转移到Uninitialized状态。
- End-of-Stream:将一个带有end-of-stream标记的输入buffer入队列时,MediaCodec将转入End-of-Stream子状态。在这种状态下,MediaCodec不再接收之后的输入buffer,但它仍然产生输出buffer直到end-of-stream标记输出。
Released
- 当使用完MediaCodec后,必须调用release()方法释放其资源。调用 release()方法进入最终的Released状态。
5. 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:释放编解码器实例使用的资源。
5.1 MediaCodec创建
MediaCodec的一个实例处理一种特定类型的数据(例如MP3音频或H.264视频),进行编码或解码操作。
MediaCodec创建:
- 可以使用MediaCodecList为特定的媒体格式创建一个MediaCodec。
- 可以从MediaExtractor#getTrackFormat获得track的格式。
- 使用MediaFormat#setFeatureEnabled注入想要添加的任何特性。
- 然后调用MediaCodecList#findDecoderForFormat来获取能够处理该特定媒体格式的编解码器的名称。
- 最后,使用createByCodecName(字符串)创建编解码器。
- 还可以使用createDecoder/EncoderByType(java.lang.String)为特定MIME类型创建首选的编解码器。但是,这不能用于注入特性,并且可能会创建一个不能处理特定媒体格式的编解码器。
5.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对。
5.3 dequeueInputBuffer
public final int dequeueInputBuffer(long timeoutUs)
- 返回用于填充有效数据的输入buffer的索引,如果当前没有可用的buffer,则返回-1。
-
long timeoutUs
:等待可用的输入buffer的时间。- 如果timeoutUs == 0,则立即返回。
- 如果timeoutUs < 0,则无限期等待可用的输入buffer。
- 如果timeoutUs > 0,则等待“timeoutUs”微秒。
5.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标志。- BUFFER_FLAG_END_OF_STREAM:用于指示这是输入数据的最后一部分。
- BUFFER_FLAG_CODEC_CONFIG:通过指定这个标志,可以在start()或flush()之后直接提交特定于codec的数据buffer。但是,如果您使用包含这些密钥的媒体格式配置编解码器,它们将在启动后由MediaCodec直接自动提交。因此,不建议使用BUFFER_FLAG_CODEC_CONFIG标志,只建议高级用户使用。
5.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;
5.6 releaseOutputBuffer
使用此方法将输出buffer返回给codec或将其渲染在输出surface。
public void releaseOutputBuffer (int index,
boolean render)
-
boolean render
:如果在配置codec时指定了一个有效的surface,则传递true会将此输出buffer在surface上渲染。一旦不再使用buffer,该surface将把buffer释放回codec。
6. 同步和异步API的使用流程
6.1 同步API的使用流程
- 创建并配置MediaCodec对象。
- 循环直到完成:
- 如果输入buffer准备好了:
- 读取一段输入,将其填充到输入buffer中
- 如果输出buffer准备好了:
- 从输出buffer中获取数据进行处理。
- 处理完毕后,release MediaCodec 对象。
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();
6.2 异步API的使用流程
在Android 5.0, API21,引入了“异步模式”。
- 创建并配置MediaCodec对象。
- 给MediaCodec对象设置回调MediaCodec.Callback
- 在onInputBufferAvailable回调中:
- 读取一段输入,将其填充到输入buffer中
- 在onOutputBufferAvailable回调中:
- 从输出buffer中获取数据进行处理。
- 处理完毕后,release MediaCodec 对象。
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();
7. 示例程序
Android源码中的CTS部分给出了很多可以关于Media编解码的Demo[2],可以去参考和学习。
7.1 设备支持的解码器
MediaCodecList可用于获取设备支持的编解码器的名字、能力,以查找合适的编解码器。
以下是获取编解码器组件名称的示例代码:
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);//REGULAR_CODECS参考api说明
MediaCodecInfo[] codecs = list.getCodecInfos();
Log.d(TAG, "Decoders: ");
for (MediaCodecInfo codec : codecs) {
if (codec.isEncoder())
continue;
Log.d(TAG, codec.getName());
}
Log.d(TAG, "Encoders: ");
for (MediaCodecInfo codec : codecs) {
if (codec.isEncoder())
Log.d(TAG, codec.getName());
}
不同设备支持的类型不同,下面本机获取到编解码器组件的名称:
Decoders:
OMX.google.mp3.decoder
OMX.google.amrnb.decoder
OMX.google.amrwb.decoder
OMX.google.aac.decoder
OMX.google.g711.alaw.decoder
OMX.google.g711.mlaw.decoder
OMX.google.vorbis.decoder
OMX.google.opus.decoder
OMX.google.raw.decoder
OMX.google.gsm.decoder
OMX.qcom.video.decoder.avc
OMX.qcom.video.decoder.mpeg4
OMX.qcom.video.decoder.mpeg2
OMX.qcom.video.decoder.h263
OMX.qcom.video.decoder.vc1
OMX.qcom.video.decoder.divx
OMX.qcom.video.decoder.divx311
OMX.qcom.video.decoder.divx4
OMX.qcom.video.decoder.vp8
OMX.qcom.video.decoder.vp9
OMX.qcom.video.decoder.hevc
OMX.ffmpeg.dsd.decoder
OMX.ffmpeg.dts.decoder
OMX.ffmpeg.adpcm.decoder
OMX.qti.audio.decoder.flac
OMX.google.mpeg4.decoder
OMX.google.h263.decoder
OMX.google.h264.decoder
OMX.google.hevc.decoder
OMX.google.vp8.decoder
OMX.google.vp9.decoder
Encoders:
OMX.google.aac.encoder
OMX.google.amrnb.encoder
OMX.google.amrwb.encoder
OMX.google.flac.encoder
OMX.qcom.video.encoder.avc
OMX.qcom.video.encoder.mpeg4
OMX.qcom.video.encoder.h263
OMX.qcom.video.encoder.vp8
OMX.qcom.video.encoder.hevc
OMX.google.h263.encoder
OMX.google.h264.encoder
OMX.google.mpeg4.encoder
OMX.google.vp8.encoder
- 以
OMX.google.
开头的通常是软件编解码器。 - 以
OMX.[硬件厂商名称]
开头的通常是硬件编解码器。
5.2 AAC解码为PCM的示例
public class AACToPCM {
private static final String TAG = "AACToPCM";
public static final int ERROR_INPUT_INVALID = 100;
public static final int ERROR_OUTPUT_FAILED = 200;
public static final int ERROR_OPEN_CODEC = 300;
public static final int OK = 0;
private static final int TIMEOUT_USEC = 0;
private MediaExtractor mExtractor;
private MediaFormat mFormat;
private MediaCodec mDecoder;
private FileOutputStream mFos;
private ByteBuffer[] mInputBuffers;
private ByteBuffer[] mOutputBuffers;
private boolean mDecodeEnd;
public AACToPCM() {}
private int checkPath(String path) {
if (path == null || path.isEmpty()) {
Log.d(TAG, "invalid path, path is empty");
return ERROR_INPUT_INVALID;
}
File file = new File(path);
if (!file.isFile()) {
Log.d(TAG, "path is not a file, path:" + path);
return ERROR_INPUT_INVALID;
} else if (!file.exists()) {
Log.d(TAG, "file not exists, path:" + path);
return ERROR_INPUT_INVALID;
} else {
Log.d(TAG, "path is a file, path:" + path);
}
return OK;
}
public int decodeAACToPCM(String audioPath, String pcmPath) {
int ret;
if (OK != (ret = openInput(audioPath))) {
return ret;
}
if (OK != (ret = openOutput(pcmPath))) {
return ret;
}
if (OK != (ret = openCodec(mFormat))) {
return ret;
}
mDecodeEnd = false;
while (!mDecodeEnd) {
if (OK != (ret = decode(mDecoder, mExtractor))) {
Log.d(TAG, "decode failed, ret=" + ret);
break;
}
}
close();
return ret;
}
private int decode(MediaCodec codec, MediaExtractor extractor) {
Log.d(TAG, "decode");
int inputIndex = codec.dequeueInputBuffer(TIMEOUT_USEC);
if (inputIndex >= 0) {
ByteBuffer inputBuffer;
if (Build.VERSION.SDK_INT >= 21) {
inputBuffer = codec.getInputBuffer(inputIndex);
} else {
inputBuffer = mInputBuffers[inputIndex];
}
inputBuffer.clear();
int sampleSize = extractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {//read end
codec.queueInputBuffer(inputIndex, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
codec.queueInputBuffer(inputIndex, 0, sampleSize, extractor.getSampleTime(), 0);
extractor.advance();
}
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {//TIMEOUT
Log.d(TAG, "INFO_TRY_AGAIN_LATER");//TODO how to declare this info
return OK;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
Log.d(TAG, "output format changed");
return OK;
} else if (outputIndex < 0) {
Log.d(TAG, "outputIndex=" + outputIndex);
return OK;
} else {
ByteBuffer outputBuffer;
if (Build.VERSION.SDK_INT >= 21) {
outputBuffer = codec.getOutputBuffer(outputIndex);
} else {
outputBuffer = mOutputBuffers[outputIndex];
}
byte[] buffer = new byte[bufferInfo.size];
outputBuffer.get(buffer);
try {
Log.d(TAG, "output write, size="+ bufferInfo.size);
mFos.write(buffer);
mFos.flush();
} catch (IOException e) {
e.printStackTrace();
return ERROR_OUTPUT_FAILED;
}
codec.releaseOutputBuffer(outputIndex, false);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
mDecodeEnd = true;
}
}
return OK;
}
private int openCodec(MediaFormat format) {
Log.d(TAG, "openCodec, format mime:" + format.getString(MediaFormat.KEY_MIME));
try {
mDecoder = MediaCodec.createDecoderByType(format.getString(MediaFormat.KEY_MIME));
} catch (IOException e) {
e.printStackTrace();
return ERROR_OPEN_CODEC;
}
mDecoder.configure(format, null, null, 0);
mDecoder.start();
if (Build.VERSION.SDK_INT < 21) {
mInputBuffers = mDecoder.getInputBuffers();
mOutputBuffers = mDecoder.getOutputBuffers();
}
return OK;
}
private int openInput(String audioPath) {
Log.d(TAG, "openInput audioPath:" + audioPath);
int ret;
if (OK != (ret = checkPath(audioPath))) {
return ret;
}
mExtractor = new MediaExtractor();
int audioTrack = -1;
boolean hasAudio = false;
try {
mExtractor.setDataSource(audioPath);
for (int i = 0; i < mExtractor.getTrackCount(); ++i) {
MediaFormat format = mExtractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
Log.d(TAG, "mime=" + mime);
if (mime.startsWith("audio/")) {
audioTrack = i;
hasAudio = true;
mFormat = format;
break;
}
}
if (!hasAudio) {
Log.d(TAG, "input contain no audio");
return ERROR_INPUT_INVALID;
}
mExtractor.selectTrack(audioTrack);
} catch (IOException e) {
return ERROR_INPUT_INVALID;
}
return OK;
}
private int openOutput(String outputPath) {
Log.d(TAG, "openOutput outputPath:" + outputPath);
try {
mFos = new FileOutputStream(outputPath);
} catch (IOException e) {
return ERROR_OUTPUT_FAILED;
}
return OK;
}
private void close() {
mExtractor.release();
mDecoder.stop();
mDecoder.release();
try {
mFos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}