在视频处理领域,FFmpeg可以说是当之无愧的"瑞士军刀"。我刚开始接触视频编解码时,第一反应就是去找各种商业SDK,后来发现FFmpeg这个开源神器不仅能完成同样的工作,还提供了更灵活的API接口。特别是对于H265(HEVC)这种高效视频编码格式,FFmpeg的支持已经相当成熟。
H265相比H264最大的优势就是在同等画质下可以节省约50%的码率,这对网络视频传输和存储来说都是巨大的提升。但它的编解码复杂度也更高,直接处理原始数据会很吃力。FFmpeg的libx265编码器和libx265解码器封装了底层细节,让我们可以用相对简单的API实现高效编解码。
在实际项目中,我经常需要处理监控摄像头传过来的H265流。最初尝试用其他库时,要么性能跟不上,要么内存占用太高。后来切换到FFmpeg方案后,不仅处理速度提升了,CPU占用还降低了30%左右。特别是它的异步编解码接口,能充分利用多核性能,这对实时视频处理特别重要。
在Ubuntu系统上安装FFmpeg开发环境最简单的方式是使用apt:
bash复制sudo apt update
sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev
如果是Windows平台,建议使用vcpkg进行安装:
bash复制vcpkg install ffmpeg:x64-windows
我建议始终使用最新稳定版的FFmpeg,因为H265相关的优化一直在持续进行。有次项目中使用旧版本遇到了内存泄漏问题,升级后立即解决。安装完成后可以通过以下命令验证:
bash复制ffmpeg -codecs | grep hevc
应该能看到HEVC (High Efficiency Video Coding)相关的解码器和编码器信息。
现代C++项目我习惯用CMake管理,这里给出一个完整的CMakeLists.txt示例:
cmake复制cmake_minimum_required(VERSION 3.10)
project(h265_encoder_decoder)
set(CMAKE_CXX_STANDARD 17)
find_package(PkgConfig REQUIRED)
pkg_check_modules(AVCODEC REQUIRED libavcodec)
pkg_check_modules(AVFORMAT REQUIRED libavformat)
pkg_check_modules(AVUTIL REQUIRED libavutil)
include_directories(${AVCODEC_INCLUDE_DIRS})
include_directories(${AVFORMAT_INCLUDE_DIRS})
include_directories(${AVUTIL_INCLUDE_DIRS})
add_executable(h265_demo main.cpp)
target_link_libraries(h265_demo ${AVCODEC_LIBRARIES} ${AVFORMAT_LIBRARIES} ${AVUTIL_LIBRARIES})
这个配置会自动查找FFmpeg的各组件,确保编译时能找到正确的头文件和库路径。我在Windows和Linux多个平台测试过,都能正确工作。
创建编码器的第一步是查找并初始化libx265编码器:
cpp复制AVCodec* codec = avcodec_find_encoder_by_name("libx265");
if (!codec) {
std::cerr << "libx265 encoder not found" << std::endl;
return -1;
}
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
std::cerr << "Could not allocate video codec context" << std::endl;
return -1;
}
编码器参数设置是关键,这里有个经验公式:对于1080p视频,比特率可以设为宽度×高度×帧率×0.1。比如:
cpp复制codec_ctx->bit_rate = 1920 * 1080 * 30 * 0.1; // 约6Mbps
codec_ctx->width = 1920;
codec_ctx->height = 1080;
codec_ctx->time_base = (AVRational){1, 30}; // 30fps
codec_ctx->framerate = (AVRational){30, 1};
codec_ctx->gop_size = 30; // 关键帧间隔
codec_ctx->max_b_frames = 2; // B帧数量
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
x265有丰富的私有参数,通过av_opt_set设置:
cpp复制av_opt_set(codec_ctx->priv_data, "preset", "fast", 0);
av_opt_set(codec_ctx->priv_data, "tune", "zerolatency", 0);
av_opt_set(codec_ctx->priv_data, "x265-params", "log-level=warning", 0);
编码过程的核心是avcodec_send_frame和avcodec_receive_packet这对API。我封装了一个简单的编码函数:
cpp复制int encode_frame(AVCodecContext* ctx, AVFrame* frame, AVPacket* pkt, FILE* outfile) {
int ret = avcodec_send_frame(ctx, frame);
if (ret < 0) {
std::cerr << "Error sending frame to encoder" << std::endl;
return ret;
}
while (ret >= 0) {
ret = avcodec_receive_packet(ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return 0;
if (ret < 0) {
std::cerr << "Error during encoding" << std::endl;
return ret;
}
fwrite(pkt->data, 1, pkt->size, outfile);
av_packet_unref(pkt);
}
return 0;
}
处理YUV数据时要注意内存对齐问题。我推荐使用av_frame_get_buffer分配帧内存:
cpp复制AVFrame* frame = av_frame_alloc();
frame->format = codec_ctx->pix_fmt;
frame->width = codec_ctx->width;
frame->height = codec_ctx->height;
if (av_frame_get_buffer(frame, 32) < 0) {
std::cerr << "Could not allocate frame data" << std::endl;
return -1;
}
解码器初始化与编码器类似,但参数更简单:
cpp复制AVCodec* codec = avcodec_find_decoder_by_name("libx265");
if (!codec) {
std::cerr << "libx265 decoder not found" << std::endl;
return -1;
}
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
std::cerr << "Could not allocate video codec context" << std::endl;
return -1;
}
if (avcodec_open2(codec_ctx, codec, nullptr) < 0) {
std::cerr << "Could not open codec" << std::endl;
return -1;
}
解码过程使用avcodec_send_packet和avcodec_receive_frame:
cpp复制AVPacket packet;
av_init_packet(&packet);
AVFrame* frame = av_frame_alloc();
while (read_packet_from_file(&packet, input_file)) {
int ret = avcodec_send_packet(codec_ctx, &packet);
if (ret < 0) {
std::cerr << "Error sending packet to decoder" << std::endl;
break;
}
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
if (ret < 0) {
std::cerr << "Error during decoding" << std::endl;
break;
}
// 处理解码后的YUV帧
process_yuv_frame(frame, output_file);
av_frame_unref(frame);
}
av_packet_unref(&packet);
}
处理YUV数据时要注意不同格式的存储方式。对于YUV420P格式:
cpp复制void save_yuv_frame(AVFrame* frame, FILE* file) {
// Y分量
for (int y = 0; y < frame->height; y++) {
fwrite(frame->data[0] + y * frame->linesize[0], 1, frame->width, file);
}
// U/V分量
for (int y = 0; y < frame->height / 2; y++) {
fwrite(frame->data[1] + y * frame->linesize[1], 1, frame->width / 2, file);
fwrite(frame->data[2] + y * frame->linesize[2], 1, frame->width / 2, file);
}
}
x265支持多线程编码,可以通过参数开启:
cpp复制av_opt_set(codec_ctx->priv_data, "x265-params", "pool=4 frame-threads=2", 0);
在我的i7-9700K上测试,8线程编码比单线程快3倍以上。但要注意线程数不是越多越好,超过物理核心数后收益会递减。
FFmpeg的内存管理有几个坑需要注意:
我曾经遇到过内存泄漏问题,后来发现是忘记释放中间帧。现在习惯用RAII封装:
cpp复制struct FrameGuard {
AVFrame* frame;
FrameGuard() : frame(av_frame_alloc()) {}
~FrameGuard() { if(frame) av_frame_free(&frame); }
};
实时视频传输需要低延迟,可以设置:
cpp复制av_opt_set(codec_ctx->priv_data, "tune", "zerolatency", 0);
av_opt_set(codec_ctx->priv_data, "x265-params", "no-scenecut=1:rc-lookahead=0", 0);
在视频会议项目中,这样设置后端到端延迟可以从500ms降到200ms以内。
x265支持多种码率控制模式:
我常用的CRF设置:
cpp复制av_opt_set(codec_ctx->priv_data, "x265-params", "crf=28", 0);
CRF值越小质量越高,18-28是常用范围。