Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ make run-frontend-local
```
Open the shown URL in your console.

## Use Ion-SFU for WebRTC
To route video through an ion-sfu instance instead of direct peer connections, set the following environment variables:

**Backend (streamer + analyzer)**
- `WEBRTC_MODE=ion-sfu`
- `SFU_SIGNALING_URL` (e.g. `ws://localhost:7000/ws`)
- `SFU_SESSION_ID` (room name, default `optibot`)
- `SFU_PUBLISHER_ID` / `SFU_SUBSCRIBER_ID` (optional identifiers)
- `SFU_ICE_SERVERS` (comma-separated STUN/TURN URLs, defaults to the STUN value)

**Frontend**
- `VITE_WEBRTC_MODE=ion-sfu`
- `VITE_SFU_URL` and `VITE_SFU_SESSION`
- `VITE_SFU_CLIENT_ID` (optional override, otherwise a random id is used)
- `VITE_ANALYZER_WS_URL` (metadata still delivered via WebSocket)

Leaving `WEBRTC_MODE`/`VITE_WEBRTC_MODE` unset keeps the existing direct `/offer` flow.

## Model Management
```bash
# Download default models (YOLO pt, MiDaS cache)
Expand Down
15 changes: 15 additions & 0 deletions src/backend/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ class Config:
# WebRTC settings
STUN_SERVER: str = os.getenv("STUN_SERVER", "stun:stun.l.google.com:19302")
ICE_GATHERING_TIMEOUT: float = float(os.getenv("ICE_GATHERING_TIMEOUT", "5.0"))
WEBRTC_MODE: str = os.getenv("WEBRTC_MODE", os.getenv("RTC_MODE", "direct")).lower()
SFU_SIGNALING_URL: str = os.getenv("SFU_SIGNALING_URL", "ws://localhost:7000/ws")
SFU_SESSION_ID: str = os.getenv("SFU_SESSION_ID", "optibot")
SFU_PUBLISHER_ID: str = os.getenv("SFU_PUBLISHER_ID", "streamer")
SFU_SUBSCRIBER_ID: str = os.getenv("SFU_SUBSCRIBER_ID", "analyzer")
SFU_NO_AUTO_SUBSCRIBE: bool = os.getenv(
"SFU_NO_AUTO_SUBSCRIBE", "false"
).lower() in ("1", "true", "yes")
SFU_ICE_SERVERS: list[str] = [
server.strip()
for server in os.getenv(
"SFU_ICE_SERVERS", os.getenv("STUN_SERVER", "stun:stun.l.google.com:19302")
).split(",")
if server.strip()
]

# Analyzer mode (for analyzer.py)
STREAMER_OFFER_URL: str = os.getenv(
Expand Down
29 changes: 29 additions & 0 deletions src/backend/common/core/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# SPDX-License-Identifier: MIT
import asyncio
import contextlib
import logging
from typing import Optional

import httpx
Expand All @@ -15,6 +16,7 @@
from aiortc.mediastreams import MediaStreamTrack

from common.config import config
from common.core.sfu_client import IonSfuClient, IonSfuSettings


class WebcamSession:
Expand All @@ -30,8 +32,15 @@ def __init__(self, offer_url: str) -> None:
self._offer_url = offer_url
self._pc: Optional[RTCPeerConnection] = None
self._track: Optional[MediaStreamTrack] = None
self._sfu_client: IonSfuClient | None = None

async def connect(self) -> MediaStreamTrack:
"""Connect using configured WebRTC mode."""
if config.WEBRTC_MODE == "ion-sfu":
return await self._connect_via_sfu()
return await self._connect_direct()

async def _connect_direct(self) -> MediaStreamTrack:
"""Establish a WebRTC connection and retrieve the video track.

Creates an SDP offer, sends it to the configured webcam service, and waits
Expand Down Expand Up @@ -85,6 +94,22 @@ def on_ice_gathering_state_change() -> None:
self._track = await track_future
return self._track

async def _connect_via_sfu(self) -> MediaStreamTrack:
"""Establish a WebRTC connection through ion-sfu and return the video track."""
settings = IonSfuSettings(
signaling_url=config.SFU_SIGNALING_URL,
session_id=config.SFU_SESSION_ID,
client_id=config.SFU_SUBSCRIBER_ID,
ice_servers=config.SFU_ICE_SERVERS,
no_subscribe=False,
no_auto_subscribe=config.SFU_NO_AUTO_SUBSCRIBE,
)
logger = logging.getLogger("ion_sfu.subscriber")
self._sfu_client = IonSfuClient(settings=settings, logger=logger)
await self._sfu_client.connect()
self._track = await self._sfu_client.wait_for_track(kind="video")
return self._track

async def close(self) -> None:
"""Close the peer connection and release resources.

Expand All @@ -99,3 +124,7 @@ async def close(self) -> None:
with contextlib.suppress(Exception):
await self._pc.close()
self._pc = None
if self._sfu_client is not None:
with contextlib.suppress(Exception):
await self._sfu_client.close()
self._sfu_client = None
Loading