简介:一套面向期末大作业、课程设计与毕业设计场景的流媒体推流示例代码,使用ffmpeg与Qt框架,将OpenCV采集到的cv::Mat帧数据推送至RTSP、RTMP、UDP及UDP广播视频流,主要解决视频采集后到流媒体输出之间的数据转换与推流链路搭建问题。项目按功能拆分为视频采集、推流封装与协议对接等模块,核心组件可独立提取重新复用,既适合课程设计快速出成果,也能作为后期视频监控、直播推流等功能开发的基础。压缩包共12个文件,包括5个cpp源文件、4个h头文件、1个vcxproj工程文件及1个filters配置文件,另附README说明文档,用于了解推流接口与编译配置,整体仅16KB,轻量易读。目前已有467人学习下载,源码经过严格测试,可直接放心用于项目开发。通过研读源码还可掌握cv::Mat帧数据与ffmpeg封装输出之间的衔接细节,以及RTSP、RTMP、UDP等推送通道的初始化与参数差异,便于根据业务场景灵活选用。
1. 把 OpenCV 的帧变成 RTSP、RTMP、UDP 视频流:这个任务到底卡在哪
期末大作业或课设里,最常见的需求是“摄像头检测到目标,同时把画面推出去给别人看”。OpenCV 负责读帧、做检测、画框都很顺手,但把处理完的帧变成真正的视频流,很多人会卡在最后这一步——OpenCV 的 VideoWriter 不支持 rtsp、rtmp 这类协议,文件名写成rtsp://地址要么直接报错,要么生成一个 0 字节文件。这个标题给的就是一条被反复验证的路:用 ffmpeg 把 OpenCV 拿到的原始帧编码成 H.264,再封装推送到 rtsp、rtmp、udp 或 udp 广播出口。适合的场景包括本机多路画面的远程查看、局域网内多台设备接收同一路检测画面,以及课设里必须演示“实时传输”这个环节的项目。读完这篇,你能得到一套最小可跑的推流代码,以及绕开编码格式、管道、延迟这几个大坑的落地经验。
2. 先看清楚 OpenCV 帧和 ffmpeg 流的差异,再决定走哪条推流路线
2.1 为什么 OpenCV 的每一帧不能直接塞给推流端
OpenCV 的 Mat 默认是 BGR 排列、每像素 3 字节(或 4 字节带 Alpha),放在内存里是一块连续的 uint8 数组。ffmpeg 的编码器要的是 AVFrame,内部像素格式通常是 yuv420p,且存在 stride 对齐问题;就算你强行按 RGB 喂,编码器也不认识这种裸排列。常见的做法是在 ffmpeg 命令里声明输入格式为 rawvideo 和 bgr24,让它内部做一次像素格式转换;如果你走 libav API,就要手动调用 sws_scale 做转换。课设里最容易踩的第一个坑就是拿 VideoWriter 去写 .mp4 时正常,一旦把文件名换成 rtmp:// 地址就翻车——这不是 OpenCV 实现问题,是 OpenCV 的 FFMPEG 后端对输出协议支持有限,而且它不接受“把处理后的 Mat 直接推流”这种用法。
另一个容易被忽略的点是帧率。OpenCV 的cap.read()是阻塞式的,USB 摄像头实际出帧速率可能只有 15fps,而你的推流命令里写-r 25,ffmpeg 会按 25fps 去消费输入。结果就是输入帧不够时,ffmpeg 会重复最后一帧或者等待新数据,导致画面看起来忽快忽慢。很多人以为是网络问题,其实问题出在输入节奏和编码参数没对齐。理解这一点,后面的最小代码里为什么要显式传 fps、为什么帧率要按实际采集速率来设,就好解释了。
2.2 RTSP、RTMP、UDP、UDP 广播:四种出口各自的适用场景
在动手写代码前,先想清楚你要推到哪里。这四种协议不是可以随便替换的,选错了后期要返工。
| 出口 | 工作模式 | 典型场景 | 延迟表现 | 对服务端的要求 |
|---|---|---|---|---|
| RTSP | 拉流,客户端主动来取 | 摄像头生态、VLC/播放器直接打开,海康/萤石取流地址就是 rtsp:// | 较低 | 需要 RTSP 服务端或支持 RTSP 的设备 |
| RTMP | 推流,客户端从流地址读 | 直播平台、SRS、nginx-rtmp | 较高 | 需要 RTMP 服务端 |
| UDP | 一对一无连接传输 | 局域网实时传输、低延迟演示 | 最低 | 不需要服务端,但客户端要先知道地址 |
| UDP 广播 | 一对多无连接传输 | 局域网内所有设备都能收到 | 最低 | 不需要服务端,靠广播地址/组播地址 |
具体到课设和项目开发,我的建议是:如果只是本机演示,UDP 或 UDP 广播最省事,因为不需要搭任何服务端;如果要做完整的直播链路演示,RTMP 加 SRS 或 nginx-rtmp 最像“真实项目”;如果目标是兼容播放器、手机扫码看流,RTSP 更合适。协议选型这件事,不要在代码写了一半的时候才考虑,因为 ffmpeg 的输出命令会跟着变。
2.3 两条落地路线:管道命令法 vs libav API
实现“OpenCV 帧 → ffmpeg 推流”有两条路线。第一条是管道法:OpenCV 把每一帧的裸字节写到标准输出(stdout),ffmpeg 从标准输入(stdin)读,编码和封装全靠命令行参数控制。这是最简洁的写法,Python 里用subprocess.Popen就能搭起来,调试时可以直接看到 ffmpeg 打出来的日志。第二条是 libav API 法:在 C++ 或 Python 里直接链接 ffmpeg 的库,手动创建 AVFormatContext、AVCodecContext,把 OpenCV 的 Mat 转成 AVFrame 再喂给编码器。这种方案控制力最强,能精确管理缓冲和延迟,但代码量会膨胀到几百行,而且 ffmpeg 的 API 不同版本差异很大,课设周期内容易卡在编译和接口变更上。
我一般会先走管道法跑通整条链路,确认编码参数和协议没问题后,再判断是否需要重构成 API 方案。管道法听起来“土”,但它把 OpenCV 和 ffmpeg 解耦了:你可以先用命令行验证 ffmpeg 这一半,再用脚本验证 OpenCV 那一半,出了问题一眼就能看到在哪。很多号称“用 ffmpeg 库开发”的课设,实际上底层也是先调命令再包一层,先跑通再优化是完全合理的工程顺序。
3. 跑通最小推流链路:OpenCV 写管道,ffmpeg 做编码和封装
3.1 用匿名管道把 BGR 帧喂给 ffmpeg:Python 最小代码
下面这段代码是整条链路最核心的部分。它不做任何图像处理,只是把摄像头读到的每一帧原始数据原样推给 ffmpeg,由 ffmpeg 编码成 H.264 后封装成 TS 流发到本机的 UDP 端口。你可以把它当作一个“帧搬运工”,后续的检测、画框、OCR 都在frame上做完再write出去。
import cv2 import subprocess as sp width, height, fps = 1280, 720, 25 out_address = "udp://127.0.0.1:1234" # ffmpeg 从标准输入读取原始帧,编码并推流 cmd = [ "ffmpeg", "-y", "-f", "rawvideo", # 输入是裸视频数据 "-vcodec", "rawvideo", # 输入不经过解码 "-s", f"{width}x{height}", # 每帧尺寸必须和 Mat 完全一致 "-pix_fmt", "bgr24", # OpenCV 默认像素格式 "-r", str(fps), # 输入帧率 "-i", "-", # 从 stdin 读取 "-c:v", "libx264", # 用 H.264 编码 "-preset", "ultrafast", # 编码速度优先 "-tune", "zerolatency", # 低延迟调优 "-f", "mpegts", # 封装成 TS 流 out_address, ] # stdout 和 stderr 分开:stdin 写帧,stderr 看 ffmpeg 日志 proc = sp.Popen(cmd, stdin=sp.PIPE, stderr=sp.PIPE) cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break # tobytes() 把 Mat 连续内存块直接转成 bytes 写入管道 proc.stdin.write(frame.tobytes()) proc.stdin.close() cap.release() proc.wait()这段代码的关键逻辑在frame.tobytes():OpenCV 的 Mat 在内存里是连续排列的 BGR 数据,转成 bytes 后直接写进管道,不需要任何中间拷贝。subprocess.Popen的stdin=sp.PIPE让 Python 进程和 ffmpeg 进程之间建立了一条匿名管道,stderr=sp.PIPE是为了后续能读到 ffmpeg 的报错信息。这里特别提醒:千万不能把 stderr 也重定向到 PIPE 却不读,管道缓冲区写满后 ffmpeg 会阻塞,你的主循环也会跟着卡死。这段代码可以直接跑,但要注意VideoCapture(0)的默认分辨率不一定是 1280x720,最好先cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)设置一下。
3.2 ffmpeg 命令参数逐段拆解:rawvideo、bgr24、-r、-i - 在说什么
上面命令里有一组参数非常容易写错,逐个说清楚它们的含义。-f rawvideo告诉 ffmpeg 输入格式是裸视频,没有容器封装,也没有起始码;-vcodec rawvideo表示输入的编码格式也是裸的,不需要解码器介入。-s 1280x720必须和frame.shape完全一致,如果你读到的帧是 640x480 而这里写 1280x720,ffmpeg 会直接报错或产出花屏。-pix_fmt bgr24对应 Mat 的默认内存排列,漏掉这个参数,ffmpeg 默认按 yuv420p 解析输入,整条流都会是花的。-r 25是输入帧率,它告诉 ffmpeg 每秒消费多少帧;如果实际帧率没有 25,ffmpeg 会等数据,推出去的流会变慢。
输出侧这几个参数同样是高频坑点。-c:v libx264选编码器,-preset ultrafast牺牲压缩率换编码速度,-tune zerolatency禁用 B 帧、减少编码器内部缓冲,这两个参数对延迟敏感的场景是必须的。如果去掉-tune zerolatency,编码器可能会为了压缩率缓存多帧,在 RTMP 和 RTSP 里表现为 1 到 3 秒的额外延迟。-f mpegts指定封装格式,UDP 推送一般用 TS 流,因为 TS 流丢包后还能从下一个关键帧恢复,比裸 H.264 的 ES 流可靠得多。最后是out_address,udp://127.0.0.1:1234会把流发到本机 1234 端口,可以用 VLC 或 ffplay 拉这个地址验证。
3.3 从 UDP 到 UDP 广播:多播地址与 TTL 参数
UDP 单播只能让一个客户端收流,课设里如果想让实验室所有电脑都能看到画面,就要用 UDP 广播或多播。这里我强烈建议用多播而不是传统广播:多播地址范围是224.0.0.0到239.255.255.255,路由器只需把报文转发给订阅了该组播组的端口,不会像广播那样把数据塞给整个子网的所有设备。改法非常简单,把输出地址换成组播地址并加上 TTL 参数:
ffmpeg -y -f rawvideo -vcodec rawvideo -s 1280x720 -pix_fmt bgr24 -r 25 -i - \ -c:v libx264 -preset ultrafast -tune zerolatency \ -f mpegts "udp://239.255.0.1:1234?ttl=2"对应 Python 里只需改out_address = "udp://239.255.0.1:1234?ttl=2"。ttl=2表示报文最多经过 2 跳路由器,在单个局域网内足够了;如果跨网段演示,需要调大 TTL 并在路由器上开启 IGMP 协议。多播接收端不需要知道推流端 IP,只要加入同一个组播组就能收流,这个特性在做“多设备同时观看一路画面”的课设演示时非常讨喜。接收端用ffplay udp://239.255.0.1:1234即可,注意接收端也要在同一网段且网络设备允许组播报文。
这里有一个容易忽略的点:如果你的交换机开启了 IGMP Snooping 但没有配置组播组,接收端可能收不到流。课设的普通交换机默认一般不启用 IGMP Snooping,但保险起见,可以先在推流端用ffplay本机回拉测试,再用另一台机器拉流,分步排除网络设备的问题。
4. 推 RTMP 和 RTSP:服务端选型与确切命令
4.1 本地起一个 RTMP 服务端:SRS 与 nginx-rtmp 的取舍
RTMP 和 UDP 最大的不同是:RTMP 必须有服务端接收推流,再向播放端分发。常见的选择有两个,一个是 SRS(Simple Realtime Server),另一个是 nginx-rtmp 模块。SRS 的优点是配置简单,官方提供 Docker 镜像,一条命令就能把服务端跑起来,还自带 HTTP 接口和延迟统计,特别适合课设演示里展示“推流端 → 服务端 → 播放端”这条完整链路。nginx-rtmp 需要自编译 nginx 或者用第三方编译好的包,配置相对繁琐,但如果你本来就在用 nginx 做 Web 服务,可以少开一个进程。我一般推荐 SRS,理由很实际:SRS 的日志里能看到每个流的连接状态和比特率,调试推流问题时比 nginx-rtmp 直观得多。
启动 SRS 的常见做法是拉取官方 Docker 镜像后映射 1935 端口(RTMP 默认端口)和 1985 端口(HTTP API):
docker run -d -p 1935:1935 -p 1985:1985 --name srs ossrs/srs跑起来后,推流地址就是rtmp://127.0.0.1:1935/live/stream1,播放地址相同。如果不想依赖 Docker,也可以直接下载 SRS 的 Release 压缩包运行./objs/srs -c conf/srs.conf,默认配置里已经开启了 RTMP。切记本机测试时推流地址可以用 127.0.0.1,但局域网内其他设备要播放时,必须把地址里的 IP 换成运行 SRS 那台机器的局域网 IP。很多课设翻车就翻在这里:推流端写的是 localhost,播放端在另一台机器上当然连不上。
4.2 推 RTSP 的两种姿势:直接推服务端,还是经过 RTMP 中转
RTSP 的推流链路稍微绕一点。ffmpeg 本身不带 RTSP 服务端,它只能作为 RTSP 客户端把编码后的流推给一个 RTSP 服务端,常见的服务端是 mediamtx(原名 rtsp-simple-server)这类单二进制程序。启动服务端后,ffmpeg 的推流命令长这样:
ffmpeg -f rawvideo -vcodec rawvideo -s 1280x720 -pix_fmt bgr24 -r 25 -i - \ -c:v libx264 -preset ultrafast -tune zerolatency \ -rtsp_transport tcp \ -f rtsp rtsp://127.0.0.1:8554/live注意-rtsp_transport tcp这个参数。RTSP 默认走 UDP 传输 RTP 包,但在 Wi-Fi 或跨路由器场景下 UDP 丢包会导致画面花屏,显式指定 TCP 后可靠性会好很多,代价是延迟略有增加。教室里的无线网络经常出现“收流断断续续”的问题,先检查这条参数有没有加。另一种姿势是让 RTMP 服务端强势介入:先把流推到 SRS,再用 ffmpeg 从 RTMP 地址拉流转推给 RTSP 服务端。这种叠路由的做法在项目开发里很常见,因为团队里有人只熟悉 RTMP,有人只认 RTSP 播放器,中间用 ffmpeg 做一次转码中转,两边都能满足,代价是延迟增加约 1 秒。
4.3 帧率、码率、GOP 与延迟:决定画面质量的关键参数
推 RTMP 和 RTSP 时,UDP 那套“只要能动就行”的参数是不够的。直播场景要同时控制延迟和画质,这组参数每次都要调。-b:v控制平均码率,720p 用-b:v 2000k比较稳妥;-maxrate和-bufsize限制瞬时码率波动,防止 Wi-Fi 环境下码率尖峰导致丢包;-g 25表示关键帧间隔为 25 帧(也就是 GOP 大小为 1 秒)。关键帧间隔对延迟影响极大:播放端必须等到关键帧才能开始解码,GOP 越大,客户端开播等待越久。SRS 播放端出现卡顿或起播慢时,优先把-g调小,而不是只盯着-preset。
| 参数 | 作用 | 建议值 | 影响 |
|---|---|---|---|
-b:v | 平均码率 | 720p 用 2000k | 码率低则模糊,高则占用带宽 |
-maxrate | 峰值码率 | 平均码率 1.5 倍 | 限制瞬时报文尖峰 |
-bufsize | 编码器缓冲 | 平均码率 2 倍 | 配合 maxrate 控制码率波动 |
-g | GOP 大小(关键帧间隔) | 25 或 50 | 越小起播越快,压缩率越低 |
-preset | 编码速度与压缩率权衡 | ultrafast 或 veryfast | 越快 CPU 占用越低,文件越大 |
-tune zerolatency | 禁用 B 帧等延迟因素 | 固定开启 | 显著降低端到端延迟 |
这套参数不是拍脑袋定的,而是结合“推流到 SRS 存在延迟”这类问题排查出来的经验值。如果你发现延迟来自编码器缓存而不是网络,检查-tune和-g;如果发现画面模糊,先加-b:v;如果发现 CPU 被打满,说明-preset ultrafast还不够,可以考虑降低分辨率。把定位思路写成“参数 → 现象 → 调整方向”,排查效率会比盲目试参数高很多。
5. 避坑:OpenCV 推流到 ffmpeg 最容易翻车的 5 个位置
5.1 现象:ffplay 拉流提示Invalid data found when processing input
原因:ffmpeg 启动时检查输入流的尺寸、像素格式与命令行声明不一致。最常见的情况是摄像头实际输出 640x480,代码里却按 1280x720 写了-s参数;或者 OpenCV 读到的 Mat 是 4 通道(带 Alpha)而-pix_fmt写的是 bgr24。 解决:先在代码里打印frame.shape,确认宽高和通道数,再同步修改-s和-pix_fmt。如果 Mat 是 BGRA,把-pix_fmt改成bgra。
5.2 现象:画面卡顿、声音正常但视频像幻灯片
原因:输入帧率和输出帧率错配。cap.read()实际返回 15fps,但 ffmpeg 被要求以 25fps 消费输入,导致编码器不断重复旧帧。另一个常见原因是proc.stdin.write()写入了慢速网络队列,主循环被阻塞。 解决:实测采集帧率后再填-r参数,或者干脆不传-r,让 ffmpeg 按数据到达的自然节奏处理。图像处理的耗时也要计入帧率计算,检测算法跑 50ms 一帧,实际帧率就是 20fps,不要按摄像头的标称帧率来。
5.3 现象:UDP 推流本机能看,局域网另一台机器花屏或黑屏
原因:UDP 没有拥塞控制和重传,Wi-Fi 环境下丢包严重。TS 流丢包会导致播放端解码器失去同步,直到下一个关键帧到来才能恢复。 解决:把 GOP 调小,比如-g 25,让关键帧频繁出现;有余力时把-b:v降低到 1500k 左右,减少带宽压力;播放端加-fflags nobuffer -flags low_delay参数降低缓存延迟。如果条件允许,优先走 RTSP over TCP 而不是裸 UDP。
5.4 现象:程序启动后立刻卡死,CPU 占用却很低
原因:stderr=sp.PIPE后从不读取,ffmpeg 输出日志写满管道缓冲区后阻塞。管道读端和写端是同步的,任何一方不消费数据,另一方就会堵住。Python 主循环往 stdin 写帧,而 ffmpeg 的 stderr 没人读,两边互相等待,形成死锁。 解决:最简单的方式是把 stderr 重定向到文件:
log_file = open("ffmpeg.log", "wb") proc = sp.Popen(cmd, stdin=sp.PIPE, stderr=log_file)调试时也可以直接把 stderr 继承到终端,这样能实时看到日志,代价是界面上会不断刷 ffmpeg 输出。
5.5 现象:推流端看着一切正常,SRS 播放端延迟越来越大
原因:延迟堆积分为编码器缓存和播放器缓冲两段。编码器侧-tune zerolatency没有生效,或者 GOP 设置过大;播放器侧缓冲积累问题,同时弱网环境下 TCP 重传会加剧延迟。 解决:先确认推流命令里明确带-tune zerolatency -g 25,再把播放端的-fflags nobuffer加上。如果延迟还是持续增长,检查 SRS 的日志看推流端的实际码率是否稳定;码率频繁波动通常意味着编码器在等待输入帧,回到第 2 条检查帧率匹配问题。
6. 把流拉回来验证一遍:ffplay、ffprobe 和 tee 多路复用技巧
6.1 用 ffplay 和 ffprobe 验证推出去的流确实活着
推流代码写完,第一件事永远是拉流验证,而不是急着接播放器。本机回拉是最快的冒烟测试:
ffplay -fflags nobuffer -flags low_delay -i udp://127.0.0.1:1234如果画面正常,再用 ffprobe 检查流的编码信息是否和预期一致:
ffprobe -v error -show_entries stream=codec_name,width,height,avg_frame_rate -of csv=p=0 udp://127.0.0.1:1234正常输出应该类似h264,1280,720,25/1。这类验证命令写进课设文档里,比截图更有说服力,因为它是可复现的。
6.2 用 tee muxer 把同一路流同时推给 RTMP 和 UDP
课设演示时最常见的尴尬是:评委的电脑只能访问 RTMP 地址,而你自己的拉流端只接了 UDP。ffmpeg 的 tee muxer 可以一份编码输出多个封装目的地,避免开两个推流进程抢摄像头:
ffmpeg -f rawvideo -vcodec rawvideo -s 1280x720 -pix_fmt bgr24 -r 25 -i - \ -c:v libx264 -preset ultrafast -tune zerolatency -g 25 \ -f tee "[f=flv]rtmp://127.0.0.1:1935/live/stream|[f=mpegts]udp://239.255.0.1:1234"tee后面的方括号内是每个输出的具体格式和地址,编码只做一次,CPU 占用比两个 ffmpeg 进程叠加低很多。我在课设里吃过大亏:当时为了同时演示 RTMP 和本地录制,开了三个进程抢同一个摄像头,结果三个流全部卡死。换成 tee 之后单进程搞定,摄像头独占不会冲突。这个教训让我养成一个习惯:任何推流方案先想清楚最终要面向几个输出端,再用 tee 合并,而不是盲目增加进程。
希望这篇把链路、参数和坑位都摊开的笔记,能帮你的课设少走一段弯路。从管道法跑通到按需换成 API 方案,这个顺序经得起反复验证,你按着走就行。
本文还有配套的精品资源,点击获取