一、H.264/AVC 概述
H.264/AVC 也可以叫做 H.264/MPEG-4 part 10 AVC,這是一個(gè)聯(lián)合名字,H.264 冠的是 ITU-T 的名稱,AVC(Advanced Video Coding) 冠的是 ISO-IEC 的名字。ITU-T 是國際電信標(biāo)準(zhǔn)化部門。ISO-IEC是國際標(biāo)準(zhǔn)化組織-國際電工委員會(huì)。在 2001 年的 12 月, ITU-T 的 VCEG(Video Coding Experts Group)和 ISO-IEC 的 MPEG(Moving Picture Experts Group)聯(lián)合成立了一個(gè)新的機(jī)構(gòu)叫 JVT(Joint Video Team),就是這個(gè)新的組織 JVT 于 2003 年 3 月 發(fā)布了 H264/AVC 視頻編碼標(biāo)準(zhǔn)。
H.264/AVC 是迄今為止視頻錄制、壓縮和分發(fā)的最常用格式。截至 2019 年 9 月,已有 91% 的視頻開發(fā)人員使用了該格式。H.264/AVC 提供了明顯優(yōu)于以前任何標(biāo)準(zhǔn)的壓縮性能。H.264/AVC 因其是藍(lán)光盤的其中一種編解碼標(biāo)準(zhǔn)而著名,所有藍(lán)光盤播放器都必須能解碼 H.264/AVC。
二、為什么要進(jìn)行視頻編碼?
視頻播放的本質(zhì)是展示一張張圖像,如果一秒鐘至少連續(xù)播放 24 張圖像,那么人眼看到的就是連續(xù)的視頻畫面。通過手機(jī)或者電腦在線觀看視頻,首先需要將這些圖像通過網(wǎng)絡(luò)傳輸?shù)侥愕氖謾C(jī)或者電腦,這需要考慮的一個(gè)問題就是視頻對(duì)網(wǎng)絡(luò)帶寬的占用情況。H.264/AVC 作為一個(gè)視頻壓縮標(biāo)準(zhǔn),其主要使命就是降低視頻的帶寬占用,提高傳輸效率。H.264/AVC 的壓縮比可以達(dá)到至少是 100:1。
是否真的需要對(duì)圖像進(jìn)行壓縮?我們知道一張大小為 1920x1080 用 yuv420p 像素格式表示的一張圖像的大小是 1920 * 1080 * 1.5 = 3110400 字節(jié)(yuv420p 像素格式每個(gè)像素占用 1.5 字節(jié)),那么一段 10 秒鐘 30fps 的 1080p(1920x1080)原始視頻的大小就是 3110400 * 30 * 10 = 933120000 字節(jié),大約是 889.89MB,可以看出來原始視頻的體積非常大。因此需要使用視頻編碼技術(shù)對(duì)原始視頻進(jìn)行壓縮來降低數(shù)據(jù)傳輸和存儲(chǔ)的成本。
三、使用 FFmpeg 命令行進(jìn)行 H.264 編碼
$ ffmpeg -s 640x360 -pix_fmt yuv420p -i IMG_1459_yuvj420p640x360fps30.yuv -c:v libx264 out.h264
在我本地 H.264 編碼器默認(rèn)就是 libx264,所以 -c:v libx264 也可省略不寫。排在最前面的編碼器即默認(rèn)編碼器:
$ ffmpeg -encoders | grep 264
V..... libx264 libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (codec h264)
V..... libx264rgb libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 RGB (codec h264)
V..... h264_videotoolbox VideoToolbox H.264 Encoder (codec h264)
查看 libx264 支持的像素格式:
$ ffmpeg -h encoder=libx264
Encoder libx264 [libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10]:
Supported pixel formats: yuv420p yuvj420p yuv422p yuvj422p yuv444p yuvj444p nv12 nv16 nv21 yuv420p10le yuv422p10le yuv444p10le nv20le gray gray10le
四、使用 FFmpeg 編程實(shí)現(xiàn) H.264 編碼
H.264 視頻編碼和 AAC 音頻編碼流程是類似的,H.264 視頻編碼使用的是編碼器 x264,在 FFmpeg 中的名稱是 libx264(libx264 并沒有默認(rèn)內(nèi)置到 FFmpeg 中,我們是在編譯 FFmpeg 時(shí)手動(dòng)將通過 homebrew 安裝到本地的 libx264 內(nèi)置到 FFmpeg 中的)。
首先需要導(dǎo)入我們需要用到的庫,主要用到 FFmpeg 兩個(gè)庫 libavcodec 和 libavutil:
macx {
INCLUDEPATH += /usr/local/ffmpeg/include
LIBS += -L/usr/local/ffmpeg/lib -lavcodec \
-lavutil
}
1、獲取編碼器
codec = avcodec_find_encoder_by_name("libx264");
也可以使用 ID 的方式獲取編碼器,使用使用 ID 的方式獲取到的是本地默認(rèn)編碼器,在我本地默認(rèn) H.264 編碼器就是 libx264,所以使用下面方式獲取的編碼器和上面使用 name 獲取到的編碼器是同一個(gè)編碼器,都是 libx264(具體情況需要查看本地環(huán)境):
codec = avcodec_find_encoder(AV_CODEC_ID_H264);
前面對(duì)音頻進(jìn) AAC 編碼時(shí),AAC 編碼器對(duì)數(shù)據(jù)的采樣格式是有要求的,比如 libfdk_aac 要求采樣格式是 s16 整型,同樣的 H.264 編碼庫 libx264 對(duì)輸入數(shù)據(jù)像素格式也有要求,雖然 avcodec_open2 函數(shù)內(nèi)部也會(huì)對(duì)像素格式進(jìn)行檢查,但是建議提前檢查輸入像素格式:
if (!check_pix_fmt(codec, in.pixFmt)) {
qDebug() << "unsupported pixel format" << av_get_pix_fmt_name(in.pixFmt);
return;
}
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;
}
codec->pix_fmts 中存放的是當(dāng)前編碼器支持的像素格式。AV_PIX_FMT_NONE 是一個(gè)邊界標(biāo)識(shí),用于判斷是否遍歷結(jié)束。
2、創(chuàng)建編碼上下文
ctx = avcodec_alloc_context3(codec);
設(shè)置編碼上下文參數(shù):
ctx->width = in.width;
ctx->height = in.height;
ctx->pix_fmt = in.pixFmt;
// 設(shè)置幀率(1秒鐘顯示的幀數(shù)是in.fps)
ctx->time_base = {1, in.fps};
3、打開編碼器
ret = avcodec_open2(ctx, codec, nullptr);
可以通過參數(shù) options 設(shè)置一些編碼器特有參數(shù)。
4、創(chuàng)建 AVFrame
frame = av_frame_alloc();
av_frame_alloc 僅僅是為 AVFrame 分配空間,數(shù)據(jù)緩沖區(qū) frame->data[0] 需要我們調(diào)用函數(shù) av_frame_get_buffer 來創(chuàng)建。調(diào)用函數(shù) av_frame_get_buffer 前設(shè)置 frame 的 width、height 和 format, 利用 width、height 和 format 可算出一幀圖像大小, frame->data[0] 指向的堆空間其實(shí)就是一幀圖像的大?。?/p>
frame->width = ctx->width;
frame->height = ctx->height;
frame->format = ctx->pix_fmt;
ret = av_frame_get_buffer(frame, 1);
5、創(chuàng)建 AVPacket
pkt = av_packet_alloc();
6、打開文件,從文件讀取數(shù)據(jù)到 AVFrame
inFile.open(QFile::ReadOnly);
// 一幀圖像的大小
int image_size = av_image_get_buffer_size(in.pixFmt, in.width, in.height, 1);
inFile.read((char *)frame->data[0], image_size);
7、解碼
// 返回 0:編碼操作正常完成;返回負(fù)數(shù):中途出現(xiàn)了錯(cuò)誤
static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, QFile &outFile) {
// 發(fā)送數(shù)據(jù)到編碼器
int ret = avcodec_send_frame(ctx, frame);
if (ret < 0) {
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
qDebug() << "avcodec_send_frame error" << errbuf;
return ret;
}
// 不斷從編碼器中取出編碼后的數(shù)據(jù)
while (true) {
ret = avcodec_receive_packet(ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { // 繼續(xù)讀取數(shù)據(jù)到 frame,然后送到編碼器
return 0;
} else if (ret < 0) { // 其他錯(cuò)誤
return ret;
}
// 成功從編碼器拿到編碼后的數(shù)據(jù),將編碼后的數(shù)據(jù)寫入文件
outFile.write((char *) pkt->data, pkt->size);
// 釋放 pkt 內(nèi)部的資源
av_packet_unref(pkt);
}
}
8、釋放資源
// 關(guān)閉文件
inFile.close();
outFile.close();
// 釋放資源
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&ctx);
最后運(yùn)行我們的程序進(jìn)行編碼,發(fā)現(xiàn) Qt 控制臺(tái)會(huì)打印如下錯(cuò)誤,是因?yàn)槲覀儧]有設(shè)置幀序號(hào)導(dǎo)致的:
[libx264 @ 0x7fd6f0061c00] non-strictly-monotonic PTS
-9223372036854775808
解決辦法:
// 幀序號(hào)初始化為 0
frame->pts = 0;
// 設(shè)置幀序號(hào),每編碼一幀,幀序號(hào)加 1
frame->pts++;
然后我們使用 ffplay 播放我們壓縮后的 h264 文件,發(fā)現(xiàn)壓縮后視頻是有問題的:

在終端使用同樣的編碼器和同樣的輸入?yún)?shù)編碼生成一個(gè) h264 文件,通過對(duì)比發(fā)現(xiàn)代碼生成的 h264 文件要比在終端生成的 h264 文件大不少:
$ ls -al
-rw-r--r-- 1 mac staff 110592000 Mar 30 16:28 in_640x480_yuv420p.yuv
-rw-r--r-- 1 mac staff 583538 Apr 12 09:59 out_640x480_yuv420p_code.h264
-rw-r--r-- 1 mac staff 437271 Apr 12 09:55 out_640x480_yuv420p_terminal.h264
通過檢查發(fā)現(xiàn)問題產(chǎn)生的原因是 frame->data 緩沖區(qū)大小超過了一幀圖像大?。?/p>
// 打印 frame->data:
qDebug() << frame->data[0] << frame->data[1] << frame->data[2];
// 控制臺(tái)輸出:
0x7fc3001a2000 0x7fc3001ed020 0x7fc3001ffc40
// 計(jì)算各平面大?。?Y平面大小 = frame->data[1] - frame->data[0] = 0x7fc3001ed020 - 0x7fc3001a2000 = 307232 字節(jié)
U平面大小 = frame->data[2] - frame->data[1] = 0x7fc3001ffc40 - 0x7fc3001ed020 = 76832 字節(jié)
// 正確的各平面大?。?Y平面大小 = 640 * 480 * 1 = 307200 字節(jié)
U平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字節(jié)
V平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字節(jié)
發(fā)現(xiàn) frame 數(shù)據(jù)緩沖區(qū)大小比我們預(yù)期的要大。查看av_frame_get_buffer 源碼,是因?yàn)楹瘮?shù) av_frame_get_buffer 內(nèi)部分配數(shù)據(jù)緩沖區(qū)空間時(shí)增加了 32 字節(jié)的 plane_padding 導(dǎo)致的??梢該Q成函數(shù) av_image_alloc 或者函數(shù) av_image_fill_arrays 分配數(shù)據(jù)緩沖區(qū)空間:
ret = av_image_alloc(frame->data, frame->linesize, in.width, in.height, in.pixFmt, 1);
// 最后不要忘記釋放數(shù)據(jù)緩沖區(qū)
av_freep(&frame->data[0]);
// 或者:
ret = av_image_fill_arrays(frame->data, frame->linesize, buf, in.pixFmt, in.width, in.height, 1);
完整示例代碼:
h264_encode.pro:
macx {
INCLUDEPATH += /usr/local/ffmpeg/include
LIBS += -L/usr/local/ffmpeg/lib -lavcodec \
-lavutil
}
ffmpegutils.h:
#ifndef FFMPEGUTILS_H
#define FFMPEGUTILS_H
#include <QObject>
extern "C" {
#include <libavutil/avutil.h>
}
typedef struct {
const char *filename;
int width;
int height;
AVPixelFormat pixFmt;
int fps;
} VideoEncodeSpec;
class FFmpegUtils : public QObject
{
Q_OBJECT
public:
explicit FFmpegUtils(QObject *parent = nullptr);
static void h264Encode(VideoEncodeSpec &in, const char *outFilename);
signals:
};
#endif // FFMPEGUTILS_H
ffmpegutils.m:
#include "ffmpegutils.h"
#include <QDebug>
#include <QFile>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
}
#define ERRBUF(ret) \
char errbuf[1024]; \
av_strerror(ret, errbuf, sizeof (errbuf))
FFmpegUtils::FFmpegUtils(QObject *parent) : QObject(parent)
{
}
int check_pix_fmt(const AVCodec *codec, enum AVPixelFormat pix_fmt)
{
const enum AVPixelFormat *p = codec->pix_fmts;
while (*p != AV_PIX_FMT_NONE) {
if (*p == pix_fmt) return 1;
p++;
}
return 0;
}
static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, QFile &outFile)
{
int ret = avcodec_send_frame(ctx, frame);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "error sending the frame to the codec: " << errbuf;
return ret;
}
while (true) {
// 從編碼器中獲取編碼后的數(shù)據(jù)
ret = avcodec_receive_packet(ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
} else if (ret < 0) {
ERRBUF(ret);
qDebug() << "error encode audio frame: " << errbuf;
return ret;
}
outFile.write((const char *)pkt->data, pkt->size);
av_packet_unref(pkt);
}
return 0;
}
void FFmpegUtils::h264Encode(VideoEncodeSpec &in, const char *outFilename)
{
int ret = 0;
// 編碼器
AVCodec *codec = nullptr;
// 上下文
AVCodecContext *ctx = nullptr;
// 用來存放編碼前的數(shù)據(jù)(yuv)
AVFrame *frame = nullptr;
// 用來存放編碼后的數(shù)據(jù)(h264)
AVPacket *pkt = nullptr;
QFile inFile(in.filename);
QFile outFile(outFilename);
// 一幀圖片的大小
int image_size = av_image_get_buffer_size(in.pixFmt, in.width, in.height, 1);
// 輸入緩沖區(qū) 方式二
uint8_t *buf = nullptr;
// 查找編碼器
codec = avcodec_find_encoder_by_name("libx264");
if (!codec) {
qDebug() << "encoder libx264 not found";
return;
}
// 檢查編碼器是否支持該編碼格式
if (!check_pix_fmt(codec, in.pixFmt)) {
qDebug() << "unsupported pixel format: " << av_get_pix_fmt_name(in.pixFmt);
goto end;
}
// 創(chuàng)建編碼上下文
ctx = avcodec_alloc_context3(codec);
if (!ctx) {
qDebug() << "could not allocate video codec context";
return;
}
// 設(shè)置 ctx 參數(shù)
ctx->width = in.width;
ctx->height = in.height;
ctx->pix_fmt = in.pixFmt;
// 設(shè)置幀率 1秒鐘顯示多少幀
ctx->time_base = {1, in.fps};
// 打開編碼器
ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "could not open codec: " << errbuf;
goto end;
}
// 創(chuàng)建packet
pkt = av_packet_alloc();
if (!pkt) {
qDebug() << "could not allocate audio packet";
goto end;
}
// 創(chuàng)建frame
frame = av_frame_alloc();
if (!frame) {
qDebug() << "could not allocate audio frame";
goto end;
}
// 保證 frame 里就是一幀 yuv 數(shù)據(jù)
frame->width = ctx->width;
frame->height = ctx->height;
// format 是通用的
frame->format = ctx->pix_fmt;
frame->pts = 0;
// 創(chuàng)建輸入緩沖區(qū) 方法一
ret = av_image_alloc(frame->data, frame->linesize, in.width, in.height, in.pixFmt, 1);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "could not allocate audio data buffers: " << errbuf;
goto end;
}
/*
// 利用width、height、format創(chuàng)建frame的數(shù)據(jù)緩沖區(qū),利用width、height、format可以算出一幀大小
ret = av_frame_get_buffer(frame, 1);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "could not allocate audio data buffers: " << errbuf;
goto end;
}
*/
/*
// 創(chuàng)建輸入緩沖區(qū) 方法二
buf = (uint8_t *)av_malloc(image_size);
ret = av_image_fill_arrays(frame->data, frame->linesize, buf, in.pixFmt, in.width, in.height, 1);
if (ret < 0) {
ERRBUF(ret);
qDebug() << "could not allocate audio data buffers: " << errbuf;
goto end;
}
*/
// 打開文件
if (!inFile.open(QFile::ReadOnly)) {
qDebug() << "open file failure: " << in.filename;
goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
qDebug() << "open file failure: " << outFilename;
goto end;
}
// 讀取數(shù)據(jù)到 frame 中
while (inFile.read((char *)frame->data[0], image_size) > 0) {
// 編碼
if (encode(ctx, frame, pkt, outFile) < 0) {
goto end;
}
// 設(shè)置幀的序號(hào)
frame->pts++;
}
// 刷新緩沖區(qū),刷出緩沖區(qū)剩余數(shù)據(jù)
encode(ctx, nullptr, pkt, outFile);
end:
inFile.close();
outFile.close();
if (frame) {
av_freep(&frame->data[0]);
av_frame_free(&frame);
}
av_packet_free(&pkt);
avcodec_free_context(&ctx);
}
函數(shù)調(diào)用:
VideoEncodeSpec spec;
spec.filename = "/users/mac/Downloads/pic/in_640x480_yuv420p.yuv";
spec.width = 640;
spec.height = 480;
spec.pixFmt = AV_PIX_FMT_YUV420P;
spec.fps = 25;
FFmpegUtils::h264Encode(spec, "/users/mac/Downloads/pic/out_640x480_yuv420p_code.h264");