From ba2824ec56263ed6d8c91fadba6701d041d34deb Mon Sep 17 00:00:00 2001 From: root Date: Wed, 25 Feb 2026 22:11:56 -0600 Subject: [PATCH] Add go2rtc proxy to fix CORS-blocked WebRTC/MSE streams --- backend/main.py | 2 + backend/requirements.txt | 1 + backend/routes/go2rtc_proxy.py | 65 +++++++++++++++++++++++++++++++++ frontend/src/services/go2rtc.ts | 21 +++++------ 4 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 backend/routes/go2rtc_proxy.py diff --git a/backend/main.py b/backend/main.py index cd6c792..8263153 100644 --- a/backend/main.py +++ b/backend/main.py @@ -10,6 +10,7 @@ from config import load_config from mqtt_bridge import start_mqtt from routes.config_routes import router as config_router from routes.ws_routes import router as ws_router +from routes.go2rtc_proxy import router as go2rtc_router logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") logger = logging.getLogger(__name__) @@ -29,6 +30,7 @@ app = FastAPI(title="Camera Viewer", lifespan=lifespan) app.include_router(config_router) app.include_router(ws_router) +app.include_router(go2rtc_router) # Serve frontend static files if FRONTEND_DIR.exists(): diff --git a/backend/requirements.txt b/backend/requirements.txt index e29bba0..76fe5f6 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -4,3 +4,4 @@ aiomqtt==2.3.0 pyyaml==6.0.2 pydantic==2.9.2 websockets==13.1 +httpx==0.28.1 diff --git a/backend/routes/go2rtc_proxy.py b/backend/routes/go2rtc_proxy.py new file mode 100644 index 0000000..b0c5c15 --- /dev/null +++ b/backend/routes/go2rtc_proxy.py @@ -0,0 +1,65 @@ +import asyncio +import logging + +import httpx +import websockets +from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect, Response + +from config import get_config + +logger = logging.getLogger(__name__) +router = APIRouter(prefix="/api/go2rtc") + + +@router.post("/webrtc") +async def proxy_webrtc(request: Request, src: str): + """Proxy WebRTC SDP exchange to go2rtc.""" + config = get_config() + target = f"{config.go2rtc.url}/api/webrtc?src={src}" + body = await request.body() + + async with httpx.AsyncClient() as client: + resp = await client.post( + target, + content=body, + headers={"Content-Type": "application/sdp"}, + timeout=10.0, + ) + + return Response( + content=resp.content, + status_code=resp.status_code, + media_type="application/sdp", + ) + + +@router.websocket("/ws") +async def proxy_mse_ws(ws: WebSocket, src: str): + """Proxy MSE WebSocket to go2rtc.""" + config = get_config() + go2rtc_ws_url = config.go2rtc.url.replace("http", "ws") + target = f"{go2rtc_ws_url}/api/ws?src={src}" + + await ws.accept() + + try: + async with websockets.connect(target) as upstream: + + async def forward_to_client(): + async for msg in upstream: + if isinstance(msg, bytes): + await ws.send_bytes(msg) + else: + await ws.send_text(msg) + + async def forward_to_upstream(): + while True: + data = await ws.receive_text() + await upstream.send(data) + + await asyncio.gather( + forward_to_client(), + forward_to_upstream(), + ) + except (WebSocketDisconnect, websockets.ConnectionClosed, Exception): + pass diff --git a/frontend/src/services/go2rtc.ts b/frontend/src/services/go2rtc.ts index 96c7ea0..83d0489 100644 --- a/frontend/src/services/go2rtc.ts +++ b/frontend/src/services/go2rtc.ts @@ -1,13 +1,13 @@ +// All requests go through backend proxy to avoid CORS issues + export class Go2RTCWebRTC { private pc: RTCPeerConnection | null = null; private mediaStream: MediaStream | null = null; private streamName: string; - private go2rtcUrl: string; private onTrackCb: ((stream: MediaStream) => void) | null = null; - constructor(streamName: string, go2rtcUrl: string) { + constructor(streamName: string, _go2rtcUrl?: string) { this.streamName = streamName; - this.go2rtcUrl = go2rtcUrl; } async connect(onTrack: (stream: MediaStream) => void): Promise { @@ -24,9 +24,7 @@ export class Go2RTCWebRTC { } }; - this.pc.onicecandidate = () => { - // go2rtc handles ICE internally via the initial SDP exchange - }; + this.pc.onicecandidate = () => {}; this.pc.addTransceiver('video', { direction: 'recvonly' }); this.pc.addTransceiver('audio', { direction: 'recvonly' }); @@ -34,7 +32,8 @@ export class Go2RTCWebRTC { const offer = await this.pc.createOffer(); await this.pc.setLocalDescription(offer); - const url = `${this.go2rtcUrl}/api/webrtc?src=${encodeURIComponent(this.streamName)}`; + // Use backend proxy instead of direct go2rtc + const url = `/api/go2rtc/webrtc?src=${encodeURIComponent(this.streamName)}`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/sdp' }, @@ -67,13 +66,11 @@ export class Go2RTCMSE { private sourceBuffer: SourceBuffer | null = null; private ws: WebSocket | null = null; private streamName: string; - private go2rtcUrl: string; private videoElement: HTMLVideoElement | null = null; private queue: ArrayBuffer[] = []; - constructor(streamName: string, go2rtcUrl: string) { + constructor(streamName: string, _go2rtcUrl?: string) { this.streamName = streamName; - this.go2rtcUrl = go2rtcUrl; } async connect(videoElement: HTMLVideoElement): Promise { @@ -85,7 +82,9 @@ export class Go2RTCMSE { this.mediaSource!.addEventListener('sourceopen', () => resolve(), { once: true }); }); - const wsUrl = `${this.go2rtcUrl.replace('http', 'ws')}/api/ws?src=${encodeURIComponent(this.streamName)}`; + // Use backend proxy WebSocket + const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${proto}//${location.host}/api/go2rtc/ws?src=${encodeURIComponent(this.streamName)}`; this.ws = new WebSocket(wsUrl); this.ws.binaryType = 'arraybuffer';