ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

树莓派+WebRTC+Node.js搭建低延迟远程监控,5步搞定公网视频流

树莓派+WebRTC+Node.js搭建低延迟远程监控,5步搞定公网视频流 前一阵朋友让我在老家装一套远程监控看鸡舍里的情况。市面上的云摄像头倒是不贵但视频得过一遍别人的服务器画质被压缩得厉害延迟也没谱每个月还要会员费。折腾过树莓派的人都懂这种活完全可以自己干。所以我就拿树莓派4B配合WebRTC搭了一套远程监控视频流直接点对点传回浏览器不经过任何第三方平台延迟低到可以实时操作关键是Node.js做信令中转这个环节特别顺手。这篇文章我就把整套方案拆开揉碎讲清楚5步搞定公网视频流传输顺便把Node.js配置过程中踩过的坑也一并交代了。无论你是想给家里做个看护猫狗的摄像头还是想给老家的鱼塘装个远程值班员这套方案都能直接抄作业。1. 方案选型与整体架构为什么偏偏是WebRTC1.1 远程监控常见方案对比RTSP、MJPEG、WebRTC做远程监控摆在面前的路不止一条最常被人提起的就是RTSP、MJPEG over HTTP还有本文的WebRTC。先把三者的差异摆出来看你就知道该怎么选。方案延迟穿透能力部署复杂度浏览器兼容性费用RTSP300ms~1s差需额外做TCP转发中需要流媒体服务差浏览器不原生支持免费但公网分发麻烦MJPEG1~3s中HTTP可穿透低一个HTTP服务即可好img标签就能放免费但带宽消耗极大WebRTC100~300ms强STUN/TURN自动协商中偏高需要信令服务器极好Chromium/Firefox/Safari均支持信令服务器可自建免费我做这套方案时直接就把RTSP排除了原因很简单浏览器不原生支持RTSP要么装插件要么写一套转发服务把RTSP转成HLS或WebSocket流等于自己造一个平台。MJPEG虽说实现最粗暴一个HTTP接口就能出图但它每一帧都是一张完整JPEG图片720p分辨率下每秒20帧带宽轻松跑到80Mbps以上公网环境根本不现实。WebRTC的核心理念是“端到端直连”视频数据尽量走点对点通道不经过服务器中转延迟能做到100毫秒量级。再加上它内置ICE框架来处理NAT穿透只要有一方能触达对方链路就能建立起来。这套机制放在摄像头场景里实在太合适了尤其适合“树莓派在老家人在城市”这种跨网络监控需求。1.2 整体架构拆解Node.js在这个项目里干了什么活先看整体架构我把每个模块的角色画一遍树莓派4B上挂一个CSI摄像头负责采集画面H.264硬件编码后交给WebRTC推流端推流端把编码后的视频帧通过RTP包发送到浏览器浏览器端有一个标准WebRTC播放器收到RTP包解码并渲染到video标签Node.js在这个链路里承担信令服务器和静态页面托管两项职责负责转发SDP和ICE候选很多人看到WebRTC以为是纯P2P认为不需要服务器其实这是个误解。WebRTC建立连接前双方需要交换一份“连接的暗号”——SDP里面包含编解码能力、IP地址、端口等信息。这个暗号的交换就需要一台信令服务器来中转而我的信令服务器就是用Node.js写的基于WebSocket协议实现。为什么选Node.js做信令因为它天生就是异步事件驱动的WebSocket连接动辄几十上百个并发Node.js毫不费力而且npm生态里ws、express这些库非常成熟几十行代码就能搭出一个可靠的信令中转服务。再加上后续要提供静态页面给浏览器播放Node.js一把梭一个进程全搞定不用额外再搞Nginx。1.3 树莓派4B到底扛不扛得住WebRTC推流树莓派4B搭载的是博通BCM2711处理器四核Cortex-A72主频1.5GHz内存有2GB/4GB/8GB可选。跑WebRTC推流需要同时做视频编码和网络传输很多人担心它吃不消。我实测的情况是分辨率控制在1280x720、帧率20fps、H.264硬编解码的情况下CPU占用长期维持在40%左右内存占用不到1GB完全没问题。关键点在于视频编码必须走树莓派自带的硬件编码器也就是利用GPU模块里的H.264编码单元CPU几乎不参与编码计算。软件编码x264在树莓派4B上跑720p 30fpsCPU会直接飙到80%以上画面稍微复杂一点就会掉帧。所以我在整个方案里反复强调一定用硬件编码把CPU资源留给信令服务和系统调度。当然如果推流分辨率上到1080p、帧率30fps树莓派4B也能跑但延迟会明显增加。我的建议是720p起步画质清晰度和带宽消耗之间是性价比最优的选择。2. 开工前准备系统、摄像头、Node.js环境一次性配齐2.1 树莓派系统烧录与摄像头开启树莓派4B的系统我用的是Raspberry Pi OS Bullseye的64位版本。选择64位系统不是因为性能有多少提升而是Node.js和gstreamer这类软件对arm64的支持明显比armhf更完善很多预编译包能直接安装省去手动编译的麻烦。烧录系统推荐用Raspberry Pi Imager选好镜像、SD卡、写入即可。烧录完成后第一次开机前建议在SD卡的boot分区里提前放一个ssh空文件这样就能通过网络SSH上去操作不需要额外接显示器和键盘。如果你是第一次玩树莓派先把系统跑起来、连上Wi-Fi、拿到IP这些基础步骤一定要先熟练。摄像头我用的树莓派官方CSI接口IMX219摄像头模块。接上排线后进入系统先执行sudo raspi-config在Interface Options里把Camera启用然后重启。重启之后验证摄像头能不能正常出图libcamera-hello -t 5000这个命令会在屏幕上显示5秒摄像头实时画面。如果用的是无桌面版系统可以加一个--qt-preview参数强制用Qt窗口预览或者干脆输出一张静态图验证libcamera-still -o test.jpg2.2 Node.js的正确安装方式与版本坑位Node.js版本选择这里有一个大坑我劝你一定不要用apt直接装。树莓派官方源里的Node.js版本非常老某些还在12.x甚至10.x而WebRTC相关的npm包比如wrtc、mediasoup对Node版本的要求越来越高老版本根本跑不起来。我建议用nvm安装先把版本管理握在自己手里curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash安装完成后重新登录SSH让nvm生效然后安装Node.js 18 LTSnvm install 18 nvm use 18 node -v为什么选Node 18而不是最新的20或21因为我踩过一个坑某些依赖原生模块的npm包版本更新滞后在Node 20上编译直接报错而在Node 18上一切正常。LTS版本稳定性好生态兼容性强做服务端程序没必要追新。2.3 依赖清单与安装顺序这套系统真正需要的依赖分两块一块是npm层面的一块是系统层面的。系统依赖主要给gstreamer WebRTC推流用先安装它们非常关键顺序不能反sudo apt update sudo apt install -y build-essential python3 git libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-tools gstreamer1.0-libav python3-pipnpm层面的依赖就简单多了后面在项目目录里执行npm install express ws即可。如果你决定用Python作为推流脚本语言后面会细说再补一个WebSocket客户端库pip3 install websocket-client这里还有一个Python虚拟环境的选择问题。树莓派系统自带的Python 3是系统级的直接pip安装包有时候会碰到外部管理环境限制。我的建议是建一个虚拟环境python3 -m venv ~/envs/webrtc source ~/envs/webrtc/bin/activate pip install websocket-client虚拟环境的好处是依赖隔离后面重装系统也不用担心污染全局环境。3. 保姆级实操5步远程监控完整落地3.1 第一步编写Node.js信令服务器与页面托管先创建项目目录结构~/remote-cam/ ├── cert/ │ ├── key.pem │ └── cert.pem ├── public/ │ ├── index.html │ └── client.js ├── sender.py ├── server.js └── package.json初始化项目并安装依赖mkdir ~/remote-cam cd ~/remote-cam npm init -y npm install express ws然后编写信令服务器核心代码server.js。这里我把逻辑简化成一个最小可运行版本它的职责有三个托管public目录下的静态页面、承受WebSocket连接、在连接之间转发消息。const express require(express); const https require(https); const WebSocket require(ws); const fs require(fs); const app express(); app.use(express.static(public)); // HTTPS服务浏览器使用getUserMedia/WebRTC必须用安全上下文 const server https.createServer({ key: fs.readFileSync(__dirname /cert/key.pem), cert: fs.readFileSync(__dirname /cert/cert.pem) }, app); const wss new WebSocket.Server({ server }); // 核心信令转发除了消息来源自己广播给所有其他连接 wss.on(connection, ws { console.log(client connected, total:, wss.clients.size); ws.on(message, data { const msg JSON.parse(data.toString()); console.log(relaying message type:, msg.type); wss.clients.forEach(client { if (client ! ws client.readyState WebSocket.OPEN) { client.send(JSON.stringify(msg)); } }); }); ws.on(close, () { console.log(client disconnected, total:, wss.clients.size); }); }); server.listen(8443, () { console.log(HTTPS server is running on port 8443); });看到这你可能觉得简单信令服务器本质上就是一个“消息邮局”谁有话要说邮局帮忙送给对方。真正关键的是推流端和浏览器端如何处理这些消息。HTTPS证书怎么来WebRTC里的getUserMedia浏览器获取媒体流的API只有在HTTPS安全上下文中才允许使用所以必须配证书。本地测试可以用自签名证书一条命令生成mkdir cert cd cert openssl req -x509 -newkey rsa:2048 -nodes \ -keyout key.pem -out cert.pem -days 365 \ -subj /CNremote-cam.local注意自签名证书浏览器会警告但本地测试点“继续访问”就能绕过。公网部署如果自己域名建议用Let’s Encrypt申请免费证书自动化续期也很成熟。3.2 第二步树莓派视频采集与WebRTC推流推流端的核心任务是把摄像头画面变成WebRTC视频流发送给浏览器。这里我用gstreamer webrtcbin来实现。gstreamer是一个跨平台的流媒体框架webrtcbin是它的WebRTC模块支持直接对接浏览器的RTCPeerConnection。第一步先验证摄像头采集链路能跑通。在树莓派上执行gst-launch-1.0 libcamerasrc ! \ video/x-raw,width1280,height720,framerate20/1 ! \ videoconvert ! v4l2h264enc ! h264parse ! \ video/x-h264,stream-formatavc,alignmentau ! \ fakesink这条命令的意思是从摄像头采集720p原始画面转换为H.264编码然后丢掉。如果能正常跑不报错说明摄像头到编码器这条链路是通的。注意v4l2h264enc是树莓派硬件H.264编码器的v4l2接口这就是前面说的硬件编码。然后编写推流脚本sender.py。为什么用Python而不用Node.js直接推流因为gstreamer在Node.js里的绑定支持比较有限而Python的gi可以提供完整的gstreamer接口代码量最小、最稳定。Node.js负责信令和页面Python负责推流分工明确。核心代码片段如下import json import threading import gi gi.require_version(Gst, 1.0) gi.require_version(GstWebRTC, 1.0) gi.require_version(GstSdp, 1.0) from gi.repository import Gst, GstWebRTC, GstSdp import websocket Gst.init(None) # 创建gstreamer管道摄像头 - 编码 - WebRTC pipeline_str ( libcamerasrc ! video/x-raw,width1280,height720,framerate20/1 ! videoconvert ! v4l2h264enc ! h264parse ! video/x-h264,stream-formatavc,alignmentau ! webrtcbin namewebrtcbin ) pipe Gst.parse_launch(pipeline_str) webrtcbin pipe.get_by_name(webrtcbin) # 会话描述生成后的回调 def on_offer_created(promise, user_data): reply promise.get_reply() offer reply.get_value(offer) promise Gst.Promise.new() webrtcbin.emit(set-local-description, offer, promise) # 把offer SDP通过WebSocket发给信令服务器 ws.send(json.dumps({ type: offer, sdp: offer.sdp.as_text() })) # 连接WebSocket信令服务器接收浏览器的answer和ICE候选 def on_ws_message(ws, message): data json.loads(message) if data[type] answer: sdp GstSdp.SDP.new_from_text(data[sdp]) answer GstWebRTC.WebRTCSessionDescription( GstWebRTC.WebRTCSDPType.ANSWER, sdp) webrtcbin.emit(set-remote-description, answer) elif data[type] candidate: # 解析ICE候选并添加 webrtcbin.emit(add-ice-candidate, data[candidate]) ws websocket.WebSocketApp( wss://localhost:8443, on_messageon_ws_message )关键点在于推流端连上信令服务器后要主动创建一个offer通过set-local-description把自己这边的SDP存下来然后发给浏览器。这个offer相当于推流端递出的一张“名片”写着“我在这我的编码能力是这些请来连接我”。3.3 第三步浏览器播放端完整实现浏览器端的核心文件是public/index.html和public/client.js。HTML部分我写得尽量简洁一个video标签搞定!DOCTYPE html html head meta charsetutf-8 title树莓派远程监控/title /head body h1树莓派 WebRTC 远程监控/h1 video idremoteVideo autoplay controls muted stylewidth:100%;max-width:960px/video button idstartBtn开始连接/button script src/client.js/script /body /htmlclient.js里是WebRTC的完整逻辑const video document.getElementById(remoteVideo); const startBtn document.getElementById(startBtn); let pc null; let ws null; function createPeerConnection() { pc new RTCPeerConnection({ // STUN服务器用于NAT穿透这里用谷歌公共STUN生产可以自建 iceServers: [ { urls: stun:stun.l.google.com:19302 } ] }); pc.ontrack event { video.srcObject event.streams[0]; }; pc.onicecandidate event { if (event.candidate) { ws.send(JSON.stringify({ type: candidate, candidate: event.candidate })); } }; } function connect() { ws new WebSocket(wss:// window.location.host); ws.onopen () { // 通知信令服务器浏览器已加入让推流端开始协商 ws.send(JSON.stringify({ type: join, role: watcher })); }; ws.onmessage async event { const data JSON.parse(event.data); if (data.type offer) { createPeerConnection(); await pc.setRemoteDescription(data.sdp); const answer await pc.createAnswer(); await pc.setLocalDescription(answer); ws.send(JSON.stringify({ type: answer, sdp: pc.localDescription })); } if (data.type candidate) { if (pc) { await pc.addIceCandidate(data.candidate); } } }; } startBtn.addEventListener(click, connect);这里要讲清楚整个信令交互的过程很多初学者在最开始就在这里卡住。推流端和浏览器端建立连接的过程分为三步。第一步是“暗号交换”推流端生成offer SDP通过信令服务器发给浏览器。第二步是“回应暗号”浏览器收到offer后生成answer SDP回传给推流端。第三步是“铺路”双方各自通过STUN服务器发现自己的公网地址这些地址被打包成ICE候选互相发给对方然后尝试建立一条直达的数据通道。我拿相亲来打比方offer就是一方递出自我介绍“我家住哪、怎么联系我”answer就是对方也递出自己的联系方式ICE候选则相当于“我这边有多个电话号码你挨个打总能打通一个”。信令服务器全程只充当传话的人视频数据本身不走它这条线。3.4 第四步本地链路联调看到画面才算数整套系统第一次联调我的建议是先不碰公网在同一个局域网内跑通。这样能最快排查是不是WebRTC链路的问题把变量缩小到最小范围。首先启动Node.js信令服务器cd ~/remote-cam node server.js然后启动Python推流脚本source ~/envs/webrtc/bin/activate python sender.py最后打开浏览器访问https://树莓派局域网IP:8443点“开始连接”。如果一切正常几秒内就能在页面上看到摄像头画面。这里有一个局域网联调容易翻车的点浏览器必须信任自签名证书。我在这一步反复折腾了很久最终的经验是先用浏览器直接访问一次HTTPS地址手动点一次“高级→继续前往”把证书信任状态记录下来再接WebSocket就不会被拦了。如果画面一直出不来按顺序做这几件事排查看Node.js控制台有没有在转发消息如果没有任何转发日志说明WebSocket连接都没建立看Python脚本有没有输出offer发送成功的日志如果卡在创建offer大概率是gstreamer pipeline里摄像头链路有问题看浏览器开发者工具的Console有没有报错如果报InvalidStateError说明SDP交换时序有问题3.5 第五步公网发布与安全加固局域网能看画面之后接下来就是公网访问。这步涉及两个核心问题一是让公网的浏览器能访问到树莓派上的信令服务和页面二是让两边能够建立WebRTC点对点连接。第一个问题信令服务器和页面托管在同一个HTTPS端口上所以只要把路由器上的公网IP端口8443转发到树莓派内网IP的8443端口即可。前提是你有一条公网IP的宽带线路。国内很多家用宽带有公网IP在路由器后台找到“端口映射”或“虚拟服务器”设置添加一条TCP 8443到树莓派IP的规则即可。第二个问题WebRTC点对点连接需要双方知道对方的公网地址。树莓派在家庭NAT后面通过STUN服务器可以发现自己的公网IP和端口映射然后把这个地址作为ICE候选发给浏览器。但要注意NAT映射有类型之分有些对称型NAT无法通过STUN自动穿透这时候就需要TURN服务器作为中继。我实测的经验是如果树莓派家庭宽带NAT是常见的锥形NATSTUN就能穿透成功如果两边都是对称NAT必须上TURN服务器。TURN服务器可以用coturn自建一台1核1G的云主机就能跑起来配置也不复杂。生产环境稳定起见我建议直接上coturn不依赖公共STUN。安全方面我强烈建议至少做三件事信令服务器增加Token鉴权浏览器连接时带一个预共享的token服务端校验通过才转发消息摄像头流本身加密传输WebRTC的SRTP默认就是加密的这一点是安全的树莓派系统层面开启防火墙只放行8443、22等必要端口Token鉴权的实现很简单在浏览器WebSocket连接时带上参数服务端解析后比对ws new WebSocket(wss:// window.location.host ?token你的密钥);服务端解析URL参数失败直接close连接几行代码的事但能挡住闲杂人等。4. 实测中的那些坑问题排查与调优实战4.1 常见问题速查表现象可能原因解决方案浏览器一直黑屏无画面SDP交换没完成或ICE没有候选人查看Server控制台是否转发offer/answer确认STUN能访问gstreamer启动报错缺少插件系统依赖安装不全按2.3节的apt命令完整安装gstreamer相关插件包Node.js启动时模块编译失败Node版本过新改用Node 18 LTS或降低npm包版本局域网能看公网看不了NAT类型限制STUN无法穿透部署coturn指定TURN服务器视频画面卡顿严重编码参数设置过高或带宽不足降低码率720p下控制目标码率2Mbps以内摄像头画面模糊或偏色聚焦没调好或白平衡问题用libcamera-still拍照检查调整摄像头位置4.2 最耗时的三个坑的详细复盘先说第一个坑也是我折腾最久的gstreamer pipeline里stream-formatavc这一个参数的丢失。如果不加这个参数webrtcbin生成的SDP里没有avc格式描述浏览器端会报“Unsupported codec”画面死活出不来。后来在gstreamer官方webrtcbin例子里看到这个参数加上之后立刻就好。记住这个参数它是H.264在WebRTC里的标准封装格式少了浏览器不认识流。第二个坑wrtc这个npm包在树莓派上的编译地狱。我最开始是想让Node.js直接承担推流工作安装了wrtc库结果树莓派上源码编译花了三个小时中途还把内存撑爆了。后来我调整了架构推流交给gstreamerNode.js专注信令既稳定又省心。这让我明白一个道理技术选型要尊重平台特性树莓派是个Linux嵌入式环境gstreamer这种成熟的系统级框架才是流媒体处理的正道。第三个坑自签名证书在手机浏览器上死活连不上WebSocket。桌面浏览器点一下警告就能继续但Safari或Chrome移动版有时候连警告弹窗都不给。后来我的解决办法是本地联调用自签名证书公网部署统一换Let’s Encrypt别再图省事用自签的证书有效期三个月但配合自动化续期工具很省心这也是生产环境的最低配置。4.3 树莓派性能优化与稳定性调优跑监控的设备一般要求7x24小时不关机稳定性比什么都重要。我的建议从供电和散热开始树莓派4B长时间满载CPU温度很容易飙到80度以上一定要加散热片和风扇有条件直接上主动散热外壳。实测加装风扇后温度稳定在55度左右推流稳定性提升一个档次。系统层面有几项优化非常值得做。关闭桌面环境可以省出大量内存和CPU资源树莓派跑监控完全不需要X Windowsudo raspi-config在System Options → Boot/Auto Login里选择Console模式顺便把GPU内存调小到128MB因为硬件编码用的不是这部分GPU内存。然后编辑/etc/rc.local把推流脚本设置为开机自启断电重启后能自动恢复监控这对无人值守场景很重要。联网稳定性方面5GHz WiFi信号不稳定的话我建议优先考虑有线网络。树莓派4B的千兆以太网口足够跑几路720p流而且延时更低。如果非要WiFi也别用2.4GHz信道太拥挤的频段实测5GHz下丢包率会低很多。5. 进阶扩展这套系统还能怎么玩5.1 延迟优化从300ms压到100ms量级WebRTC本身延迟就很低但想让画面更跟手还可以做几个优化。第一个是编码参数在gstreamer的v4l2h264enc上加bitrate2000000和key-int-max30把关键帧间隔控制在一秒左右这样新加入的观看端能快速拉到首帧。第二个是使用low-latencytrue参数虽然会牺牲一点画质但换来的是延迟降低约50毫秒。我的实测数据是在局域网环境下从摄像头采集到浏览器渲染延迟稳定在120毫秒左右这已经非常接近有线安防系统的水平了。第三个优化点是分辨率和帧率的平衡。720p25fps是我测试下来最稳的组合如果再往上加树莓派偶尔会出现编码抖动的现象。如果把帧率降到15fpsCPU占用还能再往下压一截画质基本无损适合低带宽场景。5.2 功能扩展录像、多路观看、移动侦测这套WebRTC链路稳定之后扩展功能非常方便。想加录像就在树莓派本地用gstreamer的splitmuxsink插件把H.264流同时写一份到本地存储按小时分段比云平台录像灵活得多还不用交存储费。想加多路观看只需要信令服务器支持多个watcher连接即可。目前这个简单广播版本已经天然支持多浏览器端同时观看因为每个watcher都会收到推流端发出的offer各自建立独立的点对点连接。不过要留意树莓派的网络上行带宽720p 2Mbps码率下支持5路观看就得有10Mbps上行家用宽带的限制必须考虑到。移动侦测也不复杂可以在树莓派上用OpenCV处理抽帧检测检测到画面变化时通过内置的WebRTC数据通道给浏览器发一个消息或者直接调内置GPIO触发报警器。WebRTC的数据通道是和视频流独立的一条通道传输小数据非常方便做远程遥控、报警推送都很顺手。5.3 生产化改造建议如果你打算把这个项目从玩具升级成长期稳定运行的生产系统有几个改进值得投入。一个是把信令服务器从“广播所有消息”改成“按房间匹配”用房间ID隔离不同的摄像头和观看端避免多个摄像头互相串流这也把架构从demo推向真正的产品级。另一个是接入认证系统用JWT令牌替代静态Token每条消息都做权限校验。存储层面长期录像建议上外置SSD不要用SD卡SD卡频繁写入容易损坏。我见过好几个朋友的项目都是SD卡被写坏了才后悔当初没花五十块买一块二手SSD。供电方面买一个正规的5V 3A电源劣质充电头在电压波动时会让树莓派重启监控中断往往从供电不稳开始。最后再分享一个小经验整套项目部署完成后一定要写一个“一键自检”脚本把服务状态、摄像头状态、磁盘剩余空间、连接数量全部打出来。这样远程维护时一条命令就能确认所有环节是否正常省去了排查时的茫然无措。这个脚本用Node.js几行就能写完算是我从这套远程监控项目里沉淀下来最值钱的工具之一。
返回列表