本文的主要内容:使用H.264编码对YUV视频进行压缩。
使用FFmpeg命令进行H.264编码
如果是命令行的操作,非常简单。
| ffmpeg -s 640x480 -pix_fmt yuv420p -framerate 30 -i out_640x480.yuv -c:v libx264 out.h264 |
| |
使用FFmpeg代码实现H.264编码
接下来主要介绍如何通过代码的方式使用H.264编码,用到了avcodec、avutil两个库,整体过程跟《AAC编码实战》类似。
1、获取编码器
| codec = avcodec_find_encoder_by_name("libx264"); |
前面对音频进AAC编码时,AAC编码器对数据的采样格式是有要求的,比如libfdk_aac
要求采样格式是s16整型,同样的H.264编码库libx264
对输入数据像素格式也有要求,虽然avcodec_open2
函数内部也会对像素格式进行检查,但是建议提前检查输入像素格式:
| if(!check_pix_fmt(codec,in.format)){ |
| qDebug() << "Encoder does not support sample format" |
| << av_get_pix_fmt_name(in.format); |
| 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.format; |
| |
| 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
前设置frame的width
、height
和format
,利用width
、height
和format
可算出一帧图像大小,frame->data[0]
指向的堆空间其实就是一帧图像的大小:
| frame->width = ctx->width; |
| frame->height = ctx->height; |
| frame->format = ctx->pix_fmt; |
| frame->pts = 0; |
| |
| |
| ret = av_frame_get_buffer(frame, 0); |
5、创建 AVPacket
6、打开文件,从文件读取数据到 AVFrame
| |
| int imgSize = av_image_get_buffer_size(in.format,in.width,in.height,1); |
| |
| |
| |
| 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; |
| } |
| |
| |
| while ((ret = inFile.read((char *) frame->data[0], |
| imgSize)) > 0) { |
| |
| if (encode(ctx, frame, pkt, outFile) < 0) { |
| goto end; |
| } |
| |
| |
| frame->pts++; |
| } |
这里如果我们没有设置帧的序号frame->pts++
,运行程序发现Qt控制台会打印如下错误,是因为我们没有设置帧序号导致的:

7、解码
| |
| 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) { |
| return 0; |
| } else if (ret < 0) { |
| ERROR_BUF(ret); |
| qDebug() << "avcodec_receive_packet error" << errbuf; |
| return ret; |
| } |
| |
| |
| |
| outFile.write((char *) pkt->data, pkt->size); |
| |
| |
| av_packet_unref(pkt); |
| } |
| return 0; |
| } |
8、释放资源
| |
| inFile.close(); |
| outFile.close(); |
| |
| |
| av_frame_free(&frame); |
| av_packet_free(&pkt); |
| avcodec_free_context(&ctx); |
9、播放h264文件
然后我们使用ffplay播放我们压缩后的h264文件,发现压缩后视频是有问题的:

通过和上面使用ffmpeg命令同样的输入参数编码生成的h264文件对比发现,其大小大了一些,而且视频有一层颜色覆盖。

通过检查发现问题产生的原因是frame->data
缓冲区大小超过了一帧图像大小:
| |
| qDebug() << frame->data[0] << frame->data[1] << frame->data[2]; |
| |
| 0x96edd00 0x9738d20 0x974b940 |
| |
| Y平面大小 = frame->data[1] - frame->data[0] = 0x9738d20 - 0x96edd00 = 307232 字节 |
| U平面大小 = frame->data[2] - frame->data[1] = 0x974b940 - 0x9738d20 = 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
分配数据缓冲区空间:
| |
| |
| |
| |
| buf = (uint8_t *)av_malloc(imgSize); |
| ret = av_image_fill_arrays(frame->data,frame->linesize, |
| buf, |
| in.format,in.width,in.height,1); |
| |
| |
| |
| if (ret < 0) { |
| ERROR_BUF(ret); |
| qDebug() << "av_frame_get_buffer error" << errbuf; |
| goto end; |
| } |
| |
| |
| |
| if(frame){ |
| av_freep(&frame->data[0]); |
| |
| |
| |
| av_frame_free(&frame); |
| } |
-
av_frame_get_buffer
创建缓冲区后只需要执行av_frame_free(&frame);
就可以了
| if(frame){ |
| av_frame_free(&frame); |
| } |
-
av_image_alloc
创建缓冲区后还需要执行av_freep(&frame->data[0]);
| if(frame){ |
| av_freep(&frame->data[0]); |
| |
| |
| |
| av_frame_free(&frame); |
| } |
-
av_image_fill_arrays
创建缓冲区后需要执行av_freep(&buf)
,通过qDebug() << buf <<frame->data[0];
打印知道这两个值是一样的,说明buf
和frame->data[0]
是指向同一个区域,因此只需要释放buf
就可以了不需要执行av_freep(&frame->data[0])
| qDebug() << buf <<frame->data[0]; |
| |
| 0x820cf80 0x820cf80 |
| av_freep(&buf); |
| |
| if(frame){ |
| av_frame_free(&frame); |
| } |
具体代码
ffmpegutils.h
| #ifndef FFMPEGUTILS_H |
| #define FFMPEGUTILS_H |
| |
| #include <QDebug> |
| #include <QFile> |
| |
| extern "C" { |
| #include <libavcodec/avcodec.h> |
| #include <libavutil/avutil.h> |
| #include <libavutil/imgutils.h> |
| } |
| |
| typedef struct { |
| const char *filename; |
| int width; |
| int height; |
| AVPixelFormat format; |
| int fps; |
| } VideoEncodeSpec; |
| |
| class FFmpegUtils |
| { |
| public: |
| FFmpegUtils(); |
| static void h264Encode(VideoEncodeSpec &in, |
| const char *outFilename); |
| }; |
| |
| #endif |
ffmpegutils.cpp
| #include "ffmpegutils.h" |
| |
| |
| #define ERROR_BUF(ret) \ |
| char errbuf[1024]; \ |
| av_strerror(ret, errbuf, sizeof (errbuf)); |
| |
| FFmpegUtils::FFmpegUtils(){} |
| |
| 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; |
| } |
| |
| |
| 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) { |
| return 0; |
| } else if (ret < 0) { |
| ERROR_BUF(ret); |
| qDebug() << "avcodec_receive_packet error" << errbuf; |
| return ret; |
| } |
| |
| |
| |
| outFile.write((char *) pkt->data, pkt->size); |
| |
| |
| av_packet_unref(pkt); |
| } |
| return 0; |
| } |
| |
| void FFmpegUtils::h264Encode(VideoEncodeSpec &in, const char *outFilename){ |
| |
| QFile inFile(in.filename); |
| QFile outFile(outFilename); |
| |
| |
| int imgSize = av_image_get_buffer_size(in.format,in.width,in.height,1); |
| |
| |
| int ret = 0; |
| |
| AVCodec *codec = nullptr; |
| |
| |
| AVCodecContext *ctx = nullptr; |
| |
| |
| AVFrame *frame = nullptr; |
| |
| |
| AVPacket *pkt = nullptr; |
| |
| |
| |
| |
| codec = avcodec_find_encoder(AV_CODEC_ID_H264); |
| |
| qDebug() << "codec.name:" << codec->name<<",codec.id:"<<codec->id; |
| if(!codec){ |
| qDebug() << "encoder not found"; |
| return; |
| } |
| |
| |
| if(!check_pix_fmt(codec,in.format)){ |
| qDebug() << "Encoder does not support pixel format" |
| << av_get_pix_fmt_name(in.format); |
| return; |
| } |
| |
| |
| ctx = avcodec_alloc_context3(codec); |
| if (!ctx) { |
| qDebug() << "avcodec_alloc_context3 error"; |
| return; |
| } |
| |
| |
| ctx->width = in.width; |
| ctx->height = in.height; |
| ctx->pix_fmt = in.format; |
| |
| 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; |
| } |
| |
| |
| 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; |
| |
| |
| ret = av_image_alloc(frame->data,frame->linesize,in.width,in.height,in.format,1); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (ret < 0) { |
| ERROR_BUF(ret); |
| qDebug() << "av_frame_get_buffer error" << errbuf; |
| goto end; |
| } |
| |
| |
| |
| |
| qDebug() << frame->data[0] << frame->data[1] << frame->data[2]; |
| |
| |
| 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; |
| } |
| |
| |
| |
| 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(); |
| |
| |
| |
| if(frame){ |
| av_freep(&frame->data[0]); |
| |
| |
| |
| av_frame_free(&frame); |
| } |
| av_packet_free(&pkt); |
| avcodec_free_context(&ctx); |
| } |
videothread.cpp
| #ifdef Q_OS_WIN |
| |
| #define IN_FILENAME "../test/out_640x480.yuv" |
| #define OUT_FILENAME "../test/out_640x480.h264" |
| #else |
| #define IN_FILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/out_640x480.yuv" |
| #define OUT_FILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/out_640x480.h264" |
| #endif |
| |
| void VideoThread::run(){ |
| VideoEncodeSpec in; |
| in.filename = IN_FILENAME; |
| in.width = 640; |
| in.height = 480; |
| in.fps = 30; |
| in.format = AV_PIX_FMT_YUV420P; |
| |
| FFmpegUtils::h264Encode(in,OUT_FILENAME); |
| } |
代码链接
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!