固阳音箱协会

Android视频处理之MediaCodec-2-使用

奇卓社2019-01-14 12:15:18

        上一篇文章讲解了MediaCodec的工作原理、数据类型和生命周期方面的一些基础知识,这篇从创建到开始编解码,再到结束编解码来介绍下MeidaCodec的基本使用方法和流程。

        工作流程,大致是这样: 以编码为例,首先要初始化硬件编码器,配置要编码的格式、视频文件的长宽、码率、帧率、关键帧间隔等等,这一步叫configure。之后开启编码器,当前编码器便是可用状态,随时准备接收数据。下一个过程便是编码的running过程,在此过程中,需要维护两个buffer队列,InputBuffer 和OutputBuffer,用户需要不断出队InputBuffer (即dequeueInputBuffer),往里边放入需要编码的图像数据之后再入队等待处理,然后硬件编码器开始异步处理,一旦处理结束,他会将数据放在OutputBuffer中,并且通知用户当前有输出数据可用了,那么用户就可以出队一个OutputBuffer,将其中的数据拿走,然后释放掉这个buffer。结束条件在于end-of-stream这个flag标志位的设定。在编码结束后,编码器调用stop函数停止编码,之后调用release函数将编码器完全释放掉,整体流程结束。

创建(Creation)

根据指定的MediaFormat和mime-type创建MediaCodec实例。两种模式举下例子:

  • 根据Camera提供的数据生成视频:先通过指定的mime-type和MediaCodec的工厂方法创建一个编码器,然后实例化一个MediaFormat,设置编码视频需要的帧率、视频高宽等参数。

  • 视频解码并重新编码:通过调用MediaExtractor.getTrackFormat(trackIndex)获得视频的格式(MediaFormat),然后修改期望的参数,再根据mime-type创建编码器。

创建基本的视频合成参数

//create MediaFormat by mime-typeMediaFormat format = MediaFormat.createVideoFormat("video/avc", nEncWidth, nEncHeight);//set data typeformat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);//set bit rateformat.setInteger(MediaFormat.KEY_BIT_RATE, nEncBitsRate);//set frame rateformat.setInteger(MediaFormat.KEY_FRAME_RATE, nEncFrameRate);//set key frame interval as 2sformat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);//create EncodermEncoder = MediaCodec.createEncoderByType("video/avc");

获取视频文件的媒体格式

private void init() throws IOException {
    MediaExtractor extractor = new MediaExtractor();    //extract /sdcard/test.mp4
    extractor.setDataSource("/sdcard/test.mp4");    int trackCount = mExtractor.getTrackCount();    for (int i = 0; i < trackCount; i++) {
        MediaFormat format = mExtractor.getTrackFormat(i);
        String mime = format.getString(MediaFormat.KEY_MIME);        if (mime.startsWith("video/")) {            //get video MediaFormat and trackIndex
            mVideoFormat = format;
            mVideoTrackIndex = i;
        } else if (mime.startsWith("audio/")) {            //get audio MediaFormat and trackIndex
            mAudioFormat = format;
            mAudioTrackIndex = i;
        }
    }
 }

初始化(Initialization)

        根据之前我们提到的流程,创建完编解码器之后,需要进行configure。如果只是需要原始视频数据,直接configure就可以了。如果需要结合Surface使用,需要增加configure的surface参数。具体的结合surface使用,下一篇文章会具体分析。

        在创建了编解码器后,如果你想异步地处理数据,可以通过调用setCallback方法设置一个回调方法。然后使用指定的MediaFormat配置(configure)编解码器,这时你就可以为视频原始数据(例如视频解码器)指定输出Surface,作为离线或者在线渲染。如果需要编解码加密数据,也可以为secure 编解码器设置解密参数(详见MediaCrypto) 。最后,因为编解码器可以工作于多种模式,你必须指定flag来设置该编解码器是作为一个解码器(decoder)还是编码器(encoder)运行。

        如果你想将原始视频数据(raw video data)送给视频消费者处理,你可以在配置好视频编码器(encoder)后调用createInputSurface方法创建一个目标surface来存放输入数据,如此,调用视频生产者(decoder)的setInputSurface(Surface)方法将前面创建的目的Surface配置给视频生产者作为输出缓存位置。

//编码器encoder = MediaCodec.createEncoderByType(MIME_TYPE);
encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);//inputSurface结合EGLSurface,用来编码此Surface上渲染的数据inputSurface = new InputSurface(encoder.createInputSurface());
encoder.start();//解码器decoder = MediaCodec.createDecoderByType(MIME_TYPE);//将数据解析到ouputSurface上outputSurface = new CodecOutputSurface();
decoder.configure(mVideoFormat, outputSurface.getSurface(), null, 0);
decoder.start();

数据处理(Data Processing)

        初始化完成后,就可以调用MediaCodec的start方法开始解码了。start后,有两种方式处理数据:同步和异步。在API 23之前,只能用同步方式,23之后可以使用异步方式处理数据。同步又分为两种,API 21之后建议使用ByteBuffer,之前使用ByteBuffer数组

  • 同步之ByteBuffer方式典型用法

    MediaCodec codec = MediaCodec.createByCodecName(name);
    codec.configure(format, …);
    MediaFormat outputFormat = codec.getOutputFormat(); // option Bcodec.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();
  • 同步之ByteBuffer数组典型方式

    MediaCodec codec = MediaCodec.createByCodecName(name);
    codec.configure(format, …);
    codec.start();
    ByteBuffer[] inputBuffers = codec.getInputBuffers();
    ByteBuffer[] outputBuffers = codec.getOutputBuffers();for (;;) { int inputBufferId = codec.dequeueInputBuffer(…); if (inputBufferId >= 0) {   // fill inputBuffers[inputBufferId] with valid data
       …
       codec.queueInputBuffer(inputBufferId, …);
     } int outputBufferId = codec.dequeueOutputBuffer(…); if (outputBufferId >= 0) {   // outputBuffers[outputBufferId] is ready to be processed or rendered.
       …
       codec.releaseOutputBuffer(outputBufferId, …);
     } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
       outputBuffers = codec.getOutputBuffers();
     } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {   // Subsequent data will conform to new format.
       MediaFormat format = codec.getOutputFormat();
     }
    }
    codec.stop();
    codec.release();
  • 异步典型用法(设置Callback去处理数据)

    MediaCodec codec = MediaCodec.createByCodecName(name);
    MediaFormat mOutputFormat; // member variablecodec.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 Bcodec.start();// wait for processing to completecodec.stop();
    codec.release();

    流结束处理(End-of-stream Handling)

    当到达输入数据结尾时,必须在调用queueInputBuffer方法中通过指定BUFFER_FLAG_END_OF_STREAM标记来通知编解码器已经达到结尾了。我们可以在最后一个有效的输入buffer上做标记,或者再提交一个带end-of-stream标记的空的输入buffer。当然如果提交的是空buffer,则它的时间戳将会被忽略。而编解码器将会继续返回输出buffers,直到它发现输出流结束的信号,通过dequeueOutputBuffer方法中MediaCodec.BufferInfo的end-of-stream来判断,或者是通过回调方法onOutputBufferAvailable来返回end-of-stream标记。
    当通知输入流结束后不要再提交额外的输入buffers,除非编解码器被刷新或停止或重启。在实际项目中也经常因为状态的问题导致提交数据时机错误,最终导致编解码过程出现IllegalStateException异常。

        至此,MediaCodec的整个编解码过程就结束了。下一篇文章,我们通过实际案例需求来看看怎么具体使用MediaCodec。

Copyright © 固阳音箱协会@2017