使用aiortc搭建Python WebRTC本地推流服务器遇错误排查
问题分析与修复方案
核心问题原因
- 客户端Offer缺少视频媒体约束:客户端创建Offer时未明确声明接收视频,导致SDP中没有视频媒体描述(m-line),服务器添加视频轨道后无法匹配,触发
ValueError: None is not in list错误。 - 服务器端未正确处理ICE候选:未收集ICE候选者,导致Answer的SDP中没有本地网络地址信息,无法建立P2P连接。
- 服务器响应格式错误:直接返回字典而非aiohttp要求的JSON响应对象,可能导致客户端解析异常。
具体修复步骤
1. 修复客户端JavaScript代码
创建Offer时添加媒体约束,明确告知服务器需要接收视频:
pc = new RTCPeerConnection({ iceServers: [] // 本地网络无需ICE服务器 }); // 监听远程轨道,渲染到视频元素 pc.ontrack = event => { if (event.track.kind === 'video') { video.srcObject = event.streams[0]; } }; // 创建带视频接收约束的Offer const offer = await pc.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: false // 不需要音频可关闭 }); await pc.setLocalDescription(offer); // 发送Offer到服务器并处理Answer const response = await fetch('/offer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sdp: pc.localDescription.sdp, type: pc.localDescription.type }) }); const answer = await response.json(); await pc.setRemoteDescription(new RTCSessionDescription(answer));
2. 修复服务器端Python代码
- 用
web.json_response包装响应 - 监听ICE候选事件(确保SDP包含本地地址)
- 补全
BouncingBallTrack的recv方法基础实现
修改后的服务器代码:
import asyncio from aiortc import RTCPeerConnection, RTCSessionDescription, VideoStreamTrack from av import VideoFrame import numpy as np import cv2 from aiohttp import web class BouncingBallTrack(VideoStreamTrack): def __init__(self): super().__init__() self.width = 640 self.height = 480 # 初始化球的位置和速度 self.x, self.y = 100, 100 self.vx, self.vy = 5, 5 self.radius = 20 async def recv(self): # 创建黑色背景帧 frame = np.zeros((self.height, self.width, 3), dtype=np.uint8) # 绘制绿色球 cv2.circle(frame, (self.x, self.y), self.radius, (0, 255, 0), -1) # 更新球的位置 self.x += self.vx self.y += self.vy # 边界碰撞检测 if self.x + self.radius > self.width or self.x - self.radius < 0: self.vx *= -1 if self.y + self.radius > self.height or self.y - self.radius < 0: self.vy *= -1 # 转换为VideoFrame格式 new_frame = VideoFrame.from_ndarray(frame, format='bgr24') new_frame.pts = self.pts new_frame.time_base = self.time_base self.pts += 1 return new_frame async def offer(request): params = await request.json() offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) pc = RTCPeerConnection() # 添加本地视频轨道 pc.addTrack(BouncingBallTrack()) # 监听ICE候选,收集完成后SDP会包含本地地址 @pc.on("icecandidate") def on_icecandidate(candidate): if not candidate: pass await pc.setRemoteDescription(offer) answer = await pc.createAnswer() await pc.setLocalDescription(answer) # 返回标准JSON响应 return web.json_response({ "sdp": pc.localDescription.sdp, "type": pc.localDescription.type }) if __name__ == "__main__": app = web.Application() app.router.add_post("/offer", offer) app.router.add_static('/', path='static', name='static') web.run_app(app, port=8080)
3. 验证要点
- 客户端Offer的SDP中会新增
m=video行,服务器添加的视频轨道能与之匹配 - 服务器Answer的SDP会包含本地ICE候选(如
a=candidate:xxx),用于本地网络P2P连接 - 服务器不再抛出
ValueError,浏览器能正常接收并显示跳动的绿色球视频流
内容的提问来源于stack exchange,提问作者Vitaliy Tsirkunov
相关产品推荐
相关产品推荐

