一、前语
顺应时代的技能发展潮流,逐步学习并把握音视频技能核心常识,让技能落地,让常识赋能日子,让科技谋福千万灯光。
二、 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中的帧能够分为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帧结尾
需求注意的是:
-
因为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帧,从而防止较为杂乱的反向解析
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
-
猜测块的尺度有:1616、168、816、88、84、48、44
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编码,用到了avcodec、avutil两个库,全体进程跟《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数据,用到了avcodec、avutil两个库,全体进程跟《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);