RTSP监控流网页播放:基于WebSocket与MSE的Streamedian实践
简介针对RTSP流难以在现代浏览器中直接播放的常见困境这份面向前端与音视频开发者的资源包给出了一套基于Streamedian的HTML5播放方案适用于监控视频、在线教育等需要低延迟实时画面的场景。包内包含Streamedian 2.1.5服务端组件、free.player播放器脚本、h265播放模块以及对应的HTML演示页和CSS样式可以快速搭建起从RTSP拉流到网页播放的本地演示帮助理解WebRTC/HLS流转换、视频标签接入、播放交互控制等关键环节压缩包共14个文件以js、html、css为主辅以xml工程配置、图片图标和IDE工程文件整体仅403KB轻量易用、便于二次修改。目前已有6845人学习或下载适合作为前端音视频方向的技术预研、课程设计演示或方案可行性验证资料。1. 为什么还在用RTSP的监控流偏偏不能直接放进网页在所有音视频协议里RTSP是安防和推拉流场景中最成熟的取流协议但把rtsp://拷贝到Chrome地址栏只会得到一个不支持的错误页。表面原因是浏览器没有RTSP客户端深层原因是RTSP只负责会话控制真正的视频数据走RTP/UDP浏览器不会去拆RTP包也不会把H.264 NAL单元直接喂给解码器。Streamedian的方案很取巧它用一个Java服务端把RTSP源拉过来转成WebSocket二进制流再交给前端播放器通过MediaSource Extensions写入video标签。这意味着不需要ffmpeg转码能保留原始编码延迟可以做到对标WebRTC的级别。下面要拆解的这套链路以streamedian_2.1.5服务端和free.player.1.8.7.js播放器为主线从RTSP拉流原理、服务端部署、前端接入一直讲到H.265软解生产化目标是让一个有一定前端基础的人半天内把监控摄像头画面放上网页。2. RTSP拉流协议为什么到浏览器就断代RTP、MSE与Streamedian的解法2.1 浏览器不是不认RTSP而是缺一条可注入的通道RTSP协议可分为控制面和数据面。控制面基于文本命令类似HTTP客户端发DESCRIBE服务器返回SDP描述里面包含Media类型、编码格式、端口号等信息再发SETUP建立RTP传输通道最后PLAY开始推流。数据面由RTP承载把H.264/H.265帧拆成一个个RTP包带序号和时间戳。问题是浏览器的video标签只接受带完整容器信息的mp4、WebM或HLS分片无法直接接收裸的RTP包。过去有人尝试用WebRTC的insertable streams去接RTP但需要额外的信令和SDP协商复杂度上升一个量级而且RTSP转WebRTC在浏览器端还要求HTTPS环境部署成本并不低。Streamedian的做法是把浏览器作为WebSocket客户端把RTSP这层“拉流协议”的身份接管下来服务端负责和摄像头完成RTSP握手、接收RTP包、重排序、去除RTP头并把H.264帧封装成ISO BMFF片段通过WebSocket发给前端前端再用MediaSource SourceBuffer把片段追加进去。这样整体链路里只有一个额外的代理节点没有重新编码所以延迟低。相比RTSP转HLS那种切片转发Streamedian省去了ts流分段和m3u8轮询画面延迟能控制在几百毫秒内这是它适合大屏监控场景的根本原因。2.2 认清streamedian_2.1.5包里的每个文件拿到streamedian_2.1.5.rar解包后应该看到几类组件服务端程序、web播放器JS、示例页面。下面这个表格是从项目正文和实际部署中整理出来的文件职能映射方便区分哪些放服务器、哪些放Nginx静态目录。文件/目录归属作用streamedian_2.1.5.rar压缩包释放整个2.1.5环境包含服务端与web demofree.player.1.8.7.js浏览器静态资源用于H.264 RTSP流的WebSocket-MSE播放器h265.player.2.1.5.js浏览器静态资源封装H.265解码流程配合libde265.js使用libde265.js浏览器静态资源libde265的JS软解库index.html / style.cssdemo页面示例页面可做接入模板.idea / modules.xml / misc.xml工程配置说明原项目是IntelliJ IDEA工程服务端用Javaworkspace.xml工程配置IDE状态文件部署时不需要这个表格的价值在于打包下载的源码包里有时会把IDE配置也塞进去新手容易把.idea目录误传上服务器。实际上线只需要服务端jar/war、配置文件和前端JS.idea、.gitignore都是本地开发残留。2.3 RTSP取流地址的写法与验证要接入摄像头或者VLC模拟源第一件事是把RTSP URL写对。海康的地址格式是rtsp://用户:密码IP:554/Streaming/Channels/101其中101代表主码流102代表子码流大华是rtsp://用户:密码IP:554/cam/realmonitor?channel1subtype0channel是通道号subtype的0是主码流、1是子码流。子码流分辨率低码率小适合网页多画面预览主码流清晰但需要更大的带宽和缓冲。如果使用大华NVR作为取流网关地址里的IP要填NVR的IP而不是摄像头的内网IP端口也是NVR的RTSP端口。在配置Streamedian之前先用ffprobe确认源是否可达ffprobe -rtsp_transport tcp -stimeout 5000000 -i rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101 -show_streams -format json这里的-rtsp_transport tcp指定用TCP承载RTP而不是UDP因为在跨网段或防火墙环境下UDP容易丢包导致花屏-stimeout是握手超时时间单位微秒5000000表示5秒。如果ffprobe能正常打印出视频流信息说明地址、账号、端口都没问题。如果报401先检查URL里的特殊字符是否需要转义比如密码里的或#要写成%40或%23如果超时则要检查摄像头是否允许RTSP访问以及554端口是否被防火墙拦截。这个验证步骤能避免后面把问题全部堆到Streamedian服务端上。3. 搭建streamedian_2.1.5的RTSP转发服务端3.1 从压缩包到可运行的Java服务Streamedian服务端在2.1.5版本时还是一个可执行的Java服务器需要JDK1.8及以上。先释放包并检查Java环境unrar x streamedian_2.1.5.rar /opt/streamedian cd /opt/streamedian java -version如果服务器上没有JavaDebian系执行apt install openjdk-8-jreCentOS执行yum install java-1.8.0-openjdk。这里需要注意unrar命令需要额外安装也可以直接在图形系统里用WinRAR解压后上传整个目录只要保证目录结构完整即可。服务端的启动入口通常是streamedian.jar或start.sh启动命令是java -jar streamedian.jar server.properties启动后观察控制台输出正常会打印出HTTP端口和WS端口监听信息。server.properties是核心配置资源包里一般会附带缺了的话服务端会因为读不到端口配置而直接退出。如果手上没有摄像头可以用VLC把本地mp4循环推成一个RTSP测试源这样后面前端调试不需要依赖真实设备vlc -vvv sample.mp4 --sout #rtp{sdprtsp://:8554/live} --loop --sout-keep这条命令把sample.mp4推成rtsp://127.0.0.1:8554/live--loop让文件循环播放--sout-keep保持输出链路不中断。VLC的RTSP服务器只能是单会话模式所以它只适合本地验证生产环境还是要用摄像头或者专门的RTSP server。3.2 server.properties关键参数说明下面是2.1.5里常用配置的整理我按实际使用频率排了序参数示例含义listenerHttpPort8080WebSocket握手前的HTTP探测端口浏览器需要先通过HTTP请求获得服务端版本listenerWsPort8081WebSocket端口前端播放器通过ws://ip:port/ws连接rtspPort554服务端向RTSP源发起连接的默认端口一般不配置在URL里也可以websocket_binary_typearraybuffer指定前端收到的二进制类型JS侧需要匹配buffer.maxSize65536单个媒体包的最大字节数超过会断开对流里的I帧过大时需调大buffer.timeout10000没有收到客户端数据时判定断连的超时时间单位毫秒这里的listenerWsPort不是RTSP源端口而是浏览器和Streamedian之间的WebSocket端口很多首次部署的人会误填成554导致连接失败。buffer.maxSize值得注意H.264的IDR帧在某些摄像头上会超过64KB如果日志频繁出现frame too large就要把这个值调到128000或256000。另外还有一个常见参数rtsp_transport可以设tcp或udp默认udp在公网或Wi-Fi环境下建议显式开启tcp避免RTP包乱序导致画面花屏。服务端配置改完后需要重启Java进程不要热改因为NIO channel已经绑定端口了。3.3 启动服务端并验证WebSocket端口配置完成后通过curl命令验证HTTP端口是否响应curl http://127.0.0.1:8080/如果返回HTML或JSON说明HTTP服务正常。再用一段Node.js代码验证WebSocket通道const WebSocket require(ws); const ws new WebSocket(ws://127.0.0.1:8081/ws); ws.on(open, () console.log(open)); ws.on(message, data { console.log(binary:, data.byteLength); ws.close(); }); ws.on(error, e console.error(e.message));这段代码能确认Streamedian服务端已经把WebSocket通道建立起来。如果连接被拒绝用ss -lntp | grep 8081查端口监听如果连接建立但没有消息说明还没有播放器发送RTSP地址属于正常现象。这种验证方式也可以集成到自动化巡检脚本里用于监控Streamedian服务是否存活。4. 前端接入free.player.1.8.7.js从握手到画面出现4.1 页面结构和初始化代码前端部分需要把free.player.1.8.7.js放到静态目录。最简单的是新建一个index.html把包里的index.html和style.css都保留。不要直接用示例页里的硬编码IP而是要根据自己的服务端地址改video idplayer-view autoplay controls muted playsinline/video script src/js/free.player.1.8.7.js/script script var player Streamedian.player(player-view, { server: ws://127.0.0.1:8081/ws, url: rtsp://admin:123456192.168.1.64:554/Streaming/Channels/101, streamType: h264, maxBufferSize: 1024 * 1024 * 8, minBufferSize: 1024 * 512, autoPlay: true, debug: false }); /script这里Streamedian.player是free.player暴露出来的工厂函数第一个参数是video元素的id第二个参数是配置对象。server必须指向Streamedian的WebSocket端口注意带/ws路径url就是前面验证过的RTSP地址streamType可以是h264或h265如果写错在H.265源上会用默认H.264解析器去解析H.265帧直接导致花屏或黑屏。maxBufferSize控制SourceBuffer最大缓存字节数minBufferSize是低于这个值时会触发buffer填充两个值一起配合能避免频繁内存膨胀。debug置为true后F12控制台会打印MediaSource的状态变化定位问题时建议开。4.2 监听播放器事件而不是操作video事件free.player的播放逻辑由JS接管直接监听video的play、pause事件在某些版本里无效因为底层通过WebSocket消息来控制。正确姿势是监听插件抛出的自定义事件player.on(error, function (err) { console.error(播放器错误, err.message, err.code); }); player.on(connect, function () { console.log(WebSocket已连接); }); player.on(connectFail, function (e) { console.warn(连接失败, e.message); }); player.on(playing, function () { console.log(已收到媒体数据并开始播放); }); player.on(ended, function () { console.log(流结束或断连); });其中connectFail事件非常关键它至少能透出三类错误服务端没启动、RTSP地址不可达、认证失败。错误码一般会用数字表示比如5表示RTSP会话建立失败14表示鉴权失败。如果connect都触发了但playing一直不来优先怀疑是RTSP源的主码流太大超过了服务端buffer.maxSize其次是摄像头不支持TCP传输需要在服务端配置rtsp_transporttcp。不要用video的onerror去判断网络流故障因为MSE模式下video只负责解码渲染网络问题都发生在WebSocket层。提示如果在Nginx环境下部署前端页面需要把/ws的WebSocket升级请求也代理到8081端口否则浏览器会一直停在connectFail。Nginx最低配是proxy_set_header Upgrade $http_upgrade;和proxy_set_header Connection upgrade;漏掉任一个都会导致1006异常。4.3 从websocket消息到MSE SourceBuffer的完整路径这里值得把数据通路讲透因为排错时需要定位是服务端丢帧还是前端buffer没有追加。free.player在内部做了三件事第一通过WebSocket收到二进制数据前两个字节是消息类型ID后续是封装好的ISO BMFF片段第二根据类型ID区分关键帧和普通帧关键帧到达时调用endOfStream或重新初始化SourceBuffer保证解码器从I帧开始第三每次追加前要判断SourceBuffer的buffered范围落后太多就执行remove()清理旧数据。代码不需要你自己写但理解后可以解决很多黑屏问题。一个常见的调优手段是加大maxBufferSize这能让弱网下的画面保持更久不花屏代价是延迟会增大如果项目要求低延迟到300ms以内则要适当调小minBufferSize让buffer快速填满后立刻播放不要等攒成秒级缓存。自己实现MSE时有几个坑这里用代码片段说明正确的追加逻辑function appendMseData(sourceBuffer, chunk) { if (sourceBuffer.updating) return; try { sourceBuffer.appendBuffer(chunk); } catch (e) { sourceBuffer.timestampOffset 0; sourceBuffer.remove(0, sourceBuffer.buffered.end(0)); } }appendBuffer是异步操作如果上一次还没完成就再次调用会抛InvalidStateError所以必须检查updating状态。捕获异常后先重置timestampOffset再清空旧缓冲这是处理编码不同但容器格式一致时最常用的手法。free.player内部已经把这类逻辑封装好了但如果需要自己写客户端接收h265软解数据这套规则同样适用。5. H.265软解、断线重连与低延迟调优5.1 用h265.player.2.1.5.js处理H.265摄像头如果你的摄像头输出H.265直接用free.player会黑屏因为Chrome的MSE不支持H.265 Main Profile。包里的h265.player.2.1.5.js配合libde265.js可以在前端做软解先用WebAssembly解码H.265帧再通过canvas或WebGL渲染。使用方式只需要把JS换成h265.player并在配置中把streamType改为h265。软解性能比硬解差1080P高码率下CPU占用可能到40%以上所以生产环境建议优先把摄像头改成H.264编码实在改不了才上软解。var player Streamedian.player(video, { server: ws://127.0.0.1:8081/ws, url: rtsp://192.168.1.64:554/cam/realmonitor?channel1subtype0, streamType: h265, workerUrl: /libde265.js, wasm: true });参数说明workerUrl指向libde265的Worker加载入口wasm: true指示播放器优先使用WebAssembly版本。如果控制台报Cannot find module libde265说明workerUrl路径写错或者跨域被拦。注意H.265软解首次加载需要下载几百KB的wasm应放到CDN并配置缓存。5.2 断线重连与延迟参数经验值监控场景下摄像头重启或网络闪断是常态前端需要在ended后做指数退避重连。我一般这样写player.on(ended, function () { let delay Math.min(1000 * Math.pow(2, retryCount), 30000); setTimeout(restartPlayer, delay); });restartPlayer先执行player.destroy()再新建Streamedian.player避免旧WebSocket和SourceBuffer泄漏。延迟调优方面内网环境下把maxBufferSize调到2MB、minBufferSize调到256KB延迟能稳定在300ms内公网环境则优先保证流畅缓冲放宽到8MB。另一个不起眼但影响大的参数是buffer.timeout如果摄像头长时间不发数据服务端会主动断开前端需要把它当成正常断流处理而不是报错。最后提醒一点不要试图在浏览器里直接连接rtsp://或者用iframe嵌VLC网页插件现代浏览器已经全面移除NPAPI这种方案唯一可持续路径就是WebSocket代理。如果你的项目已经有Nginx RTMP或ZLMediaKit也可以把它推成FLV后走flv.js但那样需要额外搭建流媒体服务配置比Streamedian更重。相比之下streamedian_2.1.5这套方案的优势是服务端轻、前端静态文件即可适合中小并发场景比如几百路以内的监控大屏和可视化看板。本文还有配套的精品资源点击获取