FFmpeg6先拉取RTSP流再进行RTMP推流,为什么不用av_usleep限速?

目录

一、整体数据流

二、完整可运行C代码(FFmpeg 6.x)

三、关键 API 与坑位

1️. RTSP 输入选项

2️. 输出必须 "flv"

3️. no_duration_filesize是直播命门

4️. 时间戳处理(比文件推流更麻烦)

5️. 为什么不 av_usleep限速

6️. 音频兼容性

四、运行与验证

五、生产级增强

六、一句话总结


觉得有用,就请您帮忙点赞转发收藏吧,您的鼓励是我创作的动力,多谢看官。

由于能力水平有限,文中的错误或不严谨的地方在所难免,还请批评指正。

FFmpeg 6 拉 RTSP 推 RTMP = TCP 拉实时流 → 不解码 → FLV 封包 → 时间戳归一 → 网络直吐。
1.avformat_open_input+ rtsp_transport=tcp拉摄像头,禁用 UDP 防花屏;
2.avformat_find_stream_info拿音视频流,H264/AAC 直接 avcodec_parameters_copy建输出流;
3.avformat_alloc_output_context2(..., "flv", rtmp_url)强制 FLV(RTMP 线格式);
4.flvflags=no_duration_filesize + rtmp_live=live,禁止写 trailer metadata,保直播不断流;
5.主循环 av_read_frame拿 AVPacket,用 av_packet_rescale_ts把 RTSP 时间戳重定基到 FLV time_base,首帧 DTS 归零消负偏移;
6.av_interleaved_write_frame直接写 RTMP,avio走 TCP,RTSP 本身实时流无需 -re限速;
7.EOF/IO 错时重建 input 或退出,close 前 av_write_trailer。
本质:RTSP 负责“实时拿帧”,RTMP 负责“FLV tag over TCP 持续吐”,API 层只做 流复制 + 时间戳搬家


一、整体数据流

IPC摄像头 └─ RTSP/TCP (avformat_open_input) ↓ av_read_frame (AVPacket, 不解码) AVFormatContext(ic) ↓ av_packet_rescale_ts (input tb → FLV tb) av_interleaved_write_frame ↓ RTMP/FLV (avio_open + write_header) ↓ SRS / nginx-rtmp / MediaMTX

三个铁律

  1. RTSP 输入必须rtsp_transport=tcp,UDP 在弱网必花屏

  2. RTMP 输出avformat_alloc_output_context2(&oc, NULL, "flv", url)第三个参数必须是"flv"

  3. 直播推流flvflags=no_duration_filesize,否则 trailer 回写 duration 断流


二、完整可运行C代码(FFmpeg 6.x)

编译:

g++ rtsp_to_rtmp.cpp -o push \ $(pkg-config --cflags --libs libavformat libavutil)
#include <iostream> extern "C" { #include <libavformat/avformat.h> #include <libavutil/time.h> #include <libavutil/timestamp.h> } static void log_err(int ret) { char buf[AV_ERROR_MAX_STRING_SIZE]; av_strerror(ret, buf, sizeof(buf)); std::cerr << "err: " << buf << "\n"; } int main(int argc, char** argv) { if (argc < 3) { std::cerr << "usage: " << argv[0] << " <rtsp://...> <rtmp://host:1935/live/stream>\n"; return 1; } const char* rtsp_url = argv[1]; const char* rtmp_url = argv[2]; avformat_network_init(); AVFormatContext* ic = nullptr; AVFormatContext* oc = nullptr; AVDictionary* in_opt = nullptr; // ── 1. RTSP 输入(TCP + 超时)── av_dict_set(&in_opt, "rtsp_transport", "tcp", 0); av_dict_set(&in_opt, "stimeout", "5000000", 0); // 5s socket 超时 av_dict_set(&in_opt, "buffer_size", "4096000", 0); av_dict_set(&in_opt, "max_delay", "500000", 0); // 500ms if (avformat_open_input(&ic, rtsp_url, nullptr, &in_opt) < 0) { std::cerr << "open rtsp failed\n"; goto fail; } av_dict_free(&in_opt); if (avformat_find_stream_info(ic, nullptr) < 0) { std::cerr << "find stream info failed\n"; goto fail; } av_dump_format(ic, 0, rtsp_url, 0); // ── 2. RTMP/FLV 输出 ── if (avformat_alloc_output_context2(&oc, nullptr, "flv", rtmp_url) < 0) { std::cerr << "alloc output(flv) failed\n"; goto fail; } // 复制流(不解码) 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) goto fail; 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* out_opt = nullptr; av_dict_set(&out_opt, "flvflags", "no_duration_filesize", 0); av_dict_set(&out_opt, "rtmp_live", "live", 0); if (!(oc->oformat->flags & AVFMT_NOFILE)) { if (avio_open(&oc->pb, rtmp_url, AVIO_FLAG_WRITE) < 0) { std::cerr << "avio_open rtmp failed\n"; goto fail; } } if (avformat_write_header(oc, &out_opt) < 0) { std::cerr << "write_header failed\n"; goto fail; } av_dict_free(&out_opt); // ── 3. 主循环:读 RTSP → 重定基 → 写 RTMP ── AVPacket* pkt = av_packet_alloc(); int64_t start_us = av_gettime(); int64_t base_dts = AV_NOPTS_VALUE; int count = 0; while (true) { int ret = av_read_frame(ic, pkt); if (ret == AVERROR_EOF) { std::cerr << "rtsp eof, retry after 2s...\n"; av_usleep(2000000); // 生产环境这里应重连 ic,示例简化为退出 break; } if (ret < 0) { log_err(ret); av_usleep(500000); continue; } AVStream* in_st = ic->streams[pkt->stream_index]; AVStream* out_st = oc->streams[pkt->stream_index]; // 时间戳重定基(核心) av_packet_rescale_ts(pkt, in_st->time_base, out_st->time_base); // 首帧把基准 DTS 归零,避免 FLV 负时间戳 if (base_dts == AV_NOPTS_VALUE && pkt->dts != AV_NOPTS_VALUE) { base_dts = pkt->dts; } if (pkt->dts != AV_NOPTS_VALUE) pkt->dts -= base_dts; if (pkt->pts != AV_NOPTS_VALUE) pkt->pts -= base_dts; pkt->pos = -1; // RTSP 是实时流,不需要像文件那样 sleep 限速; // 但如果服务器要求严格单调 DTS,可在此做单调校正 ret = av_interleaved_write_frame(oc, pkt); av_packet_unref(pkt); if (ret < 0) { log_err(ret); std::cerr << "write frame failed, abort\n"; break; } if ((count++ & 0x3F) == 0) std::cout << "pushed " << count << " packets\n"; } av_write_trailer(oc); av_packet_free(&pkt); fail: if (oc) { if (!(oc->oformat->flags & AVFMT_NOFILE) && oc->pb) avio_closep(&oc->pb); avformat_free_context(oc); } if (ic) avformat_close_input(&ic); return 0; }

三、关键 API 与坑位

1️. RTSP 输入选项

  • rtsp_transport=tcp:防 UDP 丢包花屏,监控场景唯一选择

  • stimeout=5000000:微秒,socket 读超时,避免av_read_frame永久阻塞

  • max_delay:包排队上限,太大延迟高,太小易报max delay reached

2️. 输出必须"flv"

RTMP 线格式 = FLV tag。avformat_alloc_output_context2第三个参数填"flv",填nullptr让 FFmpeg 从rtmp://猜格式在部分版本会错 。

3️.no_duration_filesize是直播命门

FLV muxer 默认在write_trailer回写onMetaData.duration/filesize。直播没有结尾,SRS/NGINX-RTMP 收到 trailer 直接断开 。

4️. 时间戳处理(比文件推流更麻烦)

  • RTSP 常带负偏移 PTS​ 或DTS 非单调

  • 必须av_packet_rescale_ts到输出time_base

  • 首帧base_dts归零,后续所有包减基准,消除负时间戳

  • FLV 视频轨time_base一般是{1,1000},音频{1,44100},不 rescale 必音画错位

5️. 为什么不av_usleep限速

RTSP 本身就是实时流,av_read_frame阻塞等帧到达;文件推流才需要按 DTS 模拟-re限速。

6️. 音频兼容性

RTSP 摄像头常出G.711/PCM​ 音频,RTMP(FLV) 只认AAC。本例copy模式若源不是 AAC 会推流失败——生产环境需:

  • 视频copy+ 音频单独libvo_aacenc/aac重编码

  • 或 SRS 开aac_sample_rate兼容


四、运行与验证

./push "rtsp://admin:pass@192.168.1.64:554/Streaming/Channels/101" \ "rtmp://127.0.0.1:1935/live/cam1"

另开终端:

ffplay rtmp://127.0.0.1:1935/live/cam1

五、生产级增强

需求

做法

断线重连

avformat_close_input后重open_input保留 oc 重新 write_header​ 或重建 oc

非 AAC 音频

拆音视频,视频 copy,音频 swr+libx264/aac 重编码

H265 over RTMP

FFmpeg 6 native 支持 Enhanced RTMP,SRS 需enhanced_rtmp on

低延迟

fflags=+nobuffer,max_delay调小, SRS 关wait_key

多路摄像头

每路独立 oc + 独立线程,或teemuxer 一路分多推


六、一句话总结

RTSP→RTMP 网关 = TCP 拉流 + FLV 封包 + 时间戳搬家 + 不写 duration,代码 200 行内可跑,难点全在超时/重连/时间戳校正,不在 API 本身。