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

声 明
首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。
码字不易,转载请注明出处!
目录
一、Android音视频硬解码篇:
二、使用OpenGL渲染视频画面篇
- 1,初步了解OpenGL ES
 - 2,使用OpenGL渲染视频画面
 - 3,OpenGL渲染多视频,实现画中画
 - 4,深入了解OpenGL之EGL
 - 5,OpenGL FBO数据缓冲区
 - 6,Android音视频硬编码:生成一个MP4
 
三、Android FFmpeg音视频解码篇
- 1,FFmpeg so库编译
 - 2,Android 引入FFmpeg
 - 3,Android FFmpeg视频解码播放
 - 4,Android FFmpeg+OpenSL ES音频解码播放
 - 5,Android FFmpeg+OpenGL ES播放视频
 - 6,Android FFmpeg简单合成MP4:视屏解封与重新封装
 - 7,Android FFmpeg视频编码
 
本文你可以了解到
如何使用 FFmepg 对编辑好的视频进行重新编码,生成可以播放的音视频文件。
写在前面
本文是音视频系列文章的最后一篇了,也是拖了最久的一篇(懒癌发作-_-!!),终于下定决心,把坑填完。话不多说了,马上进入正文。
在【上一篇文章】中,介绍了如何对音视频文件进行解封和重新封装,这个过程不涉及音视频的解码和编码,也就是没有对音视频进行编辑,这无法满足日常的开发需求。
因此,本文将填上编辑过程的空缺,为本系列画上句号。
一、整体流程说明
在前面的几篇文章中,我们已经做好了 解码器, OpenGL 渲染器,因此,编码的时候,除了需要 编码器 外,还需要将之前的内容做好整合。下面通过一张图做一下简要说明:

模块
首先可以关注到,这个过程有三个大模块,也是三个 独立又互相关联 的线程,分别负责:
- 原视频解码
 - OpenGL 画面渲染
 - 目标视频编码
 
数据流向
看下视频数据是如何流转的:
原视频经过
解码器解码后,得到YUV数据,经过格式转换,成为RGB数据。解码器将RGB数据传递给绘制器,等待OpenGL 渲染器使用。OpenGL 渲染器通过内部的线程循环,在适当的时候,调用绘制器渲染画面。画面绘制完毕以后,得到经过
OpenGL渲染(编辑过)的画面,送到编码器进行编码。最后,将编码好的数据,写入本地文件。
说明:
本文将主要讲音视频的
编码知识,由于整个过程涉及到解码、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() 方法:
- 根据编码格式 ID 查找编码器
 - 分配编码上下文
 - 初始化编码数据包
 
最后,创建编码线程。
封装编码流程
// 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 个步骤:
- 从缓冲队列中获取待解码数据
 - 将原始数据交给子类处理(音频和视频根据自己的需求处理)
 - 通过 
avcodec_send_frame将数据发送到编码器编码 - 将编码好的数据抽取出来
 
还有一点,既第 5 点,重新发送数据。
需要说明一下这里采取的 双循环 编码逻辑:除了最外层的 while(tue) 循环以外,里面还有一个 while (m_src_frames.size() > 0) 循环。
在缓冲队列有数据,并且 FFmpeg 内部编码队列未满 的情况下,会不断地往 FFmpeg 发送数据,直到发现 FFmpeg 编码返回 AVERROR(EAGAIN) ,则说明内部队列已满,需要先将编码的数据抽取出来,也就是调用 DrainEncode() 方法。
还有一点需要说明的是:如何判读所有数据已经都发送给编码器了?
这里通过
one_frame->line_size来判断。当监听到解码器通知解码完成的时候,则把一个空的帧数据
OneFrame的line_size设置为0,并压入缓冲队列。
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 中已经完成编码的数据,如果该方法返回 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 初始化上下文和子类内部数据,主要时配置编码视频的 宽高、码率、帧率、时间基 等。
还有一个比较重要的参数就是 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);
}
可以看到,解码流程和以前几乎时一模一样的,三个不一样的地方是:
需要告诉解码器,这是合成过程,无需在解码后加入时间同步。
OpenGL 渲染是离屏渲染,需要设置渲染尺寸
音频无需渲染到 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;
}
至此,整个流程就完整了!!!
本系列文章终于完结啦,总算是把坑填完,为自己撒花~ 哈哈哈~ 🎉🎉🎉🎉🎉🎉🎉🎉🎉
