简介:本资源面向Java后端与流媒体开发初学者,提供一套基于Spring Boot将RTSP、RTMP等流地址推送给前端播放的完整示例,前端可选用FLV或WebSocket方式接收,解决浏览器无法直接播放传统流协议的问题。压缩包共10个文件,包含8个Java源码与2个HTML页面,整体约14KB,Java文件承担服务启动、媒体服务、流转换与客户端类型定义等核心逻辑,HTML页面则用于演示FLV与WebSocket两种播放方式,结构紧凑、开箱即用。目前已有107人学习下载,适合作为流媒体转发入门的参考模板。读者可从中了解Spring Boot集成媒体服务的整体思路,掌握RTSP/RTMP转FLV及WebSocket推送的关键实现,并借助现成页面快速验证播放效果,为二次开发或项目集成提供可复用的基础代码。
1. 摄像头画面怎么进浏览器:从 RTSP 到 FLV 与 WS 的完整链路
手里有一台网络摄像头或者一台 NVR,它吐出来的是 RTSP 流,地址形如rtsp://user:pass@ip:554/stream。可前端同事只会写<video>标签,浏览器原生既不认 RTSP,也不认 RTMP。这个标题要解决的就是这段落差:后端用 Spring Boot 把 RTSP/RTMP 这类流地址接进来,转封装成浏览器能直接播的 HTTP-FLV,同时把裸流通过 WebSocket 推给需要自己解码的场景。它适合做安防看板、工业设备监控、大屏可视化、在线教育直播回传的开发者,尤其是那些不想引入重型流媒体服务器、希望一个 Java 进程搞定转发的团队。核心难点不在“连上流”,而在转封装、协议选择、延迟控制和断流重连这四件事上,下面按落地顺序拆开讲。
2. 先想清楚协议选型:为什么是 FLV 加 WS,而不是别的
2.1 浏览器能播什么,不能播什么
浏览器<video>标签原生支持的容器就那么几种:MP4、WebM、Ogg,加上 HLS(m3u8)在 Safari 和移动端比较稳,桌面 Chrome 要靠 Media Source Extensions 才能播 HLS。RTSP 是信令加 RTP 传输的协议,浏览器完全不认;RTMP 是 Adobe 的老协议,2020 年底 Flash 退役后浏览器也彻底不认了。所以后端必须做一次“翻译”。
翻译有两条主流路线。第一条是转协议:把 RTSP 拉过来,解复用出 H.264/H.265 裸流,再重新封装成 FLV 或 fMP4,通过 HTTP 长连接或 WebSocket 推给前端。第二条是转码:如果源是 H.265,而目标浏览器只支持 H.264,那就得真正解码再编码,CPU 开销陡增。绝大多数场景下,摄像头出来的就是 H.264,我们只需要转封装(remux),不碰编码,CPU 占用能压到很低。
FLV 之所以成为首选,是因为它结构简单、封装开销小、延迟可以做到 1 到 3 秒,而且 flv.js 这个前端库成熟稳定,能直接喂给<video>。相比之下 HLS 延迟动辄 5 到 10 秒,做实时监控体验很差;WebRTC 延迟最低但信令复杂、服务端实现重。所以“HTTP-FLV 为主,WebSocket 裸流为辅”是性价比最高的组合。
2.2 HTTP-FLV 和 WebSocket 推流各自适合谁
HTTP-FLV 走的是普通 HTTP 长连接,前端用 flv.js 拉流,优点是兼容性好、能过大多数反向代理、断线重连逻辑简单。缺点是它是单向的,服务端只管推,客户端只管收,而且每个客户端会占用一个服务端连接和一份转发资源。
WebSocket 推流则是把 FLV 或裸 H.264 数据通过 WS 帧发出去,前端拿到 ArrayBuffer 后自己用 WebCodecs 或者 MSE 喂给播放器。它适合需要双向通信的场景,比如前端要发控制指令、要动态切换码流,或者你想在同一个连接里复用信令。代价是前端要写更多解码逻辑,WebCodecs 的兼容性目前还不如 flv.js 广。
我一般的做法是:默认给 HTTP-FLV,遇到需要低延迟交互或者要传非 FLV 数据的场景,再开 WebSocket 通道。两条链路共用同一个“拉流-转封装”核心,只是出口不同。
2.3 转封装和转码的边界在哪
判断标准很简单:看源的编码格式和目标播放器要求是否一致。H.264 转 FLV,纯转封装,几乎不耗 CPU;H.265 转 FLV,flv.js 默认不支持 H.265,要么前端换支持 H.265 的播放器(如 jessibuca、wasm 解码),要么后端转码成 H.264。转码的代价是每路流可能吃掉 0.5 到 2 个 CPU 核,路数一多就得横向扩展。
还有一个坑是音频。摄像头常见的是 G.711(PCMA/PCMU)或者 AAC。FLV 容器里音频一般用 AAC,如果源是 G.711,要么转码成 AAC,要么前端播放器支持 G.711 解码。很多“能出画面没声音”的问题就出在这里。
提示:动手前先用 ffprobe 把源的编码格式、分辨率、帧率、音频格式全部打出来,别等写完代码才发现要转码。
3. 用 Spring Boot 搭一个最小可用的拉流转发服务
3.1 技术选型:JavaCV 还是纯 FFmpeg 命令行
Java 生态里做流处理,主流是 JavaCV(对 FFmpeg 的 JNI 封装)或者直接调 FFmpeg 命令行进程。JavaCV 的优点是能在 JVM 内拿到帧数据,方便做二次处理(加水印、AI 分析);缺点是 JNI 层出问题排查困难,内存管理要小心。命令行方式的优点是稳定、隔离性好、FFmpeg 崩了不影响 JVM;缺点是进程管理、输出解析要自己写。
对于“只做转发”这个需求,我更推荐命令行方式,因为转封装这件事 FFmpeg 已经做到极致,我们只需要管好进程和输出管道。下面给一个基于进程 + 输出到 WebSocket 的最小实现思路。
3.2 拉流并转封装成 FLV 的核心代码
先看依赖,只需要 Spring Boot Web 和 WebSocket:
<!-- pom.xml 关键依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>核心思路是:每个客户端连接对应一个 FFmpeg 进程,把 RTSP 拉过来转成 FLV 写到 stdout,Java 侧读 stdout 的字节流,通过 WebSocket 二进制帧发出去。
// StreamWebSocketHandler.java // 每个 WebSocket 连接启动一个 ffmpeg 进程,读取其 stdout 转发给前端 @Component public class StreamWebSocketHandler extends BinaryWebSocketHandler { // 源流地址,实际项目里应从请求参数或配置读取 private static final String RTSP_URL = "rtsp://user:pass@192.168.1.10:554/stream"; @Override protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) { // 本场景前端不发数据,忽略 } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // -rtsp_transport tcp 强制 TCP 拉流,避免 UDP 丢包花屏 // -c copy 表示不重新编码,只转封装,CPU 占用极低 // -f flv 输出 FLV 格式到管道 ProcessBuilder pb = new ProcessBuilder( "ffmpeg", "-rtsp_transport", "tcp", "-i", RTSP_URL, "-c", "copy", "-f", "flv", "-" ); pb.redirectErrorStream(false); Process process = pb.start(); session.getAttributes().put("process", process); // 单独线程读 stdout,避免阻塞 WebSocket 线程 new Thread(() -> { try (InputStream in = process.getInputStream()) { byte[] buf = new byte[8192]; int len; while ((len = in.read(buf)) != -1) { if (session.isOpen()) { session.sendMessage(new BinaryMessage(Arrays.copyOf(buf, len))); } else { break; } } } catch (IOException e) { // 连接断开或进程结束,正常退出 } finally { process.destroyForcibly(); } }).start(); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { // 前端断开时必须杀掉进程,否则会泄漏 Process process = (Process) session.getAttributes().get("process"); if (process != null) { process.destroyForcibly(); } } }这段代码的逻辑说明:-rtsp_transport tcp是关键参数,RTSP 默认走 UDP,网络稍有抖动就丢包,表现为画面花屏、绿屏,强制 TCP 后稳定很多,代价是延迟略增。-c copy告诉 FFmpeg 不要解码再编码,直接把 H.264 裸流塞进 FLV 容器,这是低 CPU 的核心。输出-表示写到标准输出,Java 侧读出来就是 FLV 字节流。
参数说明:buf大小 8192 是经验值,太小会增加系统调用次数,太大增加单帧延迟。process.destroyForcibly()在连接关闭时必须调用,否则每个断开的客户端都会留下一个僵尸 FFmpeg 进程,跑一天服务器就满了。
3.3 前端用 flv.js 播放 WebSocket 流
前端拿到的是 FLV 字节流,但 flv.js 默认走 HTTP,要播 WebSocket 需要自己包一层。更省事的做法是后端同时提供一个 HTTP-FLV 接口,前端直接用 flv.js。这里给 WebSocket 手动喂数据的写法:
// 前端:通过 WebSocket 接收 FLV 数据,用 flv.js 的 MSE 接口播放 const video = document.getElementById('player'); const ws = new WebSocket('ws://localhost:8080/stream'); // flv.js 支持传入一个自定义的 loader,这里简化演示 // 实际项目中更推荐后端提供 HTTP-FLV,flv.js 原生支持 const mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); mediaSource.addEventListener('sourceopen', () => { const sourceBuffer = mediaSource.addSourceBuffer('video/x-flv; codecs="avc1.42E01E"'); const queue = []; ws.binaryType = 'arraybuffer'; ws.onmessage = (event) => { queue.push(new Uint8Array(event.data)); flush(); }; function flush() { if (queue.length === 0 || sourceBuffer.updating) return; sourceBuffer.appendBuffer(queue.shift()); } sourceBuffer.addEventListener('updateend', flush); });逻辑说明:MSE 的addSourceBuffer需要正确的 MIME 类型,video/x-flv在部分浏览器上不被接受,所以生产环境更稳的是让后端输出 fMP4 或者直接用 HTTP-FLV 配 flv.js。这段代码的价值在于说明 WebSocket 通道的数据怎么进播放器,实际选型时优先 HTTP-FLV。
注意:
codecs="avc1.42E01E"里的编码字符串要和实际流的 profile 匹配,写错了会直接报错不播。用 ffprobe 看profile字段来填。
4. 避坑与排查:那些让你加班到凌晨的细节
4.1 现象:前端一直转圈,控制台报 404 或 502
原因通常是反向代理没配对。HTTP-FLV 是长连接,Nginx 默认的proxy_read_timeout是 60 秒,超时就把连接掐了。解决是在 location 里加proxy_read_timeout 3600s;、proxy_buffering off;,并且确认proxy_http_version 1.1。WebSocket 还要额外加Upgrade和Connection头。
4.2 现象:画面能出但延迟越来越大,几分钟后卡死
原因是消费速度跟不上生产速度,数据在缓冲区堆积。FFmpeg 输出到管道,如果 Java 侧读得慢,管道缓冲区满了 FFmpeg 就会阻塞,但更常见的是前端 MSE 的SourceBuffer堆积。解决是给SourceBuffer设一个缓冲上限,超过就remove旧数据,或者在后端丢弃非关键帧。另一个办法是给 FFmpeg 加-fflags nobuffer -flags low_delay降低内部缓冲。
4.3 现象:多路流跑一会儿服务器 CPU 飙满
先确认是不是在转码。用top -H看 FFmpeg 线程,如果 CPU 高且-c copy已经加了,那可能是源本身是 H.265 而你在转 H.264。另一个常见原因是每路流都开了独立进程但没限制,几十路就是几十个进程。解决是引入进程池或者用支持多路复用的方案,比如把多路流合成一路再分发,或者上专门的流媒体服务做这层。
4.4 现象:断流后不自动恢复,必须刷新页面
FFmpeg 进程在源断开后会退出,但 WebSocket 还开着,前端以为还在播。解决是在 Java 侧监听进程退出,退出后要么重启进程,要么给前端发一个关闭帧让它重连。前端也要做心跳检测,超过 N 秒没收到数据就主动重连。这个“后悔药”一定要提前埋,不然线上就是人工刷新。
4.5 现象:音频对不上,或者干脆没声音
前面提过,G.711 音频进 FLV 需要处理。如果-c copy后没声音,用 ffprobe 看音频编码,是pcm_alaw或pcm_mulaw的话,要么加-c:a aac转码,要么前端播放器支持。转码音频的 CPU 开销远小于视频,一般可以接受。
5. 进阶:把延迟压到 1 秒内和一套可复用的验证方法
5.1 延迟从哪来,怎么一段段砍
端到端延迟由四段组成:摄像头编码缓冲、网络传输、服务端转封装缓冲、前端播放缓冲。摄像头那段的 GOP 大小是硬伤,如果 I 帧间隔是 2 秒,那首帧最多要等 2 秒。能调的是服务端和前端:FFmpeg 加-probesize 32 -analyzeduration 0减少探测时间,加-fflags nobuffer减少输入缓冲;前端 flv.js 设enableStashBuffer: false、stashInitialSize: 128。这几项加起来能把延迟从 3 秒压到 1 秒左右。
5.2 一套不依赖前端的验证流程
排查问题时不要一上来就开浏览器,先用命令行确认链路。第一步,ffplay -rtsp_transport tcp "rtsp://..."确认源本身正常。第二步,ffmpeg -rtsp_transport tcp -i "rtsp://..." -c copy -f flv test.flv录 10 秒,用ffplay test.flv确认转封装没问题。第三步,curl -N http://localhost:8080/live/stream.flv -o out.flv确认后端输出正常。三步都过了再查前端,能省掉大量扯皮时间。
5.3 一个可复用的参数速查表
| 参数 | 作用 | 推荐值 | 代价 |
|---|---|---|---|
-rtsp_transport tcp | 强制 TCP 拉流 | 必加 | 延迟略增 |
-c copy | 不转码只转封装 | 源为 H.264 时必加 | 无 |
-fflags nobuffer | 减少输入缓冲 | 低延迟场景 | 可能丢帧 |
-probesize 32 | 减少流探测 | 配合 nobuffer | 偶发识别失败 |
-g | 设置 GOP 大小 | 转码时设 25~50 | 影响 seek |
proxy_read_timeout | Nginx 长连接超时 | 3600s | 无 |
这张表是我每次搭新环境都会翻出来对一遍的,尤其是-rtsp_transport tcp和-c copy,漏掉任何一个都会让你怀疑人生。
5.4 我踩过最狠的一次坑
有次上线后一切正常,第二天早上发现服务器上挂了三百多个 FFmpeg 进程,内存直接爆掉。查下来是前端页面在弱网下频繁重连,每次重连都新建 WebSocket,但旧连接的afterConnectionClosed因为线程池满没及时执行,进程没杀掉。后来我在afterConnectionEstablished里加了一个全局的进程注册表,用 sessionId 做 key,连接建立时先清理同 key 的旧进程,才算根治。这件事之后我养成了一个习惯:凡是“每个连接起一个外部进程”的设计,第一件事就是写进程生命周期管理,别等出事。希望帮到你。
本文还有配套的精品资源,点击获取