做安防项目的人大概都被同一件事折磨过:监控摄像头出来的RTSP流,不能在浏览器里直接打开,VLC能播、手机App能播,唯独网页这个最容易分发的终端成了老大难。之前试过把视频推成HLS,延迟高得离谱,声音和画面能差上好几秒;后来换WebRTC,又要自己搭信令服务器,维护成本蹭蹭涨。直到用了mpromonet/webrtc-streamer这个开源项目,配合Docker部署,一条命令就能把RTSP流变成浏览器直接播放的WebRTC直播流,实测延迟压到500毫秒内。这篇教程把我从零开始部署到解决各种坑的经验完整记录下来,不管你是Windows、macOS还是Linux,只要跟着一步步走,基本都能跑起来;如果你已经跑起来但播放不流畅,也建议直接跳到第6章看排查清单。
1. 为什么我最后选了mpromonet/webrtc-streamer
1.1 它解决的是浏览器播放RTSP的老大难
监控摄像头最通用的输出协议是RTSP,但浏览器对这个协议完全没有原生支持。你没法在HTML5的<video>标签里直接填一个rtsp://地址,Chrome、Edge、Firefox都没戏。传统的折中方案是拉取RTSP后转成HLS或者RTMP再播放,HLS虽然兼容性好,但切片缓冲把延迟推到了5到10秒,对监控和对讲场景基本不可用。RTMP则需要Flash,早就被浏览器淘汰了,现代项目里不会再碰。
webrtc-streamer的思路是:服务器端替你接住RTSP流,然后通过WebRTC协议投递给浏览器。浏览器端不需要任何插件,只要是一个支持WebRTC的现代浏览器,就能以低于1秒的延迟看到画面。它本质上是一个C++写的轻量流媒体代理,官方仓库mpromonet/webrtc-streamer在GitHub上已经维护了多年,很多安防、无人值守、远程巡视项目都在用,稳定性经过了不少真实环境的验证。
1.2 它和"动辄全家桶"的WebRTC网关有什么不同
如果你搜过WebRTC流媒体服务,大概率会看到Janus、mediasoup这些重量级项目。它们确实很强大,能处理复杂的SFU场景、万人会议、多房间管理,但部署复杂度也上了一个量级:依赖数据库、需要单独的信令服务、客户端SDK要花时间学习。对于"把一路RTSP变成网页可看"这种单点需求,它们属于杀鸡用牛刀。
webrtc-streamer定位很明确,就是轻、快、直接。它把信令服务、媒体代理、HTTP接口打包在一个容器里,启动后既提供WebRTC信令,也负责媒体流的拉取和转发。浏览器端可以直接用官方内置的HTML页面,也可以通过HTTP API和JavaScript库嵌入到自己的项目里。下面这张表格是我当时做选型时的对比:
| 方案 | 部署复杂度 | WebRTC能力 | 适合场景 | 上手时间 |
|---|---|---|---|---|
| webrtc-streamer | 低,单镜像即可 | 点对点投递,适合少量并发 | 监控视频上网页、远程巡视 | 半天 |
| Janus | 高,需编译+配置+数据库 | 完整SFU,支持大规模会议 | 视频会议、互动直播 | 一周以上 |
| mediasoup | 高,Node.js开发,需要自己写逻辑 | 纯SFU,能力灵活但全要自己搭 | 需要深度定制的复杂场景 | 两周以上 |
| 传统HLS转流 | 低,nginx+ffmpeg即可 | 无WebRTC,延迟高 | 对延迟不敏感的直播 | 一天 |
所以,如果你的核心诉求是"快速把摄像头画面搬到浏览器里,路径越短越好",webrtc-streamer几乎是最省事的选择。
1.3 为什么必须用Docker而不是源码编译
这个项目是C++写的,源码编译依赖一大堆库:FFmpeg、libnice、libsrtp、jsoncpp等等。我在Ubuntu上手动编译过一次,光拉依赖装环境就折腾了大半天,中途还遇到某个库版本不对导致编译失败,最后换成Docker镜像,十分钟就把服务跑起来了。Docker镜像里已经把所有的编译产物和运行环境封装好了,你不需要关心源码怎么编、缺什么依赖,只要拉镜像、跑容器、映射端口就能启动。而且Docker可以固定在某个镜像Tag,升级或回滚都很干净,不会在宿主机上留下一堆散落的库文件。
2. 部署前的环境准备,先把这三个坑排掉
2.1 Docker环境的检查与虚拟化报错
无论用Docker Desktop还是Linux原生的Docker Engine,部署这本项目前都得先确认Docker本身能正常工作。在终端执行:
docker version docker info如果docker version能输出客户端和服务端版本,说明Docker基本可用。我看到很多新手在Windows上装完Docker Desktop,打开就报“virtualization support not detected”或者“Virtualization support wasn't detected”,这一般不是Docker的问题,而是宿主机虚拟化环境没有开。后面第6章我会详细讲排查链路,这里先提醒你:Windows下务必确认BIOS/UEFI里的VT-x或AMD-V开启,Hyper-V、虚拟机平台、WSL2这几个Windows功能建议全部勾上。装完以后重启系统,再用docker version确认。
Linux上如果用的是老内核,某些模块没加载也会出问题,最常见的是overlay文件系统、netfilter相关模块,可以提前执行sudo modprobe overlay && sudo modprobe br_netfilter跑一遍,顺手加上docker compose插件。macOS相对省心,Docker Desktop一般装上就能用,但要注意如果电脑是M1/M2这类Apple Silicon芯片,镜像能不能跑取决于Docker Desktop的Rosetta兼容层,后面部署时最好指定--platform linux/amd64以防万一。
2.2 端口、防火墙和网络拓扑
webrtc-streamer默认用8000端口提供HTTP访问,8443提供HTTPS访问。Docker启动时如果只映射了8000:8000,那就只开了一个HTTP入口。这里有个容易忽略的细节:WebRTC的真正媒体传输走的是UDP,虽然webrtc-streamer的HTTP端口同时承担了一部分WebRTC信令和媒体协商的职责,但如果你的浏览器和服务器之间UDP被防火墙挡了,播放就会出现卡顿甚至完全黑屏。
所以部署前最好确认三条规则:
- 宿主机防火墙要放行TCP 8000/8443端口。
- 如果WebRTC媒体协商后走的是UDP传输,宿主机上要能转发UDP包。
- 容器和摄像头要能网络互通,尤其是摄像头在另一个网段或容器里访问不到时,需要检查路由和安全组。
最简单的方式是先在本地网络内测试,不开任何云安全组,等全部跑通再逐层收紧规则。
2.3 准备视频源:真摄像头和模拟流
部署完总得有一路源来测试。真摄像头通常给一个RTSP地址,例如:
rtsp://admin:password@192.168.1.100:554/Streaming/Channels/101如果手里没有摄像头,可以用FFmpeg搭一个本地模拟RTSP流。需要先装一个RTSP服务器,我用的是开源的rtsp-simple-server(现在叫mediamtx),然后执行:
ffmpeg -re -i test.mp4 -c copy -f rtsp rtsp://localhost:8554/test这样本地就有了一路rtsp://localhost:8554/test的流。需要注意的是,webrtc-streamer容器是独立网络命名空间,如果模拟流跑在宿主机上,容器里不能直接用localhost,要改成宿主机在局域网里的IP,或者使用Docker的host网络模式。最稳妥的办法是用docker run --network host,这样容器和宿主机共享网络,localhost就能直接访问模拟流。
3. Docker部署,从拉镜像到看到画面
3.1 最小启动命令
直接把官方镜像拉下来跑:
docker pull mpromonet/webrtc-streamer docker run -d --name webrtc-streamer -p 8000:8000 mpromonet/webrtc-streamer第一行拉取镜像,第二行以后台模式启动一个名为webrtc-streamer的容器,并把宿主机的8000端口映射到容器的8000端口。启动后访问http://localhost:8000,你会看到一个很简朴的网页,页面里通常包含一个流列表和输入框,把RTSP地址填进去,点连接,浏览器就会请求摄像头画面。如果一切顺利,页面很快会显示实时视频。
这个最小命令基本够用,但有个问题:容器停掉就自动删除了吗?其实不是,-d后台运行时容器还会保留。如果你不想让容器堆积,可以在第一次测试时临时加上--rm参数,容器一停就自动清理。我倒是建议在正式使用前,都给容器起一个稳定名字,方便后面看日志和改参数。
3.2 常用参数和目录挂载
这个镜像默认入口就是webrtc-streamer可执行文件,你可以在docker run时直接追加参数。比如要改HTTP端口:
docker run -d --name webrtc-streamer -p 8080:8080 mpromonet/webrtc-streamer --port=8080我一般会加上--restart unless-stopped,这样宿主机重启后容器能自动拉起来,适合部署在摄像头机房里长期跑。
如果希望容器内生成的日志持久化,或者需要挂载配置文件,可以在启动时加上-v参数。项目本身支持通过-v挂载配置文件,但不同版本参数略有差异,最稳妥的办法是先不带配置文件跑起来,再用docker exec -it webrtc-streamer webrtc-streamer --help看一眼当前镜像支持的参数列表。所谓“保姆级”不是把命令背下来,而是知道怎么在出问题时自己找到答案。
3.3 启动验证与日志观察
容器启动后,用三个手段验证是否正常:
docker ps # 看容器是否有“Up”状态 docker logs webrtc-streamer # 看启动日志有没有报错 curl http://localhost:8000/api/getCameraList # 请求一个HTTP接口,看是否有JSON返回/api/getCameraList是项目的一个简易接口,用来确认HTTP服务是否响应。如果curl有返回,说明HTTP层通了。日志里如果出现类似Receive timeout、Connection refused之类的内容,通常是后端拉流问题,而不是服务本身挂了。
我第一次部署时踩了个小坑:docker run后马上执行curl,结果连接拒绝,吓得以为没起来。后来发现是容器内部还在初始化,FFmpeg库加载需要几秒钟。所以验证时最好先docker logs看一眼,确认启动完成后再测。
4. 这一条直播链路到底是怎么打通的
4.1 浏览器不认RTSP,webrtc-streamer做了什么
我们平时说WebRTC直播,其实包含两条链路:第一,服务器拉取摄像头或视频源的RTSP流;第二,浏览器和服务器之间建立WebRTC连接,服务器把媒体数据推给浏览器。webrtc-streamer做的事情就是在这两条链路的中间做一个协议转换。它内部通过FFmpeg库识别并解码/转封装RTSP流,然后按照WebRTC的要求进行编码和打包,最终从服务器发出RTP/UDP包。
浏览器端不需要安装任何插件,因为WebRTC本身就是浏览器内置的能力。你在网页里调用了RTCPeerConnection,浏览器会自动完成本地的音视频采集、编码协商、网络打洞等工作。webrtc-streamer相当于是把摄像头那边的RTSP“吃”进肚子里,再用浏览器听得懂的WebRTC语言吐出来。
4.2 信令、SDP和ICE:三个不可绕开的概念
WebRTC最让人头疼的是信令(Signaling)过程。浏览器和服务器要建立连接,得先交换双方的音视频能力、网络地址、加密指纹等信息,这些信息放在SDP(Session Description Protocol)里面。webrtc-streamer用HTTP/WebSocket接口实现了这套信令,浏览器端加载一个JavaScript库后,会自动和服务器交换SDP。
交换完SDP后,双方开始进行网络探测,这一步叫ICE。ICE的作用是找出所有可用的网络路径:直连、中继、NAT穿透等等。如果浏览器和服务器在同一个局域网内,ICE会走一条很短的主机路径,延迟最低;如果跨越公网,就需要STUN/TURN服务器辅助。webrtc-streamer内置了基本的ICE处理流程,但对公网环境下的TURN支持比较有限,所以如果跨网络延迟很高,更推荐用Nginx或者其他网关先打通网络再接入。
4.3 H.264直通 vs 转码的取舍
一条摄像头RTSP流能不能被webrtc-streamer高效转发,很大程度取决于视频编码格式。H.264是浏览器和WebRTC都支持的主流格式,如果源流是H.264,webrtc-streamer可以尽量做“直通”转发,也就是不重新编码,直接把已经压缩好的H.264数据封装进WebRTC的RTP包。这种方式CPU开销很小,延迟也最低。
但如果摄像头输出的是H.265(HEVC),很多浏览器默认不支持,WebRTC对H.265的兼容性也差,webrtc-streamer可能就需要转码。转码意味着FFmpeg要解码再重新编码,CPU占用成倍增长,延迟也会增加。我实测过一台普通4核服务器,直通H.264可以同时转发多路1080P,但转码一路4K就快被吃满了。所以如果方案还没锁定摄像头型号,优先选带H.264编码输出的型号,会省很多心。
5. 浏览器端接入:先快速体验,再写页面
5.1 打开内置页面,填入RTSP地址
镜像内置了一个HTML页面,路径一般是/stream.html。比如你是本机部署,直接访问:
http://localhost:8000/stream.html?url=rtsp%3A%2F%2Fadmin%3Apassword%40192.168.1.100%3A554%2FStreaming%2FChannels%2F101注意URL需要编码,密码里的特殊字符如果不编码,服务器解析会出错。如果你不想手动编码,直接访问http://localhost:8000/stream.html,页面上提供一个输入框,把原始的RTSP地址粘贴进去也行。
这个内置页面虽然简陋,但非常适合调试。输入地址后观察视频能不能出,如果这里都出不来,等会儿写自己的页面大概率也出不来。
5.2 用iframe嵌入自己的后台系统
很多人的需求是把自己内部的监控后台里嵌入实时画面。最快的办法不是引入JavaScript SDK,而是用iframe嵌套内置页面:
<iframe src="http://your-server:8000/stream.html?url=..." width="800" height="450" scrolling="no" frameborder="0"></iframe>这种方式简单粗暴,但有明显的缺点:地址栏直接暴露了RTSP URL,而且内置页面的样式和你自己的后台往往不搭。所以正式集成时,我建议使用官方JavaScript库webrtcstreamer.js。它在镜像的/webrtcstreamer.js路径下可以加载,还需要一个HTML5<video>标签。大致代码结构如下:
<script src="http://your-server:8000/webrtc-streamer.js"></script> <script> var webRtcStreamer = new WebRtcStreamer(video, 'http://your-server:8000'); webRtcStreamer.connect('rtsp://admin:password@192.168.1.100:554/Streaming/Channels/101'); </script>官方API在不同版本之间有小改动,使用前最好在浏览器开发者工具的Console里打印一下WebRtcStreamer对象,确认当前版本支持的构造函数方式。对于大多数人来说,把这些代码抄下来,再配合Angular或者Vue的生命周期函数去调用connect和disconnect,就能嵌进自己的项目。
5.3 接入HTTPS的坑
如果你是在http://localhost下调试,一切正常。但一旦部署到服务器,通过IP访问,或者域名是http,Chrome在获取麦克风/摄像头权限时就会比较严苛。WebRTC采集本地音视频时需要getUserMedia权限,浏览器通常要求安全上下文(HTTPS或localhost)才能调用。如果你只需要看摄像头画面,不采集本地麦克风,问题还不大;一旦需要双向语音,就要保证页面在HTTPS环境下运行。
生产环境建议用Nginx反代一层,把对外端口设为443并配置证书,然后反向代理到容器内部的8000端口。Nginx配置大致是:
server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里的location /会代理所有HTTP和WebSocket请求,webrtc-streamer内置页面和信令请求都能通。跑起来后浏览器访问https://your-domain.com/stream.html就可以了,不会再有权限弹窗的麻烦。
6. 常见问题排查,都是实测踩过的
6.1 Docker Desktop虚拟化检测不到
这个问题在Windows上出现频率极高,我帮两个同事处理过。现象是Docker Desktop图标启动了,但几秒钟后弹窗:“Docker Desktop failed to start because virtualisation support wasn't detected.” 排查链路从硬件到软件层层往下:
- 确认CPU虚拟化开关。重启电脑进BIOS/UEFI,找到“Intel Virtualization Technology”或者“SVM Mode”,设为Enabled。
- 打开Windows功能。进入“控制面板 -> 程序 -> 启用或关闭Windows功能”,勾选“Hyper-V”、“虚拟机平台”、“适用于Linux的Windows子系统”。
- 以管理员身份打开PowerShell,执行
bcdedit /set hypervisorlaunchtype auto,然后重启。 - 如果电脑本身就是一台云服务器或虚拟机,嵌套虚拟化往往不支持,Docker Desktop在本地虚拟机里会大概率启动失败。这种情况考虑换物理机,或者直接在云服务器上的Linux里装原生的Docker Engine。
装完以后重新运行Docker Desktop,等托盘图标变绿,再执行docker version看服务端信息。
6.2 容器起来了,但8000端口打不开
docker ps明明显示容器是Up状态,但浏览器访问http://localhost:8000一直超时。排查时先按顺序检查:
docker logs webrtc-streamer docker port webrtc-streamer curl http://127.0.0.1:8000/api/getCameraList如果docker port显示映射结果为0.0.0.0:8000->8000/tcp,说明端口映射没问题。这时候先看是不是容器内部服务没起来,日志里有没有启动异常。如果日志干干净净但依然不通,考虑防火墙。Windows下打开“高级安全Windows Defender防火墙”,新建入站规则,放行TCP 8000端口;Linux下执行sudo ufw allow 8000。macOS一般在系统设置隐私与安全里检查。
还有一种少见但很坑的情况:宿主机上恰好有别的服务也占用了8000端口。Docker启动时不报错,但映射失败或冲突,这时候先netstat -anp | grep 8000确认端口归属。
6.3 RTSP流在VLC正常,webrtc-streamer却拉不动
这是最常见的“能播但拉不动”问题。VLC能播放,只证明网络里有一路RTSP流;但webrtc-streamer容器去访问时,走的是另一条路径。可能原因有三个:
第一,RTSP URL里的特殊字符没编码。比如密码是abc@123,@会把URL解析到主机名位置,越传越乱。要对用户名密码做百分号编码,比如把@转成%40。
第二,网络隔离。容器和摄像头不在同一网段,或者主机防火墙限制了来源IP。最简单的验证方法是在容器里执行:
docker exec -it webrtc-streamer bash apt update && apt install -y vlc但容器里往往没有调试工具,也可以改用ffmpeg测试:ffmpeg -i rtsp://... -t 5 out.mp4,如果能出来文件,说明容器到摄像头的网络是通的。
第三,RTSP传输协议问题。很多摄像头默认用UDP传输RTSP,但防火墙对UDP不友好,容器内尝试TCP传输反而更稳定。webrtc-streamer在启动参数或配置里可以指定RTSP传输方式,但不同版本写法不同,建议先看日志里的FFmpeg报错。日志如果出现Connection timed out,大概率就是UDP被防火墙丢了。
6.4 页面一直黑屏,或播放几秒后卡住
黑屏和卡顿要区别对待。黑屏说明播放器组件没拿到正确的视频流,先确认两点:浏览器控制台有没有报错,服务端日志有没有反馈媒体状态。正常情况下webrtc-streamer页面打开后,会有一段时间建立WebRTC连接,视网络和CPU情况,一般1到3秒就出画面。超过5秒没画面,多半是媒体协商卡住了。
播放几秒后卡住,常见原因有两种:摄像头码率太高,服务器转发不过来,CPU被占满;或者是网络不稳定,UDP丢包严重。WebRTC本身有拥塞控制,会根据丢包动态降低码率,但如果网络抖动太激烈,画面就会花屏或卡死。这种情况的临时手段是降低摄像头编码码率、降低分辨率,或者把摄像头改成H.264 baseline profile。长期方案是确保浏览器和服务器之间的UDP路径畅通,尽量别让TURN中继参与,能直连就直连。
6.5 自签证书和混合内容导致的播放失败
如果你用Nginx反代后只配了HTTPS,但页面里加载的webrtc-streamer.js地址仍然是http://,浏览器会拦截“混合内容”,JavaScript无法执行。排查方法是打开浏览器开发者工具,切到Console页,看到类似Mixed Content: The page at 'https://...' was loaded over HTTPS, but requested an insecure resource 'http://...',就是这个问题。把脚本地址改成HTTPS,或者直接用相对路径/webrtc-streamer.js即可。
如果用了自签证书,浏览器也会警告甚至阻止WebRTC连接。本地调试可以在浏览器里点“高级”继续访问,但生产建议用免费的Let's Encrypt证书,而不是自签名。
7. 生产环境里我再补几个操作
7.1 加个restart策略,让容器自己活下来
开发调试时无所谓,但部署到现场后,如果设备断电重启,容器拉不起来就很麻烦。所以正式运行的容器一定要加重启策略:
docker run -d --name webrtc-streamer \ --restart unless-stopped \ -p 8000:8000 \ mpromonet/webrtc-streamerunless-stopped意味着即使Docker守护进程重启,只要不是手动docker stop,容器就会自动拉起来。这对现场无人值守的场景特别实用。
7.2 资源限制与监控
WebRTC转发是CPU密集型任务,如果同一台机器上还跑着别的东西,最好给容器限制资源。Docker提供了--cpus和--memory参数:
docker run -d --name webrtc-streamer \ --cpus=2 --memory=1g \ -p 8000:8000 \ mpromonet/webrtc-streamer这样容器最多用2个CPU核和1G内存,避免某个流量高峰把宿主机拖垮。监控方面可以用docker stats webrtc-streamer实时查看CPU和内存占用。我在一个中型项目里用这种方式限制到1核,单路720P H.264直播时CPU占用在30%左右,表现很稳定。
7.3 反向代理和真正的HTTPS
前面已经写了用Nginx反代的配置,这里再强调两个细节。第一,webrtc-streamer会用到WebSocket信令,Nginx代理时需要设置Upgrade头,否则页面会一直停留在“连接中”状态。第二,如果容器暴露了多个端口(8000和8443),建议统一由Nginx代理443端口对外,避免直接暴露8000端口引来扫描和攻击。
安全方面,生产环境一定要给RTSP地址加访问控制,不要让未认证的前端页面直接把摄像头地址传给用户。可以在自己的后端做一个转发逻辑:前端请求自己的接口,后端再向webrtc-streamer发起拉流,RTSP地址始终保存在服务端。这样即使页面被爬虫拿到,也不会泄露摄像头内网地址。
按这个路径把容器跑起来后,我实际测试下来,从填入RTSP地址到出画面基本在2秒内,本地局域网延迟只有两三百毫秒。如果你部署中遇到比上面更诡异的现象,我的建议是先把问题分解成容器层、网络层、WebRTC协商层三层,逐层用日志和curl排除。这个项目本身不复杂,大部分坑都出在环境上,而不是代码里。