私有化部署WebRTC:基于aiortc实现Web浏览器直接预览远程摄像头
私有化部署WebRTC:基于aiortc实现Web浏览器直接预览远程摄像头
引言:为什么需要这个方案?
在现代物联网和安防监控领域,远程查看摄像头视频流是一个常见需求。传统的解决方案通常采用RTSP协议,但这种方案存在一个致命缺陷:现代Web浏览器(如Chrome、Firefox)无法直接播放RTSP流。
之前我尝试了以下方案:
- 第三方P2P穿透方案:仅支持Windows/Linux Qt客户端,不支持Web浏览器
- EasyRTC等现成方案:无法私有化部署,稳定性欠佳
- 直接使用libwebrtc:编译复杂,依赖庞大
今天,我介绍一个基于aiortc的简化方案,它能够:
- ✅ 在Web浏览器中直接预览远程摄像头
- ✅ 完全私有化部署,数据不经过第三方
- ✅ 支持P2P直连、STUN穿透和TURN中转多种连接方式
- ✅ 跨平台支持(Windows、Linux、Mac)
- ✅ 代码简洁,易于理解和定制
一、WebRTC与aiortc:技术基础
什么是WebRTC?
WebRTC(Web实时通信)是一个开源项目,允许Web浏览器和移动应用进行实时音视频通信和数据传输,而无需安装任何插件。它的核心优势在于:
- 点对点通信:数据直接在两个客户端之间传输
- 低延迟:专为实时通信设计
- 浏览器原生支持:现代浏览器都内置WebRTC API
- 自动NAT穿透:通过STUN/TURN技术解决网络障碍
为什么选择aiortc?
aiortc是一个基于Python asyncio的WebRTC实现库,相比原生libwebrtc,它有如下优势:
| 特性 | aiortc | libwebrtc |
|---|---|---|
| 安装复杂度 | pip一键安装 | 需要复杂编译 |
| 语言友好性 | Python,易于集成 | C++,集成复杂 |
| 代码简洁性 | 高级API,代码简洁 | 低级API,代码冗长 |
| 灵活性 | 易于自定义和扩展 | 修改困难 |
二、系统架构设计
我们的解决方案包含四个核心组件:
视频流
信令交换
ICE协商
SDP/ICE转发
SDP/ICE转发
远程设备
WebRTC PeerConnection
Web浏览器
信令服务器
STUN/TURN服务器
组件说明:
- STUN/TURN服务器(coturn)
- STUN:帮助设备发现自己的公网IP和端口
- TURN:当P2P无法建立时,作为数据中转服务器
- 信令服务器(Python WebSocket)
- 交换WebRTC连接所需的SDP(会话描述协议)和ICE候选地址
- 管理设备注册和发现
- Web服务器(Python HTTP)
- 提供Web页面给用户访问
- 集成WebRTC JavaScript客户端
- 设备端(Python aiortc)
- 捕获视频流(摄像头、文件、虚拟视频)
- 通过WebRTC传输视频到Web端
三、详细部署步骤
步骤1:环境准备
在开始之前,请确保你有一台具有公网IP的云服务器(如阿里云ECS)。我们将在这台服务器上部署所有服务。
首先设置环境变量(这些变量将在后续脚本中使用):
exportECS_IP_ADDR=替换为你的云服务器实际IP地址 exportSTUN_SRV=$ECS_IP_ADDRexportSTUN_PORT=3478exportSIGNALING_SRV=$ECS_IP_ADDR#信令服务器,用来交换WebRTC连接需要的信息exportSIGNALING_PORT=9901exportWEB_SRV=$ECS_IP_ADDR# Web服务器exportWEB_PORT=9900重要提示:将这些变量添加到你的~/.bashrc文件中,以便每次登录时自动加载。
步骤2:部署STUN/TURN服务器
STUN/TURN服务器是WebRTC的"网络向导",帮助两个设备找到彼此并建立连接。
2.1 安装coturn
# 在CentOS/RHEL系统上安装 yum install coturn -y # 配置coturncat> /etc/coturn/turnserver.conf <<EOF listening-ip=0.0.0.0 listening-port=$STUN_PORT relay-ip=$(ifconfig eth0 |grep -w inet |awk'{print $2}') external-ip=$ECS_IP_ADDR log-file=/var/tmp/turn.log min-port=40000 max-port=65535 EOF systemctl restart coturn systemctl status coturn 2.2 配置防火墙
云服务器需要开放以下端口:
- 3478:STUN/TURN服务端口
- 9900:Web服务端口
- 9901:信令服务器端口
- 40000-65535:TURN中继端口范围
在阿里云控制台的安全组中,添加入站规则允许这些端口的TCP和UDP流量。

2.3 测试STUN/TURN服务

stun:你的服务器IP:3478 turn:你的服务器IP:3478 使用用户名webrtc,密码webrtc。如果测试成功,你会看到类似以下结果:
- srflx:STUN穿透成功
- relay:TURN中转可用
步骤3:部署信令服务器
信令服务器是WebRTC连接的"媒人",负责交换连接信息。它使用WebSocket协议进行实时通信。
3.1 创建信令服务器脚本
cat > signaling_server.py << EOF import asyncio import json import logging from aiohttp import web import aiohttp_cors logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__)classSignalingServer:def__init__(self): self.app = web.Application() self.clients ={ }# 存储连接的用户 self.setup_routes()defsetup_routes(self): cors = aiohttp_cors.setup(self.app, defaults={ "*": aiohttp_cors.ResourceOptions( allow_credentials=True, expose_headers="*", allow_headers="*",)})# 添加WebSocket路由 resource = cors.add(self.app.router.add_resource('/ws')) cors.add(resource.add_route("GET", self.websocket_handler))# 健康检查路由 self.app.router.add_get('/health', self.health_check)# 获取在线设备列表 self.app.router.add_get('/devices', self.get_devices)asyncdefhealth_check(self, request):return web.Response(text='OK')asyncdefget_devices(self, request):"""获取所有在线的设备""" devices =[]for client_id, client_info in self.clients.items():if client_info['type']=='device': devices.append({ 'id': client_id,'type': client_info['type']})return web.json_response({ 'devices': devices})asyncdefwebsocket_handler(self, request): ws = web.WebSocketResponse()await ws.prepare(request) client_id =None client_type =Nonetry:asyncfor msg in ws:if msg.type== web.WSMsgType.TEXT:try: data = json.loads(msg.data) message_type = data.get('type')if message_type =='register':# 客户端注册 client_id = data.get('client_id') client_type = data.get('client_type')# 'web' 或 'device'if client_id in self.clients:try:await self.clients[client_id]['ws'].close()except:pass self.clients[client_id]={ 'ws': ws,'type': client_type,'client_id': client_id } logger.info(f"Client registered: { client_id} ({ client_type})")await ws.send_str(json.dumps({ 'type':'registered','status':'success'}))elif message_type =='offer':# 转发 offer 到对应的设备端 target_device = data.get('target_device') offer_data = data.get('offer')if target_device in self.clients:await self.clients[target_device]['ws'].send_str(json.dumps({ 'type':'offer','offer': offer_data,'from_client': client_id })) logger.info(f"Offer forwarded from { client_id} to { target_device}")else: logger.warning(f"Target device not found: { target_device}")await ws.send_str(json.dumps({ 'type':'error','message':f'Device { target_device} not found'}))elif message_type =='answer':# 转发 answer 到对应的网页端 target_web = data.get('target_web') answer_data = data.get('answer')if target_web in self.clients:await self.clients[target_web]['ws'].send_str(json.dumps({ 'type':'answer','answer': answer_data,'from_client': client_id })) logger.info(f"Answer forwarded from { client_id} to { target_web}")elif message_type =='ice_candidate':# 转发 ICE candidate target = data.get('target') candidate = data.get('candidate')if target in self.clients:await self.clients[target]['ws'].send_str(json.dumps({ 'type':'ice_candidate','candidate': candidate,'from_client': client_id }))elif message_type =='ping':# 心跳检测await ws.send_str(json.dumps({ 'type':'pong'}))except json.JSONDecodeError as e: logger.error(f"JSON解析错误: { e}")elif msg.type== web.WSMsgType.ERROR: logger.error(f'WebSocket error: { ws.exception()}')except Exception as e: logger.error(f"WebSocket error: { e}")finally:# 清理客户端if client_id and client_id in self.clients:del self.clients[client_id] logger.info(f"Client disconnected: { client_id}")return ws defmain(): server = SignalingServer() logger.info("Starting signaling server on http://0.0.0.0:$SIGNALING_PORT") web.run_app(server.app, host='0.0.0.0', port=$SIGNALING_PORT)if __name__ =='__main__': main() EOF 3.2 安装依赖并运行
# 安装Python环境(如果使用conda) yum install conda -y conda create -n myenv python=3.12source ~/.bashrc conda activate myenv # 安装Python依赖 pip install aiohttp pip install aiohttp_cors # 注意:脚本中的环境变量需要替换为实际值# 运行信令服务器 python signaling_server.py 步骤4:部署Web服务器
Web服务器提供用户访问的界面,包含WebRTC JavaScript客户端。
4.1 创建Web服务器
cat > web_server.py << EOF from aiohttp import web import aiohttp_cors import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__)classWebServer:def__init__(self): self.app = web.Application() self.setup_routes()defsetup_routes(self): cors = aiohttp_cors.setup(self.app, defaults={ "*": aiohttp_cors.ResourceOptions( allow_credentials=True, expose_headers="*", allow_headers="*",)})# 主页面 self.app.router.add_get('/', self.index_handler) self.app.router.add_get('/index.html', self.index_handler)# 设备端页面 self.app.router.add_get