使用FFmpeg实现H.264编码

一、H.264/AVC 概述

H.264/AVC 也可以叫做 H.264/MPEG-4 part 10 AVC,这是一个联合名字,H.264 冠的是 ITU-T 的名称,AVC(Advanced Video Coding) 冠的是 ISO-IEC 的名字。ITU-T 是国际电信标准化部门。ISO-IEC是国际标准化组织-国际电工委员会。在 2001 年的 12 月, ITU-T 的 VCEG(Video Coding Experts Group)和 ISO-IEC 的 MPEG(Moving Picture Experts Group)联合成立了一个新的机构叫 JVT(Joint Video Team),就是这个新的组织 JVT 于 2003 年 3 月 发布了 H264/AVC 视频编码标准。

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

二、为什么要进行视频编码?

视频播放的本质是展示一张张图像,如果一秒钟至少连续播放 24 张图像,那么人眼看到的就是连续的视频画面。通过手机或者电脑在线观看视频,首先需要将这些图像通过网络传输到你的手机或者电脑,这需要考虑的一个问题就是视频对网络带宽的占用情况。H.264/AVC 作为一个视频压缩标准,其主要使命就是降低视频的带宽占用,提高传输效率。H.264/AVC 的压缩比可以达到至少是 100:1。

是否真的需要对图像进行压缩?我们知道一张大小为 1920x1080 用 yuv420p 像素格式表示的一张图像的大小是 1920 * 1080 * 1.5 = 3110400 字节(yuv420p 像素格式每个像素占用 1.5 字节),那么一段 10 秒钟 30fps 的 1080p(1920x1080)原始视频的大小就是 3110400 * 30 * 10 = 933120000 字节,大约是 889.89MB,可以看出来原始视频的体积非常大。因此需要使用视频编码技术对原始视频进行压缩来降低数据传输和存储的成本。

三、使用 FFmpeg 命令行进行 H.264 编码
$ ffmpeg -s 640x360 -pix_fmt yuv420p -i IMG_1459_yuvj420p640x360fps30.yuv -c:v libx264 out.h264

在我本地 H.264 编码器默认就是 libx264,所以 -c:v libx264 也可省略不写。排在最前面的编码器即默认编码器:

$ 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 编程实现 H.264 编码

H.264 视频编码和 AAC 音频编码流程是类似的,H.264 视频编码使用的是编码器 x264,在 FFmpeg 中的名称是 libx264libx264 并没有默认内置到 FFmpeg 中,我们是在编译 FFmpeg 时手动将通过 homebrew 安装到本地的 libx264 内置到 FFmpeg 中的)。

首先需要导入我们需要用到的库,主要用到 FFmpeg 两个库 libavcodeclibavutil

macx {
    INCLUDEPATH += /usr/local/ffmpeg/include
    LIBS += -L/usr/local/ffmpeg/lib -lavcodec \
        -lavutil
}

1、获取编码器

codec = avcodec_find_encoder_by_name("libx264");

也可以使用 ID 的方式获取编码器,使用使用 ID 的方式获取到的是本地默认编码器,在我本地默认 H.264 编码器就是 libx264,所以使用下面方式获取的编码器和上面使用 name 获取到的编码器是同一个编码器,都是 libx264(具体情况需要查看本地环境):

codec = avcodec_find_encoder(AV_CODEC_ID_H264);

前面对音频进 AAC 编码时,AAC 编码器对数据的采样格式是有要求的,比如 libfdk_aac 要求采样格式是 s16 整型,同样的 H.264 编码库 libx264 对输入数据像素格式也有要求,虽然 avcodec_open2 函数内部也会对像素格式进行检查,但是建议提前检查输入像素格式:

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 中存放的是当前编码器支持的像素格式。AV_PIX_FMT_NONE 是一个边界标识,用于判断是否遍历结束。

2、创建编码上下文

ctx = avcodec_alloc_context3(codec);

设置编码上下文参数:

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

3、打开编码器

ret = avcodec_open2(ctx, codec, nullptr);

可以通过参数 options 设置一些编码器特有参数。

4、创建 AVFrame

frame = av_frame_alloc();

av_frame_alloc 仅仅是为 AVFrame 分配空间,数据缓冲区 frame->data[0] 需要我们调用函数 av_frame_get_buffer 来创建。调用函数 av_frame_get_buffer 前设置 framewidthheightformat, 利用 widthheightformat 可算出一帧图像大小, frame->data[0] 指向的堆空间其实就是一帧图像的大小:

frame->width = ctx->width;
frame->height = ctx->height;
frame->format = ctx->pix_fmt;
ret = av_frame_get_buffer(frame, 1);

5、创建 AVPacket

pkt = av_packet_alloc();

6、打开文件,从文件读取数据到 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:编码操作正常完成;返回负数:中途出现了错误
static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, QFile &outFile) {
    // 发送数据到编码器
    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;
    }

    // 不断从编码器中取出编码后的数据
    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);
    }
}

8、释放资源

// 关闭文件
inFile.close();
outFile.close();
// 释放资源
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&ctx);

最后运行我们的程序进行编码,发现 Qt 控制台会打印如下错误,是因为我们没有设置帧序号导致的:

[libx264 @ 0x7fd6f0061c00] non-strictly-monotonic PTS
-9223372036854775808

解决办法:

// 帧序号初始化为 0
frame->pts = 0;
// 设置帧序号,每编码一帧,帧序号加 1
frame->pts++;

然后我们使用 ffplay 播放我们压缩后的 h264 文件,发现压缩后视频是有问题的:


问题截图

在终端使用同样的编码器和同样的输入参数编码生成一个 h264 文件,通过对比发现代码生成的 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

通过检查发现问题产生的原因是 frame->data 缓冲区大小超过了一帧图像大小:

// 打印 frame->data:
qDebug() << frame->data[0] << frame->data[1] << frame->data[2];
// 控制台输出:
0x7fc3001a2000 0x7fc3001ed020 0x7fc3001ffc40
// 计算各平面大小:
Y平面大小 = frame->data[1] - frame->data[0] = 0x7fc3001ed020 - 0x7fc3001a2000 = 307232 字节
U平面大小 = frame->data[2] - frame->data[1] = 0x7fc3001ffc40 - 0x7fc3001ed020 = 76832 字节
// 正确的各平面大小:
Y平面大小 = 640 * 480 * 1 = 307200 字节
U平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字节
V平面大小 = (640 / 2) * (480 / 2) * 1 = 76800 字节

发现 frame 数据缓冲区大小比我们预期的要大。查看av_frame_get_buffer 源码,是因为函数 av_frame_get_buffer 内部分配数据缓冲区空间时增加了 32 字节的 plane_padding 导致的。可以换成函数 av_image_alloc 或者函数 av_image_fill_arrays 分配数据缓冲区空间:

ret = av_image_alloc(frame->data, frame->linesize, in.width, in.height, in.pixFmt, 1);
// 最后不要忘记释放数据缓冲区
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) {
        // 从编码器中获取编码后的数据
        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;
    // 用来存放编码前的数据(yuv)
    AVFrame *frame = nullptr;
    // 用来存放编码后的数据(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);

    // 输入缓冲区 方式二
    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;
    }

    // 创建编码上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        qDebug() << "could not allocate video codec context";
        return;
    }

    // 设置 ctx 参数
    ctx->width = in.width;
    ctx->height = in.height;
    ctx->pix_fmt = in.pixFmt;
    // 设置帧率 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;
    }

    // 创建packet
    pkt = av_packet_alloc();
    if (!pkt) {
        qDebug() << "could not allocate audio packet";
        goto end;
    }

    // 创建frame
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "could not allocate audio frame";
        goto end;
    }

    // 保证 frame 里就是一帧 yuv 数据
    frame->width = ctx->width;
    frame->height = ctx->height;
    // format 是通用的
    frame->format = ctx->pix_fmt;
    frame->pts = 0;

    // 创建输入缓冲区 方法一
    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创建frame的数据缓冲区,利用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;
    }
    */

    /*
    // 创建输入缓冲区 方法二
    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;
    }

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

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

    // 刷新缓冲区,刷出缓冲区剩余数据
    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);
}

函数调用:

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");
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容