咱们晓得 Camera 采集回传的是 YUV 数据,AudioRecord 是 PCM,咱们要对这些数据进行编码(压缩编码),这里咱们来说在 Android 上音视频编解码逃不过的坑-MediaCodec
MediaCodec
PSMediaCodec 能够用来
编/解码
音/视频
。
MediaCodec 简略介绍
MediaCodec 类可用于拜访低级媒体编解码器,即编码器/解码器组件。 它是 Android 低级多媒体反对根底构造的一部分(通常与 MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface 和 AudioTrack 一起应用)。对于 MediaCodec 的形容可参看官网介绍MediaCodec
狭义而言,编解码器解决输出数据以生成输入数据。 它异步解决数据,并应用一组输出和输入缓冲区。 在简略的状况下,您申请(或接管)一个空的输出缓冲区,将其填充数据并将其发送到编解码器进行解决。 编解码器用完了数据并将其转换为空的输入缓冲区之一。 最初,您申请(或接管)已填充的输入缓冲区,应用其内容并将其开释回编解码器。
PS 读者如果对生产者-消费者模型还有印象的话,那么 MediaCodec 的运行模式其实也不难理解。
上面是 MediaCodec 的简略类图
MediaCodec 状态机
在 MediaCodec 生命周期内,编解码器从概念上讲处于以下三种状态之一:Stopped,Executing 或 Released。Stopped 的个体状态实际上是三个状态的汇合:Uninitialized,Configured 和 Error,而 Executing 状态从概念上讲通过三个子状态:Flushed,Running 和 Stream-of-Stream。
应用工厂办法之一创立编解码器时,编解码器处于未初始化状态。首先,您须要通过 configure(…)对其进行配置,使它进入已配置状态,而后调用 start()将其移至执行状态。在这种状态下,您能够通过上述缓冲区队列操作来解决数据。
执行状态具备三个子状态:Flushed,Running 和 Stream-of-Stream。在 start()之后,编解码器立刻处于 Flushed 子状态,其中蕴含所有缓冲区。一旦第一个输出缓冲区出队,编解码器将移至“Running”子状态,在此状态下将破费大部分工夫。当您将输出缓冲区与流完结标记排队时,编解码器将转换为 End-of-Stream 子状态。在这种状态下,编解码器将不再承受其余输出缓冲区,但仍会生成输入缓冲区,直到在输入端达到流完结为止。在执行状态下,您能够应用 flush()随时返回到“刷新”子状态。
调用 stop()使编解码器返回 Uninitialized 状态,随后能够再次对其进行配置。应用编解码器实现操作后,必须通过调用 release()开释它。
在极少数状况下,编解码器可能会遇到谬误并进入“谬误”状态。应用来自排队操作的有效返回值或有时通过异样来传播此信息。调用 reset()使编解码器再次可用。您能够从任何状态调用它,以将编解码器移回“Uninitialized”状态。否则,请调用 release()以移至终端的“Released”状态。
PSMediaCodec 数据处理的模式可分为同步和异步,上面咱们会一一剖析
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(); } } } } }); }
PS 编解码这种耗时操作要在独自的线程中实现,咱们这里有个缓冲队列
ArrayBlockingQueue<byte[]> yuv420Queue = new ArrayBlockingQueue<>(10);
,用来接管从 Camera 回调中传入的 byte[] YUV 数据,咱们又新建设了一个现成来从缓冲队列yuv420Queue
中循环读取数据交给 MediaCodec 进行编码解决,编码实现的格局是由mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);
指定的,这里输入的是目前最为宽泛应用的H264
格局
残缺代码请看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