H264编码实战

0.编码流程图

视频编码.png

有了前面的知识铺垫,今天我们进入视频编码的相关内容,在这里不得不介绍了一种非常流行的适配编码H.264

1.前言

本文打算使用352x288-yuv420p.yuv 作为视频案例,我们先计算一下:10秒钟352x288、30fps的YUVP原始视频,需要占用多大的存储空间?

10 * 30 * 352 * 288 * 1.5 = 45619200 约等于45.61MB

可以再mac中直接查看大小,从途中发现,我们的计算方式是正确的


[图片上传中...(IBPOrder.png-1b5d54-1623471564494-0)]

2.暴露的问题

从前面的计算方式,可以知道,计算出来一个10秒的352x288-yuv420p视频是需要45MB,试想一下,如果分辨率更高的话,那么占用的空间肯定是更大的,由于网络带宽和硬盘存储空间都是非常有限的,因此需要使用视频编码技术(比如H.264编码),对原始视频进行压缩,然后再进行存储和分发,H.264编码的压缩比可以达到至少是100:1

  • 处理结果分析
 songlin@feng-sl  ~/audio/h264_encode   master ±✚  ls -l
-rw-r--r--@ 1 songlin  staff    147325 Jun 12 11:16 out.h264 (程序)
-rw-r--r--  1 songlin  staff    147325 Jun 12 10:56 outCommand.h264(命令行生成ffmpeg -s 352x288 -pix_fmt yuv420p -i 352x288-yuv420p.yuv -c:v libx264 outCommand.h264)

可以看到大小差了好几百倍,下面我们详细介绍一下h264

3.简介

H.264,又称为MPEG-4 Part 10,Advanced Video Coding,是目前为止视频录制、视频和分发的最常用格式。H.264因其是蓝光盘的其中一种编解码标准而著名,所有蓝光盘播放器都必须能解码H.264。

4.编码器

H.264 标准运行制造厂商自由地开发具有竞争力的创新产品,它并没有定义一个编码器,而是定义了编码器应该产生的输出码流。

程序中获取h264编码
AVCodec *codec = avcodec_find_encoder_by_name("libx264");

5.解码器

FFmpeg默认已经内置了一个H.264的解码器,名称是h264
AVCodec *codec1 = avcodec_find_decoder_by_name("h264");
 
// 或者
AVCodec *codec2 = avcodec_find_decoder(AV_CODEC_ID_H264);

6.编码过程与原理

H.264 可以分为几个主要步骤

  • 划分帧类型
  • 帧内/帧间编码
  • 变换 + 量化
  • 滤波
  • 熵编码
6.1 划分帧类型
在H.264中定义了三种幁
I帧:完整编码的帧叫I帧
P帧:参考之前的I帧生成的只包含差异部分编码的帧叫P帧
B帧:参考前后的帧编码的帧叫B帧

H264采用的核心算法是帧内压缩和帧间压缩:
帧内压缩是生成I帧的算法
帧间压缩是生成B帧和P帧的算法
压缩方法:
    分组:把几帧图像分为一组(GOP,也就是一个序列),为防止运动变化,帧数不宜取多
    定义帧:将每组内各帧图像定义为三种类型,即I帧、B帧和P帧;
    预测帧:以I帧做为基础帧,以I帧预测P帧,再由I帧和P帧预测B帧;
    数据传输:最后将I帧数据与预测的差值信息进行存储和传输。
6.1.1 GOP

可以将一串连续的相似的幁归到一个图像数组(Group Of Pictures,GOP)


GOP.png

IBPOrder.png

参数解释

  • I帧(I Picture、I Frame、Intra Coded Picture),译为:帧内编码图像,也叫做关键帧(Keyframe)
是视频的第一帧,也是GOP的第一帧,一个GOP只有一个I帧
- 编码
对整帧图像数据进行编码
- 解码
仅用当前I帧的编码数据就可以解码出完整的图像

是一种自带全部信息的独立帧,无需参考其他图像便可独立进行解码,可以简单理解为一张静态图像
  • P帧(P Picture、P Frame、Predictive Coded Picture),译为:预测编码图像
- 编码
并不会对整帧图像数据进行编码
以前面的I帧或P帧作为参考帧,只编码当前P帧与参考帧的差异数据
- 解码
需要先解码出前面的参考帧,再结合差异数据解码出当前P帧完整的图像
  • B帧(B Picture、B Frame、Bipredictive Coded Picture)
- 编码
并不会对整帧图像数据进行编码
同时以前面、后面的I帧或P帧作为参考帧,只编码当前B帧与前后参考帧的差异数据
因为可参考的帧变多了,所以只需要存储更少的差异数据
- 解码
需要先解码出前后的参考帧,再结合差异数据解码出当前B帧完整的图像
不难看出,编码后的数据大小:I帧 > P帧 > B帧。
IBPShow.png
6.1.2

GOP 的长度表示GOP的幁数,GOP的长度需要控制在合理范围内,以平衡视频质量、视频大小(网络带宽)和seek效果(拖动、快进的响应速度)等。

  • 加大GOP长度有利于减小视频文件大小,但也不宜设置过大,太大则会导致GOP后部帧的画面失真,影响视频质量
  • 由于P、B帧的复杂度大于I帧,GOP值过大,过多的P、B帧会影响编码效率,使编码效率降低
  • 如果设置过小的GOP值,视频文件会比较大,则需要提高视频的输出码率,以确保画面质量不会降低,故会增加网络带宽
  • GOP长度也是影响视频seek响应速度的关键因素,seek时播放器需要定位到离指定位置最近的前一个I帧,如果GOP太大意味着距离指定位置可能越远(需要解码的参考帧就越多)、seek响应的时间(缓冲时间)也越长
6.1.3 GOP的类型

GOP又可以分为开放(Open)、封闭(Closed)两种

- Open
前一个GOP的B帧可以参考下一个GOP的I帧
- Closed
前一个GOP的B帧不能参考下一个GOP的I帧
GOP不能以B帧结尾
15IBP.png

15Closed.png
注意点:
- 由于P帧、B帧都对前面的参考帧(P帧、I帧)有依赖性,因此,一旦前面的参考帧出现数据错误,就会导致后面的P帧、B帧也出现数据错误,而且这种错误还会继续向后传播
- 对于普通的I帧,其后的P帧和B帧可以参考该普通I帧之前的其他I帧

在Closed GOP中,有一种特殊的I帧,叫做IDR帧(Instantaneous Decoder Refresh,译为:即时解码刷新)。
- 当遇到IDR帧时,会清空参考帧队列
- 如果前一个序列出现重大错误,在这里可以获得重新同步的机会,使错误不会继续往下传播
- 一个IDR帧之后的所有帧,永远都不会参考该IDR帧之前的帧
- 视频播放时,播放器一般都支持随机seek(拖动)到指定位置,而播放器直接选择到指定位置附近的IDR帧进行播放最为便捷,因为可以明确知道该IDR帧之后的所有帧都不会参考其之前的其他I帧,从而避免较为复杂的反向解析
IDR.png
6.2 帧内/帧间编码

I帧采用的是帧内(Intra Frame)编码,处理的是空间冗余。
P帧、B帧采用的是帧间(Inter Frame)编码,处理的是时间冗余。

6.2.1 划分宏块

在进行编码之前,首先要将一张完整的帧切割成多个宏块(Macroblock),H.264中的宏块大小通常是16x16。
宏块可以进一步拆分为多个更小的变换块(Transform blocks)、预测块(Prediction blocks)。

- 变换块的尺寸有:16x16、8x8、4x4
- 预测块的尺寸有:16×16、16×8、8×16、8×8、8×4、4×8、4×4
预测块.png
6.2.2 帧内编码

帧内编码,也称帧内预测。以4x4的预测块为例,共有9种可选的预测模式。


预测模式.png

预测模式描述.png

利用帧内预测技术,可以得到预测帧,最终只需要保留预测模式信息、以及预测帧与原始帧的残差值。

编码器会选取最佳预测模式,使预测帧更加接近原始帧,减少相互间的差异,提高编码的压缩效率。

6.2.3 帧间编码

帧间编码,也称帧间预测,用到了运动补偿(Motion compensation)技术。

编码器利用块匹配算法,尝试在先前已编码的帧(称为参考帧)上搜索与正在编码的块相似的块。如果编码器搜索成功,则可以使用称为运动矢量的向量对块进行编码,该向量指向匹配块在参考帧处的位置。

在大多数情况下,编码器将成功执行,但是找到的块可能与它正在编码的块不完全匹配。这就是编码器将计算它们之间差异的原因。这些残差值称为预测误差,需要进行变换并将其发送给解码器。

综上所述,如果编码器在参考帧上成功找到匹配块,它将获得指向匹配块的运动矢量和预测误差。使用这两个元素,解码器将能够恢复该块的原始像素。

如果一切顺利,该算法将能够找到一个几乎没有预测误差的匹配块,因此,一旦进行变换,运动矢量加上预测误差的总大小将小于原始编码的大小。

如果块匹配算法未能找到合适的匹配,则预测误差将是可观的。因此,运动矢量的总大小加上预测误差将大于原始编码。在这种情况下,编码器将产生异常,并为该特定块发送原始编码。

6.3 变换与量化

接下来对残差值进行DCT变换(Discrete Cosine Transform,译为离散余弦变换)。

7 规格

H.264的主要规格有:

  • Baseline Profile(BP)
  • 支持I/P帧,只支持无交错(Progressive)和CAVLC
  • 一般用于低阶或需要额外容错的应用,比如视频通话、手机视频等即时通信领域
  • Extended Profile(XP)
  • 在Baseline的基础上增加了额外的功能,支持流之间的切换,改进误码性能
  • 支持I/P/B/SP/SI帧,只支持无交错(Progressive)和CAVLC
  • 适合于视频流在网络上的传输场合,比如视频点播
  • Main Profile(MP)
  • 提供I/P/B帧,支持无交错(Progressive)和交错(Interlaced),支持CAVLC和CABAC
  • 用于主流消费类电子产品规格如低解码(相对而言)的MP4、便携的视频播放器、PSP和iPod等
  • High Profile(HiP)
  • 最常用的规格
  • 在Main的基础上增加了8x8内部预测、自定义量化、无损视频编码和更多的YUV格式(如4:4:4)
High 4:2:2 Profile(Hi422P)
High 4:4:4 Predictive Profile(Hi444PP)
High 4:2:2 Intra Profile
High 4:4:4 Intra Profile
  • 用于广播及视频碟片存储(蓝光影片),高清电视的应用

8 实战

8.1 命令行实战
ffmpeg -s 640x480 -pix_fmt yuv420p -i in.yuv -c:v libx264 out.h264
# -c:v libx264是指定使用libx264作为编码器
8.2 程序代码实践
  • ffmpegs.h
#ifndef FFMPEGS_H
#define FFMPEGS_H

extern "C" {
#include 
}

typedef struct {
    const char *filename;
    int width;
    int height;
    AVPixelFormat pixFmt;
    int fps;
} VideoEncodeSpec;

class FFmpegs
{
public:
    FFmpegs();
    static void h264Encode(VideoEncodeSpec &in,const char *outFilename);
};

#endif // FFMPEGS_H

  • ffmpegs.cpp
#include "ffmpegs.h"
#include 
#include 

extern "C" {
#include 
#include 
#include 
}

#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));

FFmpegs::FFmpegs() {

}

// 检查像素格式
static int check_pix_fmt(const AVCodec *codec,
                         enum AVPixelFormat pixFmt) {
    const enum AVPixelFormat *p = codec->pix_fmts;
    while (*p != AV_PIX_FMT_NONE) {
        if (*p == pixFmt) return 1;
        p++;
    }
    return 0;
}

// 返回负数:中途出现了错误
// 返回0:编码操作正常完成
static int encode(AVCodecContext *ctx,
                  AVFrame *frame,
                  AVPacket *pkt,
                  QFile &outFile) {
    // 发送数据到编码器
    int ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_frame error" << errbuf;
        return ret;
    }

    // 不断从编码器中取出编码后的数据
    while (true) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            // 继续读取数据到frame,然后送到编码器
            return 0;
        } else if (ret < 0) { // 其他错误
            return ret;
        }

        // 成功从编码器拿到编码后的数据
        // 将编码后的数据写入文件
        outFile.write((char *) pkt->data, pkt->size);

        // 释放pkt内部的资源
        av_packet_unref(pkt);
    }
}

void FFmpegs::h264Encode(VideoEncodeSpec &in,
                         const char *outFilename) {
    // 文件
    QFile inFile(in.filename);
    QFile outFile(outFilename);

    // 一帧图片的大小
    int imgSize = av_image_get_buffer_size(in.pixFmt, in.width, in.height, 1);
    qDebug() << "输出一幁的大小" << imgSize;

    // 返回结果
    int ret = 0;

    // 编码器
    AVCodec *codec = nullptr;

    // 编码上下文
    AVCodecContext *ctx = nullptr;

    // 存放编码前的数据(yuv)
    AVFrame *frame = nullptr;

    // 存放编码后的数据(h264)
    AVPacket *pkt = nullptr;

    uint8_t *buf = nullptr;

    // 获取编码器
    codec = avcodec_find_encoder_by_name("libx264");
    if (!codec) {
        qDebug() << "encoder not found";
        return;
    }

    // 检查输入数据的采样格式
    if (!check_pix_fmt(codec, in.pixFmt)) {
        qDebug() << "unsupported pixel format"
                 << av_get_pix_fmt_name(in.pixFmt);
        return;
    }

    // 创建编码上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        qDebug() << "avcodec_alloc_context3 error";
        return;
    }

    // 设置yuv参数
    ctx->width = in.width;
    ctx->height = in.height;
    ctx->pix_fmt = in.pixFmt;
    // 设置帧率(1秒钟显示的帧数是in.fps)
    ctx->time_base = {1, in.fps};

    // 打开编码器
    ret = avcodec_open2(ctx, codec, nullptr);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_open2 error" << errbuf;
        goto end;
    }

    // 创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        goto end;
    }

    frame->width = ctx->width;
    frame->height = ctx->height;
    frame->format = ctx->pix_fmt;
    frame->pts = 0;

    // 利用width、height、format创建缓冲区
//    ret = av_image_alloc(frame->data, frame->linesize,
//                         in.width, in.height, in.pixFmt, 1);
//    if (ret < 0) {
//        ERROR_BUF(ret);
//        qDebug() << "av_frame_get_buffer error" << errbuf;
//        goto end;
//    }

    // 创建输入缓冲区(方法2)
    buf = (uint8_t *) av_malloc(imgSize);
    ret = av_image_fill_arrays(frame->data, frame->linesize,
                               buf,
                               in.pixFmt, in.width, in.height, 1);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "av_image_fill_arrays error" << errbuf;
        goto end;
    }
    qDebug() << buf << frame->data[0];

    // 创建输入缓冲区(方法3)
//    ret = av_frame_get_buffer(frame, 0);
//    if (ret < 0) {
//        ERROR_BUF(ret);
//        qDebug() << "av_frame_get_buffer error" << errbuf;
//        goto end;
//    }

    // 创建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        qDebug() << "av_packet_alloc error";
        goto end;
    }

    // 打开文件
    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "file open error" << in.filename;
        goto end;
    }
    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "file open error" << outFilename;
        goto end;
    }

    // 读取数据到frame中
    while ((ret = inFile.read((char *) frame->data[0],
                              imgSize)) > 0) {
        // 进行编码
        if (encode(ctx, frame, pkt, outFile) < 0) {
            goto end;
        }

        // 设置帧的序号
        frame->pts++;
    }

    // 刷新缓冲区
    encode(ctx, nullptr, pkt, outFile);

end:
    // 关闭文件
    inFile.close();
    outFile.close();

//    av_freep(&buf);

    // 释放资源
    if (frame) {
        av_freep(&frame->data[0]);
//        av_free(frame->data[0]);
//        frame->data[0] = nullptr;
        av_frame_free(&frame);
    }
    av_packet_free(&pkt);
    avcodec_free_context(&ctx);

    qDebug() << "线程正常结束";
}

  • audiothread.h
#ifndef AUDIOTHREAD_H
#define AUDIOTHREAD_H

#include 

class AudioThread : public QThread
{
    Q_OBJECT
public:
    explicit AudioThread(QObject *parent = nullptr);
    ~AudioThread();
private:
    void run();

signals:

};

#endif // AUDIOTHREAD_H

  • audiothread.cpp
#include "audiothread.h"
#include 
#include "ffmpegs.h"

AudioThread::AudioThread(QObject *parent) : QThread(parent)
{
    //当监听到线程结束时(finished),就调用deleteLater回收内存
    connect(this,&AudioThread::finished,
            this,&AudioThread::deleteLater);
}

AudioThread::~AudioThread(){
    //断开所有的连接
    disconnect();
    //内存回收之前,正常结束线程
    requestInterruption();
    //安全退出
    quit();
    wait();
    qDebug() << this << "析构 (内存被回收)";
}

void AudioThread::run(){
    VideoEncodeSpec in;
    in.filename = "/Users/songlin/audio/h264_encode/352x288-yuv420p.yuv";
    in.width = 352;
    in.height = 288;
    in.fps = 25;//这里不是30而是25,是因为终端输出的是25
    in.pixFmt = AV_PIX_FMT_YUV420P;
    FFmpegs::h264Encode(in,"/Users/songlin/audio/h264_encode/out.h264");

// ffplay -video_size 352x288 -pixel_format yuv420p -framerate 30 /Users/songlin/audio/h264_encode/352x288-yuv420p.yuv 命令行
//    Stream #0:0: Video: h264 (libx264), yuv420p, 352x288, q=-1--1, 25 fps, 25 tbn, 25 tbc
}

9.坑点

程序

 VideoEncodeSpec in;
    in.filename = "/Users/songlin/audio/h264_encode/352x288-yuv420p.yuv";
    in.width = 352;
    in.height = 288;
    in.fps = 25;//这里不是30而是25,是因为终端输出的是25
    in.pixFmt = AV_PIX_FMT_YUV420P;
    FFmpegs::h264Encode(in,"/Users/songlin/audio/h264_encode/out.h264");

终端

ffmpeg -s 352x288 -pix_fmt yuv420p -i 352x288-yuv420p.yuv -framerate 30 -c:v libx264 outCommand.h264 命令行

这里出现一个问题就是大小问题,本身程序里面 是设置 in.fps = 30;但是发现出来的out.h264 和out Command.h264 大小不一样,经过分析之后,发现是虽然命令行设置了framerate 30,但是mac 确只能以25fps写进去,可以从下面的输出信息看到

Stream #0:0: Video: h264 (libx264), yuv420p, 352x288, q=-1--1, 25 fps, 25 tbn, 25 tbc

所以修复就是将in.fp2 = 25 即可

你可能感兴趣的:(H264编码实战)