使用aiortc在NAT后的Django服务器、IoT设备与浏览器间建立WebRTC连接时遭遇ICE失败问题
看起来你遇到的问题很典型——单独用aiohttp+aiortc能跑通,但移植到Django就触发ICE失败,而且还是本地环境,这大概率不是网络问题,而是Django的同步异步模型、WebRTC会话生命周期或者ICE候选交换的细节出了问题。我来帮你一步步排查和解决:
1. 核心问题:后端RTCPeerConnection缺少ICE服务器配置
你前端的RTCPeerConnection配置了STUN服务器,但后端创建RTCPeerConnection时是空参数(pc = RTCPeerConnection()),完全没配置ICE服务器!这会导致后端无法收集和交换ICE候选,直接触发ICE失败。
修复代码:
在create_peer_connection函数里,给RTCPeerConnection添加和前端一致的ICE服务器配置:
async def create_peer_connection(sdp, con_type): global pcs # 新增:和前端保持一致的ICE服务器配置 ice_config = { "iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}] } pc = RTCPeerConnection(ice_config) pcs.add(pc) # 剩下的代码保持不变 @pc.on("connectionstatechange") async def on_connectionstatechange(): print("Connection state is %s" % pc.connectionState) if pc.connectionState == "failed": await pc.close() pcs.discard(pc) audio, video = create_media_streams() if video: pc.addTrack(video) await pc.setRemoteDescription(offer) answer = await pc.createAnswer() await pc.setLocalDescription(answer) return (pc.localDescription.sdp, pc.localDescription.type)
2. 异步上下文与会话生命周期问题
你用async_to_sync把异步的aiortc代码包装成同步视图,但Django的WSGI服务器是短连接模型,请求结束后异步上下文会被销毁,导致RTCPeerConnection实例被提前回收,ICE候选还没完成交换就夭折了。而aiohttp是异步服务器,能保持会话上下文直到WebRTC连接结束。
解决方案:
- 升级到Django 3.1+,改用异步视图,并使用ASGI服务器(比如daphne)运行项目,保持异步上下文的持续性:
class RequestStream(APIView): authentication_classes = [SessionAuthentication, BasicAuthentication, LocalhostAuth] # 去掉async_to_sync,直接用异步方法 async def post(self, request, *args, **kwargs): sdp, con_type = await create_peer_connection(request.data['sdp'], request.data['type']) return Response( status=200, data={"sdp": sdp, "type": con_type}, )
- 更彻底的方案是用Django Channels实现WebSocket通信:WebRTC的ICE交换需要双向实时传递候选,短连接POST无法满足这个需求,WebSocket能保持长连接,实时传递SDP和ICE候选消息。
3. ICE候选交换不完整
你的代码里完全没有处理ICE候选的实时交换逻辑!在aiohttp示例中,通常会通过onicecandidate事件把后端收集到的候选推送给前端,同时前端也要把自己的候选发给后端。但你现在只交换了SDP Offer/Answer,漏掉了关键的ICE候选步骤。
WebSocket模式下的修复思路:
- 后端WebSocket视图中,给RTCPeerConnection添加ICE候选监听:
@pc.on("icecandidate") async def on_icecandidate(event): if event.candidate: # 通过WebSocket发送候选给前端 await websocket.send(json.dumps({ "type": "ice_candidate", "candidate": event.candidate.toJSON() }))
- 前端监听WebSocket消息,收到候选后添加到本地RTCPeerConnection:
ws.onmessage = function(event) { const data = JSON.parse(event.data); if (data.type === "ice_candidate") { pc.addIceCandidate(new RTCIceCandidate(data.candidate)) .catch(e => console.error("添加ICE候选失败:", e)); } else if (data.type === "answer") { pc.setRemoteDescription(new RTCSessionDescription(data)) .catch(e => console.error("设置远端描述失败:", e)); } };
4. 全局Media实例的资源竞争问题
你用了global relay, webcam来复用媒体资源,但在Django的多进程/多线程环境下,多个请求会共用同一个MediaPlayer,导致/dev/video0设备被占用,可能间接影响WebRTC连接稳定性。
修复代码:
去掉全局变量,为每个连接创建独立的媒体实例:
def create_media_streams(): options = {"framerate": "30", "video_size": "640x480"} # 不再用全局变量,每个连接创建独立实例 webcam = MediaPlayer("/dev/video0", format="v4l2", options=options) relay = MediaRelay() return None, relay.subscribe(webcam.video)
(注:如果需要支持多并发请求,可能需要添加设备排队或共享机制,本地测试先解决ICE问题即可)
按照以上步骤调整后,本地环境下的ICE失败问题应该就能解决了。
备注:内容来源于stack exchange,提问作者AndreyPr

