Add go2rtc proxy to fix CORS-blocked WebRTC/MSE streams
This commit is contained in:
@@ -10,6 +10,7 @@ from config import load_config
|
|||||||
from mqtt_bridge import start_mqtt
|
from mqtt_bridge import start_mqtt
|
||||||
from routes.config_routes import router as config_router
|
from routes.config_routes import router as config_router
|
||||||
from routes.ws_routes import router as ws_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")
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -29,6 +30,7 @@ app = FastAPI(title="Camera Viewer", lifespan=lifespan)
|
|||||||
|
|
||||||
app.include_router(config_router)
|
app.include_router(config_router)
|
||||||
app.include_router(ws_router)
|
app.include_router(ws_router)
|
||||||
|
app.include_router(go2rtc_router)
|
||||||
|
|
||||||
# Serve frontend static files
|
# Serve frontend static files
|
||||||
if FRONTEND_DIR.exists():
|
if FRONTEND_DIR.exists():
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ aiomqtt==2.3.0
|
|||||||
pyyaml==6.0.2
|
pyyaml==6.0.2
|
||||||
pydantic==2.9.2
|
pydantic==2.9.2
|
||||||
websockets==13.1
|
websockets==13.1
|
||||||
|
httpx==0.28.1
|
||||||
|
|||||||
65
backend/routes/go2rtc_proxy.py
Normal file
65
backend/routes/go2rtc_proxy.py
Normal file
@@ -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
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
|
// All requests go through backend proxy to avoid CORS issues
|
||||||
|
|
||||||
export class Go2RTCWebRTC {
|
export class Go2RTCWebRTC {
|
||||||
private pc: RTCPeerConnection | null = null;
|
private pc: RTCPeerConnection | null = null;
|
||||||
private mediaStream: MediaStream | null = null;
|
private mediaStream: MediaStream | null = null;
|
||||||
private streamName: string;
|
private streamName: string;
|
||||||
private go2rtcUrl: string;
|
|
||||||
private onTrackCb: ((stream: MediaStream) => void) | null = null;
|
private onTrackCb: ((stream: MediaStream) => void) | null = null;
|
||||||
|
|
||||||
constructor(streamName: string, go2rtcUrl: string) {
|
constructor(streamName: string, _go2rtcUrl?: string) {
|
||||||
this.streamName = streamName;
|
this.streamName = streamName;
|
||||||
this.go2rtcUrl = go2rtcUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(onTrack: (stream: MediaStream) => void): Promise<void> {
|
async connect(onTrack: (stream: MediaStream) => void): Promise<void> {
|
||||||
@@ -24,9 +24,7 @@ export class Go2RTCWebRTC {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pc.onicecandidate = () => {
|
this.pc.onicecandidate = () => {};
|
||||||
// go2rtc handles ICE internally via the initial SDP exchange
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pc.addTransceiver('video', { direction: 'recvonly' });
|
this.pc.addTransceiver('video', { direction: 'recvonly' });
|
||||||
this.pc.addTransceiver('audio', { direction: 'recvonly' });
|
this.pc.addTransceiver('audio', { direction: 'recvonly' });
|
||||||
@@ -34,7 +32,8 @@ export class Go2RTCWebRTC {
|
|||||||
const offer = await this.pc.createOffer();
|
const offer = await this.pc.createOffer();
|
||||||
await this.pc.setLocalDescription(offer);
|
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, {
|
const res = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/sdp' },
|
headers: { 'Content-Type': 'application/sdp' },
|
||||||
@@ -67,13 +66,11 @@ export class Go2RTCMSE {
|
|||||||
private sourceBuffer: SourceBuffer | null = null;
|
private sourceBuffer: SourceBuffer | null = null;
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
private streamName: string;
|
private streamName: string;
|
||||||
private go2rtcUrl: string;
|
|
||||||
private videoElement: HTMLVideoElement | null = null;
|
private videoElement: HTMLVideoElement | null = null;
|
||||||
private queue: ArrayBuffer[] = [];
|
private queue: ArrayBuffer[] = [];
|
||||||
|
|
||||||
constructor(streamName: string, go2rtcUrl: string) {
|
constructor(streamName: string, _go2rtcUrl?: string) {
|
||||||
this.streamName = streamName;
|
this.streamName = streamName;
|
||||||
this.go2rtcUrl = go2rtcUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(videoElement: HTMLVideoElement): Promise<void> {
|
async connect(videoElement: HTMLVideoElement): Promise<void> {
|
||||||
@@ -85,7 +82,9 @@ export class Go2RTCMSE {
|
|||||||
this.mediaSource!.addEventListener('sourceopen', () => resolve(), { once: true });
|
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 = new WebSocket(wsUrl);
|
||||||
this.ws.binaryType = 'arraybuffer';
|
this.ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user