天天看點

NDK中使用 MediaCodec 編解碼視訊

背景

MediaCodec 作為Android自帶的視訊編解碼工具,可以直接利用底層硬體編解碼能力,現在已經逐漸成為主流了。API21已經支援NDK方法了,MediaCodec api設計得非常精妙,另一個方面也是很多人覺得不好懂。

内容

MediaCodec的兩個Buffer和三闆斧

MediaCodec内部包含InputBuffer和OutputBuffer,内部有一個自啟線程,不斷去查詢兩個Buffer,是一個生産者消費者模型。

進行資料處理時主要靠三闆斧。

  • 第一步:取buffer位址
AMediaCodec_dequeueInputBuffer      
  • 第二步:擷取buffer資料
AMediaCodec_getInputBuffer      
  • 第三步:buffer入隊
AMediaCodec_queueInputBuffer      

InputBuffer和OutputBuffer基本是對稱的:

  • 第一步:取buffer位址
AMediaCodec_dequeueOutputBuffer      
  • 第二步:擷取buffer資料
AMediaCodec_getOutputBuffer      
  • 第三步:buffer釋放
AMediaCodec_releaseOutputBuffer      

隻有第三步不同,AMediaCodec_queueInputBuffer是資料入隊等待消費,AMediaCodec_releaseOutputBuffer是釋放資料。

編碼和解碼過程,InputBuffer和OutputBuffer就互相置換下。

解碼:原始資料(視訊流)-> 提取器AMediaExtractor->InputBuffer->OutputBuffer->幀資料(YUV420sp,PCM)

編碼:幀資料(視訊流)->InputBuffer->OutputBuffer->合成器AMediaMuxer

解碼

解碼配置

解碼開始需要配置AMediaCodec和AMediaExtractor,MediaCodec start後就可以開始解碼。

AMediaExtractor需要設定檔案描述符,通過AAssetManager_open或者fopen就可以得到。起始點和長度也同樣。然後設定進提取器。

AMediaExtractor_setDataSourceFd(mExtractor, 
                                virtualFile.fd,
                                virtualFile.start,
                                virtualFile.length);      

AMediaCodec建立需要設定資料格式,通過AMediaExtractor擷取到的AMediaFormat可以得到mime和format。

mCodec = AMediaCodec_createDecoderByType(mime);
AMediaCodec_configure(mCodec, format, NULL, NULL, 0);
AMediaCodec_start(mCodec);      

解碼配置第三個參數為NativeWindow,加了後解碼後可以直接吐到surface上,GPU資料直接渲軟,效率高但不夠靈活。不加的話解碼資料就需要輸出拷貝。

解碼流程

解碼也就是操作兩個Buffer的過程,執行玩三闆斧就可以,然後有一些狀态需要處理。

if (!mInputEof) {
    ssize_t bufidx = AMediaCodec_dequeueInputBuffer(mCodec, 1);
    log_info(NULL, "input buffer %zd", bufidx);
    if (bufidx >= 0) {
        size_t bufsize;
        uint8_t *buf = AMediaCodec_getInputBuffer(mCodec, bufidx, &bufsize);
        int sampleSize = AMediaExtractor_readSampleData(mExtractor, buf, bufsize);
        if (sampleSize < 0) {
            sampleSize = 0;
            mInputEof = true;
            log_info(NULL, "video producer input EOS");
        }
        int64_t presentationTimeUs = AMediaExtractor_getSampleTime(mExtractor);

        AMediaCodec_queueInputBuffer(mCodec, bufidx, 0, sampleSize, presentationTimeUs,
                                     mInputEof ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM
                                               : 0);
        AMediaExtractor_advance(mExtractor);
    }
}

if (!mOutputEof) {
    AMediaCodecBufferInfo info;
    ssize_t status = AMediaCodec_dequeueOutputBuffer(mCodec, &info, 1);

    if (status >= 0) {

        if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) {
            log_info(NULL, "video producer output EOS");

            eof = true;
            mOutputEof = true;
        }

        uint8_t *outputBuf = AMediaCodec_getOutputBuffer(mCodec, status, NULL/* out_size */);
        size_t dataSize = info.size;
        if (outputBuf != nullptr && dataSize != 0) {
            long pts = info.presentationTimeUs;
            int32_t pts32 = (int32_t) pts;

            *buffer = (uint8_t *) mlt_pool_alloc(dataSize);
            memcpy(*buffer, outputBuf + info.offset, dataSize);
            *buffersize = dataSize;
        }

        int64_t presentationNano = info.presentationTimeUs * 1000;
        log_info(NULL, "video pts %lld outsize %d", info.presentationTimeUs, dataSize);
        /*if (delay > 0) {
            usleep(delay / 1000);
        }*/
        AMediaCodec_releaseOutputBuffer(mCodec, status, info.size != 0);
    } else if (status == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
        log_info(NULL, "output buffers changed");
    } else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
        AMediaFormat_delete(format);
    } else if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
        log_info(NULL, "video no output buffer right now");
    } else {
        log_info(NULL, "unexpected info code: %zd", status);
    }

}      

AMediaCodec和AMediaExtractor是沒有直接交流的,AMediaCodec取到InputBuffer後實際資料為空,需要從AMediaExtractor_readSampleData擷取到buffer資料。

AMediaCodec資料入隊後,AMediaExtractor調用 AMediaExtractor_advance前進到下一個資料位置。

OutputBuffer操作時有些不一樣,AMediaCodec_dequeueOutputBuffer擷取的是解碼好的幀,AMediaCodec_getOutputBuffer取到的就已經是解碼好的資料了,可以直接拷貝使用。

AMediaCodec_releaseOutputBuffer是釋放buffer,如果配置了surface,就會渲軟到surface上。

編碼

編碼配置

編碼是解碼的逆過程,首先設定格式,然後根據格式建立編碼器MediaCodec,再根據檔案建立合成器MediaMuxer。

void NativeEncoder::prepareEncoder(int width, int height, int fps, std::string strPath) {

    mWidth = width;
    mHeight = height;
    mFps = fps;

    AMediaFormat *format = AMediaFormat_new();
    AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, mStrMime.c_str());
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, mWidth);
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, mHeight);

    AMediaFormat_setInt32(format,AMEDIAFORMAT_KEY_COLOR_FORMAT, COLOR_FORMAT_SURFACE);
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_BIT_RATE, mBitRate);
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_FRAME_RATE, mFps);
    AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, mIFrameInternal);

    const char *s = AMediaFormat_toString(format);
    log_info(NULL, "encoder video format: %s", s);


    mCodec = AMediaCodec_createEncoderByType(mStrMime);


    media_status_t status = AMediaCodec_configure(mCodec, format, NULL, NULL,
                                                  AMEDIACODEC_CONFIGURE_FLAG_ENCODE);
    if (status != 0) {
        log_error(NULL, "AMediaCodec_configure() failed with error %i for format %u",
                      (int) status, 21);
    } else {

    }
    AMediaFormat_delete(format);

    FILE *fp = fopen(strPath.c_str(), "wb");

    if (fp != NULL) {
        mFd = fileno(fp);
    } else {
        mFd = -1;
        log_error(NULL, "create file %s fail", strPath.c_str());
    }

    if(mMuxer == NULL)
        mMuxer = AMediaMuxer_new(mFd, AMEDIAMUXER_OUTPUT_FORMAT_MPEG_4);

    mMuxerStarted = false;

    fclose(fp);

}      

這裡注意下配置類型是 “video/avc”,基本視訊都是這個格式,可以看官網格式支援資訊,比特率mBitRate是6000000,這個要根據需求對應配置,I幀間隔mIFrameInternal是1秒,間隔長擷取關鍵幀資訊會有問題。

編碼準備

編碼視訊流需要建立一個surface,再把這個surface綁定到共享的EGLContext上。

void NativeEncoder::prepareEncoderWithShareCtx(int width, int height, int fps, std::string strPath,
                                   EGLContext shareCtx) {

    prepareEncoder(width,height,fps,strPath);
    ANativeWindow *surface;
    AMediaCodec_createInputSurface(mCodec, &surface);
    media_status_t status;
    if ((status = AMediaCodec_start(mCodec)) != AMEDIA_OK) {
        log_error(NULL, "AMediaCodec_start: Could not start encoder.");
    } else {
        log_info(NULL, "AMediaCodec_start: encoder successfully started");
    }
    mCodecInputSurface = new CodecInputSurface(surface);
    mCodecInputSurface->setupEGL(shareCtx);
}      

編碼流程

編碼需要先進行渲染,從外部共享的EGLContext傳入一個紋理,渲軟到編碼器對應的surface上,再進行編碼。

傳入紋理并渲染:

void NativeEncoder::feedFrame(uint64_t pts, int tex) {
    drainEncoder(false);

    mCodecInputSurface->makeCurrent();
    glViewport(0,0,mWidth,mHeight);
    mCodecInputSurface->renderOnSurface(tex);
    mCodecInputSurface->setPresentationTime(pts);
    mCodecInputSurface->swapBuffers();
    mCodecInputSurface->makeNothingCurrent();
}      

編碼:

void NativeEncoder::drainEncoder(bool eof) {

    if (eof) {

        ssize_t ret = AMediaCodec_signalEndOfInputStream(mCodec);
        log_info(NULL, "drainEncoder eof = %d",ret);
    }

    while (true) {

        AMediaCodecBufferInfo info;
        //time out usec 1
        ssize_t status = AMediaCodec_dequeueOutputBuffer(mCodec, &info, 1);

        if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {

            if (!eof) {
                break;
            } else {
                log_info(NULL, "video no output available, spinning to await EOS");
            }
        } else if (status == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
            // not expected for an encoder
        } else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
            if (mMuxerStarted) {
                log_warning(NULL, "format changed twice");
            }

            AMediaFormat *fmt = AMediaCodec_getOutputFormat(mCodec);
            const char *s = AMediaFormat_toString(fmt);
            log_info(NULL, "video output format %s", s);

            mTrackIndex = AMediaMuxer_addTrack(mMuxer, fmt);

            if(mAudioTrackIndex != -1 && mTrackIndex != -1) {

                log_info(NULL,"AMediaMuxer_start");
                AMediaMuxer_start(mMuxer);
                mMuxerStarted = true;
            }

        } else {

            uint8_t *encodeData = AMediaCodec_getOutputBuffer(mCodec, status, NULL/* out_size */);

            if (encodeData == NULL) {
                log_error(NULL, "encoder output buffer was null");
            }

            if ((info.flags & AMEDIACODEC_BUFFER_FLAG_CODEC_CONFIG) != 0) {
                log_info(NULL, "ignoring AMEDIACODEC_BUFFER_FLAG_CODEC_CONFIG");
                info.size = 0;
            }

            size_t dataSize = info.size;

            if (dataSize != 0) {

                if (!mMuxerStarted) {
                    log_error(NULL, "muxer has't started");
                }
                log_info(NULL,"AMediaMuxer_writeSampleData video size %d",dataSize);
                AMediaMuxer_writeSampleData(mMuxer, mTrackIndex, encodeData, &info);
            }

            AMediaCodec_releaseOutputBuffer(mCodec, status, false);

            if ((info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) != 0) {

                if (!eof) {
                    log_warning(NULL, "reached end of stream unexpectly");
                } else {
                    log_info(NULL, "video end of stream reached");
                }

                break;
            }
        }
    }
}      

除了結尾标記,編碼時沒有操作InputBuffer,因為InputBuffer對應的就是surface的源,是以編碼第一步實際是渲軟,通過opengl render到surface上再交換緩沖區到surface上。

第二步擷取到OutputBuffer資料,調用AMediaCodec_getOutputBuffer;第三步合成器寫資料,調用AMediaMuxer_writeSampleData然後釋放outputBuffer,調用AMediaCodec_releaseOutputBuffer。

總結