资讯详情

RTSP转HLS网页播放实战:FFmpeg+Nginx+video.js完整方案

📅 2026/9/20 12:31:11 | 华诺云谱 👁 阅读
RTSP转HLS网页播放实战:FFmpeg+Nginx+video.js完整方案
简介一套基于SSM架构、Nginx与FFmpeg的RTSP转HLS视频播放方案面向Java后端初学者和有实时预览需求的开发者解决监控、在线教育等场景中网页无法直接播放RTSP流的问题。资源共52个文件包含前端页面html/css/js/png、Java后端代码java/jsp、配置文件conf/hosts以及FFmpeg、Nginx的Windows安装包压缩包大小约70.26MB目录结构较为清晰。目前已有259人学习或下载使用。资料中提供FFmpeg转流命令、rtsp调试源、部署必读和更新日志并附有RtspHlsController、RTSPtoHLS等关键代码可帮助读者快速理解HLS切片原理、Nginx配置要点及playerJQueryDemo播放器集成方式适合边看边操作、按步骤复现整个流程。 做视频接入这块好几年了遇到最多的需求就是“能不能在网页上直接看摄像头”。一听摄像头大家第一反应是推流上云、或者用平台SDK但其实很多场景根本没有那么复杂——只要求内网或者某个局域网里打开浏览器就能实时看到RTSP摄像头的画面。这里最大的拦路虎就是协议摄像头出的RTSP流浏览器原生根本播不了。我试过一堆插件方案ActiveX控件只能跑IEWebRTC网关又太重最后稳定落地的还是“FFmpeg转HLS Nginx托管 前端播放器”这套组合。SSM架构负责流程控制Nginx负责把切片文件发布出去前端用video.js直接拉HLS播放整条链路清晰、可控、不需要给浏览器装任何东西。如果你也想把RTSP流在网页上播放这篇实战记录应该能帮你少踩不少坑。1. 链路设计为什么是RTSP转HLS而不是别的方案1.1 协议差异决定的技术选型RTSP是实时流传输协议它本身只管会话控制和流媒体传输的协商数据面往往跟着RTP走。这个协议族在安防领域一统天下海康、大华、宇视这些厂家的摄像头基本都是RTSP取流。但浏览器端是个“封闭”的生态没有原生的RTSP解码能力也不允许网页直接发起这种二进制会话。HLS则是苹果推出来的基于HTTP的流媒体协议。它的原理很直白把视频切成一个个小TS文件再生成一个m3u8索引文件。播放器先请求m3u8拿到文件列表然后按顺序拉TS切片来播。HTTP协议加静态文件这套逻辑浏览器天然支持不用装插件、不用折腾编解码器兼容性。所以要做网页播放把RTSP转成HLS是成本最低的路线。1.2 我最初踩过的方案对比坑做这功能之前我翻了很久的WebRTC网关方案比如用ZLMediaKit、SRS这类流媒体服务器直接拉RTSP再转WebRTC播放。说实话延时确实低基本在500毫秒以内但部署重、信令复杂还要处理STUN/TURN那一堆穿透问题。对于“内网放个页面看监控”这种需求属于杀鸡用牛刀。还有一类是改前端播放协议比如用WebSocket加JSMpeg直接解码MPEG1但画质和流畅度都一般比较适合低成本玩具项目。真正在项目里扛过需求的还是“FFmpeg转HLS Nginx发布”这套经典方案。它延时通常在3到10秒之间监控场景完全能接受稳定性好Nginx扛高并发也毫无压力。1.3 SSM在这里扮演什么角色SSM架构Spring SpringMVC MyBatis在这条链路里不是用来做流媒体处理的而是做“管家”。开发时页面或App需要一个统一入口去获取播放地址后端就负责三件事管理摄像头信息比如名称、RTSP地址、所属分组都存数据库调用外部命令启动FFmpeg拉流转码任务并记录任务状态向前端返回已经生成的播放地址m3u8的HTTP URL。这样前端不用知道RTSP地址、不用直接操作服务器所有播放请求都走SSM的接口权限控制和后续扩展都好做得多。比如要加一个录像回看功能只需要给FFmpeg加一条输出路径后端再挂个查询接口就行。2. 环境准备FFmpeg和Nginx的安装与关键配置2.1 FFmpeg安装Windows和Linux两边都要会Windows环境下去FFmpeg官网下载编译好的二进制包解压后把bin目录加到系统环境变量PATH里即可。我在项目里用的是ffmpeg-6.0-full_build版本功能全转HLS需要的libx264已经是内置的。装好后在CMD里执行ffmpeg -version能打印版本信息就算成功。Linux下更简单拿CentOS 7为例yum install -y epel-release yum localinstall -y --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm yum install -y ffmpeg如果仓库里没有就用源码编译的方式装但编译依赖比较多yasm、nasm、libx264-dev这些一个都不能少。实测下来用RPM Fusion源最省事。注意FFmpeg转HLS必须依赖H.264编码器编解码库没装好的话执行转码命令时直接报Unknown encoder libx264。装好后建议先用ffmpeg -encoders | findstr x264确认一下。2.2 Nginx安装与模块选择Nginx不是用来推RTMP的这里只做静态文件服务所以不需要额外装nginx-rtmp-module。直接装官方稳定版就行我用的是nginx-1.24.0。Windows版解压即用Linux用包管理装也行yum install -y nginx配置里只需要把HLS切片文件目录暴露成HTTP可以访问的静态资源路径再配上CORS头允许跨域请求就够前端播放器用了。2.3 目录规划让文件看起来规规矩矩这是实操中最容易被忽略的一步。我习惯在服务器上建一个统一的媒体目录比如/data/hls下面按摄像头ID分子目录/data/hls/camera_001/ /data/hls/camera_002/每个摄像头对应的m3u8索引文件和TS切片都放在自己的子目录里。这样Nginx配置一个root指向/data/hls前端访问路径就变成http://服务器IP:8088/camera_001/index.m3u8路径清晰排查问题也方便。3. 核心实操FFmpeg转流命令与参数逐项拆解3.1 一条能直接跑的最小命令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 3 -hls_flags delete_segments /data/hls/camera_001/index.m3u8这条命令是整套方案的基石。先解释每个参数的含义-rtsp_transport tcp指定RTSP用TCP传输不写默认走UDP。UDP在弱网环境丢包严重会出现画面花屏和频繁重连TCP推流稳定得多。-i输入流地址就是摄像头取流URL。-c:v copy视频编码直接拷贝不做转码。这里有个前提摄像头输出的本来就是H.264编码。安防摄像头99%都是H.264或者H.265H.265拷贝出来浏览器播不了所以如果你是H.265摄像头这里的copy要改成-c:v libx264。-c:a aac音频转成AACHLS标准音频编码格式。-f hls输出格式为HLS。-hls_time 2每个切片2秒。切片越短延迟越低但文件数变多磁盘I/O压力大切片太长延迟高。2秒是监控项目里比较平衡的选择。-hls_list_size 3索引文件里只保留最近3个切片旧切片会被清出列表。这样播放器永远只追赶最新的内容实现“低延迟直播”效果。-hls_flags delete_segments清理旧切片文件防止磁盘被塞满。最后的路径是生成的索引文件路径FFmpeg会自动在同目录下生成切片文件。3.2 如果是H.265摄像头命令要这样改很多新摄像头默认输出H.265而浏览器原生不支持解码H.265。这时候必须强制转码ffmpeg -rtsp_transport tcp -i rtsp://admin:password192.168.1.64:554/Streaming/Channels/101 -c:v libx264 -preset veryfast -tune zerolatency -c:a aac -f hls -hls_time 2 -hls_list_size 3 -hls_flags delete_segments /data/hls/camera_001/index.m3u8-preset veryfast表示编码速度优先牺牲一部分压缩率-tune zerolatency是针对直播场景的调优参数能显著降低编码延迟。加了这两个参数后转码的CPU开销会有明显上升但为了浏览器能播放只能这样。我之前在一个项目里接了12路H.265摄像头全部走转码一台8核16G的服务器CPU占用在70%左右还算能扛。如果路数再多就得考虑用GPU硬编码NVIDIA的NVENC来分流CPU压力了。3.3 FFmpeg进程拉起的细节别裸奔要管命在项目环境里FFmpeg进程不能手动拉得加守护。我在SSM后端用Java的ProcessBuilder启动命令再定期检查进程状态挂掉了自动拉起ProcessBuilder pb new ProcessBuilder( ffmpeg, -rtsp_transport, tcp, -i, rtspUrl, -c:v, copy, -c:a, aac, -f, hls, -hls_time, 2, -hls_list_size, 3, -hls_flags, delete_segments, hlsOutputPath ); pb.redirectErrorStream(true); Process process pb.start();需要注意一点摄像头偶尔会断流FFmpeg进程会直接退出。所以每路视频应该有一个独立的守护线程每隔30秒检查一次进程是否存活如果当前任务状态是“开启”但进程已死就重新执行拉起逻辑。还有一个坑如果多个摄像头共用一个FFmpeg输出目录切片文件会互相覆盖播放画面会出现跳变错乱。所以务必保证每个摄像头一个独立输出目录这个在目录规划小节已经强调过。4. Nginx配置把切片文件发布成HTTP流4.1 一个能用的Nginx server配置server { listen 8088; server_name localhost; location /hls/ { alias /data/hls/; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, OPTIONS; add_header Access-Control-Allow-Headers Range; add_header Cache-Control no-cache; types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } } }这里有几个关键点listen 8088避免和SSM项目的Tomcat 8080端口冲突我习惯把流媒体服务独立端口。alias /data/hls/把/hls/路径映射到磁盘目录注意alias后面最好带末尾斜杠不然容易路径拼接错。Access-Control-Allow-Origin *必须的。前端页面如果在8080端口由SSM托管播放器发请求到8088端口这就是跨域。不配这个头播放器会因CORS报错拿不到m3u8。Cache-Control no-cache禁止对m3u8索引文件缓存保证播放器能及时获取最新的切片列表。TS切片可以缓存但m3u8一定不能缓存。types块手动指定.m3u8和.ts的MIME类型防止某些环境下被当成普通文本或二进制下载。配置完执行nginx -s reload然后浏览器访问http://服务器IP:8088/hls/camera_001/index.m3u8如果能直接下载下来或者播放器能打开就说明Nginx发布OK了。4.2 为什么CORS问题经常翻车前端播放器本质是个“浏览器里的JavaScript”它发请求拉流时会自动带上Origin头。如果后端静态服务器不响应Access-Control-Allow-Origin浏览器会直接拦截响应。很多新手在本地用文件方式打开HTML页面测试时Origin是nullNginx如果不放行*就会一直转圈不出画面。所以我建议刚开始调试时直接把前端页面也放到Nginx的8088端口下托管这样就是同源的不会有CORS问题等确认能出画面了再改回跨域模式并配上CORS头。5. SSM后端接口设计让摄像头“可管理”5.1 数据表与任务状态管理SSM里至少需要建两张大表一张存摄像头信息一张存拉流任务。摄像头表字段类似CREATE TABLE camera ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50), rtsp_url VARCHAR(255), group_id INT, enabled TINYINT DEFAULT 1 );拉流任务表可以不要因为FFmpeg进程是否存活可以由Java侧定时检测但如果你想记录任务启动时间、最后存活确认时间、异常退出次数这些建一张任务表会方便很多。我在实际项目里还加了last_check_time字段配合定时任务判断“任务显示开启但已经很久没有切片生成”这种情况直接强制重启。5.2 播放地址接口示例接口返回的JSON结构非常简单前端拿到hlsUrl直接丢给播放器即可// GET /api/camera/play?id1 { code: 200, data: { cameraName: 厂房东门, hlsUrl: http://192.168.1.10:8088/hls/camera_001/index.m3u8 } }Controller层代码示意RequestMapping(/play) ResponseBody public MapString, Object play(Integer id) { Camera camera cameraService.getById(id); if (camera null) { return error(摄像头不存在); } String hlsUrl http://192.168.1.10:8088/hls/camera_ id /index.m3u8; // 检查并拉起FFmpeg进程 ffmpegTaskManager.ensureRunning(camera); return success(hlsUrl); }ensureRunning是封装好的核心方法内部逻辑就是如果任务未启动调用ProcessBuilder拉起如果已启动什么都不做。这样用户每次点击“播放”后端都能确保流是通的不用手动去服务器上敲命令。5.3 定时巡检不要相信FFmpeg很稳定FFmpeg进程挂掉太常见了网络抖动、摄像头重启、内存不够都会导致退出。所以我在项目里加了Spring的Scheduled定时任务每30秒扫描一遍所有enabled1的摄像头检查对应FFmpeg进程是否存活检查输出目录下最新的m3u8文件修改时间是否在1分钟以内如果文件超过1分钟没更新说明拉流卡住了杀掉进程重新拉起。这一步非常关键。没有巡检机制第一天装好画面正常第二天可能就悄悄黑屏了日志里只有FFmpeg的报错用户根本不知道怎么回事。6. 前端播放实现video.js与HLS播放器的选择6.1 播放器选型HLS播放有两条主流路线如果浏览器原生支持HLSSafari、iOS、macOS直接用video标签配src就行其他浏览器Chrome、Firefox、Edge原生不支持HLS必须用MSEMedia Source Extensions技术解码。我推荐用video.js加videojs-contrib-hls插件集成简单、文档多、坑少。如果需要做多路监控墙还可以配合videojs-contrib-quality-levels做清晰度切换。如果不想引入大框架直接用hls.js库也能实现体积小、API简洁。6.2 最小可运行的播放页面!DOCTYPE html html langzh-CN head meta charsetUTF-8 link hrefhttps://cdn.bootcdn.net/ajax/libs/video.js/7.21.4/video-js.min.css relstylesheet script srchttps://cdn.bootcdn.net/ajax/libs/video.js/7.21.4/video.min.js/script script srchttps://cdn.bootcdn.net/ajax/libs/videojs-contrib-hls/5.15.0/videojs-contrib-hls.min.js/script /head body video idplayer classvideo-js vjs-default-skin controls preloadauto width800 height450/video script var player videojs(player, { autoplay: false, muted: true, sources: [{ src: http://192.168.1.10:8088/hls/camera_001/index.m3u8, type: application/x-mpegURL }] }); player.play(); /script /body /html几个使用体会muted: true建议默认开启。浏览器自动播放策略规定带声音的视频不能自动播放静音可以绕过限制。用户点开页面能看到画面再手动打开声音。autoplay: false时播放器会显示一个大播放按钮。如果做监控墙需要多路自动播放把autoplay设为true、muted设为true即可。type固定为application/x-mpegURL不要写成application/vnd.apple.mpegurl前者兼容性更好video.js官方示例也是用这个。7. 常见问题与排查技巧实录7.1 播放器报MediaError或fragparsingerror播放器黑屏并出现类似MediaError或fragparsingerror的错误说明它拿到了m3u8但拉取TS切片失败。先确认TS文件是否可访问curl http://127.0.0.1:8088/hls/camera_001/index.m3u8 curl http://127.0.0.1:8088/hls/camera_001/index0.ts如果m3u8能拿到、TS拿不到多半是Nginx的alias路径配置错了或者FFmpeg输出目录权限不够。如果两个文件都正常再看浏览器Network面板里TS请求的响应头重点确认有没有Access-Control-Allow-Origin。跨域请求TS切片同样受CORS限制缺失这个头文件就会报错。7.2 延迟越来越大最后卡死这个坑我印象很深。起初用默认配置跑-hls_list_size没设切片文件越积越多播放器不断往后追延迟从2秒涨到20秒最后画面卡死。解决方式就是把列表大小限制在3到5个同时开启delete_segments让旧切片及时清理。延迟跟切片时长也有直接关系。-hls_time 2时端到端延迟一般在5秒左右调到1秒延迟能压到2到3秒但服务器切文件的频率翻倍对磁盘IO有点压力。如果对延迟有极致要求建议换WebRTC方案HLS再优化也就这个水平了。7.3 画面有声音但黑屏或者只有声音没画面多半是编解码器问题。先确认摄像头输出编码H.264-c:v copy没问题H.265浏览器不支持必须加-c:v libx264转码MPEG-4部分老摄像头会出这种编码也需要转成H.264。音频同理摄像头如果输出G.711音频-c:a copy等于把浏览器不认识的声音格式直接塞给播放器表现就是画面正常但没声音。需要指定-c:a aac转成AAC。7.4 摄像头取流地址总是写错海康威视的RTSP地址常见格式是rtsp://用户名:密码IP:554/Streaming/Channels/101最后的101中第一位1表示主码流01表示通道1。改成201就是子码流。如果你只想在网页上看流畅画面建议用子码流带宽消耗小、FFmpeg压力也小rtsp://admin:123456192.168.1.64:554/Streaming/Channels/201大华摄像头的取流格式略有不同一般是rtsp://用户名:密码IP:554/cam/realmonitor?channel1subtype0。拿不准的时候访问摄像头管理页面的“媒体设置”里通常能直接复制RTSP地址比自己拼保险得多。7.5 排查问题时的最后手段实在查不出原因时把FFmpeg的日志完整打出来看。调试阶段我把ProcessBuilder的redirectErrorStream输出实时打印到服务器日志FFmpeg的错误信息非常直白Connection timed out网络不通或者RTSP地址写错先ping摄像头IP401 Unauthorized用户名密码错误No such file or directory输出目录不存在或者没权限403 Forbidden摄像头限制了访问IP或者通道数。日志定位问题比瞎猜快得多这招帮我省过无数次排查时间。8. 还不够多路播放与录像回看的扩展思路这套架构跑通单路播放后扩展多路监控是非常自然的。每路一个FFmpeg进程加一个Nginx路径SSM接口层对应增加“播放墙”接口返回一个摄像头和播放地址的列表。前端用video.js初始化多个播放器实例就完成了一个简化版监控中心。录像回看也容易FFmpeg转HLS到本地目录时再加一条输出流ffmpeg -rtsp_transport tcp -i rtsp://... -c:v copy -c:a aac -f mp4 -y /data/record/camera_001_20250115.mp4SSM后端增加“查询某摄像头某天的录像文件”接口前端把m3u8地址换成mp4文件路径就能点播。从协议选型到命令参数、从Nginx配置到前后端联调这套RTSP转HLS的链路我已经在不同项目里用过好几轮了。整体跑下来最想强调的还是那句老话先把最小链路跑通再考虑做管理功能。不要一上来就想做运维监控、做多级权限、做动态码率切换那些都是在基础播放正常之上的锦上添花。基础链路的每一个环节——FFmpeg命令、Nginx路径、CORS头部、播放器类型——单独拎出来都不难但连接在一起时任何一个地方出错画面上都只会是一个永远转圈或直接黑屏的播放器。按这篇文章的顺序从后往前测先用浏览器直接访问m3u8再让播放器拉流最后加上SSM后端的自动化控制。层层验证下来问题就永远逃不出你的手掌心。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。