09-?音视频技术核心知识|视频编码解码【了解H.264编码、H.264编码、H.264编码解码】

一、前言

顺应时代的技术发展潮流,逐步学习并掌握音视频技术核心知识,让技术落地,让知识赋能生活,让科技造福千万灯火。

二、 H.264编码

本文主要介绍一种非常流行的视频编码:H.264

计算一下:10秒钟1080p(1920×1080)、30fps的YUV420P原始视频,需要占用多大的存储空间?

  • (10 * 30) * (1920 * 1080) * 1.5 = 933120000字节 ≈ 889.89MB
  • 可以看得出来,原始视频的体积是非常巨大的

由于网络带宽和硬盘存储空间都是非常有限的,因此,需要先使用视频编码技术(比如H.264编码)对原始视频进行压缩,然后再进行存储和分发。H.264编码的压缩比可以达到至少是100:1。

1. 简介

H.264,又称为MPEG-4 Part 10,Advanced Video Coding

  • 译为:MPEG-4第10部分,高级视频编码
  • 简称:MPEG-4 AVC

H.264是迄今为止视频录制、压缩和分发的最常用格式。截至2019年9月,已有91%的视频开发人员使用了该格式。H.264提供了明显优于以前任何标准的压缩性能。H.264因其是蓝光盘的其中一种编解码标准而著名,所有蓝光盘播放器都必须能解码H.264。

2. 编码器

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

x264是一款免费的高性能的H.264开源编码器。x264编码器在FFmpeg中的名称是libx264

AVCodec *codec = avcodec_find_encoder_by_name("libx264");

3. 解码器

H.264标准中定义了一个解码方法,但是制造厂商可以自由地开发可选的具有竞争力的、新的解码器,前提是他们能够获得与标准中采用的方法同样的结果。

FFmpeg默认已经内置了一个H.264的解码器,名称是h264

AVCodec *codec1 = avcodec_find_decoder_by_name("h264");

// 或者
AVCodec *codec2 = avcodec_find_decoder(AV_CODEC_ID_H264);

4. 编码过程与原理

H.264的编程过程比较复杂,本文只介绍大体的框架和脉络,具体细节就不展开了。

大体可以归纳为以下几个主要步骤:

  • 划分帧类型
  • 帧内/帧间编码
  • 变换 + 量化
  • 滤波
  • 熵编码

4.1 划分帧类型

有统计结果表明:在连续的几帧图像中,一般只有10%以内的像素有差别,亮度的差值变化不超过2%,而色度的差值变化只在1%以内。

4.1.1 GOP

于是可以将一串连续的相似的帧归到一个图像群组(Group Of Pictures,GOP)。

GOP

GOP中的帧可以分为3种类型:

  • 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帧。

显示和编码顺序

在较早的视频编码标准(例如MPEG-2)中,P帧只能使用一个参考帧,而一些现代视频编码标准(比如H.264),允许使用多个参考帧。

多个参考帧

4.1.2 GOP的长度

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

  • 加大GOP长度有利于减小视频文件大小,但也不宜设置过大,太大则会导致GOP后部帧的画面失真,影响视频质量

  • 由于P、B帧的复杂度大于I帧,GOP值过大,过多的P、B帧会影响编码效率,使编码效率降低

  • 如果设置过小的GOP值,视频文件会比较大,则需要提高视频的输出码率,以确保画面质量不会降低,故会增加网络带宽

  • GOP长度也是影响视频seek响应速度的关键因素,seek时播放器需要定位到离指定位置最近的前一个I帧,如果GOP太大意味着距离指定位置可能越远(需要解码的参考帧就越多)、seek响应的时间(缓冲时间)也越长

4.1.3 GOP的类型

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

  • Open
    • 前一个GOP的B帧可以参考下一个GOP的I帧
  • Closed
    • 前一个GOP的B帧不能参考下一个GOP的I帧
    • GOP不能以B帧结尾

长度为15的Open GOP

长度为15的Closed GOP

需要注意的是:

  • 由于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

4.2 帧内/帧间编码

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

4.2.1 划分宏块

在进行编码之前,首先要将一张完整的帧切割成多个宏块(Macroblock),H.264中的宏块大小通常是16×16。

宏块可以进一步拆分为多个更小的变换块(Transform blocks)、预测块(Prediction blocks)。

  • 变换块的尺寸有:16×16、8×8、4×4

  • 预测块的尺寸有:16×16、16×8、8×16、8×8、8×4、4×8、4×4

预测块

4.2.2 帧内编码

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

预测模式

预测模式描述

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

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

4.2.3 帧间编码

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

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

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

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

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

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

4.3 变换与量化

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

5. 规格

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的基础上增加了8×8内部预测、自定义量化、无损视频编码和更多的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
    • 用于广播及视频碟片存储(蓝光影片),高清电视的应用

三、H.264编码实战

本文的主要内容:使用H.264编码对YUV视频进行压缩。

如果是命令行的操作,非常简单。

ffmpeg -s 640x480 -pix_fmt yuv420p -i in.yuv -c:v libx264 out.h264
# -c:v libx264是指定使用libx264作为编码器

接下来主要讲解如何通过代码的方式使用H.264编码,用到了avcodecavutil两个库,整体过程跟《AAC编码实战》类似。

1. 类的声明

extern "C" {



#include <libavutil/avutil.h>

}




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);
};

2. 类的使用

VideoEncodeSpec in;
in.filename = "F:/res/in.yuv";
in.width = 640;
in.height = 480;
in.fps = 30;
in.pixFmt = AV_PIX_FMT_YUV420P;




FFmpegs::h264Encode(in, "F:/res/out.h264");

3. 宏定义

extern "C" {



#include <libavcodec/avcodec.h>

#include <libavutil/avutil.h>

#include <libavutil/imgutils.h>

}





#define ERROR_BUF(ret) \

    char errbuf[1024]; \

    av_strerror(ret, errbuf, sizeof (errbuf));

4. 变量定义

// 文件
QFile inFile(in.filename);
QFile outFile(outFilename);



// 一帧图片的大小
int imgSize = av_image_get_buffer_size(in.pixFmt, in.width, in.height, 1);




// 返回结果
int ret = 0;




// 编码器
AVCodec *codec = nullptr;


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




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


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

5. 初始化

// 获取编码器
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;
}

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

6. 编码

// 打开文件

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);

encode函数的实现如下所示:

// 返回负数:中途出现了错误
// 返回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);
    }
}

7. 回收资源

end:

    // 关闭文件
    inFile.close();
    outFile.close();
    // 释放资源
    if (frame) {
        av_freep(&frame->data[0]);
        av_frame_free(&frame);
    }
    av_packet_free(&pkt);
    avcodec_free_context(&ctx);

四、H.264解码实战

本文的主要内容:对H.264数据进行解码(解压缩)。

如果是命令行的操作,非常简单。

ffmpeg -c:v h264 -i in.h264 out.yuv
# -c:v h264是指定使用h264作为解码器

接下来主要讲解如何通过代码的方式解码H.264数据,用到了avcodecavutil两个库,整体过程跟《AAC解码实战》类似。

1. 类的声明

extern "C" {



#include <libavutil/avutil.h>

}




typedef struct {

    const char *filename;

    int width;

    int height;

    AVPixelFormat pixFmt;

    int fps;

} VideoDecodeSpec;



class FFmpegs {

public:

    FFmpegs();





    static void h264Decode(const char *inFilename,
                           VideoDecodeSpec &out);
};

2. 类的使用

VideoDecodeSpec out;
out.filename = "F:/res/out.yuv";


FFmpegs::h264Decode("F:/res/in.h264", out);

qDebug() << out.width << out.height
         << out.fps << av_get_pix_fmt_name(out.pixFmt);

3. 宏定义

extern "C" {



#include <libavcodec/avcodec.h>

#include <libavutil/avutil.h>

#include <libavutil/imgutils.h>

}





#define ERROR_BUF(ret) \

    char errbuf[1024]; \

    av_strerror(ret, errbuf, sizeof (errbuf));





// 输入缓冲区的大小
#define IN_DATA_SIZE 4096

4. 变量定义

// 返回结果
int ret = 0;


// 用来存放读取的输入文件数据(h264)
char inDataArray[IN_DATA_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
char *inData = inDataArray;




// 每次从输入文件中读取的长度(h264)
// 输入缓冲区中,剩下的等待进行解码的有效数据长度
int inLen;
// 是否已经读取到了输入文件的尾部
int inEnd = 0;


// 文件
QFile inFile(inFilename);
QFile outFile(out.filename);

// 解码器
AVCodec *codec = nullptr;
// 上下文
AVCodecContext *ctx = nullptr;
// 解析器上下文
AVCodecParserContext *parserCtx = nullptr;


// 存放解码前的数据(h264)
AVPacket *pkt = nullptr;
// 存放解码后的数据(yuv)
AVFrame *frame = nullptr;

5. 初始化

// 获取解码器
//    codec = avcodec_find_decoder_by_name("h264");
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
    qDebug() << "decoder not found";
    return;
}

// 初始化解析器上下文
parserCtx = av_parser_init(codec->id);
if (!parserCtx) {
    qDebug() << "av_parser_init error";
    return;
}

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



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

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

}




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

6. 解码

// 打开文件

if (!inFile.open(QFile::ReadOnly)) {

    qDebug() << "file open error:" << inFilename;
    goto end;

}



if (!outFile.open(QFile::WriteOnly)) {

    qDebug() << "file open error:" << out.filename;
    goto end;

}





// 读取文件数据
do {
    inLen = inFile.read(inDataArray, IN_DATA_SIZE);
    // 设置是否到了文件尾部
    inEnd = !inLen;




    // 让inData指向数组的首元素
    inData = inDataArray;


    // 只要输入缓冲区中还有等待进行解码的数据
    while (inLen > 0 || inEnd) {
        // 到了文件尾部(虽然没有读取任何数据,但也要调用av_parser_parse2,修复bug)
        // 经过解析器解析
        ret = av_parser_parse2(parserCtx, ctx,
                               &pkt->data, &pkt->size,
                               (uint8_t *) inData, inLen,
                               AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);



        if (ret < 0) {
            ERROR_BUF(ret);
            qDebug() << "av_parser_parse2 error" << errbuf;
            goto end;
        }

        // 跳过已经解析过的数据
        inData += ret;
        // 减去已经解析过的数据大小
        inLen -= ret;

        qDebug() << inEnd << pkt->size << ret;

        // 解码
        if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
            goto end;
        }

        // 如果到了文件尾部
        if (inEnd) break;
    }
} while (!inEnd);

// 刷新缓冲区
//    pkt->data = nullptr;
//    pkt->size = 0;
//    decode(ctx, pkt, frame, outFile);
decode(ctx, nullptr, frame, outFile);

// 赋值输出参数
out.width = ctx->width;
out.height = ctx->height;
out.pixFmt = ctx->pix_fmt;
// 用framerate.num获取帧率,并不是time_base.den
out.fps = ctx->framerate.num;

end:
inFile.close();
outFile.close();
av_packet_free(&pkt);
av_frame_free(&frame);
av_parser_close(parserCtx);
avcodec_free_context(&ctx);

decode函数的实现如下所示:

static int decode(AVCodecContext *ctx,
                  AVPacket *pkt,
                  AVFrame *frame,
                  QFile &outFile) {
    // 发送压缩数据到解码器
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_packet error" << errbuf;
        return ret;
    }



    while (true) {
        // 获取解码后的数据
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            ERROR_BUF(ret);
            qDebug() << "avcodec_receive_frame error" << errbuf;
            return ret;
        }

        // 将解码后的数据写入文件
        // 写入Y平面
        outFile.write((char *) frame->data[0],
                      frame->linesize[0] * ctx->height);
        // 写入U平面
        outFile.write((char *) frame->data[1],
                      frame->linesize[1] * ctx->height >> 1);
        // 写入V平面
        outFile.write((char *) frame->data[2],
                      frame->linesize[2] * ctx->height >> 1);
    }
}


7. 回收资源

end:

    inFile.close();
    outFile.close();
    av_packet_free(&pkt);
    av_frame_free(&frame);
    av_parser_close(parserCtx);
    avcodec_free_context(&ctx);

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY4L4Fb5' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片