RTSP转HLS播放方案:FFmpeg+Nginx+SSM实现浏览器视频监控

📅 发布时间:2026/10/10 3:33:19
RTSP转HLS播放方案:FFmpeg+Nginx+SSM实现浏览器视频监控
简介一套面向Java后端开发者与流媒体入门者的完整实践资源基于SSM架构整合Nginx与FFmpeg实现RTSP实时流到HLS流的转换并交付可在前端网页播放的工程解决摄像头或RTSP源无法直接在浏览器预览的常见问题。压缩包共52个文件、约70.26MB包含FFmpeg与Nginx安装包、SSM架构的Java源码及jsp/conf配置、基于jQuery的播放器页面html/css/js以及配置说明、调试源、部署文档等辅助材料覆盖从环境搭建到业务联调的主要环节。配套的转流命令、Nginx配置示例和部署必读笔记能帮助读者快速复现作者亲测可行的全流程尤其适合想动手实践但又缺少头绪的小白用户。资源已有262人学习浏览从中可直接获取可运行骨架和排错思路并进一步理解RTSP、HLS协议转换及前后端联调的关键细节便于迁移到监控、在线课堂等实时预览场景。1. 一套SSMNginxFFmpeg方案专治RTSP流在浏览器里放不出来监控摄像头几乎都走RTSP协议但浏览器偏偏不认它。想在内网Web系统里直接看监控画面就得先把RTSP拉成HLS切片再用Nginx吐给前端整个过程还得有个后端工程去编排转流进程。这套SSM架构NginxFFmpeg的资源包正好把三段链路全串好FFmpeg负责转流切片、Nginx负责静态分发、SSM工程负责业务控制和参数管理外加一个playerJQueryDemo网页打开即用。适合给园区做远程视频巡检、给车间接旧摄像头、给管理系统嵌入实时监控画面的开发者。链路走通了后续加摄像头只是改配置的事不用再动架构。2. FFmpeg把RTSP转成HLS切片先想清楚为什么选HLS2.1 三种转流协议对比HLS、RTMP、WebRTC各自的边界拿到一个RTSP流转发的需求第一反应往往是选协议。RTMP延迟低但2020年后浏览器对Flash彻底断供纯前端方案废掉一半WebRTC延迟最低但信令服务、STUN/TURN的部署成本对一个小工程来说太重HLS走的是HTTP拉流后端切成ts切片、写m3u8索引前端拿video标签或者hls.js直接播实现路径最顺。HLS的代价是延迟比不上RTMP和WebRTC切片2秒的情况下端到端通常有4到8秒延迟。但监控场景里这个延迟可接受安防画面的核心诉求是流畅和稳定不是实时对讲。另一个优势是HLS天然穿过HTTP端口公司内网防火墙很少拦80或8080跨部门协调成本低。这个资源包选HLS作为中间协议是合理的。FFmpeg对RTSP的解码兼容性比市面上的SDK封装要稳切片参数可以调后端可以随时重启转流进程这些都是商业播放器给不了的灵活性。2.2 FFmpeg转流命令拆解从RTSP到m3u8的关键参数先看最基本的转流命令。假设摄像头RTSP地址是rtsp://admin:password192.168.1.64:554/Streaming/Channels/101我要把它转成HLS切片输出到E:/hls/cam01目录。ffmpeg -rtsp_transport tcp -i rtsp://admin:password192.168.1.64:554/Streaming/Channels/101 \ -c:v copy -c:a aac -f hls \ -hls_time 2 -hls_list_size 0 -hls_flags delete_segments \ -hls_segment_filename E:/hls/cam01_%05d.ts \ E:/hls/cam01.m3u8说明几个关键参数。-rtsp_transport tcp强制走TCPRTSP默认用UDP传输摄像头到服务器跨交换机时UDP丢包会直接花屏或卡帧TCP虽然增加一点延迟但稳定得多。-c:v copy是视频流直接复制不重新编码CPU占用几乎为零前提是摄像头输出的编码格式是H.264绝大多数IPC默认就是这个。-c:a aac是音频转成AAC有些摄像头输出的是PCM或G711HLS的TS封装不认必须转一下。-hls_time 2表示每个ts切片时长为2秒-hls_list_size 0表示m3u8索引文件里保留所有切片如果做纯直播建议改成-hls_list_size 5这样播放器只看到最近5个切片延迟能压低一截。-hls_flags delete_segments是切完的旧切片自动删除防止转流跑一天后磁盘被几万个ts文件塞满。2.3 用Java的ProcessBuilder管理FFmpeg进程转流命令在命令行里跑通之后下一步是交给后端Java工程去拉起和管理。SSM工程里最常见的做法是封装一个转流服务内部用ProcessBuilder启动FFmpeg子进程。public class FfmpegStreamService { private Process ffmpegProcess; // 启动转流把RTSP转成HLS切片 public void startStream(String cameraName, String rtspUrl, String hlsDir) { // 目录不存在先创建否则FFmpeg会直接报错退出 Files.createDirectories(Paths.get(hlsDir)); String m3u8Path hlsDir / cameraName .m3u8; String tsPattern hlsDir / cameraName _%05d.ts; ProcessBuilder pb new ProcessBuilder( ffmpeg, -rtsp_transport, tcp, -i, rtspUrl, -c:v, copy, -c:a, aac, -f, hls, -hls_time, 2, -hls_list_size, 5, -hls_flags, delete_segments, -hls_segment_filename, tsPattern, m3u8Path ); // 合并错误输出流方便排查转流失败原因 pb.redirectErrorStream(true); ffmpegProcess pb.start(); // 用独立线程消费输出日志避免缓冲区阻塞 consumeOutput(ffmpegProcess); } public void stopStream() { if (ffmpegProcess ! null) { ffmpegProcess.destroy(); ffmpegProcess null; } } }这段代码有两点要特别注意。第一pb.redirectErrorStream(true)必须加FFmpeg日志量大不把子进程输出读掉缓冲区一满FFmpeg会阻塞假死。第二stopStream()直接 destroy 进程FFmpeg可能来不及把m3u8索引写完整稳妥做法是先发destroy()等500毫秒再destroyForcibly()兜底。转流进程的启动速度取决于RTSP握手耗时通常1到3秒前端的waiting动画要按这个心理预期来设计。3. Nginx托管HLS流切片目录、MIME类型与跨域配置3.1 HLS的访问链路m3u8主索引和ts分片各干什么FFmpeg切出来的HLS流在磁盘上是两种文件一个m3u8索引文件若干ts切片文件。播放器拿到m3u8后才知道ts切片叫什么名字、按什么顺序播。所以Nginx要做的不是转发什么特殊协议而是把这两个静态文件正确吐给浏览器。这条链路的坑藏在MIME类型上。ts切片如果被Nginx当成application/octet-stream返回浏览器会直接触发下载而不是播放m3u8如果没配application/vnd.apple.mpegurlSafari会显示一个黑屏播放器。这类问题不报错肉眼很难定位。3.2 nginx.conf关键配置段逐行说明资源包里的Nginx是Windows和Linux通用的解压后改配置文件即可。我通常维护一个专门托管流媒体的server块跟业务服务分开避免互相污染。server { listen 8080; server_name localhost; # 托管HLS切片目录FFmpeg输出到这里 location /hls/ { types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } alias E:/hls/; add_header Cache-Control no-cache; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, HEAD, OPTIONS; add_header Access-Control-Allow-Headers Range; } # 可选限制只允许内网访问 # allow 192.168.0.0/16; # deny all; }location /hls/末尾的斜杠和alias E:/hls/末尾的斜杠是配套关系。请求/hls/cam01.m3u8时Nginx把/hls/前缀剥掉拼上E:/hls/最终指向E:/hls/cam01.m3u8。如果alias漏了末尾斜杠文件路径会拼接错直接404。add_header Cache-Control no-cache给播放器一个明确信号每次都要重新拉m3u8索引不能缓存。直播场景下m3u8是动态更新的缓存机制会直接导致画面停留在几秒前甚至卡死。Access-Control-Allow-Origin *是给跨域播放用的如果你的管理页面和流媒体服务不同端口这一行缺了前端播放器连ts分片都拉不回来。改完配置后执行nginx -t校验语法再nginx -s reload热加载。Windows下注意Nginx目录不能放在路径带空格的目录里否则reload时经常报open() failed的错误。3.3 跨域细节和防盗链配置跨域是HLS播放最隐形的问题。页面在8080端口流媒体在8080端口看起来同源。但前端工程可能跑在http://192.168.1.100:8080Nginx流媒体跑在http://192.168.1.100:8080只要服务端口不同就算跨域。如果不加CORS头ts切片请求会被浏览器拦截报错信息则是No Access-Control-Allow-Origin header is present。防盗链按需配置。内网监控系统一般不设门槛但如果有公网暴露需求可以加上Referer判断location /hls/ { # 只允许指定来源的页面拉流 if ($http_referer !~* your-web-domain.com) { return 403; } }注意防盗链作用有限Referer可以伪造真正安全的做法是给m3u8加鉴权token这个超出资源包范围内网部署不必折腾。4. SSM后端工程怎么编排转流服务接口、状态与前端播放器4.1 SSM架构包里转流服务如何与Spring融合资源包里那个SSM架构包结构是标准的Spring SpringMVC MyBatis分层Controller管接口Service管业务Mapper管数据库。转流服务在这个架构里的定位是摄像头配置持久化到MySQLController接收到播放请求后查配置把rtsp地址传给FFmpeg服务去启动转流进程。摄像头配置表可以按这个结构设计字段类型说明idint primary key auto_increment主键camera_namevarchar(64)摄像头别名同时作为HLS文件名前缀rtsp_urlvarchar(255)RTSP完整地址含用户名密码hls_pathvarchar(255)切片输出目录statustinyint0未启动 1转流中 2失败这样设计的好处是摄像头信息不进代码新加摄像头只需INSERT一条记录不用改工程、不用重新部署。MyBatis的mapper里写标准的insert/selectController层直接复用。4.2 转流接口设计start、stop、status三个操作的返回约定工程里的核心Controller要暴露三个接口前端根据返回状态决定播放器怎么表现。RestController RequestMapping(/api/stream) public class StreamController { Autowired private FfmpegStreamService streamService; // 启动某摄像头的转流 PostMapping(/start) public Result start(RequestBody StreamRequest req) { CameraConfig config cameraMapper.selectByName(req.getCameraName()); if (config null) { return Result.error(摄像头不存在: req.getCameraName()); } boolean ok streamService.startStream( config.getCameraName(), config.getRtspUrl(), config.getHlsPath()); // 把状态回写到数据库便于下次查询 cameraMapper.updateStatus(config.getId(), ok ? 1 : 2); return ok ? Result.success(转流已启动) : Result.error(转流启动失败); } // 查询转流状态 GetMapping(/status) public Result status(String cameraName) { CameraConfig config cameraMapper.selectByName(cameraName); return Result.success(config null ? null : config.getStatus()); } }返回的Result对象是个统一包装结构里面带code、message、data三个字段。前端拿到code 0认为是成功。启动转流后建议轮询status接口看到状态从纯0变成1或2再让播放器去拉m3u8避免用户打开页面时转流还没就绪导致黑屏。一个容易忽略的细节是转流进程必须以cameraName为粒度做单例控制。同一个摄像头配了两次启动会拉起两个FFmpeg进程同时写同一个m3u8文件索引错乱、切片互相覆盖画面必然卡顿。这个校验必须放在Service层入口处。4.3 前端playerJQueryDemovideo.js接入和设备列表切换资源包里的playerJQueryDemo网页核心是利用video.js播放HLS流外层用jQuery控制设备列表切换和播放器状态。播放器初始化部分参照这个写法video idmyPlayer classvideo-js vjs-default-skin controls preloadauto width960 height540>ffmpeg -rtsp_transport tcp -i rtsp://admin:password192.168.1.64:554/Streaming/Channels/101 \ -an -c:v copy -f hls \ -hls_time 1 -hls_list_size 3 -hls_flags delete_segmentsindependent_segments \ -hls_segment_filename E:/hls/cam01_%05d.ts \ E:/hls/cam01.m3u8改动有三个。-hls_time 1切片降到1秒前提是摄像头的GOP关键帧间隔也要设为1秒否则FFmpeg想切1秒也切不出来切片边界必须落在IDR帧上。第二加了-an把音频流去掉监控场景经常不关心声音少了音频转码能省一点CPU和起播时间。第三independent_segments告诉播放器每个ts切片从独立关键帧开始拖拽和历史回放时不用依赖前一个切片的数据。还有一个容易被忽略的参数组合-hls_flags delete_segmentsindependent_segments中间用加号连接这是FFmpeg对多个flags的固定语法。分开写或者用逗号都会报Option hls_flags not found之类的解析错误。切片参数之外延迟还受摄像头编码侧影响。把摄像头的GOP设为1秒、码率上限设为4Mbps左右画面清晰度和流畅度都能兼顾。如果是海思方案的老摄像头GOP设1秒可能导致码率波动变大那就把切片时间改为2秒延迟在3到4秒之间也够用。判断延迟有没有达标的方法很直接打开手机秒表对准监控画面再用自己手机上打开播放页面对比两个数字的差值就是端到端延迟。我每次部署完都强制走一遍这个测试低于3秒才算验收通过。从那以后我每次接视频类需求第一件事不是写代码而是先跑一遍这条延迟验收流程确认输入端的编码参数没问题再动工程。希望帮到你。本文还有配套的精品资源点击获取