Skip to content

Commit bdfd4c4

Browse files
committed
fix: fixing p2p webrtc
1 parent de318c3 commit bdfd4c4

1 file changed

Lines changed: 65 additions & 50 deletions

File tree

main.py

Lines changed: 65 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def __init__(self, config: Config):
3434
self.config = config
3535
self.token_store = TokenStore(config)
3636
self.telemetry = TelemetryClient(config, self.token_store)
37-
self.pc: RTCPeerConnection | None = None
37+
self.peer_connections: dict[str, RTCPeerConnection] = {}
3838
self.ws = None
3939
self.running = True
4040
self.turn_credentials: dict | None = None
@@ -129,16 +129,8 @@ def refresh_flow(self):
129129
# WebRTC + Signaling
130130
# =========================
131131

132-
async def setup_peer_connection(self):
133-
# Null out self.pc before closing the old one so that the stale
134-
# "closed" connectionstatechange event doesn't see itself as current
135-
# and close the active signaling WebSocket.
136-
old_pc = self.pc
137-
self.pc = None
138-
if old_pc:
139-
await old_pc.close()
140-
141-
ice_servers = [
132+
def _build_ice_servers(self) -> list[RTCIceServer]:
133+
servers = [
142134
RTCIceServer(urls="stun:stun.l.google.com:19302"),
143135
RTCIceServer(urls="stun:stun1.l.google.com:19302"),
144136
]
@@ -147,49 +139,62 @@ async def setup_peer_connection(self):
147139
turn_username = creds.get("username") or self.config.turn_username
148140
turn_credential = creds.get("credential") or self.config.turn_credential
149141
if turn_url and turn_username and turn_credential:
150-
ice_servers.append(
142+
servers.append(
151143
RTCIceServer(
152144
urls=turn_url,
153145
username=turn_username,
154146
credential=turn_credential,
155147
)
156148
)
157-
pc = RTCPeerConnection(configuration=RTCConfiguration(iceServers=ice_servers))
158-
self.pc = pc
149+
return servers
150+
151+
async def setup_peer_connection(self, viewer_channel: str) -> RTCPeerConnection:
152+
# Close existing PC for this viewer if there is one
153+
old_pc = self.peer_connections.pop(viewer_channel, None)
154+
if old_pc:
155+
await old_pc.close()
156+
157+
pc = RTCPeerConnection(
158+
configuration=RTCConfiguration(iceServers=self._build_ice_servers())
159+
)
160+
self.peer_connections[viewer_channel] = pc
159161

160162
@pc.on("connectionstatechange")
161163
async def on_connectionstatechange():
162-
if pc is not self.pc:
163-
return # stale event from a replaced PC
164+
if self.peer_connections.get(viewer_channel) is not pc:
165+
return
164166
state = pc.connectionState
165-
LOG.info("WebRTC state: %s", state)
167+
LOG.info("WebRTC state for %s: %s", viewer_channel, state)
166168
if state == "connected":
167169
await asyncio.to_thread(self.telemetry.send, "streaming")
168170
elif state == "failed":
169171
await asyncio.to_thread(
170172
self.telemetry.send, "error", "WebRTC connection failed", "WARNING"
171173
)
172-
# Reset the PC so we're ready for the next offer.
173-
# The signaling WebSocket stays alive — don't close it here.
174-
await self.setup_peer_connection()
175-
self._register_ice_handler()
174+
await self.setup_peer_connection(viewer_channel)
175+
self._register_ice_handler(viewer_channel)
176+
177+
return pc
176178

177-
def _register_ice_handler(self):
178-
pc = self.pc
179+
def _register_ice_handler(self, viewer_channel: str):
180+
pc = self.peer_connections[viewer_channel]
179181

180182
@pc.on("icecandidate")
181183
async def on_icecandidate(candidate):
182-
if pc is not self.pc:
184+
if self.peer_connections.get(viewer_channel) is not pc:
183185
return
184186
if candidate is not None:
185-
LOG.info("ICE candidate generated: %s", candidate)
187+
LOG.info(
188+
"ICE candidate generated for %s: %s", viewer_channel, candidate
189+
)
186190
await self.ws.send(
187191
json.dumps(
188192
{
189193
"type": "candidate",
190194
"candidate": candidate.candidate,
191195
"sdpMid": candidate.sdpMid,
192196
"sdpMLineIndex": candidate.sdpMLineIndex,
197+
"viewer_channel": viewer_channel,
193198
}
194199
)
195200
)
@@ -199,43 +204,44 @@ async def signaling_loop(self):
199204
access_token = token_store["access"]["token_value"]
200205
device_code = token_store["device_code"]
201206

207+
self.turn_credentials = await asyncio.to_thread(
208+
fetch_turn_credentials, self.config.http_api_base_url, access_token
209+
)
210+
202211
headers = {"Authorization": f"Bearer {access_token}"}
203212

204213
base = self.config.websocket_api_base_url.rstrip("/")
205214
if "/ws/live_stream" not in base:
206215
base = f"{base}/ws/live_stream"
207216
websocket_url = f"{base}/{device_code}/"
208217

209-
self.turn_credentials = await asyncio.to_thread(
210-
fetch_turn_credentials, self.config.http_api_base_url, access_token
211-
)
212-
213218
async with websockets.connect(
214219
websocket_url,
215220
additional_headers=headers,
216221
) as ws:
217222
self.ws = ws
218223
LOG.info("Connected to signaling server")
219-
220-
await self.setup_peer_connection()
221-
self._register_ice_handler()
222224
await asyncio.to_thread(self.telemetry.send, "connected")
223225

224226
async for message in ws:
225227
data = json.loads(message)
226228
LOG.info("Received message: %s", data)
227229

228230
if data["type"] == "offer":
229-
await self.pc.setRemoteDescription(
231+
viewer_channel = data.get("viewer_channel", "")
232+
pc = await self.setup_peer_connection(viewer_channel)
233+
self._register_ice_handler(viewer_channel)
234+
235+
await pc.setRemoteDescription(
230236
RTCSessionDescription(
231237
sdp=data["sdp"],
232-
type=data["type"],
238+
type="offer",
233239
)
234240
)
235241

236242
try:
237243
camera_track = CameraVideoStreamTrack(self.config)
238-
self.pc.addTrack(camera_track)
244+
pc.addTrack(camera_track)
239245
LOG.info("Camera track added successfully")
240246
except Exception as e:
241247
LOG.warning(
@@ -245,42 +251,50 @@ async def signaling_loop(self):
245251
self.telemetry.send, "error", str(e), "WARNING"
246252
)
247253

248-
answer = await self.pc.createAnswer()
249-
250-
await self.pc.setLocalDescription(answer)
254+
answer = await pc.createAnswer()
255+
await pc.setLocalDescription(answer)
251256
await self.ws.send(
252257
json.dumps(
253258
{
254-
"type": self.pc.localDescription.type,
255-
"sdp": self.pc.localDescription.sdp,
259+
"type": pc.localDescription.type,
260+
"sdp": pc.localDescription.sdp,
261+
"viewer_channel": viewer_channel,
256262
}
257263
)
258264
)
259-
LOG.info("Answer sent successfully")
265+
LOG.info("Answer sent to viewer %s", viewer_channel)
260266

261267
elif data["type"] in ("ice", "candidate") and data.get("candidate"):
268+
viewer_channel = data.get("viewer_channel", "")
269+
pc = self.peer_connections.get(viewer_channel)
270+
if not pc:
271+
LOG.warning(
272+
"Received candidate for unknown viewer %s", viewer_channel
273+
)
274+
continue
262275
sdp_str = data["candidate"]
263276
if sdp_str.startswith("candidate:"):
264277
sdp_str = sdp_str[len("candidate:") :]
265278
ice_candidate = candidate_from_sdp(sdp_str)
266279
ice_candidate.sdpMid = data.get("sdpMid")
267280
ice_candidate.sdpMLineIndex = data.get("sdpMLineIndex")
268-
await self.pc.addIceCandidate(ice_candidate)
281+
await pc.addIceCandidate(ice_candidate)
269282

270283
elif (
271284
data["type"] == "status"
272285
and data.get("event") == "peer_disconnected"
273286
and data.get("peer_type") != "device"
274287
):
275-
LOG.info(
276-
"Viewer disconnected; resetting peer connection for new offer"
277-
)
288+
viewer_channel = data.get("viewer_channel", "")
289+
pc = self.peer_connections.pop(viewer_channel, None)
290+
if pc:
291+
await pc.close()
292+
LOG.info("Viewer %s disconnected", viewer_channel)
278293
await asyncio.to_thread(
279294
self.telemetry.send, "disconnected", "Viewer disconnected"
280295
)
281-
await self.setup_peer_connection()
282-
self._register_ice_handler()
283-
await asyncio.to_thread(self.telemetry.send, "connected")
296+
if not self.peer_connections:
297+
await asyncio.to_thread(self.telemetry.send, "connected")
284298

285299
# =========================
286300
# Lifecycle
@@ -309,8 +323,9 @@ async def shutdown(self):
309323
self.telemetry.send, "disconnected", "Client shutting down"
310324
)
311325

312-
if self.pc:
313-
await self.pc.close()
326+
for pc in list(self.peer_connections.values()):
327+
await pc.close()
328+
self.peer_connections.clear()
314329

315330
if self.ws:
316331
await self.ws.close()

0 commit comments

Comments
 (0)