乐趣区

关于android:Android音视频-MediaCodec编解码音视频

前情提要

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

MediaCodec

PS
MediaCodec 能够用来 编 / 解码 音 / 视频

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”状态。

PS
MediaCodec 数据处理的模式可分为同步和异步,上面咱们会一一剖析

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

退出移动版