【Android 音视频开发打怪升级:FFmpeg音视频编解码篇】七、Android FFmpeg 视频编码

声 明

首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。

码字不易,转载请注明出处!

教程代码:【Github传送门

目录

一、Android音视频硬解码篇:

二、使用OpenGL渲染视频画面篇

三、Android FFmpeg音视频解码篇

本文你可以了解到

如何使用 FFmepg 对编辑好的视频进行重新编码,生成可以播放的音视频文件。

写在前面

本文是音视频系列文章的最后一篇了,也是拖了最久的一篇(懒癌发作-_-!!),终于下定决心,把坑填完。

话不多说了,马上进入正文。

在上一篇文章中,介绍了如何对音视频文件进行解封和重新封装,这个过程不涉及音视频的解码和编码,也就是没有对音视频进行编辑,这无法满足日常的开发需求。

因此,本文将填上编辑过程的空缺,为本系列画上句号。

一、整体流程说明

在前面的几篇文章中,我们已经做好了 解码器OpenGL 渲染器,因此,编码的时候,除了需要 编码器 外,还需要将之前的内容做好整合。下面通过一张图做一下简要说明:

模块

首先可以关注到,这个过程有三个大模块,也是三个 独立又互相关联 的线程,分别负责:

  • 原视频解码
  • 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_muxerm_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 来判断。

当监听到解码器通知解码完成的时候,则把一个空的帧数据 OneFrameline_size 设置为 0,并压入缓冲队列。

BaseEncoder 拿到这个空数据帧时,往 FFmpegavcodec_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 中已经完成编码的数据,如果该方法返回 0 说明获取成功,可以将数据写入 MP4 中。

EncodeOneFrame() 返回的就是 avcodec_receive_packet 的返回值,那么当其为 0 时,循环获取下一帧数据,直到返回值为 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 初始化上下文和子类内部数据,主要时配置编码视频的 宽高码率帧率时间基 等。

还有一个比较重要的参数就是 qminqmax,其值范围为 [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 编辑以后,是无法直接送到编码器进行编码的,需要通过 OpenGLglReadPixels 方法来获取。

下面就改造一下原来定义的 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();
}

当调用了 BaseDecoderGoOn() 方法以后,整个【解码-->编码】流程将被启动。

而将它们粘合起来的,就是解码器的状态回调方法 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;
}

当接收到解码器的一帧数据后,

  • 如果是音频数据,直接将数据通过 BaseDecoderPushFrame() 方法压入队列。
  • 如果是视频数据,将当前帧数据信息保存下来,并通知 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;
}

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

本系列文章终于完结,总算是把坑填完,为自己撒花~哈哈哈~ 🎉🎉🎉🎉🎉🎉🎉🎉🎉

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容