☰
SpringBoot+ffmpeg实现RTSP转HTTP-FLV与WebSocket推流实战
2026/10/24 4:23:43 网站建设 项目流程

简介:面向 Java 后端开发者的流媒体播放集成示例,聚焦于将 RTSP、RTMP 等协议的视频流地址转换为前端可直接播放的 FLV 与 WebSocket 形式,解决浏览器无法原生播放监控或直播流的问题。资源共 10 个文件,以 8 个 Java 源码为主体,涵盖服务启动、媒体服务、流转换与客户端类型等核心逻辑,另含 2 个 HTML 页面用于前端播放验证,压缩包约 14KB,体量轻巧、开箱即用。已有 108 人学习。代码围绕 javacv 实现 FLV 转封装与 WebSocket 推送,配合前端页面可快速跑通从流地址接入到页面播放的完整链路,适合需要搭建流媒体转发原型、理解协议转换思路或做二次开发的读者参考,目录结构清晰,便于按模块定位与调试。

1. 摄像头画面进浏览器:一条 RTSP 流要过几道手

手里有一台网络摄像头,或者一台 NVR,输出的是 RTSP 地址,比如rtsp://192.168.1.64:554/Streaming/Channels/101。前端同事要的是能在<video>标签里直接播的地址。这两者之间隔着的不是一行代码,而是一整套协议转换链路。浏览器原生不支持 RTSP,也不支持 RTMP,能吃的只有 HTTP-FLV、HLS、WebRTC 这几种。所以“SpringBoot 推流给前端播 FLV 和 WS”这件事,本质是在服务端做一次协议搬运:把 RTSP 拉下来,转成 FLV 或 WebSocket 承载的裸流,再喂给前端播放器。

这套东西适合谁?做安防可视化、工业设备监控、教学演示系统的 Java 后端。你不需要自己写编解码器,但必须搞清楚 ffmpeg 进程怎么管、流地址怎么拼、前端用什么播放器接。下面按我实际搭过的一套链路拆开讲,代码能直接抄,坑也一并标出来。

2. 链路选型:为什么是 ffmpeg + HTTP-FLV 而不是别的

2.1 三种前端可播方案的取舍

浏览器能播的流,主流就三条路,选错了后面全是返工。

方案延迟服务端复杂度浏览器兼容适用场景
HLS(m3u8)5~30 秒低,切片即可全兼容回放、非实时监控
HTTP-FLV1~3 秒中,需长连接需 flv.js实时预览、低延迟
WebSocket 裸流1~3 秒中高,需自定义封装需自写解析特殊封装、内网定制

安防预览要的是“点开就能看到人”,HLS 那几秒延迟在调云台的时候会让人抓狂,所以主选 HTTP-FLV。WebSocket 那条路我保留,是因为有些内网环境中间设备会掐长连接的 HTTP 流,换成 WS 反而稳,而且前端可以自己控制重连节奏。RTMP 在这里只是 ffmpeg 的输出中间态,浏览器不直接播它,别被标题里的 rtmp 误导——它是链路的一环,不是终点。

选型结论:ffmpeg 负责拉 RTSP 并转封装,SpringBoot 负责起进程、管生命周期、把输出流通过 HTTP-FLV 或 WS 吐给前端,前端用 flv.js 或自写 WS 解析器播。

2.2 ffmpeg 转封装命令的每个参数

核心就一条命令,但每个参数都有讲究:

ffmpeg -rtsp_transport tcp \ -i "rtsp://192.168.1.64:554/Streaming/Channels/101" \ -c:v copy -c:a aac \ -f flv \ -flvflags no_duration_filesize \ -listen 1 \ http://127.0.0.1:8888/live/cam01.flv

逐段说:

  • -rtsp_transport tcp:强制 RTSP 走 TCP。UDP 在丢包网络下会花屏,安防场景几乎都该加这个,代价是延迟略增。
  • -i:输入地址。摄像头地址里的通道号因厂商而异,别照抄,先拿 VLC 验证能出画面再往下走。
  • -c:v copy:视频不重新编码,直接复制。这是低延迟和低 CPU 的关键,前提是摄像头输出 H.264。如果是 H.265,浏览器侧 flv.js 解不了,必须改成-c:v libx264转码,CPU 会明显上去。
  • -c:a aac:音频转 AAC。很多摄像头是 G.711,FLV 容器不认,必须转。
  • -f flv:输出封装格式。
  • -flvflags no_duration_filesize:直播流没有总时长和文件大小,不加这个 ffmpeg 会等元数据,表现为前端一直转圈。
  • -listen 1:让 ffmpeg 自己起一个 HTTP 服务监听。这是最省事的做法,SpringBoot 只负责拉起进程,前端直接连这个端口。

提示:-listen 1模式下 ffmpeg 是服务端,同一路流只能被一个客户端消费。多路并发预览要换成推给一个流媒体服务再分发,或者每路单独起进程绑不同端口。

2.3 SpringBoot 里怎么管这个进程

不能让 ffmpeg 进程裸奔,得有启动、健康检查、回收。我一般封装成一个管理器:

@Component public class StreamProcessManager { // 用流地址做 key,避免同一路重复起进程 private final Map<String, Process> running = new ConcurrentHashMap<>(); public String start(String cameraId, String rtspUrl, int port) throws IOException { if (running.containsKey(cameraId)) { return "http://127.0.0.1:" + port + "/live/" + cameraId + ".flv"; } List<String> cmd = List.of( "ffmpeg", "-rtsp_transport", "tcp", "-i", rtspUrl, "-c:v", "copy", "-c:a", "aac", "-f", "flv", "-flvflags", "no_duration_filesize", "-listen", "1", "http://127.0.0.1:" + port + "/live/" + cameraId + ".flv" ); ProcessBuilder pb = new ProcessBuilder(cmd); pb.redirectErrorStream(true); // 日志落到文件,排错全靠它 pb.redirectOutput(ProcessBuilder.Redirect.appendTo( new File("/var/log/stream/" + cameraId + ".log"))); Process p = pb.start(); running.put(cameraId, p); return "http://127.0.0.1:" + port + "/live/" + cameraId + ".flv"; } public void stop(String cameraId) { Process p = running.remove(cameraId); if (p != null) { p.destroy(); // 先温和终止 try { if (!p.waitFor(3, TimeUnit.SECONDS)) { p.destroyForcibly(); // 3 秒不退就强杀 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } }

逻辑说明:running这个 Map 是防重复启动的第一道闸,前端反复点预览不会堆出一堆 ffmpeg。redirectErrorStream(true)把 stderr 并进 stdout,因为 ffmpeg 的报错全在 stderr,不并的话日志文件是空的,排错时就是黑匣子。destroy()之后必须waitFor加超时兜底,ffmpeg 有时卡在关闭 socket 上不退出,不强杀会残留进程占端口。

参数上,port建议按摄像头分配固定端口段,比如 20000 起递增,别用随机端口,否则前端拿到的地址和实际监听对不上。cameraId用业务主键,别用 IP,IP 会变。

2.4 前端 flv.js 接入与 WS 兜底

HTTP-FLV 这条路前端就三行核心代码:

import flvjs from 'flv.js'; if (flvjs.isSupported()) { const player = flvjs.createPlayer({ type: 'flv', isLive: true, url: 'http://your-host:20001/live/cam01.flv' }, { enableStashBuffer: false, // 直播关掉缓存,降延迟 stashInitialSize: 128 }); player.attachMediaElement(document.getElementById('video')); player.load(); player.play(); }

isLive: true和enableStashBuffer: false是低延迟的关键,不关缓存会越播越滞后。stashInitialSize调小能进一步压延迟,但网络抖动时会更容易卡,内网可以设 128,公网建议留默认。

WebSocket 兜底那条路,服务端用 Spring 的WebSocketHandler把 ffmpeg 输出(改成-f mpegts pipe:1)读出来转发:

public class FlvWsHandler extends BinaryWebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { Process p = new ProcessBuilder( "ffmpeg", "-rtsp_transport", "tcp", "-i", rtspUrl, "-c:v", "copy", "-c:a", "aac", "-f", "mpegts", "pipe:1" ).start(); // 独立线程读 stdout 推给 session,避免阻塞 WS 线程 new Thread(() -> { try (InputStream in = p.getInputStream()) { byte[] buf = new byte[8192]; int n; while ((n = in.read(buf)) > 0) { session.sendMessage(new BinaryMessage(buf, 0, n, true)); } } catch (IOException ignored) { } finally { p.destroy(); } }).start(); } }

这里输出的是 MPEG-TS 而不是 FLV,因为 WS 传的是裸字节,前端用 mpegts.js 解析比自写 FLV 解析省事。注意sendMessage必须放在独立线程,WS 容器线程被阻塞会拖垮整个连接池。

3. 把链路跑起来:从单路验证到多路并发

3.1 先别写代码,用命令行把流打通

血泪经验:Java 代码调不通的时候,九成是 ffmpeg 命令本身有问题。所以第一步永远是命令行验证。

# 第一步:确认 RTSP 能拉下来 ffplay -rtsp_transport tcp "rtsp://192.168.1.64:554/Streaming/Channels/101" # 第二步:确认能转成 FLV 并本地播放 ffmpeg -rtsp_transport tcp -i "rtsp://..." -c:v copy -c:a aac \ -f flv -flvflags no_duration_filesize test.flv # 另开终端 ffplay test.flv

ffplay能出画面,说明地址、编码、网络都没问题,再往 SpringBoot 里搬。如果ffplay就黑屏,先查摄像头是否开了多播、鉴权是否要带用户名密码(格式是rtsp://user:pass@ip:554/...),别急着怀疑代码。

3.2 多路并发的端口与资源规划

单路跑通后,多路是另一个问题。每路一个 ffmpeg 进程,-c:v copy时 CPU 占用很低,但内存和文件描述符会累积。我的做法是:

  • 端口按20000 + 摄像头序号分配,写进配置表,启动前用ServerSocket探测端口是否被占。
  • 进程数设上限,比如 20 路,超出的排队或拒绝,防止一台机器被拖死。
  • 每路日志单独文件,按天切割,否则一个卡死的进程能把磁盘写满。
// 启动前探测端口,避免 ffmpeg 起不来还不报错 private boolean portFree(int port) { try (ServerSocket s = new ServerSocket(port)) { return true; } catch (IOException e) { return false; } }

这个探测很土但有效。ffmpeg 在-listen 1模式下端口被占会直接退出,而ProcessBuilder不会抛异常,你只能从日志里看到Address already in use。提前探测能把错误挡在启动阶段。

3.3 健康检查与自动拉起

ffmpeg 进程会因为网络抖动、摄像头重启而退出,前端表现就是画面突然卡住。需要一个定时任务扫进程状态:

@Scheduled(fixedDelay = 10000) public void healthCheck() { running.forEach((cameraId, p) -> { if (!p.isAlive()) { running.remove(cameraId); // 记录退出码,非 0 说明是异常退出,触发重拉 log.warn("stream {} exited with {}", cameraId, p.exitValue()); // 这里调 start 重新拉起,注意加重试次数上限 } }); }

p.isAlive()是唯一可靠的存活判断,别去 ping 端口,端口通不代表流还在出数据。退出码要记下来,1通常是参数或网络错误,255常见于输入地址不可达,对着日志能快速定位。重拉要设上限,比如连续失败 5 次就停,否则摄像头断电时你会看到无限重启的日志刷屏。

4. 避坑清单:五个让我返工过的问题

4.1 前端一直转圈,日志里没有报错

现象:flv.js 的load()调了,video元素一直缓冲,ffmpeg 日志正常输出帧。

原因:漏了-flvflags no_duration_filesize。直播流没有总时长,ffmpeg 默认会等元数据写全,FLV 头里的 duration 字段一直是 0,flv.js 判定流未就绪。

解决:加上这个参数,并且确认isLive: true。两个条件缺一不可。

4.2 画面花屏、马赛克

现象:能播,但每隔几秒出现绿色块或马赛克。

原因:RTSP 走了 UDP,丢包后解码器拿到残缺帧。或者-c:v copy但摄像头实际输出 H.265,flv.js 按 H.264 解,必然花。

解决:先加-rtsp_transport tcp。还花就用ffprobe查编码:

ffprobe -v error -select_streams v:0 -show_entries stream=codec_name \ -of default=nw=1 "rtsp://..."

输出hevc就说明是 H.265,要么前端换支持 H.265 的播放器,要么服务端加-c:v libx264转码,后者 CPU 会翻几倍,机器要提前评估。

4.3 进程杀了端口还占着

现象:stop()调了,重新start()报端口占用。

原因:destroy()只是发信号,ffmpeg 关闭监听 socket 有延迟,紧接着重启会撞上 TIME_WAIT。

解决:waitFor超时后强杀,并且在start前用 3.2 的portFree探测,探测失败就等 500ms 重试,最多三次。别用固定sleep,机器负载不同延迟不一样。

4.4 多路预览时浏览器卡死

现象:开 4 路以上,浏览器标签页内存飙升,播放卡顿。

原因:每路 flv.js 实例都在解码,浏览器软解 4 路 1080p 扛不住。这不是服务端问题。

解决:前端做按需加载,非焦点画面暂停或降码率;服务端可以提供一路低分辨率子码流地址(多数摄像头支持Streaming/Channels/102这种子码流通道),预览用子码流,放大再看主码流。

4.5 WS 模式下连接几秒就断

现象:WebSocket 连上后几秒被服务端关闭,前端报 1006。

原因:Spring 的 WS 容器有默认空闲超时,或者sendMessage在非 WS 线程调用导致状态异常。

解决:配置ServletServerContainerFactoryBean把maxSessionIdleTimeout调大,并且确保所有sendMessage都在同一个发送线程串行执行,别多线程并发写同一个 session。

5. 进阶:用 ffmpeg 的-re与切片做可控回放

实时预览跑通后,很多场景会追加一个需求:能不能回看最近几分钟。这时候不要另起一套架构,直接在现有链路上加参数就行。

录制用分段切片,避免单文件无限增长:

ffmpeg -rtsp_transport tcp -i "rtsp://..." \ -c:v copy -c:a aac \ -f segment -segment_time 60 -segment_format mp4 \ -reset_timestamps 1 \ /data/record/cam01_%Y%m%d%H%M%S.mp4

-segment_time 60每分钟一个文件,-reset_timestamps 1让每个切片时间戳从零开始,否则播放器拖进度条会错乱。回放时前端按文件名列表拉取,用普通<video>播 mp4 即可,不需要 flv.js。

另一个技巧是-re。如果你要把本地文件当直播源推给前端测试,不加-re会瞬间推完,前端只看到一帧。加上它 ffmpeg 会按原始帧率读文件:

ffmpeg -re -i demo.mp4 -c:v copy -f flv -listen 1 http://127.0.0.1:20001/live/test.flv

这个参数在联调阶段特别有用,不用真摄像头也能验证整条链路。

验证链路是否健康,我习惯看三个指标:ffmpeg 进程的 CPU 占用(copy 模式应在个位数)、日志里有没有Non-monotonous DTS警告(有就说明时间戳乱了,回放会跳帧)、前端video.currentTime是否持续增长(停了就是流断了)。这三个对上了,基本不用再猜。

从那以后我每次接新的摄像头型号,都强制先用ffprobe把编码、分辨率、音频格式打出来,再决定-c:v copy还是转码,不再凭经验直接上命令。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询