【安卓】Android音视频 - MediaCodec编解码音视频

前情提要

上一篇博客我们分析了如何使用Android的硬件设备(摄像机、录音设备)来进行采集原生数据,Camera采集回传的是YUV数据,AudioRecord是PCM,我们要对这些数据进行编码(压缩编码),关于为什么要这么做,我们在Android音视频系列的首篇文章也已经分析过了,关于音视频的编解码方案,最有名的应该是FFMpeg了,这是一套跨平台的编码方案,适用于Windows、Linux、Android、IOS。关于FFMpeg我们后面的文章还会仔细分析,这里我们来说在Android上音视频编解码逃不过的坑-MediaCodec。

MediaCodec

MediaCodec简单介绍

MediaCodec类可用于访问低级媒体编解码器,即编码器/解码器组件。 它是Android低级多媒体支持基础结构的一部分(通常与MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface和AudioTrack一起使用)。关于MediaCodec的描述可参看官方介绍MediaCodec

【安卓】Android音视频 - MediaCodec编解码音视频

广义而言,编解码器处理输入数据以生成输出数据。 它异步处理数据,并使用一组输入和输出缓冲区。 在简单的情况下,您请求(或接收)一个空的输入缓冲区,将其填充数据并将其发送到编解码器进行处理。 编解码器用完了数据并将其转换为空的输出缓冲区之一。 最后,您请求(或接收)已填充的输出缓冲区,使用其内容并将其释放回编解码器。

下面是MediaCodec的简单类图
【安卓】Android音视频 - MediaCodec编解码音视频

MediaCodec 状态机

在MediaCodec生命周期内,编解码器从概念上讲处于以下三种状态之一:Stopped,Executing或Released。Stopped的集体状态实际上是三个状态的集合:Uninitialized,Configured和Error,而Executing状态从概念上讲经过三个子状态:Flushed,Running和Stream-of-Stream。
【安卓】Android音视频 - MediaCodec编解码音视频
使用工厂方法之一创建编解码器时,编解码器处于未初始化状态。首先,您需要通过configure(…)对其进行配置,使它进入已配置状态,然后调用start()将其移至执行状态。在这种状态下,您可以通过上述缓冲区队列操作来处理数据。

执行状态具有三个子状态:Flushed,Running和Stream-of-Stream。在start()之后,编解码器立即处于Flushed子状态,其中包含所有缓冲区。一旦第一个输入缓冲区出队,编解码器将移至“Running”子状态,在此状态下将花费大部分时间。当您将输入缓冲区与流结束标记排队时,编解码器将转换为 End-of-Stream子状态。在这种状态下,编解码器将不再接受其他输入缓冲区,但仍会生成输出缓冲区,直到在输出端达到流结束为止。在执行状态下,您可以使用flush()随时返回到“刷新”子状态。

调用stop()使编解码器返回Uninitialized状态,随后可以再次对其进行配置。使用编解码器完成操作后,必须通过调用release()释放它。

在极少数情况下,编解码器可能会遇到错误并进入“错误”状态。使用来自排队操作的无效返回值或有时通过异常来传达此信息。调用reset()使编解码器再次可用。您可以从任何状态调用它,以将编解码器移回“Uninitialized”状态。否则,请调用release()以移至终端的“Released”状态。

MediaCodec 同步模式

【安卓】Android音视频 - MediaCodec编解码音视频
上代码

    public H264MediaCodecEncoder(int width, int height) {

//设置MediaFormat的参数

MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIMETYPE_VIDEO_AVC, width, height);

mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);

mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);

mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);//FPS

mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

try {

//通过MIMETYPE创建MediaCodec实例

mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);

//调用configure,传入的MediaCodec.CONFIGURE_FLAG_ENCODE表示编码

mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

//调用start

mMediaCodec.start();

} catch (Exception e) {

e.printStackTrace();

}

}

调用putData向队列中add 原始YUV数据

  public void putData(byte[] buffer) {

if (yuv420Queue.size() >= 10) {

yuv420Queue.poll();

}

yuv420Queue.add(buffer);

}

//开启编码

public void startEncoder() {

isRunning = true;

ExecutorService executorService = Executors.newSingleThreadExecutor();

executorService.execute(new Runnable() {

@Override

public void run() {

byte[] input = null;

while (isRunning) {

if (yuv420Queue.size() > 0) {

//从队列中取数据

input = yuv420Queue.poll();

}

if (input != null) {

try {

//【1】dequeueInputBuffer

int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_S);

if (inputBufferIndex >= 0) {

//【2】getInputBuffer

ByteBuffer inputBuffer = null;

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {

inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex);

} else {

inputBuffer = mMediaCodec.getInputBuffers()[inputBufferIndex];

}

inputBuffer.clear();

inputBuffer.put(input);

//【3】queueInputBuffer

mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, getPTSUs(), 0);

}

MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

//【4】dequeueOutputBuffer

int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_S);

if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {

MediaFormat newFormat = mMediaCodec.getOutputFormat();

if (null != mEncoderCallback) {

mEncoderCallback.outputMediaFormatChanged(H264_ENCODER, newFormat);

}

if (mMuxer != null) {

if (mMuxerStarted) {

throw new RuntimeException("format changed twice");

}

// now that we have the Magic Goodies, start the muxer

mTrackIndex = mMuxer.addTrack(newFormat);

mMuxer.start();

mMuxerStarted = true;

}

}

while (outputBufferIndex >= 0) {

ByteBuffer outputBuffer = null;

//【5】getOutputBuffer

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {

outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);

} else {

outputBuffer = mMediaCodec.getOutputBuffers()[outputBufferIndex];

}

if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {

bufferInfo.size = 0;

}

if (bufferInfo.size > 0) {

// adjust the ByteBuffer values to match BufferInfo (not needed?)

outputBuffer.position(bufferInfo.offset);

outputBuffer.limit(bufferInfo.offset + bufferInfo.size);

// write encoded data to muxer(need to adjust presentationTimeUs.

bufferInfo.presentationTimeUs = getPTSUs();

if (mEncoderCallback != null) {

//回调

mEncoderCallback.onEncodeOutput(H264_ENCODER, outputBuffer, bufferInfo);

}

prevOutputPTSUs = bufferInfo.presentationTimeUs;

if (mMuxer != null) {

if (!mMuxerStarted) {

throw new RuntimeException("muxer hasn't started");

}

mMuxer.writeSampleData(mTrackIndex, outputBuffer, bufferInfo);

}

}

mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);

bufferInfo = new MediaCodec.BufferInfo();

outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_S);

}

} catch (Throwable throwable) {

throwable.printStackTrace();

}

} else {

try {

Thread.sleep(500);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

});

}

完整代码请看H264MediaCodecEncoder

MediaCodec 异步模式

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)

public H264MediaCodecAsyncEncoder(int width, int height) {

MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIMETYPE_VIDEO_AVC, width, height);

mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);

mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);

mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);//FPS

mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

try {

mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);

mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

//设置回调

mMediaCodec.setCallback(new MediaCodec.Callback() {

@Override

/**

* Called when an input buffer becomes available.

*

* @param codec The MediaCodec object.

* @param index The index of the available input buffer.

*/

public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {

Log.i("MFB", "onInputBufferAvailable:" + index);

byte[] input = null;

if (isRunning) {

if (yuv420Queue.size() > 0) {

input = yuv420Queue.poll();

}

if (input != null) {

ByteBuffer inputBuffer = codec.getInputBuffer(index);

inputBuffer.clear();

inputBuffer.put(input);

codec.queueInputBuffer(index, 0, input.length, getPTSUs(), 0);

}

}

}

@Override

/**

* Called when an output buffer becomes available.

*

* @param codec The MediaCodec object.

* @param index The index of the available output buffer.

* @param info Info regarding the available output buffer {@link MediaCodec.BufferInfo}.

*/

public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {

Log.i("MFB", "onOutputBufferAvailable:" + index);

ByteBuffer outputBuffer = codec.getOutputBuffer(index);

if (info.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {

info.size = 0;

}

if (info.size > 0) {

// adjust the ByteBuffer values to match BufferInfo (not needed?)

outputBuffer.position(info.offset);

outputBuffer.limit(info.offset + info.size);

// write encoded data to muxer(need to adjust presentationTimeUs.

info.presentationTimeUs = getPTSUs();

if (mEncoderCallback != null) {

//回调

mEncoderCallback.onEncodeOutput(H264_ENCODER, outputBuffer, info);

}

prevOutputPTSUs = info.presentationTimeUs;

if (mMuxer != null) {

if (!mMuxerStarted) {

throw new RuntimeException("muxer hasn't started");

}

mMuxer.writeSampleData(mTrackIndex, outputBuffer, info);

}

}

codec.releaseOutputBuffer(index, false);

}

@Override

public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {

}

@Override

/**

* Called when the output format has changed

*

* @param codec The MediaCodec object.

* @param format The new output format.

*/

public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {

if (null != mEncoderCallback) {

mEncoderCallback.outputMediaFormatChanged(H264_ENCODER, format);

}

if (mMuxer != null) {

if (mMuxerStarted) {

throw new RuntimeException("format changed twice");

}

// now that we have the Magic Goodies, start the muxer

mTrackIndex = mMuxer.addTrack(format);

mMuxer.start();

mMuxerStarted = true;

}

}

});

mMediaCodec.start();

} catch (Exception e) {

e.printStackTrace();

}

}

完整代码请看H264MediaCodecAsyncEncoder

MediaCodec 小结

MediaCodec 用来音视频的编解码工作(这个过程有的文章也称为硬解),通过MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC)函数中的参数来创建音频或者视频的编码器,同理通过MediaCodec.createDecoderByType(MIMETYPE_VIDEO_AVC)创建音频或者视频的解码器。对于音视频编解码中需要的不同参数用MediaFormat来指定

小结

本篇文章详细的对MediaCodec进行了分析,读者可根据博客对应Demo来进行实际操练

放上Demo地址详细Demo

以上是 【安卓】Android音视频 - MediaCodec编解码音视频 的全部内容, 来源链接: utcz.com/a/98655.html

回到顶部