天天看點

【Android 音視訊開發打怪更新:FFmpeg音視訊編解碼篇】七、Android FFmpeg 視訊編碼

碼字不易,轉載請注明出處!

本文你可以了解到

如何使用 FFmepg 對編輯好的視訊進行重新編碼,生成可以播放的音視訊檔案。

寫在前面

本文是音視訊系列文章的最後一篇了,也是拖了最久的一篇(懶癌發作-_-!!),終于下定決心,把坑填完。

話不多說了,馬上進入正文。

在上一篇文章中,介紹了如何對音視訊檔案進行解封和重新封裝,這個過程不涉及音視訊的解碼和編碼,也就是沒有對音視訊進行編輯,這無法滿足日常的開發需求。

是以,本文将填上編輯過程的空缺,為本系列畫上句号。

一、整體流程說明

在前面的幾篇文章中,我們已經做好了

解碼器

OpenGL 渲染器

,是以,編碼的時候,除了需要

編碼器

外,還需要将之前的内容做好整合。下面通過一張圖做一下簡要說明:

【Android 音視訊開發打怪更新:FFmpeg音視訊編解碼篇】七、Android FFmpeg 視訊編碼

子產品

首先可以關注到,這個過程有三個大子產品,也是三個

獨立又互相關聯

的線程,分别負責:

  • 原視訊解碼
  • OpenGL 畫面渲染
  • 目标視訊編碼

資料流向

看下視訊資料是如何流轉的:

  1. 原視訊經過

    解碼器

    解碼後,得到

    YUV

    資料,經過格式轉換,成為

    RGB

    資料。
  2. 解碼器

    RGB

    資料傳遞給

    繪制器

    ,等待

    OpenGL 渲染器

    使用。
  3. OpenGL 渲染器

    通過内部的線程循環,在适當的時候,調用

    繪制器

    渲染畫面。
  4. 畫面繪制完畢以後,得到經過

    OpenGL

    渲染(編輯過)的畫面,送到

    編碼器

    進行編碼。
  5. 最後,将編碼好的資料,寫入本地檔案。

說明:

本文将主要講音視訊的

編碼

知識,由于整個過程涉及到

解碼

OpenGL 渲染

這兩個前面介紹過的知識點,我們将複用之前封裝好的工具,并在一些特殊地方根據編碼的需要做一些适配。

是以接下來在涉及到解碼和OpenGL的地方,至貼出适配的代碼,具體可以檢視之前的文章,或者直接檢視源碼。

二、關于 x264 so 庫編譯和引入

由于

x264

是基于

GPL

開源協定的,而

FFmpeg

預設是基于

LGPL

協定的,當引入

x264

時,由于

GPL

的傳染性,導緻我們的代碼也必須開源,你可以使用

OpenH264

來代替。

這裡仍然使用

x264

來學習相關的編碼過程。

另外,限于篇幅,本文不會介紹關于

x264

的編譯,會另外寫文章介紹。

x264 so 庫的引入和其他 so 引入是一樣的,具體請參考之前的文章,或者檢視源碼中的 CMakeList.txt 。

FFmpeg

已經内置了 h264 解碼器,是以如果隻是解碼,并不需要引入

x264

三、封裝編碼器

編碼過程和解碼過程是非常類似的,其實就是解碼的逆過程,是以整個代碼架構流程和解碼器

BaseDecoder

基本是一緻的。

定義

BaseEncoder

// BaseEncoder.h

class BaseEncoder: public IEncoder {
private:

    // 編碼格式 ID
    AVCodecID m_codec_id;

    // 線程依附的JVM環境
    JavaVM *m_jvm_for_thread = NULL;

    // 編碼器
    AVCodec *m_codec = NULL;

    // 編碼上下文
    AVCodecContext *m_codec_ctx = NULL;

    // 編碼資料包
    AVPacket *m_encoded_pkt = NULL;

    // 寫入Mp4的輸入流索引
    int m_encode_stream_index = 0;

    // 原資料時間基
    AVRational m_src_time_base;

    // 緩沖隊列
    std::queue<OneFrame *> m_src_frames;

    // 操作資料鎖
    std::mutex m_frames_lock;

    // 狀态回調
    IEncodeStateCb *m_state_cb = NULL;

    bool Init();

    /**
     * 循環拉去已經編碼的資料,直到沒有資料或者編碼完畢
     * @return true 編碼結束;false 編碼未完成
     */
    bool DrainEncode();

    /**
     * 編碼一幀資料
     * @return 錯誤資訊
     */
    int EncodeOneFrame();

    // 建立編碼線程
    void CreateEncodeThread();

    // 解碼靜态方法,給線程調用
    static void Encode(std::shared_ptr<BaseEncoder> that);

    void OpenEncoder();

    // 循環編碼
    void LoopEncode();

    void DoRelease();
    
    // 省略一些非重點代碼(具體請檢視源碼)
    // .......

protected:

    // Mp4 封裝器
    Mp4Muxer *m_muxer = NULL;

//-------------子類需要複寫的方法 begin-----------
    // 初始化編碼參數(上下文)
    virtual void InitContext(AVCodecContext *codec_ctx) = 0;

    // 配置Mp4 混淆通道資訊
    virtual int ConfigureMuxerStream(Mp4Muxer *muxer, AVCodecContext *ctx) = 0;

    // 處理一幀資料
    virtual AVFrame* DealFrame(OneFrame *one_frame) = 0;

    // 釋放資源
    virtual void Release() = 0;
    
    virtual const char *const LogSpec() = 0;
//-------------子類需要複寫的方法 end-----------

public:
    BaseEncoder(JNIEnv *env, Mp4Muxer *muxer, AVCodecID codec_id);

    // 壓入一幀待編碼資料(由外部調用)
    void PushFrame(OneFrame *one_frame) override ;

    // 判斷是否緩沖資料過多,用于控制緩沖隊列大小
    bool TooMuchData() override {
        return m_src_frames.size() > 100;
    }

    // 設定編碼狀态監聽器
    void SetStateReceiver(IEncodeStateCb *cb) override {
        this->m_state_cb = cb;
    }
};           

複制

編碼器定義并不複雜,無非就是編碼需要用到的編碼器

m_codec

、解碼上下文

m_codec_id

等,以及封裝對應的函數方法來拆分編碼過程中的幾個步驟。這裡主要強調幾點:

  • 控制編碼緩沖隊列大小
由于編碼過程中,編碼速度遠遠小于解碼速度,是以需要控制緩沖隊列大小,避免大量的資料堆積,導緻内容溢出或申請記憶體失敗問題。
  • 時間戳轉換
時間戳轉換在上篇文章中已經有說明,具體請檢視上篇文章。總之,由于原視訊和目标視訊時間基是不一樣的,是以需要對時間戳進行轉換,才能保證編碼儲存後的時間是正常的。
  • 確定 MP4 軌道索引是正确的
MP4 有音頻和視訊兩個軌道,需要在寫入的時候,對應好,具體檢視代碼中的

m_encode_stream_index

實作

BaseEncoder

初始化
// BaseEncoder.cpp

BaseEncoder::BaseEncoder(JNIEnv *env, Mp4Muxer *muxer, AVCodecID codec_id)
: m_muxer(muxer),
m_codec_id(codec_id) {
    if (Init()) {
        env->GetJavaVM(&m_jvm_for_thread);
        CreateEncodeThread();
    }
}

bool BaseEncoder::Init() {
    // 1. 查找編碼器
    m_codec = avcodec_find_encoder(m_codec_id);
    if (m_codec == NULL) {
        LOGE(TAG, "Fail to find encoder, code id is %d", m_codec_id)
        return false;
    }
    // 2. 配置設定編碼上下文
    m_codec_ctx = avcodec_alloc_context3(m_codec);
    if (m_codec_ctx == NULL) {
        LOGE(TAG, "Fail to alloc encoder context")
        return false;
    }

    // 3. 初始化編碼資料包
    m_encoded_pkt = av_packet_alloc();
    av_init_packet(m_encoded_pkt);

    return true;
}

void BaseEncoder::CreateEncodeThread() {
    // 使用智能指針,線程結束時,自動删除本類指針
    std::shared_ptr<BaseEncoder> that(this);
    std::thread t(Encode, that);
    t.detach();
}           

複制

編碼需要兩個參數,

m_muxer

m_codec_id

,既: MP4 混合器和編碼格式ID。

其中,編碼格式 ID 根據音頻和視訊需要來設定,比如視訊 H264 為:

AV_CODEC_ID_H264

,音頻 AAC 為:

AV_CODEC_ID_AAC

接着,調用

Init()

方法:

  1. 根據編碼格式 ID 查找編碼器
  2. 配置設定編碼上下文
  3. 初始化編碼資料包

最後,建立編碼線程。

封裝編碼流程

// BaseEncoder.cpp

void BaseEncoder::Encode(std::shared_ptr<BaseEncoder> that) {
    JNIEnv * env;

    //将線程附加到虛拟機,并擷取env
    if (that->m_jvm_for_thread->AttachCurrentThread(&env, NULL) != JNI_OK) {
        LOG_ERROR(that->TAG, that->LogSpec(), "Fail to Init encode thread");
        return;
    }

    that->OpenEncoder(); // 1
    that->LoopEncode();  // 2
    that->DoRelease();   // 3
    
    //解除線程和jvm關聯
    that->m_jvm_for_thread->DetachCurrentThread();

}           

複制

過程和解碼非常類似。

第1步,打開編碼器
// BaseEncoder.cpp

void BaseEncoder::OpenEncoder() {
    // 調用子類方法,根據音頻和視訊的不同,初始化編碼上下文
    InitContext(m_codec_ctx);

    int ret = avcodec_open2(m_codec_ctx, m_codec, NULL);
    if (ret < 0) {
        LOG_ERROR(TAG, LogSpec(), "Fail to open encoder : %d", m_codec);
        return;
    }

    m_encode_stream_index = ConfigureMuxerStream(m_muxer, m_codec_ctx);
}           

複制

第2步,開啟編碼循環

編碼的核心方法隻有兩個:

avcodec_send_frame

: 資料發到編碼隊列

avcodec_receive_packet

: 接收編碼好的資料

編碼過程主要有 5 個步驟:

  1. 從緩沖隊列中擷取待解碼資料
  2. 将原始資料交給子類處理(音頻和視訊根據自己的需求處理)
  3. 通過

    avcodec_send_frame

    将資料發送到編碼器編碼
  4. 将編碼好的資料抽取出來

還有一點,既第 5 點,重新發送資料。

需要說明一下這裡采取的

雙循環

編碼邏輯:除了最外層的

while(tue)

循環以外,裡面還有一個

while (m_src_frames.size() > 0)

循環。

在緩沖隊列有資料,并且 FFmpeg 内部編碼隊列未滿

的情況下,會不斷地往

FFmpeg

發送資料,直到發現

FFmpeg

編碼傳回

AVERROR(EAGAIN)

,則說明内部隊列已滿,需要先将編碼的資料抽取出來,也就是調用

DrainEncode()

方法。

還有一點需要說明的是:如何判讀所有資料已經都發送給編碼器了?

這裡通過

one_frame->line_size

來判斷。

當監聽到解碼器通知解碼完成的時候,則把一個空的幀資料

OneFrame

line_size

設定為 ,并壓入緩沖隊列。

BaseEncoder

拿到這個空資料幀時,往

FFmpeg

avcodec_send_frame()

發送一個

NULL

資料,則

FFmpeg

會自動結束編碼。

具體請看以下代碼:

// BaseEncoder.cpp

void BaseEncoder::LoopEncode() {
    if (m_state_cb != NULL) {
        m_state_cb->EncodeStart();
    }
    while (true) {
        if (m_src_frames.size() == 0) {
            Wait();
        }
        while (m_src_frames.size() > 0) {
            // 1. 擷取待解碼資料
            m_frames_lock.lock();
            OneFrame *one_frame = m_src_frames.front();
            m_src_frames.pop();
            m_frames_lock.unlock();

            AVFrame *frame = NULL;
            if (one_frame->line_size != 0) {
                m_src_time_base = one_frame->time_base;
                // 2. 子類處理資料
                frame = DealFrame(one_frame);
                delete one_frame;
                if (m_state_cb != NULL) {
                    m_state_cb->EncodeSend();
                }
                if (frame == NULL) {
                    continue;
                }
            } else { //如果資料長度為0,說明編碼已經結束,壓入空frame,使編碼器進入結束狀态
                delete one_frame;
            }
            // 3. 将資料發送到編碼器
            int ret = avcodec_send_frame(m_codec_ctx, frame);
            switch (ret) {
                case AVERROR_EOF:
                    LOG_ERROR(TAG, LogSpec(), "Send frame finish [AVERROR_EOF]")
                    break;
                case AVERROR(EAGAIN): //編碼編碼器已滿,先取出已編碼資料,再嘗試發送資料
                    while (ret == AVERROR(EAGAIN)) {
                        LOG_ERROR(TAG, LogSpec(), "Send frame error[EAGAIN]: %s", av_err2str(AVERROR(EAGAIN)));
                        // 4. 将編碼好的資料榨幹
                        if (DrainEncode()) return; //編碼結束
                        // 5. 重新發送資料
                        ret = avcodec_send_frame(m_codec_ctx, frame);
                    }
                    break;
                case AVERROR(EINVAL):
                    LOG_ERROR(TAG, LogSpec(), "Send frame error[EINVAL]: %s", av_err2str(AVERROR(EINVAL)));
                    break;
                case AVERROR(ENOMEM):
                    LOG_ERROR(TAG, LogSpec(), "Send frame error[ENOMEM]: %s", av_err2str(AVERROR(ENOMEM)));
                    break;
                default:
                    break;
            }
            if (ret != 0) break;
        }

        if (DrainEncode()) break; //編碼結束
    }
}           

複制

接下來看下上面提到的

DrainEncode()

方法:

// BaseEncoder.cpp

bool BaseEncoder::DrainEncode() {
    int state = EncodeOneFrame();
    while (state == 0) {
        state = EncodeOneFrame();
    }
    return state == AVERROR_EOF;
}

int BaseEncoder::EncodeOneFrame() {
    int state = avcodec_receive_packet(m_codec_ctx, m_encoded_pkt);
    switch (state) {
        case AVERROR_EOF: //解碼結束
            LOG_ERROR(TAG, LogSpec(), "Encode finish")
            break;
        case AVERROR(EAGAIN): //編碼還未完成,待會再來
            LOG_INFO(TAG, LogSpec(), "Encode error[EAGAIN]: %s", av_err2str(AVERROR(EAGAIN)));
            break;
        case AVERROR(EINVAL):
            LOG_ERROR(TAG, LogSpec(),  "Encode error[EINVAL]: %s", av_err2str(AVERROR(EINVAL)));
            break;
        case AVERROR(ENOMEM):
            LOG_ERROR(TAG, LogSpec(), "Encode error[ENOMEM]: %s", av_err2str(AVERROR(ENOMEM)));
            break;
        default: // 成功擷取到一幀編碼好的資料,寫入 MP4
            //将視訊pts/dts轉換為容器pts/dts
            av_packet_rescale_ts(m_encoded_pkt, m_src_time_base,
                                 m_muxer->GetTimeBase(m_encode_stream_index));
            if (m_state_cb != NULL) {
                m_state_cb->EncodeFrame(m_encoded_pkt->data);
                long cur_time = (long)(m_encoded_pkt->pts*av_q2d(m_muxer->GetTimeBase(m_encode_stream_index))*1000);
                m_state_cb->EncodeProgress(cur_time);
            }
            m_encoded_pkt->stream_index = m_encode_stream_index;
            m_muxer->Write(m_encoded_pkt);
            break;
    }
    av_packet_unref(m_encoded_pkt);
    return state;
}           

複制

同樣是一個

while

循環,根據接收資料的狀态來判斷是否結束循環。

主要邏輯在

EncodeOneFrame()

中,通過

avcodec_receive_packet()

擷取

FFmpeg

中已經完成編碼的資料,如果該方法傳回

說明擷取成功,可以将資料寫入

MP4

中。

EncodeOneFrame()

傳回的就是

avcodec_receive_packet

的傳回值,那麼當其為

時,循環擷取下一幀資料,直到傳回值為

AVERROR(EAGAIN)

AVERROR_EOF

,既:沒有資料 或 編碼結束。

如此,通過以上幾個循環,不斷往編碼器塞入資料,和拉取資料,直到完成所有資料編碼,結束編碼。

第3步,結束編碼,釋放資源

完成編碼後,需要釋放相關的資源

// BaseEncoder.cpp

void BaseEncoder::DoRelease() {
    if (m_encoded_pkt != NULL) {
        av_packet_free(&m_encoded_pkt);
        m_encoded_pkt = NULL;
    }
    if (m_codec_ctx != NULL) {
        avcodec_close(m_codec_ctx);
        avcodec_free_context(&m_codec_ctx);
    }
    // 調用子類方法,釋放子類資源
    Release();

    if (m_state_cb != NULL) {
        m_state_cb->EncodeFinish();
    }
}           

複制

封裝視訊編碼器

視訊編碼器繼承自上面定義好的基礎編碼器

BaseEncoder

// VideoEncoder.h

class VideoEncoder: public BaseEncoder {
private:

    const char * TAG = "VideoEncoder";

    // 視訊格式轉化工具
    SwsContext *m_sws_ctx = NULL;

    // 一陣 YUV 資料
    AVFrame *m_yuv_frame = NULL;

    // 目标視訊寬高
    int m_width = 0, m_height = 0;

    void InitYUVFrame();

protected:

    const char *const LogSpec() override {
        return "視訊";
    };

    void InitContext(AVCodecContext *codec_ctx) override;
    int ConfigureMuxerStream(Mp4Muxer *muxer, AVCodecContext *ctx) override;
    AVFrame* DealFrame(OneFrame *one_frame) override;
    void Release() override;

public:
    VideoEncoder(JNIEnv *env, Mp4Muxer *muxer, int width, int height);

};           

複制

具體實作:

1. 構造方法:

// VideoEncoder.cpp

VideoEncoder::VideoEncoder(JNIEnv *env, Mp4Muxer *muxer, int width, int height)
: BaseEncoder(env, muxer, AV_CODEC_ID_H264),
m_width(width),
m_height(height) {
    m_sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGBA,
                               width, height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR,
                               NULL, NULL, NULL);
}           

複制

這裡根據目标輸出視訊的寬高,原格式(OpenGL輸出的RGBA資料)/目标格式(YUV),初始化格式轉換器,這個與解碼剛好是相反的過程。

2. 編碼參數初始化:

2.1 初始化上下文和子類内部資料,主要時配置編碼視訊的

寬高

碼率

幀率

時間基

等。

還有一個比較重要的參數就是

qmin

qmax

,其值範圍為 [0~51],用于配置編碼畫面品質,值越大,畫面品質越低,視訊檔案越小。可以跟自己的需求配置。

還有就是

InitYUVFrame()

申請轉碼需要用到的

YUV

資料記憶體空間。

// VideoEncoder.cpp

void VideoEncoder::InitContext(AVCodecContext *codec_ctx) {

    codec_ctx->bit_rate = 3*m_width*m_height;

    codec_ctx->width = m_width;
    codec_ctx->height = m_height;

    //把1秒鐘分成fps個機關
    codec_ctx->time_base = {1, ENCODE_VIDEO_FPS};
    codec_ctx->framerate = {ENCODE_VIDEO_FPS, 1};

    //畫面組大小
    codec_ctx->gop_size = 50;
    //沒有B幀
    codec_ctx->max_b_frames = 0;

    codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;

    codec_ctx->thread_count = 8;

    av_opt_set(codec_ctx->priv_data, "preset", "ultrafast", 0);
    av_opt_set(codec_ctx->priv_data, "tune", "zerolatency", 0);

    //這是量化範圍設定,其值範圍為0~51,
    //越小品質越高,需要的比特率越大,0為無損編碼
    codec_ctx->qmin = 28;
    codec_ctx->qmax = 50;

    //全局的編碼資訊
    codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

    InitYUVFrame();

    LOGI(TAG, "Init codec context success")
}

void VideoEncoder::InitYUVFrame() {
    //設定YUV輸出空間
    m_yuv_frame = av_frame_alloc();
    m_yuv_frame->format = AV_PIX_FMT_YUV420P;
    m_yuv_frame->width = m_width;
    m_yuv_frame->height = m_height;
    //配置設定空間
    int ret = av_frame_get_buffer(m_yuv_frame, 0);
    if (ret < 0) {
        LOGE(TAG, "Fail to get yuv frame buffer");
    }
}           

複制

2.2 根據解碼器資訊,寫入對應的 MP4 軌道資訊。

// VideoEncoder.cpp

int VideoEncoder::ConfigureMuxerStream(Mp4Muxer *muxer, AVCodecContext *ctx) {
    return muxer->AddVideoStream(ctx);
}           

複制

3. 處理資料

還記得父類定義的子類資料處理方法嗎?

視訊編碼器需要将

OpenGL

輸出到

RGBA

資料轉化為

YUV

資料,才能送進編碼器編碼。

// VideoEncoder.cpp

AVFrame* VideoEncoder::DealFrame(OneFrame *one_frame) {
    uint8_t *in_data[AV_NUM_DATA_POINTERS] = { 0 };
    in_data[0] = one_frame->data;
    int src_line_size[AV_NUM_DATA_POINTERS] = { 0 };
    src_line_size[0] = one_frame->line_size;

    int h = sws_scale(m_sws_ctx, in_data, src_line_size, 0, m_height,
                      m_yuv_frame->data, m_yuv_frame->linesize);
    if (h <= 0) {
        LOGE(TAG, "轉碼出錯");
        return NULL;
    }

    m_yuv_frame->pts = one_frame->pts;

    return m_yuv_frame;
}           

複制

4. 釋放子類資源

編碼結束後,父類回調子類方法,方法資源,通知

Mp4Muxer

結束視訊通道寫入。

// VideoEncoder.cpp

void VideoEncoder::Release() {
    if (m_yuv_frame != NULL) {
        av_frame_free(&m_yuv_frame);
        m_yuv_frame = NULL;
    }
    if (m_sws_ctx != NULL) {
        sws_freeContext(m_sws_ctx);
        m_sws_ctx = NULL;
    }
    // 結束視訊通道資料寫入
    m_muxer->EndVideoStream();
}           

複制

封裝音頻編碼器

音頻編碼器基本視訊是一樣的,隻是參數配置有所不同,直接來看實作就好。

正常的音頻參數配置:比特率,編碼格式,通道數量等

重點看下

InitFrame()

方法,這裡需要通過通道數、編碼格式等,借助

av_samples_get_buffer_size()

方法,計算用來儲存目标幀資料的記憶體大小。

// AudioEncoder.cpp

AudioEncoder::AudioEncoder(JNIEnv *env, Mp4Muxer *muxer)
: BaseEncoder(env, muxer, AV_CODEC_ID_AAC) {

}

void AudioEncoder::InitContext(AVCodecContext *codec_ctx) {
    codec_ctx->codec_type = AVMEDIA_TYPE_AUDIO;
    codec_ctx->sample_fmt = ENCODE_AUDIO_DEST_FORMAT;
    codec_ctx->sample_rate = ENCODE_AUDIO_DEST_SAMPLE_RATE;
    codec_ctx->channel_layout = ENCODE_AUDIO_DEST_CHANNEL_LAYOUT;
    codec_ctx->channels = ENCODE_AUDIO_DEST_CHANNEL_COUNTS;
    codec_ctx->bit_rate = ENCODE_AUDIO_DEST_BIT_RATE;

    InitFrame();
}

void AudioEncoder::InitFrame() {
    m_frame = av_frame_alloc();
    m_frame->nb_samples = 1024;
    m_frame->format = ENCODE_AUDIO_DEST_FORMAT;
    m_frame->channel_layout = ENCODE_AUDIO_DEST_CHANNEL_LAYOUT;

    int size = av_samples_get_buffer_size(NULL, ENCODE_AUDIO_DEST_CHANNEL_COUNTS, m_frame->nb_samples,
                                          ENCODE_AUDIO_DEST_FORMAT, 1);
    uint8_t *frame_buf = (uint8_t *) av_malloc(size);
    avcodec_fill_audio_frame(m_frame, ENCODE_AUDIO_DEST_CHANNEL_COUNTS, ENCODE_AUDIO_DEST_FORMAT,
                             frame_buf, size, 1);
}

int AudioEncoder::ConfigureMuxerStream(Mp4Muxer *muxer, AVCodecContext *ctx) {
    return muxer->AddAudioStream(ctx);
}

AVFrame* AudioEncoder::DealFrame(OneFrame *one_frame) {
    m_frame->pts = one_frame->pts;
    memcpy(m_frame->data[0], one_frame->data, 4096);
    memcpy(m_frame->data[1], one_frame->ext_data, 4096);
    return m_frame;
}

void AudioEncoder::Release() {
    m_muxer->EndAudioStream();
}           

複制

最後,

DealFrame

需要将

one_frame

中儲存的左右聲道的資料複制到

m_frame

申請的記憶體中,并傳回給

父類

送到編碼器編碼。

四、擷取 OpenGL 渲染的視訊資料

我們知道,視訊資料經過 OpenGL 編輯以後,是無法直接送到編碼器進行編碼的,需要通過

OpenGL

glReadPixels

方法來擷取。

下面就改造一下原來定義的

OpenGLRender

來實作。

完整代碼請檢視工程源碼。

在渲染方法

Render()

中,增加擷取的畫面的方法:

// OpenGLRender.cpp

void OpenGLRender::Render() {
    if (RENDERING == m_state) {
        m_drawer_proxy->Draw();
        m_egl_surface->SwapBuffers();

        if (m_need_output_pixels && m_pixel_receiver != NULL) {//輸出畫面rgba
            m_need_output_pixels = false;
            Render(); //再次渲染最新的畫面

            size_t size = m_window_width * m_window_height * 4 * sizeof(uint8_t);

            uint8_t *rgb = (uint8_t *) malloc(size);
            if (rgb == NULL) {
                realloc(rgb, size);
                LOGE(TAG, "記憶體配置設定失敗: %d", rgb)
            }
            glReadPixels(0, 0, m_window_width, m_window_height, GL_RGBA, GL_UNSIGNED_BYTE, rgb);
            
            // 将資料發送出去
            m_pixel_receiver->ReceivePixel(rgb);
        }
    }
}           

複制

增加一個請求方法,用于通知

OpenGLRender

将資料輸發送出來:

// OpenGLRender.cpp

void OpenGLRender::RequestRgbaData() {
    m_need_output_pixels = true;
}           

複制

原理很簡單,在解碼器解碼一幀資料送入

OpenGL

渲染以後,就馬上通知

OpenGLRender

将畫面發送出來。

當然了,還需要定義一個接收器:

// OpenGLPixelReceiver.h

class OpenGLPixelReceiver {
public:
    virtual void ReceivePixel(uint8_t *rgba) = 0;
};           

複制

五、MP4 封裝器

該部分内容基本就是上一篇文章的定義的重打包

FFRepack

工具的重新封裝,這裡不再贅述,請檢視上一篇文章,或源碼。

// Mp4Muxer.cpp

void Mp4Muxer::Init(JNIEnv *env, jstring path) {
    const char *u_path = env->GetStringUTFChars(path, NULL);

    int len = strlen(u_path);
    m_path = new char[len];
    strcpy(m_path, u_path);

    //建立輸出上下文
    avformat_alloc_output_context2(&m_fmt_ctx, NULL, NULL, m_path);

    // 釋放引用
    env->ReleaseStringUTFChars(path, u_path);
}

int Mp4Muxer::AddVideoStream(AVCodecContext *ctx) {
    int stream_index = AddStream(ctx);
    m_video_configured = true;
    Start();
    return stream_index;
}

int Mp4Muxer::AddAudioStream(AVCodecContext *ctx) {
    int stream_index = AddStream(ctx);
    m_audio_configured = true;
    Start();
    return stream_index;
}

int Mp4Muxer::AddStream(AVCodecContext *ctx) {
    AVStream *video_stream = avformat_new_stream(m_fmt_ctx, NULL);
    avcodec_parameters_from_context(video_stream->codecpar, ctx);
    video_stream->codecpar->codec_tag = 0;
    return video_stream->index;
}

void Mp4Muxer::Start() {
    if (m_video_configured && m_audio_configured) {
        av_dump_format(m_fmt_ctx, 0, m_path, 1);
        //打開檔案輸入
        int ret = avio_open(&m_fmt_ctx->pb, m_path, AVIO_FLAG_WRITE);
        if (ret < 0) {
            LOGE(TAG, "Open av io fail")
            return;
        } else {
            LOGI(TAG, "Open av io: %s", m_path)
        }
        //寫入頭部資訊
        ret = avformat_write_header(m_fmt_ctx, NULL);
        if (ret < 0) {
            LOGE(TAG, "Write header fail")
            return;
        } else {
            LOGI(TAG, "Write header success")
        }
    }
}

void Mp4Muxer::Write(AVPacket *pkt) {
    int ret = av_interleaved_write_frame(m_fmt_ctx, pkt);
//    uint64_t time = uint64_t (pkt->pts*av_q2d(GetTimeBase(pkt->stream_index))*1000);
//    LOGE(TAG, "Write one frame pts: %lld, ret = %s", time , av_err2str(ret))
}

void Mp4Muxer::EndAudioStream() {
    LOGI(TAG, "End audio stream")
    m_audio_end = true;
    Release();
}

void Mp4Muxer::EndVideoStream() {
    LOGI(TAG, "End video stream")
    m_video_end = true;
    Release();
}

void Mp4Muxer::Release() {
    if (m_video_end && m_audio_end) {
        if (m_fmt_ctx) {
            //寫入檔案尾部
            av_write_trailer(m_fmt_ctx);

            //關閉輸出IO
            avio_close(m_fmt_ctx->pb);

            //釋放資源
            avformat_free_context(m_fmt_ctx);

            m_fmt_ctx = NULL;
        }
        delete [] m_path;
        LOGI(TAG, "Muxer Release")
        if (m_mux_finish_cb) {
            m_mux_finish_cb->OnMuxFinished();
        }
    }
}           

複制

六、整合調用

有了以上工具的定義和封裝,加上之前的解碼器和渲染器,就萬事俱備,隻欠東風了!

我們需要将他們整合在一起,串聯起整個【解碼--編輯--編碼--寫入MP4】流程。

定義合成器

Synthesizer

初始化

// Synthesizer.cpp

// 這裡直接寫死視訊寬高了, 需要根據自己的需求動态配置
static int WIDTH = 1920;
static int HEIGHT = 1080;

Synthesizer::Synthesizer(JNIEnv *env, jstring src_path, jstring dst_path) {

    // 封裝器
    m_mp4_muxer = new Mp4Muxer();
    m_mp4_muxer->Init(env, dst_path);
    m_mp4_muxer->SetMuxFinishCallback(this);
    
    // --------------------------視訊配置--------------------------
    // 【視訊編碼器】
    m_v_encoder = new VideoEncoder(env, m_mp4_muxer, WIDTH, HEIGHT);
    m_v_encoder->SetStateReceiver(this);

    // 【繪制器】
    m_drawer_proxy = new DefDrawerProxyImpl();
    VideoDrawer *drawer = new VideoDrawer();
    m_drawer_proxy->AddDrawer(drawer);

    // 【OpenGL 渲染器】
    m_gl_render = new OpenGLRender(env, m_drawer_proxy);
    // 設定離屏渲染畫面寬高
    m_gl_render->SetOffScreenSize(WIDTH, HEIGHT);
    // 接收經過(編輯)渲染的畫面資料
    m_gl_render->SetPixelReceiver(this);

    // 【視訊解碼器】
    m_video_decoder = new VideoDecoder(env, src_path, true);
    m_video_decoder->SetRender(drawer);

    // 監聽解碼狀态
    m_video_decoder->SetStateReceiver(this);

    //--------------------------音頻配置--------------------------
    // 【音頻編碼器】
    m_a_encoder = new AudioEncoder(env, m_mp4_muxer);
    // 監聽編碼狀态
    m_a_encoder->SetStateReceiver(this);

    // 【音頻解碼器】
    m_audio_decoder = new AudioDecoder(env, src_path, true);
    // 監聽解碼狀态
    m_audio_decoder->SetStateReceiver(this);
}           

複制

可以看到,解碼流程和以前幾乎時一模一樣的,三個不一樣的地方是:

  1. 需要告訴解碼器,這是合成過程,無需在解碼後加入時間同步。
  2. OpenGL 渲染是離屏渲染,需要設定渲染尺寸
  3. 音頻無需渲染到 OpenSL 中,直接發送出來壓入編碼即可。

啟動

初始化完畢後,解碼器進入等待,需要外面觸發進入循環解碼流程。

// Synthesizer.cpp

void Synthesizer::Start() {
    m_video_decoder->GoOn();
    m_audio_decoder->GoOn();
}           

複制

當調用了

BaseDecoder

GoOn()

方法以後,整個【解碼-->編碼】流程将被啟動。

而将它們粘合起來的,就是解碼器的狀态回調方法

DecodeOneFrame()

// Synthesizer.cpp

bool Synthesizer::DecodeOneFrame(IDecoder *decoder, OneFrame *frame) {
    if (decoder == m_video_decoder) {
        // 等待上一幀畫面資料壓入編碼緩沖隊列 
        while (m_cur_v_frame) {
            av_usleep(2000); // 2ms
        }
        m_cur_v_frame = frame;
        m_gl_render->RequestRgbaData();
        return m_v_encoder->TooMuchData();
    } else {
        m_cur_a_frame = frame;
        m_a_encoder->PushFrame(frame);
        return m_a_encoder->TooMuchData();
    }
}

void Synthesizer::ReceivePixel(uint8_t *rgba) {
    OneFrame *rgbFrame = new OneFrame(rgba, m_cur_v_frame->line_size,
                                      m_cur_v_frame->pts, m_cur_v_frame->time_base);
    m_v_encoder->PushFrame(rgbFrame);
    // 清空上一幀資料資訊
    m_cur_v_frame = NULL;
}           

複制

當接收到解碼器的一幀資料後,

  • 如果是音頻資料,直接将資料通過

    BaseDecoder

    PushFrame()

    方法壓入隊列。
  • 如果是視訊資料,将目前幀資料資訊儲存下來,并通知

    OpenGLRender

    将畫面資料發送出來。在

    ReceivePixel()

    方法中接收到畫面資料後,将資料

    PushFrame()

    到視訊編碼器中。

直到解碼完畢,在

DecodeFinish()

方法中,壓入空資料幀,通知編碼器結束編碼。

// Synthesizer.cpp

void Synthesizer::DecodeFinish(IDecoder *decoder) {
    // 編碼結束,壓入一幀空資料,通知編碼器結束編碼
    if (decoder == m_video_decoder) {
        m_v_encoder->PushFrame(new OneFrame(NULL, 0, 0, AVRational{1, 25}, NULL));
    } else {
        m_a_encoder->PushFrame(new OneFrame(NULL, 0, 0, AVRational{1, 25}, NULL));
    }
}

void Synthesizer::EncodeFinish() {
    LOGI("Synthesizer", "EncodeFinish ...");
}

void Synthesizer::OnMuxFinished() {
    LOGI("Synthesizer", "OnMuxFinished ...");
    m_gl_render->Stop();

    if (m_mp4_muxer != NULL) {
        delete m_mp4_muxer;
    }
    m_drawer_proxy = NULL;
}           

複制

至此,整個流程就完整了!!!