FFmpeg6对本地文件进行RTMP推流
目录
一、RTMP 推流的「三层本质」
二、完整可运行C代码(FFmpeg6)
三、关键 API 逐行讲解(高分博客该讲的坑)
1️.为什么用 avformat_alloc_output_context2(..., "flv", ...)
2️.flvflags=no_duration_filesize是直播命门
3️.av_packet_rescale_ts不是可选
4️.为什么自己 av_usleep
5️.av_interleaved_write_framevs av_write_frame
四、运行与验证
五、生产级增强
六、一句话总结
觉得有用,就请您帮忙点赞转发收藏吧,您的鼓励是我创作的动力,多谢看官。
由于能力水平有限,文中的错误或不严谨的地方在所难免,还请批评指正。
FFmpeg 6 本地文件 RTMP 推流 = 解封装 → 复制流参数 → FLV 封装 → 按时间戳限速发送,不重编码时就是“文件读帧、时间戳搬家、网络吐包”的死循环。
1.avformat_open_input + avformat_find_stream_info打开本地文件,只解封装不解码;
2.avformat_alloc_output_context2(..., "flv", rtmp_url)强制 FLV 封装(RTMP 线格式);
3.为每个输入流 avformat_new_stream并 avcodec_parameters_copy,保持 H264/AAC 原样;
4.av_dict_set("flvflags","no_duration_filesize")+ avformat_write_header,禁止写 duration/filesize(直播必开);
5.循环 av_read_frame拿 AVPacket,用 av_packet_rescale_ts把 DTS/PTS 从输入 time_base 转成 FLV time_base;
6.按 pkt->dts用 av_gettime()+ av_usleep做 -re限速,防止瞬间发爆服务器;
7.av_interleaved_write_frame写 RTMP,avio底层走 TCP 推流;
8.EOF 后 av_write_trailer关流,释放 context。
一、RTMP 推流的「三层本质」
RTMP 本身不是容器,线格式是FLV tag stream,所以 FFmpeg 里永远是:
URL: rtmp://... └── protocol : rtmpproto (native) └── muxer : flv ← 必须显式指定三个铁律:
avformat_alloc_output_context2(&oc, NULL, "flv", url)—— 第三个参数必须是"flv"直播推流必须带
flvflags=no_duration_filesize,否则写 trailer 时刷 duration/filesize 导致服务器断流输入是文件时必须按时间戳限速(自己 sleep),否则 1 秒发完 10 分钟视频
二、完整可运行C代码(FFmpeg6)
编译(Linux/macOS):
g++ push_rtmp.cpp -o push_rtmp \ $(pkg-config --cflags --libs libavformat libavcodec libavutil)Windows(MSVC/MinGW)链接avformat avcodec avutil swresample swscale。
// push_rtmp.cpp #include <iostream> extern "C" { #include <libavformat/avformat.h> #include <libavutil/time.h> #include <libavutil/timestamp.h> } static int open_input(const char* file, AVFormatContext** ic) { if (avformat_open_input(ic, file, nullptr, nullptr) < 0) { std::cerr << "avformat_open_input failed: " << file << "\n"; return -1; } if (avformat_find_stream_info(*ic, nullptr) < 0) { std::cerr << "avformat_find_stream_info failed\n"; return -1; } av_dump_format(*ic, 0, file, 0); return 0; } static int setup_output(const char* rtmp_url, AVFormatContext* ic, AVFormat64Context** oc_out, AVDictionary** write_opts) { AVFormatContext* oc = nullptr; // RTMP 必须指定 flv 封装 if (avformat_alloc_output_context2(&oc, nullptr, "flv", rtmp_url) < 0) { std::cerr << "alloc output context failed\n"; return -1; } // 复制输入流到输出(stream copy,不解码) for (unsigned i = 0; i < ic->nb_streams; i++) { AVStream* in_st = ic->streams[i]; AVStream* out_st = avformat_new_stream(oc, nullptr); if (!out_st) return -1; avcodec_parameters_copy(out_st->codecpar, in_st->codecpar); out_st->codecpar->codec_tag = 0; out_st->time_base = in_st->time_base; } av_dump_format(oc, 0, rtmp_url, 1); // 直播关键参数 AVDictionary* opt = nullptr; av_dict_set(&opt, "flvflags", "no_duration_filesize", 0); av_dict_set(&opt, "rtmp_live", "live", 0); // 纯直播语义 av_dict_set(&opt, "buffer_size", "1024000", 0); if (!(oc->oformat->flags & AVFMT_NOFILE)) { if (avio_open(&oc->pb, rtmp_url, AVIO_FLAG_WRITE) < 0) { std::cerr << "avio_open failed: " << rtmp_url << "\n"; return -1; } } if (avformat_write_header(oc, &opt) < 0) { std::cerr << "write header failed\n"; return -1; } av_dict_free(&opt); *oc_out = oc; return 0; } int main(int argc, char** argv) { if (argc < 3) { std::cerr << "usage: " << argv[0] << " <input.mp4> <rtmp://host:1935/live/stream>\n"; return 1; } const char* in_file = argv[1]; const char* rtmp_url = argv[2]; avformat_network_init(); AVFormatContext* ic = nullptr; AVFormatContext* oc = nullptr; if (open_input(in_file, &ic) < 0) goto fail; if (setup_output(rtmp_url, ic, &oc, nullptr) < 0) goto fail; AVPacket* pkt = av_packet_alloc(); int64_t start_time = av_gettime(); int frame_cnt = 0; while (true) { int ret = av_read_frame(ic, pkt); if (ret == AVERROR_EOF) break; if (ret < 0) { std::cerr << "read_frame error\n"; break; } AVStream* in_st = ic->streams[pkt->stream_index]; AVStream* out_st = oc->streams[pkt->stream_index]; // 时间戳重定基(输入 time_base → 输出 time_base) av_packet_rescale_ts(pkt, in_st->time_base, out_st->time_base); // 按真实时间戳限速(文件推流核心) if (pkt->dts != AV_NOPTS_VALUE) { AVRational tb = out_st->time_base; int64_t pts_us = av_rescale_q(pkt->dts, tb, {1, AV_TIME_BASE}); int64_t now_us = av_gettime() - start_time; if (pts_us > now_us) av_usleep(pts_us - now_us); } pkt->pos = -1; ret = av_interleaved_write_frame(oc, pkt); if (ret < 0) { char err[AV_ERROR_MAX_STRING_SIZE]; av_strerror(ret, err, sizeof(err)); std::cerr << "write_frame failed: " << err << "\n"; av_packet_unref(pkt); break; } av_packet_unref(pkt); if ((frame_cnt++ & 0x1F) == 0) std::cout << "pushed packets: " << frame_cnt << "\n"; } av_packet_free(&pkt); av_write_trailer(oc); fail: if (oc) { if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb); avformat_free_context(oc); } if (ic) avformat_close_input(&ic); return 0; }三、关键 API 逐行讲解(高分博客该讲的坑)
1️.为什么用avformat_alloc_output_context2(..., "flv", ...)
RTMP URL 本身不带封装信息,FFmpeg 不能反推用 FLV,必须强制指定。
不写这个 →Failed to find a suitable output format。
2️.flvflags=no_duration_filesize是直播命门
FLV muxer 默认在write_trailer回写duration和filesize到 header。
直播流没有“文件尾”,服务器收到 trailer 直接断。
设了这个 flag → 不回写,流可以一直活。
3️.av_packet_rescale_ts不是可选
输入可能是{1,1000}(ms),输出 FLV 视频轨通常是{1,1000}但音频{1,44100}。
不 rescale → 播放端音画快进/倒放。
4️.为什么自己av_usleep
命令行ffmpeg -re做的事就是按pkt->dts节流。
API 层没人帮你做,不 sleep → 瞬间把文件发完,RTMP 服务器当垃圾包丢弃。
5️.av_interleaved_write_framevsav_write_frame
interleaved:muxer 内部做 DTS 排序,多音视频轨必用单轨 copy 用
av_write_frame也行,但 interleaved 更安全
四、运行与验证
# 假设本地有 test.mp4,SRS / nginx-rtmp 在 1935 ./push_rtmp test.mp4 rtmp://127.0.0.1:1935/live/room1拉流验证:
ffplay rtmp://127.0.0.1:1935/live/room1五、生产级增强
需求 | 做法 |
|---|---|
断线重连 |
|
循环推流 |
|
H265 over RTMP | FFmpeg 6 native 支持 Enhanced RTMP,但 SRS 需开 |
动态码率 | 必须走 encode 流程(libx264),不能 stream copy |
多路输出 |
|
日志回调 |
|
六、一句话总结
RTMP 推流 =
flvmuxer +no_duration_filesize+ 时间戳 rescale + 按 DTS 限速,剩下就是av_read_frame → rescale → interleaved_write_frame的死循环。