简介:这份源码是一个以Qt与FFmpeg为基础实现的推流软件,面向具备C++开发经验、希望深入音视频领域的读者。工程完整呈现了推流核心链路:初始化FFmpeg组件、选择编码器、设定输出协议与目标地址、采集本地音视频、完成编码压缩,再发送至流媒体服务器,同时包含异常处理和状态监控设计,可作研发实时推流工具的基础框架。压缩包内共1150个文件,大小约40.76MB;其中头文件1032个,便于从接口层理解模块关系,另外还有C++源文件、工程配置文件、动态库与导入库、静态库文件、界面定义文件与Qt资源文件,基本做到解压后即可在Windows环境中编译运行。当前已有64人学习下载,特别适合学习音视频同步、延迟控制以及网络传输细节的开发者,具备较强的工程参考价值。
1. 一台设备要把画面推到服务器,Qt + FFmpeg 做推流端是最省事的组合
很多现场都会遇到这个需求:工控机上的摄像头画面,或者一块屏幕上的操作过程,要实时送到服务器,让远端的人打开网页或播放器就能看到。设备上不能装 OBS,也不能用带界面的重型软件,最可靠的做法就是自己写一个推流程序。这类程序业内习惯叫 ffmpegPusher:用 Qt 把控制面板、推流地址、启动停止按钮搭起来,把 FFmpeg 当作推流引擎,负责读取画面、编码、封装成 FLV 再推到 RTMP 服务器。整个软件解决的就是一件事:在没有人工干预的环境里,稳定地把一路画面推到指定地址。它适合直播推流、远程设备调试、多路画面感知这类场景,也是嵌入式板卡上做音视频上传的常用方案。
2. 工程骨架先立住:Qt 版本怎么选,FFmpeg 库怎么才能被程序找到
写推流软件最忌讳一上来就写写包循环。我一般先花半小时把工程搭稳,确认一条命令行能把视频推到服务器,再进入 Qt 代码阶段。这一章先把 Qt 和 FFmpeg 的关系理清楚,再把工程接入方式、依赖库列表讲透。
2.1 为什么是“Qt 做面板、FFmpeg 做引擎”,而不是用 QtMultimedia 一把梭
Qt 自带的多媒体模块能做采集和播放,但推流这件事它并不擅长。RTMP 封装、H.264 编码参数控制、断线重连、时间基换算,这些能力 FFmpeg 体系里已经沉淀了十几年。用 QtMultimedia 去拼这些逻辑,等于把 FFmpeg 已经解决过的问题重新发明一遍。常见做法是:界面层完全交给 Qt,把所有推流逻辑封装在一个后台线程里,通过信号槽把进度和错误传回界面。FFmpeg 不感知 Qt 存在,它只管拿到一帧数据,编码,写进网络。两边各干各的,接口清晰,后期不管是把推流协议从 RTMP 换成 SRT,还是把编码从软编换成硬编,界面层都不用动。
2.2 把 FFmpeg 头文件和库接进 Qt 工程:pro 文件到底要写什么
如果你是 windows 环境,常见做法是去 FFmpeg 官网下载 essentials 构建包,解压后得到 include、lib、bin 三个目录;然后把 bin 目录加进 PATH,把 include 和 lib 路径写进 Qt 的 pro 文件。注意 32 位和 64 位要跟你的 Qt 套件一致,这个不一致会在程序运行时才暴露,后面避坑章节会专门讲。
# ffmpegPusher.pro 关键片段 QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = ffmpegPusher TEMPLATE = app CONFIG += console c++11 CONFIG -= app_bundle # FFmpeg 头文件路径,按你自己的解压目录改 INCLUDEPATH += D:/libs/ffmpeg/include LIBS += -LD:/libs/ffmpeg/lib \ -lavformat \ -lavcodec \ -lavutil \ -lswscale \ -lswresample这段配置里,CONFIG += console很有用,它保证程序启动时把 FFmpeg 自己的日志输出到终端,出问题时能直接看到是哪一步报错。swscale和swresample在纯转封装场景下用不到,但如果后面要做画面缩放或者音频重采样,就得提前链进来。还有一个细节:如果是 MinGW 套件,FFmpeg 的库文件要选libxxx.dll.a那种导入库;如果是 MSVC,则要选.lib文件。两者混用会出现链接错误,而且报错信息很有迷惑性。
工程能编译通过算第一步,接下来要确认程序运行时能不能找到 FFmpeg 的 DLL。Windows 上最常见的报错是“找不到 avformat-59.dll”。我一般把 FFmpeg 的 bin 目录里所有 DLL 直接拷到 exe 同级目录,然后用 windeployqt 部署 Qt 的运行库。在这之前,先手动把 bin 目录加到系统 PATH,也方便后面直接跑命令行验证。
2.3 先别写代码,用一条命令把本机和服务器之间的通路验证掉
推流软件的调试难点在于:如果程序推不上去,你不知道是代码问题、网络问题,还是服务器问题。所以我在动手写推流循环之前,一定会先具备两个条件:一个可用的 RTMP 服务器,一条能推通的 ffmpeg 命令行。服务器最简单的方式是用 Docker 跑一个 SRS,推流和拉流都能测。
# 拉取 SRS 镜像并启动,默认监听 1935 端口 docker run --rm -p 1935:1935 -p 8085:8080 ossrs/srs:4 ./objs/srs -c conf/rtmp.confSRS 启动后,用 ffmpeg 推一条测试视频流过去,再用 ffplay 拉流确认通了。这一步跑通,说明网络、服务器、端口都没问题,后面写的代码就只需要聚焦在 FFmpeg 接口调用上。
# 把本地 flv 文件以原始编码方式推流,不加转码,用来验证链路 ffmpeg -re -i test.flv -c copy -f flv rtmp://127.0.0.1/live/test # 另一个终端执行,看到画面说明服务器和端口都正常 ffplay rtmp://127.0.0.1/live/test-re参数的意思是按文件原始帧率读取,模拟实时推流;-c copy不做重编码,速度最快,只验证封装和网络链路。如果 test.flv 还没有,先用 FFmpeg 生成一个测试视频源:ffmpeg -f lavfi -i testsrc=size=1280x720:rate=25 -t 10 test.flv。这一步的目的很纯粹——把所有可能出问题的外部因素先排除掉,后面写推流代码的时候,出问题就只在代码里找原因。
3. 推流主链路手写一遍:从输出上下文到写包收尾,接口逐个过
链路验证通了,下面进入核心。这一章我会把推流主流程按顺序拆成四段代码:创建输出上下文、给输出流设置参数、写封装头、循环写包,最后收尾释放。这套流程无论是读一个本地 FLV 转而推流,还是接收编码器输出的 AVPacket 再推送,骨架都一样。我把每一段代码的关键参数和常见取舍写清楚。
3.1 创建输出上下文:avformat_alloc_output_context2 和 avio_open
第一步是拿到一个 AVFormatContext。这个结构体代表“你要输出成什么格式”,指定 flv 就是封装成 FLV,指定 mpegts 就是封装成 TS。推 RTMP 地址时,格式可以显式传 flv,也可以让它从 URL 后缀推断,但显式传更稳。
// 创建推流上下文,输出格式指定为 flv AVFormatContext *fmt_ctx = nullptr; const char *rtmp_url = "rtmp://127.0.0.1/live/test"; int ret = avformat_alloc_output_context2(&fmt_ctx, nullptr, "flv", rtmp_url); if (ret < 0 || !fmt_ctx) { // 用 av_strerror 把负数错误码转成可读信息 char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0}; av_strerror(ret, errbuf, sizeof(errbuf)); qCritical() << "创建输出上下文失败:" << errbuf; return; } // 打开网络输出通道 ret = avio_open(&fmt_ctx->pb, rtmp_url, AVIO_FLAG_WRITE); if (ret < 0) { char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0}; av_strerror(ret, errbuf, sizeof(errbuf)); qCritical() << "打开 RTMP 输出失败:" << errbuf; return; }avformat_alloc_output_context2 的第三个参数传 "flv",第四个参数传 rtmp 地址,这个组合在 FFmpeg 内部会找到对应的 RTMP 协议处理器和 FLV 封装器。avio_open 打开的是网络 IO,RTMP 握手在这个阶段发生。如果这一步失败,大概率是地址写错、端口没监听,或者服务器回包异常。错误码是负数,比如 -5 表示 I/O 错误,-1094995529 这种看着像乱码的值其实是 AVERROR 系列,用 av_strerror 转一下就知道具体原因了。
3.2 给输出流设置参数:codecpar、time_base、gop_size 一个都不能省
输出上下文创建完,要往里添加一条或多条流。最常见的推流软件只有视频流,但直播场景通常需要音频,所以代码里用两个 avformat_new_stream 分别建视频和音频流。这里的关键是把 AVCodecParameters 填对,FFmpeg 新版里不再直接操作 AVStream 的 codec 字段,全部改为通过 codecpar 传递参数。
// 如果是从文件转封装,直接复制输入流参数是最稳的写法 AVStream *in_stream = ifmt_ctx->streams[video_stream_index]; AVStream *out_stream = avformat_new_stream(fmt_ctx, nullptr); avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar); out_stream->time_base = in_stream->time_base; // 如果是自己编码后的流,需要手动逐项设置 out_stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; out_stream->codecpar->codec_id = AV_CODEC_ID_H264; out_stream->codecpar->width = 1280; out_stream->codecpar->height = 720; out_stream->codecpar->format = AV_PIX_FMT_YUV420P; out_stream->codecpar->bit_rate = 2500000; out_stream->time_base = (AVRational){1, 90000};video_stream_index 是从输入上下文里遍历出来的,遍历时判断in_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO。转封装场景直接用 avcodec_parameters_copy,这样 SPS、PPS、音频采样率这些信息全部原样带过去,最不容易出错。手动设置参数时,time_base 这里写成 1/90000 是遵循 H.264 的 90k 时间基惯例,避免 dts/pts 精度不够导致跳帧。width 和 height 必须是偶数,FFmpeg 对奇数分辨率会报错,这是一个隐藏很深的坑。
如果推出去的流要兼容大多数播放器,视频流还需要在写头之前把 extradata 准备好。H.264 的 SPS/PPS 有两种携带方式:一种是放在 avcC 容器里,即写在 extradata 中;另一种是放在每个关键帧 NALU 前面。RTMP/FLV 要求的是前者。如果你用的是转封装方式,extradata 已经通过 avcodec_parameters_copy 复制过去了;如果是编码器刚编码完的流,需要在编码器初始化时设置AV_CODEC_FLAG_GLOBAL_HEADER使编码器把 SPS/PPS 放进 extradata,否则写头时 FLV 封装器找不到参数集,流就推不出去。
3.3 写封装头与写包循环:pts/dts 换算和 av_interleaved_write_frame
所有流参数设置完成,调用 avformat_write_header 写入 FLV 头,然后进入推流循环。推流循环的核心是拿 AVPacket,调整时间基,交给 av_interleaved_write_frame 写出去。这个过程对新手来说最容易写错的是时间基换算。
// 写入 FLV 封装头 ret = avformat_write_header(fmt_ctx, nullptr); if (ret < 0) { char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0}; av_strerror(ret, errbuf, sizeof(errbuf)); qCritical() << "写入封装头失败:" << errbuf; avio_closep(&fmt_ctx->pb); return; } // 推流循环:从输入读包,或者从编码器收包 AVPacket pkt; while (running) { // 以转封装为例,从输入文件循环读包 ret = av_read_frame(ifmt_ctx, &pkt); if (ret < 0) break; AVStream *in_st = ifmt_ctx->streams[pkt.stream_index]; AVStream *out_st = fmt_ctx->streams[pkt.stream_index]; // 时间基换算:把输入流的时间戳转换为输出流的时间基 av_packet_rescale_ts(&pkt, in_st->time_base, out_st->time_base); pkt.pos = -1; // av_interleaved_write_frame 内部会排序,防止 dts 乱序 ret = av_interleaved_write_frame(fmt_ctx, &pkt); av_packet_unref(&pkt); if (ret < 0) { char errbuf[AV_ERROR_MAX_STRING_SIZE] = {0}; av_strerror(ret, errbuf, sizeof(errbuf)); qCritical() << "推流写包失败:" << errbuf; break; } } // 推流结束,写 trailer 并释放网络连接 av_write_trailer(fmt_ctx); if (fmt_ctx && fmt_ctx->pb) { avio_closep(&fmt_ctx->pb); } avformat_free_context(fmt_ctx);av_packet_rescale_ts 会同时对 pts、dts、duration 做等比换算。这里有个细节:pts 和 dts 如果是 AV_NOPTS_VALUE,这个函数会自动跳过,所以不用额外判断。pkt.pos = -1表示这是流媒体数据,不是文件数据,不设置也行,但设置后不会往 FLV 里写入多余的偏移信息。
av_interleaved_write_frame 和 av_write_frame 的区别值得单独说明。av_write_frame 是直接把包交给封装器,不保证包的顺序;av_interleaved_write_frame 会维护内部缓冲,把 dts 乱序的包排好再写。从编码器出来的包可能因为 B 帧导致 dts 不单调递增,所以优先用 av_interleaved_write_frame。代价是它会拷贝缓冲、增加一点延迟,但对推流稳定性来说完全值得。
3.4 收尾阶段最容易忽略的细节:trailer 写失败也要清理
av_write_trailer 之前必须先确保所有包都写完了。有时候推流到一半网络断了,trailer 写不进去,这是正常的。但无论如何,avio_closep、avformat_free_context 必须执行,否则内存和 socket 句柄都会泄漏。推流软件常常是长时间运行的程序,泄漏一次两次看不出来,挂机一天必然崩。收尾代码要放在 RAII 封装里,或者用一个 finally 逻辑包起来,确保任何路径都能走到。
很多推流软件死在“退出时崩溃”而不是“推流中崩溃”,就是因为退出逻辑里没有处理部分初始化的情况。比如 avio_open 失败后 fmt_ctx 已经分配了,但 pb 是空的,这时候直接 avio_closep(&fmt_ctx->pb) 会触发空指针。所以收尾时先判断 fmt_ctx 和 fmt_ctx->pb 是否有效,再逐个释放,这个习惯建议从第一天就养成。
4. 数据从哪来:三种采集编码接法,以及让延迟可控的关键参数
推流管道的源头在数据。FFmpegPusher 这类软件的输入可以是本地文件、摄像头采集画面、或者另一个程序送进来的 H.264 裸流。不同来源决定了代码里要不要挂编码器,也决定了 CPU 占用和延迟表现。这一章先讲清楚数据源的几种接法,再给出我常用的编码参数组合。
4.1 三种数据接入方式:转封装、软编、硬编怎么选
第一种是转封装,输入已经是编码后的流,比如本地 FLV 文件、RTSP 摄像头的 H.264 流,代码里只做解封装和重新封装,不做像素级处理。这种方式 CPU 占用极低,延迟也最低,适合“把一路已有的流搬到 RTMP 服务器”的场景,缺陷是你没办法改分辨率、加 logo、调码率。
第二种是软编,拿到的是摄像头原始画面 YUV 或 RGB,用 FFmpeg 内置的 x264 编码器压缩。这种方式最灵活,可以任意控制分辨率、码率、帧率、GOP,但 CPU 占用高——720p 25 帧编码大概要占满一个中端 CPU 核心。第三种是硬编,调用 Intel QSV、NVIDIA NVENC 或树莓派上的硬件编码器。硬编延迟低、省电,坑也最多,参数兼容性参差不齐,但工控机或嵌入式设备要跑长时间推流,硬编几乎是必选项。
4.2 编码器参数里真正影响延迟和画质的四个设置:gop、preset、tune、threads
很多人配置编码器只设置 bit_rate 和 width、height,结果推出来的流延迟两三秒,播放端看着卡。这里的问题不在网络,在编码器参数。整理一张我自己常用的参数表:
| 参数 | 推荐值 | 作用 |
|---|---|---|
| gop_size | 25 到 50 | 关键帧间隔,决定播放端能多快切入画面 |
| preset | veryfast | 编码速度与压缩率权衡,推流优先速度 |
| tune | zerolatency | 关闭编码器内部缓冲,显著降低延迟 |
| threads | auto | 自动根据 CPU 核心数分配线程 |
// 编码器上下文初始化片段 AVCodecContext *enc_ctx = avcodec_alloc_context3(codec); enc_ctx->bit_rate = 2500000; enc_ctx->width = 1280; enc_ctx->height = 720; enc_ctx->time_base = (AVRational){1, 25}; enc_ctx->framerate = (AVRational){25, 1}; enc_ctx->gop_size = 50; enc_ctx->max_b_frames = 0; // 推流场景建议设为 0 // x264 特有参数通过 priv_data 传入 av_opt_set(enc_ctx->priv_data, "preset", "veryfast", 0); av_opt_set(enc_ctx->priv_data, "tune", "zerolatency", 0); av_opt_set(enc_ctx->priv_data, "threads", "auto", 0);max_b_frames 设 0 很关键。B 帧能提升压缩率,但会引入解码顺序重排,增加端到端延迟,而且很多低端播放器对 B 帧支持不好。OBS 这类直播软件默认不开 B 帧,原因就在这里。gop_size 设 50 表示每 2 秒一个关键帧(25 fps 下),这个间隔对普通直播足够了。如果调试时需要播放端快速打开画面,可以临时把 gop_size 调到 1,确认流畅后再调回来。
4.3 推流线程该怎么排:写包循环不能占用 UI 线程,缓冲队列要限长
Qt 程序里最忌讳把 av_interleaved_write_frame 放在按钮的点击槽里直接执行。网络抖动一次,这个函数就可能阻塞几百毫秒,界面就会卡住,用户会以为程序崩溃了。常见做法是开一个专门的工作线程负责推流,UI 线程只负责把任务和参数传递进去。线程之间用 Qt 的信号槽通信,或者用一个简单的线程安全队列。
我一般会在推流线程内部再做一层缓冲:一个 std::queue 存放待推送的 AVPacket,队列长度上限设 30。达到上限后,丢弃队尾最老的包,优先保留关键帧。如果关键帧都满了,那就丢掉旧关键帧之后的所有包,保证新数据能进来。这套策略不完美,但能在网络抖动时保证延迟不无限增大。丢包的逻辑要谨慎:只丢非关键帧,不能把刚拿到的关键帧丢掉,否则播放端画面会一直卡在花屏状态。
5. 推流避坑记录:库不匹配、连不上服务器、花屏跳帧都在这
做推流软件最花时间的不是写代码,是排错。下面这几条是我自己在开发 FFmpegPusher 这类程序时反复踩过的坑,每一条都按照现象、原因、解决来写。遇到同样问题可以直接对照排查。
5.1 fatal: cannot mix incompatible Qt library (version ex50601) with this librar
现象:程序编译通过,运行瞬间弹窗报错,提示 Qt 库版本不兼容,或者干脆在 stderr 里打出一段fatal: cannot mix incompatible Qt library (version ex50601) with this library。
原因:Qt 的库有两种编译套件,MinGW 和 MSVC。程序链接时用的头文件是一套版本,运行时加载的 DLL 是另一套版本。最常见的场景是之前装过另一个 Qt 版本,它的 bin 目录残留在 PATH 里排在了前面,程序启动时加载了错误的 Qt 库。版本号里的 ex50601 这种写法表示某个构建的特定版本标记,问题本质是多个 Qt 环境互相污染。
解决:编译前先跑qmake -v,确认当前用的 qmake 路径和开发工具包一致;运行时用Dependency Walker类的工具看程序实际加载的 Qt DLL 出自哪个目录。最直接的办法是编译完程序后,把 PATH 里所有 Qt 相关的旧路径删掉,只保留当前套件的 bin 目录,然后重新运行。我自己的习惯是:项目 pro 文件里写死所有依赖库的绝对路径,绝不靠 PATH 碰运气。
5.2 avformat_write_header 返回 0,服务器也连上了,但播放端就是没画面
现象:日志里 write_header 成功,推流端没有任何报错,用 ffplay 拉流转圈,画面一直出不来。服务器管理页能看到连接,但查不到流信息。
原因:H.264 的 SPS/PPS 没有正确传给封装器。FLV 封装要求视频流第一个包必须是关键帧,而且关键帧的 NALU 里要带 SPS/PPS,或者通过 extradata 在 FLV 头里声明。如果是自己调编码器产生的流,没有设置AV_CODEC_FLAG_GLOBAL_HEADER,编码器会把 SPS/PPS 放在每个关键帧前面,FLV 封装器从 extradata 里拿不到参数,就不会往 FLV 头里写 avcC 配置。播放端拿到流之后,等不到 SPS/PPS,解码器就一直停在初始化状态。
解决:在编码器初始化时给 enc_ctx->flags 加上AV_CODEC_FLAG_GLOBAL_HEADER,然后调用 avcodec_open2 后检查 enc_ctx->extradata 是否为非空。拿到 extradata 后,通过 avcodec_parameters_from_context 把参数同步到输出流。这样 FLV 头里就会带 avcC 数据,播放端连上就能立即解码。
5.3 推流画面卡顿,服务器日志提示 keyframe too far apart 或者 dts 异常
现象:程序能推流,但播放端时不时卡一下,服务器日志报关键帧间隔过长,或者报 dts 乱序。
原因:时间基换算没写好。输入流和输出流的 time_base 不一致时,pts/dts 换算错误,服务器收到的关键帧位置错乱。另一种情况是 gop_size 设得太大,比如设了 250,25fps 下就是 10 秒才一个关键帧,网络一丢包播放端就得等下一个关键帧才能恢复画面。
解决:用 av_rescale_q 或 av_packet_rescale_ts 统一把每个包的时间戳换算到输出流的时间基。关键帧间隔控制在 2 秒以内,25fps 对应 gop_size 50。改完之后用一条命令检查实际推流结果:ffprobe -v trace -show_frames -select_streams v rtmp://127.0.0.1/live/test,看输出里是不是每隔约 50 帧出现一个key_frame=1。
5.4 嵌入式板上运行报 qt.qpa.plugin: could not find the qt platform plugin "linuxfb"
现象:程序部署到树莓派或者 ARM 工控板,运行时报错qt.qpa.plugin: could not find the qt platform plugin "linuxfb",界面起不来。
原因:Qt 的程序运行依赖平台插件。开发机上插件在 Qt 安装目录的 plugins/platforms 下,交叉编译部署到板子时,这个目录没跟着拷过去,或者环境变量没有指定插件路径。linuxfb 是 Linux 下无窗口系统时的平台插件,嵌入式推流设备经常要依赖它。
解决:把platforms/libqlinuxfb.so拷贝到程序运行目录的 platforms 文件夹下,启动程序前设置 QT_QPA_PLATFORM_PLUGIN_PATH 指向该目录。如果板子上有 X11 或 Wayland,也不建议直接用 linuxfb,先确认系统里真正的窗口系统是什么。还有一个经常犯的错:插件架构是 ARM 的,开发机上拷贝过去的还是 x86 编译版本,运行时会报 CRC 或者加载失败。
5.5 推流几小时后 CPU 占用逐渐升高,最后程序被系统杀掉
现象:程序刚启动时 CPU 正常,挂机两三个小时后 CPU 占用越来越高,内存也持续增长,最后系统 OOM 把进程杀掉。
原因:AVPacket 或 AVFrame 没有正确释放。每次 av_read_frame 返回的 packet 都要用 av_packet_unref 释放,否则 refcount 一直占用内存。另一个常见泄漏点是编码器那里,av_frame_alloc 申请的视频帧没有及时 av_frame_unref。程序崩溃时 FFmpeg 内部线程可能还在跑,不显式清理也会导致资源回收不干净。
解决:这就是“FFmpeg 是 C 库,内存全靠自己管”的典型体现。我习惯对每个 AVPacket 的释放封装成一句av_packet_unref(&pkt),并且保证它在每次循环迭代末尾执行,不管中途是否 return。推流线程退出前,按顺序 avcodec_close 关闭编码器、avformat_free_context 释放上下文。想验证是否有泄漏,可以在推流线程里加一个计数,每 1000 个包打印一次当前内存占用,观察是否线性增长。
6. 用 ffprobe 量化推流质量,再用 SRT 给软件留一条逃生通道
程序能推流是起点,能证明它推得“对”才是另一回事。我见过太多开发者只靠 ffplay 里画面能动就宣布完事,结果分辨率、码率、关键帧间隔全是乱的。ffplay 能播不代表流是对的。正确的验证方式是用 ffprobe 拉流抓取流信息,量化检查几个硬指标。建立一个自己的验证清单,养成推流后固定跑一遍的习惯,能省掉后续大量联调时间。
# 查看推流的基本信息:编码格式、分辨率、码率、音频参数 ffprobe -v error -show_streams -show_format rtmp://127.0.0.1/live/test # 下载一段推流数据落盘,再对文件做帧级检查 ffmpeg -i rtmp://127.0.0.1/live/test -c copy -t 10 dump.flv ffprobe -v error -select_streams v -show_frames -show_entries frame=key_frame,pict_type,pts_time dump.flv第二行命令的输出里,重点看关键帧间隔。每个pict_type=I的帧之间的时间差应该在 2 秒左右。如果间隔忽长忽短,说明推流端的 GOP 控制有问题。同时看 pts_time 是否单调递增,如果出现回跳,说明时间基换算逻辑还存在边界情况。
验证通过后,还有一个方向值得提前布局:RTMP 在公网上被运营商限速很常见,弱网环境下延迟和卡顿往往不可控。SRT 协议专为公网传输设计,抗丢包能力比 RTMP 好很多。设计软件架构时,不要把 RTMP 写死在核心链路里,而是把“协议名 + 地址”当作字符串传入底层。这样后面改 SRT 只需要在创建输出上下文时把格式名换成 "mpegts"、URL 前缀换成 "srt://",代码骨架完全不用动。
我当时就是因为把 RTMP 硬编码在了推流循环里,后来接 SRT 时被迫重构了半个模块,这种教训一次就够。从那以后,任何推流软件我都把协议层和业务层分开,哪怕前期只做 RTMP,也预留好抽象层。如果这篇文章能帮你省掉这部分重构成本,那这个方向就值得继续往下做。希望帮到你。
本文还有配套的精品资源,点击获取