跳到主要内容Linux 下 FFmpeg C++ 音视频解码与推流实战 | 极客日志C++算法
Linux 下 FFmpeg C++ 音视频解码与推流实战
Linux 环境下使用 FFmpeg 进行 C++ 音视频开发,涵盖源码编译安装、核心结构体解析、解码流程实现及 RTMP 推流实战。重点解决资源管理、内存泄漏规避及时间戳同步问题,提供可直接运行的工业级代码示例与避坑指南。
锁机制3 浏览 Linux 下 FFmpeg C++ 音视频解码与推流实战
掌握了命令行基础后,转向 Linux 下的 C++ 开发是进阶的必经之路。本教程将带你从零搭建环境,深入核心结构体,完成从解码到推流的完整闭环。重点讲解资源管理、错误处理及内存泄漏规避,提供可直接编译运行的工业级代码。
一、前置准备:Linux 下 FFmpeg 开发环境搭建
Linux 默认 apt install ffmpeg 仅安装命令行工具,缺少开发头文件和库文件,必须源码编译安装完整开发版。
1.1 安装编译依赖(以 Ubuntu/Debian 为例)
sudo apt update
sudo apt install -y build-essential cmake git libssl-dev libx264-dev libx265-dev \
libmp3lame-dev libfdk-aac-dev libsdl2-dev libavutil-dev libavformat-dev \
libavcodec-dev libswscale-dev libavfilter-dev
- 依赖说明:
build-essential:gcc/g++ 编译工具;
libx264/libx265-dev:H.264/H.265 编码器依赖;
libmp3lame/libfdk-aac-dev:音频编码器依赖;
libssl-dev:RTMP/HTTPS 推流依赖(openssl)。
1.2 源码编译安装 FFmpeg(带开发库)
git clone --depth 1 --branch n6.0 https://git.ffmpeg.org/ffmpeg.git
ffmpeg
cd ffmpeg
./configure \
--prefix=/usr/local/ffmpeg \
--enable-shared \
--enable-static \
--enable-gpl \
--enable-libx264 \
--enable-libx265 \
--enable-libmp3lame \
--enable-libfdk-aac \
--enable-avformat \
--enable-avcodec \
--enable-avutil \
--enable-network \
--enable-protocol=rtmp \
--enable-protocol=rtsp \
--enable-swscale \
--disable-doc \
--disable-ffplay \
--disable-ffprobe
make -j$(nproc)
sudo make install
1.3 配置环境变量
sudo vim /etc/profile
export PATH=/usr/local/ffmpeg/bin:
LD_LIBRARY_PATH=/usr/local/ffmpeg/lib:
PKG_CONFIG_PATH=/usr/local/ffmpeg/lib/pkgconfig:
/etc/profile
$PATH
export
$LD_LIBRARY_PATH
export
$PKG_CONFIG_PATH
source
1.4 验证开发环境是否成功
ls /usr/local/ffmpeg/include/libavcodec/avcodec.h
ls /usr/local/ffmpeg/lib/libavcodec.so
pkg-config --libs libavformat libavcodec libavutil
二、核心概念与开发流程
FFmpeg 开发的核心是操作一系列结构体,先拆解最关键的概念和流程,避免代码写了却不懂逻辑。
2.1 核心结构体
| 结构体 | 含义 | 核心作用 |
|---|
AVFormatContext | 格式上下文 | 管理音视频文件/流的全局信息(输入/输出),如文件名、流数量、时长 |
AVCodecContext | 编解码器上下文 | 管理编解码的核心参数(类型、码率、分辨率等) |
AVCodec | 编解码器 | 具体的编解码器实例(如 H.264 解码器) |
AVStream | 流信息 | 描述音频/视频流的属性(时间基、码率) |
AVPacket | 压缩数据包 | 存储编码后的音视频数据,解码的输入/推流的输出 |
AVFrame | 原始帧数据 | 存储解码后的原始数据(视频 YUV/音频 PCM),解码的输出/编码的输入 |
AVDictionary | 参数字典 | 传递额外参数(如超时时间、编码器参数) |
2.2 音视频解码核心流程
- 初始化 FFmpeg:
av_register_all / avformat_network_init
- 打开输入文件:
avformat_open_input
- 获取流信息:
avformat_find_stream_info
- 查找音视频流索引:遍历
AVStream,区分视频/音频
- 查找并初始化解码器:
avcodec_find_decoder -> avcodec_open2
- 循环读取数据包:
av_read_frame
- 发送数据包到解码器:
avcodec_send_packet
- 接收解码后的原始帧:
avcodec_receive_frame
- 处理原始帧:YUV/PCM 数据(保存/推流)
- 释放资源:
av_frame_unref / av_packet_unref
- 关闭上下文:
avcodec_close / avformat_close_input
2.3 视频推流核心流程(以 RTMP 为例)
- 初始化网络:
avformat_network_init
- 创建输出格式上下文:
avformat_alloc_output_context2
- 添加音视频流:
avformat_new_stream
- 配置编码器上下文:设置码率/分辨率/时间基等
- 打开编码器:
avcodec_open2
- 写入头信息:
avformat_write_header
- 循环编码原始帧:
avcodec_send_frame / avcodec_receive_packet
- 调整时间戳:
av_packet_rescale_ts
- 发送数据包:
av_interleaved_write_frame
- 写入尾信息:
av_write_trailer
- 关闭上下文:
avcodec_close / avformat_free_context
2.4 关键工具函数
- 错误处理:
av_strerror(int errnum, char *errbuf, size_t errbuf_size) → 把 FFmpeg 错误码转成可读字符串(Linux 开发必用);
- 资源释放:
av_free/av_unref/av_close → 避免内存泄漏(C++ 中配合智能指针更佳);
- 时间基转换:
av_rescale_q → FFmpeg 时间基是分数,需转换为统一单位(比如毫秒)。
三、实战 1:Linux C++ 音视频解码
这是基础中的基础,先实现从 MP4 文件解码出原始 YUV(视频)和 PCM(音频),代码逐行注释,可直接编译运行。
3.1 完整解码代码(decode_video_audio.cpp)
#include <iostream>
#include <cstdio>
#include <cstring>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
}
void print_ffmpeg_error(int errnum, const char* func_name) {
char errbuf[1024] = {0};
av_strerror(errnum, errbuf, sizeof(errbuf));
std::cerr << "FFmpeg 错误 [" << func_name << "]:" << errbuf
<< " (错误码:" << errnum << ")" << std::endl;
}
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "用法:./decode_video_audio 输入文件.mp4" << std::endl;
return -1;
}
const char* input_file = argv[1];
av_register_all();
avformat_network_init();
AVFormatContext* fmt_ctx = nullptr;
int ret = avformat_open_input(&fmt_ctx, input_file, nullptr, nullptr);
if (ret < 0) {
print_ffmpeg_error(ret, "avformat_open_input");
return -1;
}
ret = avformat_find_stream_info(fmt_ctx, nullptr);
if (ret < 0) {
print_ffmpeg_error(ret, "avformat_find_stream_info");
avformat_close_input(&fmt_ctx);
return -1;
}
int video_stream_idx = -1, audio_stream_idx = -1;
AVCodecParameters* video_codec_par = nullptr;
AVCodecParameters* audio_codec_par = nullptr;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream* stream = fmt_ctx->streams[i];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_idx = i;
video_codec_par = stream->codecpar;
std::cout << "找到视频流,索引:" << i << std::endl;
} else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
audio_stream_idx = i;
audio_codec_par = stream->codecpar;
std::cout << "找到音频流,索引:" << i << std::endl;
}
}
if (video_stream_idx == -1 && audio_stream_idx == -1) {
std::cerr << "未找到音视频流!" << std::endl;
avformat_close_input(&fmt_ctx);
return -1;
}
AVCodecContext* video_dec_ctx = nullptr;
if (video_stream_idx != -1) {
const AVCodec* video_dec = avcodec_find_decoder(video_codec_par->codec_id);
if (!video_dec) {
std::cerr << "找不到视频解码器!" << std::endl;
avformat_close_input(&fmt_ctx);
return -1;
}
video_dec_ctx = avcodec_alloc_context3(video_dec);
if (!video_dec_ctx) {
std::cerr << "分配视频解码器上下文失败!" << std::endl;
avformat_close_input(&fmt_ctx);
return -1;
}
ret = avcodec_parameters_to_context(video_dec_ctx, video_codec_par);
if (ret < 0) {
print_ffmpeg_error(ret, "avcodec_parameters_to_context");
avcodec_free_context(&video_dec_ctx);
avformat_close_input(&fmt_ctx);
return -1;
}
ret = avcodec_open2(video_dec_ctx, video_dec, nullptr);
if (ret < 0) {
print_ffmpeg_error(ret, "avcodec_open2");
avcodec_free_context(&video_dec_ctx);
avformat_close_input(&fmt_ctx);
return -1;
}
}
AVCodecContext* audio_dec_ctx = nullptr;
if (audio_stream_idx != -1) {
const AVCodec* audio_dec = avcodec_find_decoder(audio_codec_par->codec_id);
if (!audio_dec) {
std::cerr << "找不到音频解码器!" << std::endl;
avcodec_free_context(&video_dec_ctx);
avformat_close_input(&fmt_ctx);
return -1;
}
audio_dec_ctx = avcodec_alloc_context3(audio_dec);
ret = avcodec_parameters_to_context(audio_dec_ctx, audio_codec_par);
ret = avcodec_open2(audio_dec_ctx, audio_dec, nullptr);
if (ret < 0) {
print_ffmpeg_error(ret, "avcodec_open2(audio)");
avcodec_free_context(&audio_dec_ctx);
avcodec_free_context(&video_dec_ctx);
avformat_close_input(&fmt_ctx);
return -1;
}
}
AVPacket* pkt = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
if (!pkt || !frame) {
std::cerr << "分配 pkt/frame 失败!" << std::endl;
goto clean_up;
}
FILE* video_out = nullptr;
FILE* audio_out = nullptr;
if (video_stream_idx != -1) {
video_out = fopen("output.yuv", "wb");
if (!video_out) {
std::cerr << "打开 output.yuv 失败!" << std::endl;
goto clean_up;
}
}
if (audio_stream_idx != -1) {
audio_out = fopen("output.pcm", "wb");
if (!audio_out) {
std::cerr << "打开 output.pcm 失败!" << std::endl;
goto clean_up;
}
}
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == video_stream_idx) {
ret = avcodec_send_packet(video_dec_ctx, pkt);
if (ret < 0) {
print_ffmpeg_error(ret, "avcodec_send_packet(video)");
av_packet_unref(pkt);
continue;
}
while (ret >= 0) {
ret = avcodec_receive_frame(video_dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
if (ret < 0) {
print_ffmpeg_error(ret, "avcodec_receive_frame(video)");
goto clean_up;
}
int y_size = frame->width * frame->height;
fwrite(frame->data[0], 1, y_size, video_out);
fwrite(frame->data[1], 1, y_size / 4, video_out);
fwrite(frame->data[2], 1, y_size / 4, video_out);
std::cout << "解码视频帧:pts=" << frame->pts << std::endl;
av_frame_unref(frame);
}
} else if (pkt->stream_index == audio_stream_idx) {
ret = avcodec_send_packet(audio_dec_ctx, pkt);
if (ret < 0) continue;
while (ret >= 0) {
ret = avcodec_receive_frame(audio_dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
int pcm_size = av_samples_get_buffer_size(nullptr, frame->channels,
frame->nb_samples,
(AVSampleFormat)frame->format, 1);
fwrite(frame->data[0], 1, pcm_size, audio_out);
av_frame_unref(frame);
}
}
av_packet_unref(pkt);
}
clean_up:
if (video_out) fclose(video_out);
if (audio_out) fclose(audio_out);
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&video_dec_ctx);
avcodec_free_context(&audio_dec_ctx);
avformat_close_input(&fmt_ctx);
avformat_network_deinit();
std::cout << "解码完成!" << std::endl;
return 0;
}
3.2 编译与运行
g++ -std=c++11 decode_video_audio.cpp -o decode_video_audio \$(pkg-config --cflags --libs libavformat libavcodec libavutil) -lm -lpthread
./decode_video_audio test.mp4
ffplay -pix_fmt yuv420p -s 1920x1080 output.yuv
四、实战 2:Linux C++ 视频推流(RTMP)
以 RTMP 推流为例,实现从本地 YUV 文件推流到服务器。
4.1 搭建本地 RTMP 测试服务器(可选)
如果没有公网地址,可先在 Linux 搭 Nginx-RTMP 服务器:
sudo apt install -y libpcre3 libpcre3-dev libssl-dev
git clone https://github.com/arut/nginx-rtmp-module.git
sudo /usr/local/nginx/sbin/nginx
4.2 完整 RTMP 推流代码(push_rtmp.cpp)
#include <iostream>
#include <cstdio>
#include <cstring>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libavutil/time.h>
}
void print_ffmpeg_error(int errnum, const char* func_name) {
char errbuf[1024] = {0};
av_strerror(errnum, errbuf, sizeof(errbuf));
std::cerr << "FFmpeg 错误 [" << func_name << "]:" << errbuf
<< " (错误码:" << errnum << ")" << std::endl;
}
int read_yuv_frame(AVFrame* frame, FILE* yuv_file, int width, int height) {
int y_size = width * height;
if (fread(frame->data[0], 1, y_size, yuv_file) != y_size) return -1;
if (fread(frame->data[1], 1, y_size / 4, yuv_file) != y_size / 4) return -1;
if (fread(frame->data[2], 1, y_size / 4, yuv_file) != y_size / 4) return -1;
return 0;
}
int main(int argc, char* argv[]) {
if (argc < 4) {
std::cerr << "用法:./push_rtmp YUV 文件 宽度 高度 RTMP 地址" << std::endl;
return -1;
}
const char* yuv_file = argv[1];
int width = atoi(argv[2]);
int height = atoi(argv[3]);
const char* rtmp_url = argv[4];
const int fps = 25;
av_register_all();
avformat_network_init();
AVFormatContext* fmt_ctx = nullptr;
int ret = avformat_alloc_output_context2(&fmt_ctx, nullptr, "flv", rtmp_url);
if (ret < 0) {
print_ffmpeg_error(ret, "avformat_alloc_output_context2");
return -1;
}
AVStream* video_stream = avformat_new_stream(fmt_ctx, nullptr);
if (!video_stream) {
std::cerr << "创建视频流失败!" << std::endl;
avformat_free_context(fmt_ctx);
return -1;
}
AVCodecParameters* codec_par = video_stream->codecpar;
codec_par->codec_id = AV_CODEC_ID_H264;
codec_par->codec_type = AVMEDIA_TYPE_VIDEO;
codec_par->width = width;
codec_par->height = height;
codec_par->format = AV_PIX_FMT_YUV420P;
codec_par->bit_rate = 2000000;
video_stream->time_base = av_make_q(1, fps);
codec_par->framerate = av_make_q(fps, 1);
const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
std::cerr << "找不到 H.264 编码器!" << std::endl;
avformat_free_context(fmt_ctx);
return -1;
}
AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
std::cerr << "分配编码器上下文失败!" << std::endl;
avformat_free_context(fmt_ctx);
return -1;
}
ret = avcodec_parameters_to_context(codec_ctx, codec_par);
if (ret < 0) {
print_ffmpeg_error(ret, "avcodec_parameters_to_context");
avcodec_free_context(&codec_ctx);
avformat_free_context(fmt_ctx);
return -1;
}
codec_ctx->time_base = av_make_q(1, fps);
codec_ctx->gop_size = 10;
codec_ctx->max_b_frames = 1;
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
AVDictionary* opts = nullptr;
av_dict_set(&opts, "preset", "fast", 0);
av_dict_set(&opts, "tune", "zerolatency", 0);
ret = avcodec_open2(codec_ctx, codec, &opts);
if (ret < 0) {
print_ffmpeg_error(ret, "avcodec_open2");
avcodec_free_context(&codec_ctx);
avformat_free_context(fmt_ctx);
return -1;
}
av_dict_free(&opts);
if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&fmt_ctx->pb, rtmp_url, AVIO_FLAG_WRITE);
if (ret < 0) {
print_ffmpeg_error(ret, "avio_open");
avcodec_free_context(&codec_ctx);
avformat_free_context(fmt_ctx);
return -1;
}
}
ret = avformat_write_header(fmt_ctx, nullptr);
if (ret < 0) {
print_ffmpeg_error(ret, "avformat_write_header");
avio_close(fmt_ctx->pb);
avcodec_free_context(&codec_ctx);
avformat_free_context(fmt_ctx);
return -1;
}
AVFrame* frame = av_frame_alloc();
AVPacket* pkt = av_packet_alloc();
FILE* in_file = fopen(yuv_file, "rb");
if (!frame || !pkt || !in_file) {
std::cerr << "分配资源/打开 YUV 文件失败!" << std::endl;
goto clean_up;
}
frame->width = width;
frame->height = height;
frame->format = AV_PIX_FMT_YUV420P;
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
print_ffmpeg_error(ret, "av_frame_get_buffer");
goto clean_up;
}
int frame_index = 0;
int64_t start_time = av_gettime();
while (1) {
if (read_yuv_frame(frame, in_file, width, height) < 0) {
std::cout << "YUV 文件读取完毕!" << std::endl;
break;
}
frame->pts = frame_index++;
av_rescale_q(frame->pts, codec_ctx->time_base, video_stream->time_base);
ret = avcodec_send_frame(codec_ctx, frame);
if (ret < 0) {
print_ffmpeg_error(ret, "avcodec_send_frame");
break;
}
while (ret >= 0) {
ret = avcodec_receive_packet(codec_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
if (ret < 0) {
print_ffmpeg_error(ret, "avcodec_receive_packet");
goto clean_up;
}
av_packet_rescale_ts(pkt, codec_ctx->time_base, video_stream->time_base);
pkt->stream_index = video_stream->index;
ret = av_interleaved_write_frame(fmt_ctx, pkt);
if (ret < 0) {
print_ffmpeg_error(ret, "av_interleaved_write_frame");
av_packet_unref(pkt);
break;
}
av_packet_unref(pkt);
int64_t now_time = av_gettime();
int64_t delay = (frame_index * 1000000 / fps) - (now_time - start_time);
if (delay > 0) av_usleep(delay);
}
av_frame_unref(frame);
}
av_write_trailer(fmt_ctx);
clean_up:
if (in_file) fclose(in_file);
av_frame_free(&frame);
av_packet_free(&pkt);
avcodec_free_context(&codec_ctx);
if (fmt_ctx && !(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
avio_close(fmt_ctx->pb);
}
avformat_free_context(fmt_ctx);
avformat_network_deinit();
std::cout << "推流完成!" << std::endl;
return 0;
}
4.3 编译与运行
g++ -std=c++11 push_rtmp.cpp -o push_rtmp \$(pkg-config --cflags --libs libavformat libavcodec libavutil) -lm -lpthread
./push_rtmp output.yuv 1920 1080 rtmp://127.0.0.1:1935/live/test
ffplay rtmp://127.0.0.1:1935/live/test
五、进阶:解码 + 推流一体化
将解码和推流结合,实现「解码本地 MP4 文件→实时推流到 RTMP 服务器」。核心是把解码后的 AVFrame 直接传给推流的编码器。
5.1 关键修改点
- 数据流转:解码出的
AVFrame 不写入文件,而是直接传递给推流编码器;
- 时间戳同步:解码的
AVFrame PTS 要转换为推流编码器的时间基;
- 音频同步:解码出的 PCM 帧编码为 AAC,和视频一起推流。
5.2 核心代码片段
if (pkt->stream_index == video_stream_idx) {
ret = avcodec_send_packet(video_dec_ctx, pkt);
if (ret < 0) continue;
while (ret >= 0) {
ret = avcodec_receive_frame(video_dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
frame->pts = av_rescale_q(frame->pts, video_dec_ctx->time_base, video_enc_ctx->time_base);
avcodec_send_frame(video_enc_ctx, frame);
av_frame_unref(frame);
}
}
六、避坑指南
6.1 编译报错
undefined reference to avformat_open_input:检查 pkg-config 是否正确,或手动指定库路径 -L/usr/local/ffmpeg/lib -lavformat。
fatal error: libavformat/avformat.h:添加头文件路径 -I/usr/local/ffmpeg/include。
AVCodecContext has no member named codec:新版本(5.x+)移除了 codec 成员,改用 codec_par。
6.2 推流报错
Connection refused:检查 RTMP 服务器是否运行,端口 1935 是否开放。
Invalid argument:检查编码器参数(分辨率、码率)是否合法。
- 推流卡顿/花屏:检查时间戳(PTS/DTS)是否正确,或调整编码器
zerolatency 参数。
6.3 内存泄漏
- 必须调用
av_frame_unref/av_packet_unref 释放帧/包引用;
- 所有
av_alloc/av_new 分配的资源,必须对应 av_free/av_close;
- Linux 下用
valgrind 检测:valgrind --leak-check=full ./push_rtmp ...。
七、总结
- 环境搭建:必须源码编译 FFmpeg,安装开发库和依赖,配置环境变量;
- 核心流程:解码(读包→解码→原始帧)、推流(编码→发包→推流),时间戳同步是关键;
- 资源管理:Linux 下 C++ 开发必须严格释放 FFmpeg 资源,避免内存泄漏;
- 推流优化:设置
zerolatency 低延迟参数,调整时间戳,控制帧率;
- 调试技巧:用
av_strerror 打印错误信息,用 valgrind 检测内存泄漏。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online