简介:面向 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-FLV | 1~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.flvffplay能出画面,说明地址、编码、网络都没问题,再往 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还是转码,不再凭经验直接上命令。希望帮到你。
本文还有配套的精品资源,点击获取