You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用aiortc在NAT后的Django服务器、IoT设备与浏览器间建立WebRTC连接时遭遇ICE失败问题

使用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模式下的修复思路:

  1. 后端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()
        }))
  1. 前端监听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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.22 10:29:36