• 音视频技术应用(16) 调节x264编码器, 控制编码速度和质量


    有时候需要根据实际情况对x264编码器进行实际的调节,以达到编码速度和视频质量的平衡,FFmpeg提供了一系列参数方便我们直接去设定:

    1. preset 参数

    主要用于调节编码速度和质量的平衡,其可选参数如下:

    ultrafast、superfast、veryfast、faster、fast、medium、slow、slower、veryslow、placebo

    从左到右代表编码速度的快慢,越慢代表视频质量越好,如果要追求好的质量可以选择靠右的参数,如果要追求较快的速度可以选择靠左的参数;

    2. tune

    主要用于调节视频类型、对视觉效果进行优化,设定此参数,方便在编码过程中, 根据不同的视频类型选择对应的特征算法,做不同的优化处理,以达到最好的效果。其可选参数如下:

    • film: 电影类型。电影中画面的人物或自然场景颜色比较丰富,光影变化大,根据这样的场景提供的一套优化方法;
    • animation: 动画。特指普通动画,非三维动画,普通的动画中会存在颜色块,或者整片颜色,它的光影变化不大;
    • grain: 需要保留大量的颗粒效果的场景,比如像“我的世界”那种包含大量颗粒效果的视频类型;
    • stillimage: 静态图像编码时使用;比如视频相册这种,只需要一个切换过程,其它图片都是静止的;
    • fastdecode: 可以快速解码的参数;
    • zerolatency: 零延时,用在需要非常低的延迟的情况下,比如电视电话会议的编码。注意不能设置B帧。

    还有一些具体的参数,详见: libavcodec\libx264.c , 里面定义了具体支持的所有参数类型,以及各种参数的说明和默认值等:

    下面使用代码实际演示一下设置以上两种参数达到的最终效果:

    1. 使用默认的x264编码器参数编码视频:

    #include <iostream>
    #include <fstream>
    
    using namespace std;
    
    extern "C" { // 指定函数是C语言函数,以C语言的方式去编译
    #include <libavcodec/avcodec.h>
    #include <libavutil/opt.h>
    }
    
    // 以预处理指令的方式导入库
    #pragma comment(lib, "avcodec.lib")
    #pragma comment(lib, "avutil.lib")
    
    int main(int argc, char *argv[])
    {
    
        string filename = "400_300_25";
        
        AVCodecID codec_id = AV_CODEC_ID_H264;
    
        if (argc > 1)
        {
            string codec = argv[1];
            if (codec == "h265" || codec == "hevc")
            {
                codec_id = AV_CODEC_ID_HEVC;
            }
        }
    
        if (codec_id == AV_CODEC_ID_H264)
        {
            filename += ".h264";
        }
        else if (codec_id == AV_CODEC_ID_HEVC)
        {
            filename += ".h265";
        }
    
        ofstream ofs;
        ofs.open(filename, ios::binary);
    
        // 1. 查找编码器        AV_CODEC_ID_H264(h264) AV_CODEC_ID_HEVC(h265)
        auto codec = avcodec_find_encoder(codec_id);
        if (!codec)
        {
            cerr << "codec not found" << endl;
            return -1;
        }
    
        // 2. 创建上下文
        auto c = avcodec_alloc_context3(codec);
        if (!c)
        {
            cerr << "avcodec_alloc_context3 failed" << endl;
            return -1;
        }
    
        // 3. 设定编码器的上下文参数
        c->width = 400;                                            // 视频帧的宽度
        c->height = 300;                                        // 视频帧的高度
        c->time_base = { 1, 25 };                                // 时间基数,即时间戳的显示单位,可以认为是1秒内显示25帧
        
        c->pix_fmt = AV_PIX_FMT_YUV420P;                        // 指定源数据的像素格式,跟编码格式有关,如果编码格式是h264, 这里就不能指定为RGBA
        c->thread_count = 16;                                    // 编码线程数,可以通过调用系统接口获取CPU的核心数量
    
        // 4. 打开编码上下文
        int re = avcodec_open2(c, codec, nullptr);
        if (re != 0)
        {
            char buf[1024] = { 0 };
            av_strerror(re, buf, sizeof(buf) - 1);
            cerr << "avcodec_open2 failed" << buf << endl;
            return -1;
        }
    
        cout << "avcodec_open2 success" << endl;
    
        // 创建AVFrame空间,用于表示一帧数据
        auto frame = av_frame_alloc();
        frame->width = c->width;
        frame->height = c->height;
        frame->format = c->pix_fmt;
    
        re = av_frame_get_buffer(frame, 0);
        if (re != 0)
        {
            char buf[1024] = {0};
            av_strerror(re, buf, sizeof(buf) - 1);
            cerr << "av_frame_get_buffer() failed" << endl;
            return -1;
        }
    
        auto pkt = av_packet_alloc();
    
        // 创建一个10s的视频,共250帧
        for (int i = 0; i < 250; i++)
        {
            // 生成一帧图像 每帧数据都不同
            // Y
            for (int y = 0; y < frame->height; y++)
            {
                for (int x = 0; x < frame->width; x++)
                {
                    frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
                }
            }
    
            // UV
            for (int y = 0; y < frame->height / 2; y++)
            {
                for (int x = 0; x < frame->width / 2; x++)
                {
                    frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
                    frame->data[2][y * frame->linesize[2] + x] = 64 + x + i * 5;
                }
            }
    
            // 显示的时间
            frame->pts = i;
    
            re = avcodec_send_frame(c, frame);
            if (re != 0)
            {
                cerr << "avcodec_send_frame() failed" << endl;
                break;
            }
    
            // >=0 表示有数据返回,编码一个AVFrame 可能会返回多个AVPacket
            while (re >= 0)
            {
                // 接收压缩帧 一般前几次调用会返回空的 (缓冲,立即返回,编码未完成)
                // 编码是在独立的线程中完成的
                // 每次调用会重新分配pkt的数据空间,并改变内部的指针指向
                re = avcodec_receive_packet(c, pkt);
                if (re == AVERROR(EAGAIN) || re == AVERROR_EOF)
                    break;
    
                if (re < 0)
                {
                    char buf[1024] = { 0 };
                    av_strerror(re, buf, sizeof(buf) - 1);
                    cerr << "avcodec_receive_packet() failed" << endl;
                    return -1;
                }
    
                cout << pkt->size << " " << flush;
                
                // 写入编码后的压缩数据
                ofs.write((char *)pkt->data, pkt->size);
    
                // 注意 一定要调用 av_packet_unref() 删除已申请的数据空间,若不删除,下次还会重新申请,这样会造成内存泄漏
                av_packet_unref(pkt);
            }
        }
    
        // 释放AVPacket
        av_packet_free(&pkt);
    
        // 释放AVFrame
        av_frame_free(&frame);
    
        // 释放上下文
        avcodec_free_context(&c);
    }

    使用Bitrate Viewer 打开生成的h264文件:

    可以看到,码率基本维持在 163kbps, 下面指定编码器参数再来测试下,

    修改上述code,设定preset 和 tune, 添加红色部分的代码:

    #include <iostream>
    #include <fstream>
    
    using namespace std;
    
    extern "C" { // 指定函数是C语言函数,以C语言的方式去编译
    #include <libavcodec/avcodec.h>
    #include <libavutil/opt.h>
    }
    
    // 以预处理指令的方式导入库
    #pragma comment(lib, "avcodec.lib")
    #pragma comment(lib, "avutil.lib")
    
    int main(int argc, char* argv[])
    {
    
        string filename = "400_300_25_preset";
    
        AVCodecID codec_id = AV_CODEC_ID_H264;
    
        if (argc > 1)
        {
            string codec = argv[1];
            if (codec == "h265" || codec == "hevc")
            {
                codec_id = AV_CODEC_ID_HEVC;
            }
        }
    
        if (codec_id == AV_CODEC_ID_H264)
        {
            filename += ".h264";
        }
        else if (codec_id == AV_CODEC_ID_HEVC)
        {
            filename += ".h265";
        }
    
        ofstream ofs;
        ofs.open(filename, ios::binary);
    
        // 1. 查找编码器        AV_CODEC_ID_H264(h264) AV_CODEC_ID_HEVC(h265)
        auto codec = avcodec_find_encoder(codec_id);
        if (!codec)
        {
            cerr << "codec not found" << endl;
            return -1;
        }
    
        // 2. 创建上下文
        auto c = avcodec_alloc_context3(codec);
        if (!c)
        {
            cerr << "avcodec_alloc_context3 failed" << endl;
            return -1;
        }
    
        // 3. 设定编码器的上下文参数
        c->width = 400;                                            // 视频帧的宽度
        c->height = 300;                                        // 视频帧的高度
        c->time_base = { 1, 25 };                                // 时间基数,即时间戳的显示单位,可以认为是1秒内显示25帧
    
        c->pix_fmt = AV_PIX_FMT_YUV420P;                        // 指定源数据的像素格式,跟编码格式有关,如果编码格式是h264, 这里就不能指定为RGBA
        c->thread_count = 16;                                    // 编码线程数,可以通过调用系统接口获取CPU的核心数量
    
        // 设置编码器参数
        c->max_b_frames = 0;                                    // 将B帧个数设置为0
    
        // 调节编码速度(这里选择的是最快)
        int re = av_opt_set(c->priv_data, "preset", "ultrafast", 0);
        if (re != 0)
        {
            cout << "preset failed" << endl;
            return -1;
        }
    
        // 调整视频的类型 这里选择零延时
        re = av_opt_set(c->priv_data, "tune", "zerolatency", 0);
        if (re != 0)
        {
            cout << "tune failed" << endl;
            return -1;
        }
    
        // 4. 打开编码上下文
        re = avcodec_open2(c, codec, nullptr);
        if (re != 0)
        {
            char buf[1024] = { 0 };
            av_strerror(re, buf, sizeof(buf) - 1);
            cerr << "avcodec_open2 failed" << buf << endl;
            return -1;
        }
    
        cout << "avcodec_open2 success" << endl;
    
        // 创建AVFrame空间,用于表示一帧数据
        auto frame = av_frame_alloc();
        frame->width = c->width;
        frame->height = c->height;
        frame->format = c->pix_fmt;
    
        re = av_frame_get_buffer(frame, 0);
        if (re != 0)
        {
            char buf[1024] = { 0 };
            av_strerror(re, buf, sizeof(buf) - 1);
            cerr << "av_frame_get_buffer() failed" << endl;
            return -1;
        }
    
        auto pkt = av_packet_alloc();
    
        // 创建一个10s的视频,共250帧
        for (int i = 0; i < 250; i++)
        {
            // 生成一帧图像 每帧数据都不同
            // Y
            for (int y = 0; y < frame->height; y++)
            {
                for (int x = 0; x < frame->width; x++)
                {
                    frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
                }
            }
    
            // UV
            for (int y = 0; y < frame->height / 2; y++)
            {
                for (int x = 0; x < frame->width / 2; x++)
                {
                    frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
                    frame->data[2][y * frame->linesize[2] + x] = 64 + x + i * 5;
                }
            }
    
            // 显示的时间
            frame->pts = i;
    
            re = avcodec_send_frame(c, frame);
            if (re != 0)
            {
                cerr << "avcodec_send_frame() failed" << endl;
                break;
            }
    
            // >=0 表示有数据返回,编码一个AVFrame 可能会返回多个AVPacket
            while (re >= 0)
            {
                // 接收压缩帧 一般前几次调用会返回空的 (缓冲,立即返回,编码未完成)
                // 编码是在独立的线程中完成的
                // 每次调用会重新分配pkt的数据空间,并改变内部的指针指向
                re = avcodec_receive_packet(c, pkt);
                if (re == AVERROR(EAGAIN) || re == AVERROR_EOF)
                    break;
    
                if (re < 0)
                {
                    char buf[1024] = { 0 };
                    av_strerror(re, buf, sizeof(buf) - 1);
                    cerr << "avcodec_receive_packet() failed" << endl;
                    return -1;
                }
    
                cout << pkt->size << " " << flush;
    
                // 写入编码后的压缩数据
                ofs.write((char*)pkt->data, pkt->size);
    
                // 注意 一定要调用 av_packet_unref() 删除已申请的数据空间,若不删除,下次还会重新申请,这样会造成内存泄漏
                av_packet_unref(pkt);
            }
        }
    
        // 释放AVPacket
        av_packet_free(&pkt);
    
        // 释放AVFrame
        av_frame_free(&frame);
    
        // 释放上下文
        avcodec_free_context(&c);
    }

    再次查看新生成的h264文件:

    可以看到,码率还是有很明显的提升的。

  • 相关阅读:
    java内部类
    接口与继承
    数据结构
    数据I/O流
    课程总结
    第六次实训作业异常处理
    常用类的课后作业
    窗口实训1
    实训作业4
    实训作业3
  • 原文地址:https://www.cnblogs.com/yongdaimi/p/15690687.html
Copyright © 2020-2023  润新知