H264编解码实战

本文详细解读H264中的profile和level概念,深入剖析H264SPS中的分辨率、帧相关参数,以及如何通过PPS和Slice-Header控制编码。此外,介绍了常用的编码工具和步骤,以及x264参数的分类和实例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、H264中的profile和level

  • H264 Profile:对视频压缩特性的描述,Profile越高,就说明采用了越高级的压缩特性;
  • H264 Level:Level是对视频的描述,Level越高,视频的码率、分辨率、fps越高。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

二、H264 SPS中的重要参数

分辨率:
在这里插入图片描述
帧相关:

  • log2_max_frame_num_minus4:用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 + 4)。MaxFrameNum是frame_num的上限值,frame_num是图像序号的一种表示方法,在帧间编码中常用作一种参考帧标记的手段。值得注意的是 frame_num 是循环计数的,即当它到达 MaxFrameNum 后又从 0 重新开始新一轮的计数。 解码器必须要有机制检测这种循环, 不然会引起类似千年虫的问题,在图像的顺序上造成混乱。
  • pic_order_cnt_type:指明了 poc (picture order count) 的编码方法, poc 标识图像的播放顺序。由于H.264 使用了 B 帧预测,使得图像的解码顺序并不一定等于播放顺序,但它们之间存在一定的映射关系。 poc 可以由 frame-num 通过映射关系计算得来,也可以索性由编码器显式地传送。 H.264 中一共定义了三种 poc 的编码方法,这个句法元素就是用来通知解码器该用哪种方法来计算 poc。
  • max_num_ref_frames:指定参考帧队列可能达到的最大长度,解码器依照这个句法元素的值开辟存储区,这个存储区用于存放已解码的参考帧, H.264 规定最多可用 16 个参考帧,本句法元素的值最大为 16。值得注意的是这个长度以帧为单位,如果在场模式下,应该相应地扩展一倍。

帧率的计算:

framerate = (float)(sps->vui.vui_time_scale) / (float)(sps->vui.vui_num_units_in_tick) / 2;

三、H264 PPS与Slice-Header

PPS:
在这里插入图片描述
Slice Header:

  • 帧类型
  • GOP中解码帧序号
  • 预测权重
  • 滤波

四、H264分析工具

  • Electra Stream Eye(付费)
  • CodecVisa(付费)
  • 雷神开发的工具(免费)

elecard下载地址:
https://www.elecard.com/products/video-analysis
雷神:
https://sourceforge.net/projects/videoeye/files
工具详细的使用说明可以参考雷神写的博客:
https://blog.csdn.net/leixiaohua1020/article/details/34553607

五、视频编码步骤

  • 打开编码器
  • 将采集的数据转换成YUV420P
  • 准备编码数据AVFrame
  • H264编码

六、x264参数分类

  • 预设值
  • 帧相关参数
  • 码流的控制
  • 编码分析
  • 输出

1. 预设值

  • Preset
    默认:medium,在压缩率和运算时间中平衡的预设值。
  • Tune
    默认:无,在上一个选项基础上进一步优化输入。如果定义了一个tune值,它将在preset之后,其它选项之前生效。

2. 帧相关参数

  • Keyint
    默认:250,设置x264输出中最大的IDR帧(也称为关键帧)间距。
    IDR帧是视频流的“分隔符”,所有帧都不可以使用越过关键帧的帧作为参考帧。IDR帧是I帧的一种,所以它们也不参照其它帧。这意味着它们可以作为视频的搜索(seek)点。
    通过这个设置可以设置IDR帧的最大间隔帧数(亦称最大图像组长度)。较大的值将导致IDR帧减少(会用占用空间更少的P帧和B帧取代),也就同时减弱了参照帧选择的限制。较小的值导致减少搜索一个随机帧所需的平均时间。
    建议:默认值(fps的10倍)对大多数视频都很好。如果在为蓝光、广播、直播流或者其它什么专业流编码,也许会需要更小的图像组长度(一般等于fps)。
  • min-keyint
    默认:auto(keyint/10),过小的keyint范围会导致产生“错误的”IDR帧(比如说,一个闪屏场景)。此选项限制了IDR帧之间的最小距离。
  • scenecut
    默认:40,设置决策使用I帧、IDR帧的阈值(场景变换检测)。x264会计算每一帧与前一帧的不同程度并得出一个值。如果这个值低于scenecut,那么就算检测到一个“场景变换”。如果此时距离上一帧的距离小于 min-keyint则插入一个I帧,反之则插入一个IDR帧。
  • bframes
    默认:3,设置x264可使用的B帧的最大连续数量。没有B帧时,一个典型的x264流帧类型是这样的:IPPPPP…PI。如果设置了-bframes 2,那么两个连续的P帧就可以用B帧替换,然后就像这样:IBPBBPBPPPB…PI。
    B帧和P帧的区别在于它可以参照它之后的帧,这个特点让它可以显著地提升压缩率。他们的平均品质受 –pbratio选项的控制。
  • ref
    默认:3,控制DPB (Decoded Picture Buffer)的大小。可以在0-16之间选择。简单地说,就是设置P帧可以选择它之前的多少帧作为参照帧(B帧的值要小1-2,取决于那个B帧能不能作为参照)。最小可以选择值1,只参照自己前面的那帧。
    注意H.264标准限制了每个level可以参照的帧的数量。如果选择level4.1,1080p最大选4,720p最大选9。
  • no-deblock
    默认:无,完全关闭内置去块滤镜。不推荐使用。
  • deblock
    默认:0:0,调节H.264标准中的内置去块滤镜。这是个性价比很高的选择。
  • no-cabac
    默认值:无,停用弹性内容的二进制算数编码(CABAC:Context Adaptive Binary Arithmetic Coder)资料流压缩,切换到效率较低的弹性内容的可变长度编码(CAVLC:Context Adaptive Variable Length Coder)系统。大幅降低压缩效率(通常10~20%)和解码的硬件需求。

3. 流控

  • QP
    默认:无,三种速率控制方法之一。QP关注量化器,比crf码流大且与bitrate/crf互斥。
  • Bitrate
    默认:无,关注码流,无法控制质量
  • Crf
    默认值:23.0,关注质量,数越低越好。
  • Qmin
    默认值:10,定义x264可以使用的最小量化值。量化值越小,输出就越接近输入。到了一定的值,x264的输出看起来会跟输入一样,即使它并不完全相同。
  • Qmax
    默认值:51,定义x264可以使用的最大量化值。默认值51是H.264规格可供使用的最大量化值,而且品质极低。此默认值有效地停用了qpmax。如果想要限制x264可以输出的最低品质,可以将此值设小一点(通常30~40),但通常并不建议调整此值。
  • Qpstep
    默认值:4,设定两帧之间量化值的最大变更幅度。

4. 编码分析

  • Partitions
    默认值:p8x8,b8x8,i8x8,i4x4,H.264视讯在压缩过程中划分为16x16的宏区块。这些区块可以进一步划分为更小的分割,这就是此选项要控制的部分。
  • me
    默认值:hex,设定全像素(full-pixel)动态估算(motion estimation)的方法。有五个选项:dia(diamond)、hex(hexagon)、umh(uneven multi-hex)、esa(exhaustive)、tesa(transformed exhaustive)。

5. 输出

  • SAR 设置输出的宽高比
  • FPS 帧率
  • leve

详细的参数说明可以参考:
https://blog.csdn.net/dxpqxb/article/details/50755485
http://www.chaneru.com/Roku/HLS/X264_Settings.htm
https://sites.google.com/site/linuxencoding/x264-ffmpeg-mapping

七、测试代码示例

#include <iostream>

using namespace std;

//包含ffmpeg头文件
extern "C"
{
#include "libavutil/avutil.h"
#include "libavdevice/avdevice.h"
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswresample/swresample.h"
#include "libavutil/audio_fifo.h"
}

#include <windows.h>
#include <vector>
#include <string>
#include <memory>

using std::vector;
using std::string;
using std::shared_ptr;

#define V_WIDTH  1280
#define V_HEIGHT 720


void encode(AVCodecContext *ctx,
            AVFrame *frame,
            AVPacket *pkt,
            FILE *output)
{
    int ret = 0;

    if (frame) {
        printf("send frame to encoder, pts=%lld", frame->pts);
    }
    //将数据送到编码器
    ret = avcodec_send_frame(ctx, frame);

    //如果ret>=0说明数据设置成功
    while (ret >= 0) {
        //获取编码后的音频数据,如果成功(ret >= 0)需要重复获取,直到失败为止
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return;
        } else if (ret < 0) {
            cout << "Error, encoding audio frame" << endl;
            exit(-1);
        }

        //write file
        fwrite(pkt->data, static_cast<size_t>(pkt->size), 1, output);
        //刷新缓冲区,使数据写入磁盘
        fflush(output);

        av_packet_unref(pkt);
    }

    return;
}

/**
 * @brief open audio device
 * @param audio device name
 * @return succ: AVFormatContext*, fail: nullptr
 */
AVFormatContext *open_dev(void)
{
    int ret = 0;
    char errors[1024];
    AVFormatContext *fmt_ctx = nullptr;
    AVDictionary *options = nullptr;

    //get format
    //录制摄像头
    string sDeviceName = "video=HD Camera";
    const AVInputFormat *iformat = av_find_input_format("dshow");

    //录制桌面
    //string sDeviceName = "desktop";
    //const AVInputFormat *iformat = av_find_input_format("gdigrab");

    //录制摄像头
    //string sDeviceName = "0";
    //const AVInputFormat *iformat = av_find_input_format("vfwcap");

    av_dict_set(&options, "video_size", "1280x720", 0);
    av_dict_set(&options, "framerate", "10", 0);
    av_dict_set(&options, "pixel_format", "yuyv422", 0);
    av_dict_set(&options, "rtbufsize", "30412800", 0);

    //open device
    if ((ret = avformat_open_input(&fmt_ctx, sDeviceName.data(),
        iformat, &options)) < 0) {
        av_strerror(ret, errors, 1024);
        printf("Failed to open video device, [%d]%s\n", ret, errors);
        return nullptr;
    }

    return fmt_ctx;
}

/**
 * @brief creat_frame
 * @return succ: AVFrame*, fail: nullptr
 */
AVFrame *creat_frame(int width, int height)
{
    AVFrame *frame = nullptr;
    int ret = 0;

    //音频输入数据(未编码的数据)
    frame = av_frame_alloc();
    if (! frame) {
        cout << "Error, Failed to alloc frame!" << endl;
        goto __ERROR;
    }

    //set parameters
    frame->width = width;
    frame->height = height;
    frame->format = AV_PIX_FMT_YUV420P;

    //alloc inner memory
    ret = av_frame_get_buffer(frame, 32); //按 32 位对齐
    if (ret < 0) {
        printf("Error, Failed to alloc buffer for frame!\n");
        goto __ERROR;
    }

    return frame;

__ERROR:
    if (frame) {
        av_frame_free(&frame);
    }

    return nullptr;
}

static void open_encoder(int width, int height, AVCodecContext **enc_ctx)
{
    const AVCodec *codec = nullptr;
    int ret = 0;

    codec = avcodec_find_encoder_by_name("libx264");
    if (! codec) {
        printf("Codec libx264 not found\n");
        exit(1);
    }

    *enc_ctx = avcodec_alloc_context3(codec);
    if (! *enc_ctx) {
        printf("Could not allocate video codec context!\n");
        exit(1);
    }

    (*enc_ctx)->profile = FF_PROFILE_H264_HIGH_444;
    (*enc_ctx)->level = 50; //表示LEVEL是5.0

    (*enc_ctx)->width = width;
    (*enc_ctx)->height = height;

    //GOP
    (*enc_ctx)->gop_size = 250;
    //最小插入I帧的间隔
    (*enc_ctx)->keyint_min = 25;    //option

    //设置B帧的数量
    (*enc_ctx)->max_b_frames = 3;   //option
    (*enc_ctx)->has_b_frames = 1;   //option

    //设置参考帧的数量
    (*enc_ctx)->refs = 3;           //option

    //设置输入YUV格式
    (*enc_ctx)->pix_fmt = AV_PIX_FMT_YUV420P;

    //设置码率
    (*enc_ctx)->bit_rate = 600000;  //600kbps

    //设置帧率
    (*enc_ctx)->time_base = (AVRational){1, 25};    //帧与帧之间的间隔是time_base
    (*enc_ctx)->framerate = (AVRational){25, 1};    //帧率,每秒 25 帧

    ret = avcodec_open2(*enc_ctx, codec, nullptr);
    if (ret < 0) {
        printf("Could not open codec :%d!\n", ret);
        exit(1);
    }
}

void rec_video()
{
    AVFormatContext *fmt_ctx = nullptr;
    AVPacket *pkt = nullptr;
    int ret = 0;
    int base = 0;
    int count = 0;
    AVCodecContext *enc_ctx = nullptr;
    AVFrame *frame = nullptr;
    unsigned char *y = nullptr;
    unsigned char *u = nullptr;
    unsigned char *v = nullptr;

    //set log level
    av_log_set_level(AV_LOG_DEBUG);

    //create file
    FILE *outfile = fopen("D:/Study/ffmpeg/av_base/video.yuv", "wb");
    if (! outfile) {
        printf("Error, Failed to open file!\n");
        goto __ERROR;
    }

    //register audio device
    avdevice_register_all();

    //打开设备
    fmt_ctx = open_dev();
    if (! fmt_ctx) {
        printf("Error, Failed to open device!\n");
        goto __ERROR;
    }

    open_encoder(V_WIDTH, V_HEIGHT, &enc_ctx);

    //创建 AVFrame
    frame = creat_frame(V_WIDTH, V_HEIGHT);

    //创建编码后输出的packet
    pkt = av_packet_alloc();
    if (! pkt) {
        printf("Error, Failed to alloc avpacket!\n");
        goto __ERROR;
    }

    //read data from device
    while((ret = av_read_frame(fmt_ctx, pkt)) == 0) {
        av_log(nullptr, AV_LOG_INFO,
               "packet size is %d\n", pkt->size);

//        fwrite(pkt->data, 1, static_cast<size_t>(pkt->size), outfile);
//        fflush(outfile);

        //YUYVYUYVYUYV windows录屏采用的是YUYV422
        //YYYYYYYYUUVV YUV420P
        y = &frame->data[0][0];
        u = &frame->data[1][0];
        v = &frame->data[2][0];
        for (int i = 0; i < pkt->size; i += 4) {
            *y++ = pkt->data[i];
            *y++ = pkt->data[i + 2];

            if ((i / (V_WIDTH * 2) % 2) == 0) {
                *u++ = pkt->data[i + 1];
            } else {
                *v++ = pkt->data[i + 3];
            }
        }

//        fwrite(frame->data[0], 1, V_WIDTH * V_HEIGHT, outfile);
//        fwrite(frame->data[1], 1, V_WIDTH * V_HEIGHT / 4, outfile);
//        fwrite(frame->data[2], 1, V_WIDTH * V_HEIGHT / 4, outfile);
//        fflush(outfile);

//        av_packet_unref(pkt);

        frame->pts = base++;
        encode(enc_ctx, frame, pkt, outfile);

        if ( count++ >= 50)
            break;
    }

    encode(enc_ctx, nullptr, pkt, outfile);

__ERROR:
    //close device and release ctx
    if (fmt_ctx) {
        avformat_close_input(&fmt_ctx);
    }

    if (pkt) {
        av_packet_free(&pkt);
    }

    //close file
    if (outfile) {
        fclose(outfile);
    }

    av_log(nullptr, AV_LOG_DEBUG, "finish!\n");

    return;
}

int main()
{
    rec_video();

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值