ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

webrtc-streamer实战:把RTSP监控视频流搬到浏览器低延迟播放

2026/9/8 2:29:39 拓冰建站 浏览量
webrtc-streamer实战:把RTSP监控视频流搬到浏览器低延迟播放 简介这是 webrtc-streamer 在 Windows AMD64 平台下的预编译发行包版本为 v0.8.1面向需要快速搭建 WebRTC 流媒体服务的开发者和运维人员。该程序基于开源项目构建可用于浏览器实时音视频流传输、媒体服务器中继以及多端信令交互等场景适合正在学习 WebRTC 协议栈或期望在本地环境验证流媒体方案的读者。压缩包共 94 个文件约 7.29MB主要包括 webrtc-streamer.exe 可执行文件、HTML 前端预览页面、JS 控制脚本、CSS 样式以及 wasm 和 tfjs、posenet 等模型文件便于直接运行并查看效果。目前已有 846 人学习下载。借助该资源读者不必自行编译源码即可在 Windows 上继续体验 WebRTC 通信流程深入理解对等连接、ICE 穿透、媒体传输等关键机制同时可结合内置前端页面和模型资源进行二次开发与功能扩展。 浏览器里直接看 RTSP 监控画面很多从安防转 Web 的人都被这一件事折磨过IP 摄像头的视频流不是网页能直接吃进去的格式也不是随便一个 HTTP 地址就能扔给video标签。我最早做过 Flash 方案后来转过 HLS延迟好几秒回调控球的时候跟看幻灯片一样。真正让我从这套破局里解脱出来的是一个叫webrtc-streamer的开源代理。它把 RTSP、HTTP、文件这类视频输入在服务端用 GStreamer 组装成 WebRTC 会话推到浏览器里靠原生 RTCPeerConnection 播放。你不需要自己写复杂的信令协议也不用先转码成 HLS 再让人忍受延迟非常适合做安防监控、家居摄像头、园区大屏这类需要低延迟实时画面的中小型项目。1. 浏览器为什么“听不懂”RTSP它和 webrtc-streamer 之间的分工1.1 专业设备和浏览器之间天然有一道语言鸿沟绝大多数 IP 摄像头出口是 RTSP底层默认走 UDP用 RTP 封装 H.264 或 H.265 视频流。浏览器却完全没内置 RTSP 客户端video标签最多识别 HTTP 协议栈下的分片流或者是 WebRTC 这种基于 DTLS/SRTP 的实时传输。换句话说两端都在说“视频”但协议栈完全对不上。webrtc-streamer做的事就是在服务端当一名翻译它负责从摄像头拉取 RTSP把里面的 RTP 包拆开、重组、解析出完整的 H.264 帧再通过 GStreamer 的webrtcbin打包成 WebRTC 媒体流最终通过 ICE 连接交给浏览器。这一连串动作里最核心的是要把 RTP 的时间戳、SSRC 同步、SPS/PPS 等参数都处理好。另外它还会把浏览器端的 ICE candidate 转发给摄像头? 这里需要强调一下媒体流本身由握手后的webrtcbin推过来webrtc-streamer不会简单地把 RTSP 地址暴露给前端。1.2 它并不等于转码服务器很多初接触的人把webrtc-streamer和 FFmpeg 转码服务器混为一谈这是个容易跑偏的理解。它的默认做法不是把视频先软编成另一种格式而是尽量保持原始编码能在 WebRTC 里直接封装传输。也就是说摄像头出来的是 H.264它给浏览器的大概率还是 H.264只是在传输封装层做了转换。这个定位决定了它的资源占用比“拉流成 MP4 再重新编码推出去”低得多。在树莓派或者工控机这种小机器上跑个几路 720p 实时流是有可能的如果做成转码服务器小机器早就被 CPU 干趴下了。当然不是说它完全不做编码类操作如果摄像头输出的是 MJPEG 或者某些怪异的私有格式GStreamer 侧会自动拉起对应的解码/编码模块这时候 CPU 才会明显上涨。1.3 与 Janus、Kurento 这类网关的区别做 WebRTC 流媒体服务器时不少人会先想到 Janus、Kurento、LiveKit 之类。它们功能很强但把 IP 摄像头接入流程做下来要理解不少概念房间、插件、信令端口、Redis 桥接等等。webrtc-streamer的思路是典型的小而美以“摄像头/视频源”为单位提供一组简洁的 HTTP API WebSocket 信令前端只需要一个几十 KB 的 JS 封装就能把画面渲染出来。如果你只是想快速把一路 RTSP 变成网页里低延迟的实时画面webrtc-streamer的学习成本是最低的。但如果要管理大量用户、做复杂房间逻辑你再去选 Janus 这类重量级方案更合理。它的定位边界很清楚不是通用的会议服务器而是“把摄像头拉进浏览器”的专用桥。2. 部署前先想清楚端口、网络模型和鉴权2.1 用容器跑最省事但别忘了端口不只是 8080官方提供了预编译包和 Docker 镜像多数时候我用 Docker 起服务最省事docker run -d --namewebrtc-streamer \ -p 8080:8080 \ -p 8081:8081 \ -p 8443:8443 \ mpromonet/webrtc-streamer:v0.8.78080 是默认 HTTP 端口8443 是 HTTPS如果要调起摄像头麦克风浏览器要求安全上下文就必须上 HTTPS8081 一般用于内部调试或某些共享实例。如果只在局域网里用不想折腾证书8080 就够但一旦要走公网建议直接用 Caddy 或者 Nginx 做一层反向代理把 HTTPS 和基础鉴权一起解决。需要注意容器默认暴露的端口里媒体流还是通过 WebRTC 协商出来的 UDP 端口发送。如果服务跑在 Docker 里务必把所需的 UDP 端口范围映射出来否则前端的 ICE 连接会出现候选地址不匹配、一直连不上的情况。具体端口范围在不同版本里有变化稳妥做法是先把容器网络设为 host 模式验证整个链路通不通再回头细化端口映射。2.2 STUN 和 TURN同一局域网经常不需要跨公网绕不开画出 WebRTC 连接链路后会发现浏览器并不是直接去连摄像头的 RTSP 端口而是先和webrtc-streamer所在服务器建立连接。局域网内浏览器和服务器大概率处于同一网段IP 直接可达这时候不配 STUN 也能跑通我甚至在内网调试时见过没配任何 ICE Server 依旧零配置出画面的情况。但到了公网尤其是浏览器和服务器分别处于不同 NAT 后面时就必须有 STUN/TURN。STUN 的作用是让双方发现自己的公网映射地址TURN 则是在 P2P 直连失败时做媒体中继。webrtc-streamer本身不会实现 TURN 服务器逻辑它只是把 iceServer 列表透传给前端。实战里我会用 coturn 补一个 TURNturnserver -a -f -r myrealm \ --user user:password \ --cert /etc/turn_server_cert.pem \ --pkey /etc/turn_server_pkey.pem然后在调用webrtcstreamer.js时把 TURN 地址传进去。很多人以为接上 STUN 就能跨网结果 getStats 一看全是relay候选流量全砸在 TURN 上那才叫踩坑。2.3 默认没有页面鉴权别裸奔公网webrtc-streamer本身没有像样的登录系统它默认暴露一个控制页任何人都可以查询视频源列表、发起 RTSP 请求。这一点必须警惕如果你把 RTSP 地址直接写在页面上再裸扒 8080 端口到公网等于把摄像头的地址和账号都送出去了。我的做法是RTSP 地址不写死在前端而是通过后端接口动态下发鉴权通过后才返回用反向代理加 Basic Auth 或者 OAuth挡住陌生访问对外只暴露 443/HTTPS不暴露 8080在摄像头侧尽量创建只读专用账号不给写权限或者 PTZ 控制权限。这一块别图省事安全不过关的监控系统上线的第二天就可能被人扫到首页。3. 一次视频请求的完整信令握手从 call 指令到画面渲染3.1 前端不是直接把 RTSP 塞给video很多人第一次看webrtc-streamer项目的 Demo会误以为它有个隐藏播放器。实际上前端的流程是页面用webrtcstreamer.js创建 WebRtcStreamer 实例然后调用call()方法把摄像头 URL、本地 video 标签、STUN/TURN 配置传进去。它内部帮你完成了三件事创建RTCPeerConnection、和服务端的信令通道交换 SDP、把收到的MediaStream绑定到video的srcObject。下面是一段常见调用方式不同版本方法名略有差异但思路一致const streamer new WebRtcStreamer(http://127.0.0.1:8080); function startCamera() { streamer.call( rtsp://admin:password192.168.1.64:554/Streaming/Channels/101, camera1, stun:stun.l.google.com:19302 ); } function stopCamera() { streamer.hangUp(camera1); }中间那层信令你可以理解成浏览器不断和服务端交换“我能连这个地址吗”“我有哪些 ICE 候选”“我收到的 SDP 里面编码参数是什么”这类 JSON 消息。服务端收到call后会去主动连接摄像头并创建对应的 GStreamer pipeline再把生成的 SDP answer 返回给浏览器。整个交换走的是一个自定义的 WebSocket 协议这也算webrtc-streamer和标准会议系统最大的不同它不需要单独搭建 SIP 或者 XMPP 信令。3.2 握手里的关键参数H.264 profile 和 SDP 协商信令过程中浏览器和 GStreamer 侧会协商编码格式。绝大部分摄像头走 H.264但 H.264 里还有 Baseline、Main、High Profile 之分SDP 里通过profile-level-id字段描述。如果摄像头配置的是 High Profile而浏览器端某个内核版本对它的支持不完整画面可能黑屏。我遇到过的典型问题是部分老型号海康摄像头默认开启 H.265但浏览器侧对 H.265 的 WebRTC 支持并不稳定。这时候要么在摄像头后台把编码改成 H.264要么通过webrtc-streamer内部管线做转码。如果你只是临时调用不熟悉 GStreamer 参数最省心的方案还是先把摄像头输出格式改成 H.264。3.3 渲染阶段的副作用延迟和缓冲握手完成后浏览器本地会有一个MediaStream需要把它绑定到 Video 元素video.srcObject streamer.getMediaStream(camera1); video.play();这里我不建议把autoplay当万能钥匙因为浏览器对带声音且不是用户主动触发的自动播放有拦截策略。更稳的做法是让用户点击某个按钮后再启动或者直接声明muted属性后再play()。延迟方面WebRTC 本身是低延迟传输但 GStreamer 侧的 jitter buffer 和浏览器自身的播放缓冲都会引入几百毫秒的额外时延。监控场景 300~500ms 我觉得完全可以接受如果追求更低需要去调 GStreamer 的rtpjitterbuffer参数和latency设置这个后文会展开。4. 正式接入时最容易翻车的四个细节4.1 摄像头编码格式不一致导致黑屏或画面花掉这是所有坑里出现频率最高的。摄像机后台默认可能走 H.265、MJPEG、甚至私有编码。webrtc-streamer对 H.264 和 VP8/VP9 的支持比较成熟对 H.265 的支持看具体编译版本前端浏览器又有自己的限制。多路摄像头型号混用时A 品牌可以播放B 品牌黑屏多半是编码不一致。我的排查习惯是先在浏览器里打开webrtc-streamer的 debug 页看SDP里实际协商出来的codec是什么如果看到的是H264但还是黑屏再核对profile-level-id确认摄像头的 H.264 Profile 和浏览器解码能力是否匹配。最省事的兜底方案是到摄像头后台把所有通道输出编码统一改成 H.264 BaselineBaseline 兼容性最好代价是同等画质下码率会稍高一些。4.2 多个 PeerConnection 不释放端口被耗光在页面里反复切换摄像头、快速刷新页面容易在服务端留下大量未清理的 PeerConnection。尤其是前端代码没有在beforeunload或者组件的 Destroy 生命周期里调用hangUp()服务端会一直保留那些已经没用的 RTP 会话。我曾在客户现场碰到一台只跑 8 路摄像头的服务器第二天 CPU 不高但端口资源被耗尽新请求全部超时。后来梳理发现是前端切换页面时没有挂断旧流导致 GStreamer pipeline 一直存在。现在我在所有前端代码里强制绑定销毁逻辑window.addEventListener(beforeunload, () { Object.keys(cameraMap).forEach((key) { streamer.hangUp(key); }); });4.3 公网访问时 UDP 被 QoS 或防火墙限制只有 TURN 才能救很多宽带环境对 UDP 流量做了优先级限制尤其是跨运营商访问时。ICE 协商成功后浏览器选择走host、srflx还是relay候选直接决定能不能稳定出图。如果 getStats 里显示候选类型频繁切换或者出现大量stun请求超时就要考虑强制走 TURN 中继。webrtcstreamer.js支持传入多个 ICE Server我会把 STUN 和 TURN 都传进去并在业务层加入降级策略同网段或者局域网直接用host公网直接切relay避免 UDP 直连反复抖动。4.4 断网重连后不主动恢复WebRTC 连接一旦断开ontrack不会自己帮你重新建立会话。就算是摄像头断了几秒再恢复ICE 连接也不一定回到connected状态。如果出现这种“页面一片黑但服务端没有崩”的问题多数是因为前端没有做自动重连。我一般会做这样的保活机制const state streamer.getIceConnectionState(camera1); if (state failed || state disconnected) { streamer.hangUp(camera1); setTimeout(() reinitCamera(camera1), 2000); }再用一个定时器每 5 秒轮询状态状态异常时自动重建会话。注意重连要加退避避免摄像头恢复后一瞬间有几百路请求同时打到服务端。5. 并发上来了怎么办软解、硬编、码率控制的取舍5.1 先给摄像头侧做“瘦身”而不是无脑加机器很多项目一开始只想着提升webrtc-streamer所在服务器的性能却忽略了摄像头编码器设置。把摄像头输出改成固定码率CBR同时把 GOP/关键帧间隔设置成 2 秒左右能显著减少 WebRTC 传输时的码率波动。播放端出最怕的是瞬时码率尖峰导致 jitter buffer 频繁重建表现出来就是时不时卡顿半秒。摄像头侧一般可配置视频编码H.264 Baseline 或 Main Profile码率类型固定码率 CBR帧率15~25fps监控场景不需要 60fps关键帧间隔1~2 秒分辨率720p 或 1080p根据实际带宽决定这些设置在摄像头 Web 后台都能改。做并发调优时改摄像头参数往往比改服务器配置效果更大。5.2 观察服务端管线确认是不是转码拖垮了 CPU如果同样的 H.264 输入CPU 占用却高得离谱就要怀疑管线里是否偷偷发生了重编码。用htop能看到 GStreamer 进程吃满核心通常就是编码转换导致的。许多预编译版本会根据编译选项启用硬解如果你的机器有 Intel Quick Sync 或者 NVIDIA GPU可以在编译时开启对应的 GStreamer 插件让硬解码分担负载。在我实际测过的环境里同一路 1080p H.264软件解码 用 webrtcbin 直接推流CPU 占用并不算高但一旦摄像头输出 H.265GStreamer 需要软解 H.265 再以 H.264 推给浏览器CPU 立刻翻几倍。所以选型时就在设备采购清单里写明“要求支持 H.264 硬件编码”能省掉很多后续性能事故。5.3 多路并发时码率阈值和浏览器限制是两道坎一路 1080p 监控视频H.264 Main Profile 在 4Mbps 下就很清晰了。10 路就是 40Mbps这还没算 WebRTC 里rtcp、srflx/relay的额外开销。服务器出口带宽不够的话先卡住的是网络而不是 CPU。我习惯在接入前先算一下预估总码率 单路码率 × 并发路数 × 1.2如果单路 4Mbps计划开 20 路并发那预留带宽至少要到 100Mbps 才稳。否则一到早晚高峰就会有人反馈某个摄像头一直转圈。浏览器侧也有资源限制。同一个标签页里塞太多 video 元素或者一路页面打开太多 PeerConnection会让浏览器内存占用暴涨。我的建议是监控墙类页面优先做“按需开通”直播间只有切换到该路摄像头的画面时才建立连接列表页只显示静态截图不预加载所有实时流。5.4 多人同时看同一路流需要自己解决“共享频道”问题webrtc-streamer的工作方式决定了每个前端 PeerConnection 都会被当作一个独立会话。一路摄像头如果分别被 5 个浏览器观看服务端大概率会为每个浏览器建立对应的发送端管线资源和带宽线性增长。它不像某些商业平台那样天然支持“一路源万人看”的合流分发。如果只是三五个人同时看问题不大要做几十人同时直播监控墙就需要在这前面再套边缘分发或者把热门通道转成低延迟的 HLS/LL-HLS 给大并发观看保留一到两路 WebRTC 给需要低延迟操作的通道。这个架构上的取舍我在项目里一般会提前跟甲方讲清楚。6. 性能调优里藏着的几个小技巧6.1 端口复用和 host 网络模式Docker 部署时我强烈建议在负载小的内网环境直接使用--networkhost省去端口映射的痛苦。一旦跨公网访问UDP 端口映射没配全ICE 就会陷入“卡在 checking”的状态。如果确实要用 bridge 模式至少把常见 UDP 端口范围露出并且保持前后一致别让 TURN candidate 里的地址和实际端口对不上。6.2 调节 GStreamer 延迟和 jitter bufferwebrtc-streamer内部通过webrtcbin拉流在调优低延迟时可以把latency参数往下压。不过这个不是前端能随便改的一般需要修改启动参数或者重新编译。多数监控场景 300~500ms 延迟已经很能打了没必要为了极致低延迟牺牲抗抖动能力。6.3 写一个简单的 getStats 诊断脚本最后分享一个排查效率很高的小方法别靠“看画面卡不卡”来判断瓶颈。在支持 WebRTC 的浏览器控制台里调用const pc streamer.getPeerConnection(camera1); pc.getStats().then((stats) { stats.forEach((report) { if (report.type inbound-rtp) { console.log(report.framesDecoded, report.framesDropped, report.packetsLost); } }); });framesDropped过高说明解码侧不稳定packetsLost过高说明网络丢包比较严重framesDecoded长时间不变那大概率是拉流或者信令环节断了。这套数据比摄像头后台的码率统计更能反映真实的播放链路质量。我在实际项目里用这个脚本定位过不少疑难杂症最典型的一次是现场反馈“偶尔卡顿”服务器带宽、CPU 都正常结果 getStats 里显示packetsLost持续上涨最后发现是从接入交换机到服务器这段的 MTU 被改小了。这类问题光靠肉眼看画面永远都猜不到根因。如果你只是在家里看一两路摄像头webrtc-streamer是一条成本很低的轻量路线如果要在公网给几十上百人做并发就得把信令、流媒体、边缘分发拆开设计。但无论哪种规模我建议都先在自己的局域网里把摄像头编码、码率和网络链路理顺再上 WebRTC 调优。这样每一步踩到的坑都能用数据定位而不是靠玄学改配置。本文还有配套的精品资源点击获取