feat: enable FORCE_TURN_RELAY to diagnose turn connectivity for local deployment setups (#272)

* filter out local sdp candidates on non local environment

* feat: add FORCE_TURN_RELAY variable

* add FORCE_TURN_RELAY option in docker-compose

* fix: fix github workflow
This commit is contained in:
Abhishek 2026-05-11 17:13:01 +05:30 committed by GitHub
parent 01c201bf09
commit e2fe1f3cd4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 410 additions and 37 deletions

View file

@ -72,6 +72,7 @@ jobs:
cache: pip
cache-dependency-path: |
api/requirements.txt
api/requirements.dev.txt
pipecat/pyproject.toml
- name: Set up Node 22 (test_ts_bridge.py shells out to node)
@ -79,11 +80,8 @@ jobs:
with:
node-version: "22"
- name: Install api dependencies
run: |
pip install -r api/requirements.txt
pip install -r api/requirements.dev.txt
pip install './pipecat[cartesia,deepgram,openai,elevenlabs,groq,google,azure,sarvam,soundfile,silero,webrtc,speechmatics,openrouter,camb]'
- name: Install api and pipecat dependencies
run: ./scripts/setup_requirements.sh --dev
- name: Install ts_validator npm deps
working-directory: api/mcp_server/ts_validator

View file

@ -133,6 +133,11 @@ TURN_HOST = os.getenv("TURN_HOST", "localhost")
TURN_PORT = int(os.getenv("TURN_PORT", "3478"))
TURN_TLS_PORT = int(os.getenv("TURN_TLS_PORT", "5349"))
TURN_CREDENTIAL_TTL = int(os.getenv("TURN_CREDENTIAL_TTL", "86400"))
# Diagnostic flag: when true, strip all non-relay ICE candidates from the
# answer SDP so every media path must traverse the TURN server. Use for
# verifying TURN connectivity end-to-end; expect connection failures if
# TURN is misconfigured or unreachable.
FORCE_TURN_RELAY = os.getenv("FORCE_TURN_RELAY", "false").lower() == "true"
# OSS Email/Password Auth
OSS_JWT_SECRET = os.getenv("OSS_JWT_SECRET", "change-me-in-production")

View file

@ -66,11 +66,19 @@ class HealthResponse(BaseModel):
backend_api_endpoint: str
deployment_mode: str
auth_provider: str
turn_enabled: bool
force_turn_relay: bool
@router.get("/health", response_model=HealthResponse)
async def health() -> HealthResponse:
from api.constants import APP_VERSION, AUTH_PROVIDER, DEPLOYMENT_MODE
from api.constants import (
APP_VERSION,
AUTH_PROVIDER,
DEPLOYMENT_MODE,
FORCE_TURN_RELAY,
TURN_SECRET,
)
from api.utils.common import get_backend_endpoints
logger.debug("Health endpoint called")
@ -81,4 +89,6 @@ async def health() -> HealthResponse:
backend_api_endpoint=backend_endpoint,
deployment_mode=DEPLOYMENT_MODE,
auth_provider=AUTH_PROVIDER,
turn_enabled=bool(TURN_SECRET),
force_turn_relay=FORCE_TURN_RELAY,
)

View file

@ -28,7 +28,7 @@ from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
from pipecat.utils.run_context import set_current_org_id, set_current_run_id
from starlette.websockets import WebSocketState
from api.constants import ENVIRONMENT
from api.constants import ENVIRONMENT, FORCE_TURN_RELAY
from api.db import db_client
from api.db.models import UserModel
from api.enums import Environment
@ -77,6 +77,58 @@ def is_private_ip_candidate(candidate_str: str) -> bool:
return False
def filter_outbound_sdp(sdp: str) -> str:
"""Strip ICE candidates from an outbound answer SDP based on env config.
Two filters apply:
1. In non-LOCAL environments, drop host candidates with private/CGNAT IPs.
aiortc gathers host candidates from every interface on the box, including
Docker bridges (172.17.0.1, 172.18.0.1). Advertising those to the browser
causes coturn "peer IP X denied" errors when the browser asks TURN to
permit them.
2. When FORCE_TURN_RELAY is set, drop every non-relay candidate so the
only path the browser can use is via TURN. Lets you verify TURN
connectivity end-to-end if TURN is broken, the call simply fails.
"""
if ENVIRONMENT == Environment.LOCAL.value and not FORCE_TURN_RELAY:
return sdp
lines = sdp.split("\r\n")
filtered: List[str] = []
dropped_non_relay = 0
kept_relay = 0
for line in lines:
if line.startswith("a=candidate:"):
candidate_str = line[2:]
if FORCE_TURN_RELAY and " typ relay" not in candidate_str:
dropped_non_relay += 1
continue
if ENVIRONMENT != Environment.LOCAL.value and is_private_ip_candidate(
candidate_str
):
continue
if FORCE_TURN_RELAY:
kept_relay += 1
filtered.append(line)
if FORCE_TURN_RELAY:
if kept_relay == 0:
logger.warning(
"FORCE_TURN_RELAY is on but the answer SDP has no relay candidates "
f"(dropped {dropped_non_relay} non-relay). TURN may be unreachable; "
"the connection will fail."
)
else:
logger.info(
f"FORCE_TURN_RELAY: kept {kept_relay} relay candidates, "
f"dropped {dropped_non_relay} non-relay"
)
return "\r\n".join(filtered)
def get_ice_servers(user_id: Optional[str] = None) -> List[RTCIceServer]:
"""Build ICE servers configuration including TURN if configured.
@ -247,7 +299,11 @@ class SignalingManager:
await ws.send_json(
{
"type": "answer",
"payload": {"sdp": answer["sdp"], "type": "answer", "pc_id": pc_id},
"payload": {
"sdp": filter_outbound_sdp(answer["sdp"]),
"type": "answer",
"pc_id": pc_id,
},
}
)
else:
@ -299,7 +355,7 @@ class SignalingManager:
{
"type": "answer",
"payload": {
"sdp": answer["sdp"],
"sdp": filter_outbound_sdp(answer["sdp"]),
"type": answer["type"],
"pc_id": answer["pc_id"],
},
@ -380,7 +436,7 @@ class SignalingManager:
{
"type": "answer",
"payload": {
"sdp": answer["sdp"],
"sdp": filter_outbound_sdp(answer["sdp"]),
"type": "answer",
"pc_id": pc_id, # Use the client's pc_id
},

View file

@ -241,12 +241,10 @@ async def _run_pipeline(
raise HTTPException(status_code=400, detail="Workflow run already completed")
merged_call_context_vars = workflow_run.initial_context
# If there is some extra call_context_vars, update them
# If there is some extra call_context_vars, fold them in. Persistence
# happens once below, after runtime_configuration is also resolved.
if call_context_vars:
merged_call_context_vars = {**merged_call_context_vars, **call_context_vars}
await db_client.update_workflow_run(
workflow_run_id, initial_context=merged_call_context_vars
)
# Get user configuration
user_config = await db_client.get_user_configurations(user_id)
@ -312,6 +310,36 @@ async def _run_pipeline(
llm = create_llm_service(user_config)
inference_llm = None
# Stamp the providers/models actually resolved for this run onto
# initial_context so they're available for post-call analytics
# (model_overrides may have shifted them away from the org-level
# user_config).
if is_realtime:
# llm_* refers to the side-channel text LLM (variable extraction,
# voicemail detection); realtime_* is the speech-to-speech service.
runtime_configuration = {
"realtime_provider": user_config.realtime.provider,
"realtime_model": user_config.realtime.model,
"llm_provider": user_config.llm.provider,
"llm_model": user_config.llm.model,
}
else:
runtime_configuration = {
"stt_provider": user_config.stt.provider,
"stt_model": user_config.stt.model,
"tts_provider": user_config.tts.provider,
"tts_model": user_config.tts.model,
"llm_provider": user_config.llm.provider,
"llm_model": user_config.llm.model,
}
merged_call_context_vars = {
**merged_call_context_vars,
"runtime_configuration": runtime_configuration,
}
await db_client.update_workflow_run(
workflow_run_id, initial_context=merged_call_context_vars
)
workflow_graph = WorkflowGraph(ReactFlowDTO.model_validate(run_workflow_json))
# Pre-call fetch: fire early so it runs concurrently with remaining setup

View file

@ -106,7 +106,7 @@ async def check_dograh_quota(
logger.info(
f"Dograh quota check passed for key ...{api_key[-8:]}: "
f"${remaining:.2f} remaining"
f"{remaining:.2f} credits remaining"
)
except Exception as e:
logger.error(f"Failed to check quota for Dograh key: {str(e)}")

View file

@ -80,7 +80,7 @@ services:
- shared-tmp:/tmp
environment:
# Core application config
ENVIRONMENT: "local"
ENVIRONMENT: "${ENVIRONMENT:-local}"
LOG_LEVEL: "INFO"
# Replace this environment variable if you are using a custom
@ -121,6 +121,7 @@ services:
# Uses time-limited credentials via TURN REST API (HMAC-SHA1)
TURN_HOST: "${TURN_HOST:-}"
TURN_SECRET: "${TURN_SECRET:-}"
FORCE_TURN_RELAY: "${FORCE_TURN_RELAY:-false}"
OSS_JWT_SECRET: "${OSS_JWT_SECRET:-ChangeMeInProduction}"
@ -196,7 +197,7 @@ services:
image: coturn/coturn:4.8.0
container_name: coturn
restart: unless-stopped
profiles: ["remote"]
profiles: ["remote", "local-turn"]
ports:
- "3478:3478/udp"
- "3478:3478/tcp"

File diff suppressed because one or more lines are too long

View file

@ -46,6 +46,38 @@ http://localhost:3010
You can disable telemetry by setting `ENABLE_TELEMETRY=false` in the command above.
</Note>
### Troubleshooting WebRTC Connectivity
The Quick Start above relies on direct peer-to-peer WebRTC between your browser and the API container. On most single-machine setups this works without any extra configuration. If you hit any of the following, you likely need a TURN server in the path:
- The call connects but no audio flows in either direction
- The browser console reports `iceConnectionState: failed`
- You are testing from a phone or another device on your LAN against the laptop running Docker
- A VPN, corporate firewall, or strict NAT sits between the browser and Docker
For these cases, use the alternate local setup script which configures a coturn TURN server alongside the rest of the stack:
```bash
curl -o setup_local.sh https://raw.githubusercontent.com/dograh-hq/dograh/main/scripts/setup_local.sh && chmod +x setup_local.sh && ./setup_local.sh
```
The script will prompt you for:
- Whether to enable coturn (answer `y`)
- The host browsers should use to reach TURN (press Enter for `127.0.0.1`; use your LAN IP if testing from another device on the same network)
- A shared secret for the TURN server (press Enter to generate a random one)
It creates `docker-compose.yaml`, `turnserver.conf`, and a `.env` file with TURN credentials. Start the stack with the `local-turn` profile so coturn comes up alongside the other services:
```bash
docker compose --profile local-turn up --pull always
```
The application is still available at `http://localhost:3010`.
<Note>
To verify that media is actually traversing TURN (and not silently falling back to a direct path), set `FORCE_TURN_RELAY=true` in `.env` and restart the API. The browser will then only use relay ICE candidates — if TURN is misconfigured or unreachable, the call fails cleanly with `ICE failed` in the console instead of appearing to work. Set it back to `false` once you have verified connectivity.
</Note>
## Option 2: Remote Server Deployment
Watch the video tutorial below for a step-by-step walkthrough of deploying Dograh AI to a remote server.

View file

@ -93,6 +93,7 @@ Dograh uses **MinIO by default**, which is bundled with the self-hosted deployme
| `TURN_TLS_PORT` | `5349` | TURN server TLS port |
| `TURN_SECRET` | `null` | **Required for WebRTC.** Shared secret for TURN credential generation |
| `TURN_CREDENTIAL_TTL` | `86400` | TURN credential validity in seconds (default: 24h) |
| `FORCE_TURN_RELAY` | `false` | Diagnostic flag. When `true`, restricts ICE to relay-only candidates on both server (SDP filter) and browser (`iceTransportPolicy: 'relay'`). Use to verify TURN connectivity end-to-end — calls fail cleanly if TURN is misconfigured instead of silently falling back to a direct path. |
---

201
scripts/setup_local.sh Executable file
View file

@ -0,0 +1,201 @@
#!/bin/bash
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}"
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ Dograh Local Setup ║"
echo "║ Local docker deployment, optional TURN server ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo -e "${NC}"
# Ask whether to enable coturn (skip prompt if ENABLE_COTURN is already set)
if [[ -z "$ENABLE_COTURN" ]]; then
echo -e "${YELLOW}Enable coturn (TURN server) for WebRTC NAT traversal? [y/N]:${NC}"
read -p "> " ENABLE_COTURN_INPUT
if [[ "$ENABLE_COTURN_INPUT" =~ ^[Yy] ]]; then
ENABLE_COTURN=true
else
ENABLE_COTURN=false
fi
fi
if [[ "$ENABLE_COTURN" == "true" ]]; then
# Pick a TURN_HOST that's reachable from BOTH the browser (running on the
# host) and the API container (running in docker). 127.0.0.1 is tempting
# but doesn't work for the api container — its own loopback isn't where
# coturn lives, so aiortc can't allocate a relay and FORCE_TURN_RELAY
# ends up with an empty answer SDP. The host's LAN IP works for both.
detect_lan_ip() {
local ip=""
if command -v ipconfig >/dev/null 2>&1; then
for iface in en0 en1 en2 en3 en4; do
ip=$(ipconfig getifaddr "$iface" 2>/dev/null)
[[ -n "$ip" ]] && { echo "$ip"; return; }
done
fi
if command -v ip >/dev/null 2>&1; then
ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}')
[[ -n "$ip" ]] && { echo "$ip"; return; }
fi
if command -v hostname >/dev/null 2>&1; then
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
[[ -n "$ip" ]] && { echo "$ip"; return; }
fi
}
DEFAULT_TURN_HOST="$(detect_lan_ip)"
DEFAULT_TURN_HOST="${DEFAULT_TURN_HOST:-127.0.0.1}"
# Get the host browsers/peers will use to reach the TURN server
if [[ -z "$TURN_HOST" ]]; then
echo -e "${YELLOW}Enter the host browsers AND the API container will use to reach TURN${NC}"
echo -e "${YELLOW}(press Enter for ${DEFAULT_TURN_HOST}):${NC}"
read -p "> " TURN_HOST
fi
TURN_HOST="${TURN_HOST:-$DEFAULT_TURN_HOST}"
# Validate that TURN_HOST is either an IP or a hostname (basic check)
if ! [[ "$TURN_HOST" =~ ^[A-Za-z0-9.-]+$ ]]; then
echo -e "${RED}Error: TURN host must be an IP address or hostname${NC}"
exit 1
fi
# Get the TURN secret (skip prompt if TURN_SECRET is already set)
if [[ -z "$TURN_SECRET" ]]; then
echo -e "${YELLOW}Enter a shared secret for the TURN server (press Enter to generate a random one):${NC}"
read -sp "> " TURN_SECRET
echo ""
fi
if [[ -z "$TURN_SECRET" ]]; then
TURN_SECRET=$(openssl rand -hex 32)
echo -e "${BLUE}Generated random TURN secret${NC}"
fi
fi
# Telemetry opt-out (default: true)
ENABLE_TELEMETRY="${ENABLE_TELEMETRY:-true}"
# Container registry (defaults to the public OSS registry)
REGISTRY="${REGISTRY:-ghcr.io/dograh-hq}"
echo ""
echo -e "${GREEN}Configuration:${NC}"
echo -e " Coturn: ${BLUE}$ENABLE_COTURN${NC}"
if [[ "$ENABLE_COTURN" == "true" ]]; then
echo -e " TURN Host: ${BLUE}$TURN_HOST${NC}"
echo -e " TURN Secret: ${BLUE}********${NC}"
fi
echo -e " Telemetry: ${BLUE}$ENABLE_TELEMETRY${NC}"
echo -e " Registry: ${BLUE}$REGISTRY${NC}"
echo ""
# Download compose file (skip when DOGRAH_SKIP_DOWNLOAD=1 — e.g. local repo testing).
TOTAL_STEPS=2
if [[ "$ENABLE_COTURN" == "true" ]]; then
TOTAL_STEPS=3
fi
if [[ "$DOGRAH_SKIP_DOWNLOAD" != "1" ]]; then
echo -e "${BLUE}[1/$TOTAL_STEPS] Downloading docker-compose.yaml...${NC}"
curl -sS -o docker-compose.yaml https://raw.githubusercontent.com/dograh-hq/dograh/main/docker-compose.yaml
echo -e "${GREEN}✓ docker-compose.yaml downloaded${NC}"
else
echo -e "${BLUE}[1/$TOTAL_STEPS] Using docker-compose.yaml in current directory${NC}"
fi
# Generate turnserver.conf if coturn is enabled
if [[ "$ENABLE_COTURN" == "true" ]]; then
echo -e "${BLUE}[2/$TOTAL_STEPS] Creating TURN server configuration...${NC}"
cat > turnserver.conf << TURN_EOF
# Coturn TURN Server - Docker Configuration (local)
# Auto-generated by setup_local.sh
# Listener ports
listening-port=3478
tls-listening-port=5349
# Relay port range
min-port=49152
max-port=49200
# Network - external IP for NAT traversal
external-ip=$TURN_HOST
# Realm
realm=dograh.com
# Authentication (TURN REST API with time-limited credentials)
use-auth-secret
static-auth-secret=$TURN_SECRET
# Security
fingerprint
no-cli
no-multicast-peers
# Logging
log-file=stdout
TURN_EOF
echo -e "${GREEN}✓ turnserver.conf created${NC}"
fi
# Generate .env
ENV_STEP=$TOTAL_STEPS
echo -e "${BLUE}[$ENV_STEP/$TOTAL_STEPS] Creating environment file...${NC}"
OSS_JWT_SECRET=$(openssl rand -hex 32)
cat > .env << ENV_EOF
# Container registry for Dograh images
REGISTRY=$REGISTRY
# JWT secret for OSS authentication
OSS_JWT_SECRET=$OSS_JWT_SECRET
# Telemetry (set to false to disable)
ENABLE_TELEMETRY=$ENABLE_TELEMETRY
ENV_EOF
if [[ "$ENABLE_COTURN" == "true" ]]; then
cat >> .env << ENV_EOF
# TURN Server Configuration (time-limited credentials via TURN REST API)
TURN_HOST=$TURN_HOST
TURN_SECRET=$TURN_SECRET
ENV_EOF
fi
echo -e "${GREEN}✓ .env file created${NC}"
echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Setup Complete! ║${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "Files created in ${BLUE}$(pwd)${NC}:"
echo " - docker-compose.yaml"
echo " - .env"
if [[ "$ENABLE_COTURN" == "true" ]]; then
echo " - turnserver.conf"
fi
echo ""
if [[ "$ENABLE_COTURN" == "true" ]]; then
echo -e "${YELLOW}To start Dograh with TURN, run:${NC}"
echo ""
echo -e " ${BLUE}docker compose --profile local-turn up --pull always${NC}"
else
echo -e "${YELLOW}To start Dograh, run:${NC}"
echo ""
echo -e " ${BLUE}docker compose up --pull always${NC}"
fi
echo ""
echo -e "${YELLOW}Your application will be available at:${NC}"
echo ""
echo -e " ${BLUE}http://localhost:3010${NC}"
echo ""

View file

@ -204,6 +204,9 @@ echo -e "${BLUE}[6/6] Creating environment file...${NC}"
OSS_JWT_SECRET=$(openssl rand -hex 32)
cat > .env << ENV_EOF
# Change environment from local to production so that coturn filters local IPs
ENVIRONMENT=production
# Backend API endpoint (public URL the backend uses to build webhook/embed links)
BACKEND_API_ENDPOINT=https://$SERVER_IP

View file

@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: dograh-openapi-XXXXXX.json.orthnRzifa
# timestamp: 2026-05-08T10:32:18+00:00
# filename: dograh-openapi-XXXXXX.json.OGVaBJhIgZ
# timestamp: 2026-05-11T11:41:34+00:00
from __future__ import annotations

View file

@ -12,6 +12,8 @@ export async function GET() {
let apiVersion = "unknown";
let deploymentMode = "oss";
let authProvider = "local";
let turnEnabled = false;
let forceTurnRelay = false;
try {
const response = await healthApiV1HealthGet();
@ -20,6 +22,8 @@ export async function GET() {
apiVersion = data.version;
deploymentMode = data.deployment_mode;
authProvider = data.auth_provider;
turnEnabled = Boolean(data.turn_enabled);
forceTurnRelay = Boolean(data.force_turn_relay);
}
} catch {
apiVersion = "unavailable";
@ -30,5 +34,7 @@ export async function GET() {
api: apiVersion,
deploymentMode,
authProvider,
turnEnabled,
forceTurnRelay,
});
}

View file

@ -4,6 +4,7 @@ import { client } from "@/client/client.gen";
import { getTurnCredentialsApiV1TurnCredentialsGet, validateUserConfigurationsApiV1UserConfigurationsUserValidateGet, validateWorkflowApiV1WorkflowWorkflowIdValidatePost } from "@/client/sdk.gen";
import { TurnCredentialsResponse } from "@/client/types.gen";
import { WorkflowValidationError } from "@/components/flow/types";
import { useAppConfig } from "@/context/AppConfigContext";
import logger from '@/lib/logger';
import { sdpFilterCodec } from "../utils";
@ -48,6 +49,7 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
const [isStarting, setIsStarting] = useState(false);
const [feedbackMessages, setFeedbackMessages] = useState<FeedbackMessage[]>([]);
const initialContext = initialContextVariables || {};
const { config: appConfig } = useAppConfig();
const {
audioInputs,
@ -123,6 +125,16 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
iceServers
};
// Diagnostic: when the backend is started with FORCE_TURN_RELAY=true,
// restrict the browser to relay-only candidates so media must traverse
// TURN. Lets you verify TURN connectivity end-to-end — a TURN
// misconfiguration surfaces as an ICE failure instead of silently
// falling back to host/srflx.
if (appConfig?.forceTurnRelay) {
config.iceTransportPolicy = 'relay';
logger.info('FORCE_TURN_RELAY is on — restricting browser ICE to relay candidates only');
}
const pc = new RTCPeerConnection(config);
// Set up ICE candidate trickling
@ -528,24 +540,30 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
setConnectionStatus('connecting');
try {
// Fetch time-limited TURN credentials from backend API
try {
const turnResponse = await getTurnCredentialsApiV1TurnCredentialsGet({
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (turnResponse.data) {
turnCredentialsRef.current = turnResponse.data;
logger.info(`TURN credentials obtained, TTL: ${turnResponse.data.ttl}s`);
} else if (turnResponse.response.status === 503) {
// TURN not configured on server - this is OK, we'll use STUN only
logger.info('TURN server not configured, using STUN only');
} else {
logger.warn(`Failed to fetch TURN credentials: ${turnResponse.response.status}`);
// Fetch time-limited TURN credentials from backend API only if the
// server reports a TURN server is configured. Skipping the request
// avoids a 503 on OSS local deployments that don't run coturn.
if (appConfig?.turnEnabled === false) {
logger.info('TURN server disabled in app config, using STUN only');
} else {
try {
const turnResponse = await getTurnCredentialsApiV1TurnCredentialsGet({
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (turnResponse.data) {
turnCredentialsRef.current = turnResponse.data;
logger.info(`TURN credentials obtained, TTL: ${turnResponse.data.ttl}s`);
} else if (turnResponse.response.status === 503) {
// TURN not configured on server - this is OK, we'll use STUN only
logger.info('TURN server not configured, using STUN only');
} else {
logger.warn(`Failed to fetch TURN credentials: ${turnResponse.response.status}`);
}
} catch (e) {
logger.warn('Failed to fetch TURN credentials, continuing without TURN:', e);
}
} catch (e) {
logger.warn('Failed to fetch TURN credentials, continuing without TURN:', e);
}
// Validate API keys

View file

@ -1812,6 +1812,14 @@ export type HealthResponse = {
* Auth Provider
*/
auth_provider: string;
/**
* Turn Enabled
*/
turn_enabled: boolean;
/**
* Force Turn Relay
*/
force_turn_relay: boolean;
};
/**

View file

@ -7,6 +7,8 @@ interface AppConfig {
apiVersion: string;
deploymentMode: string;
authProvider: string;
turnEnabled: boolean;
forceTurnRelay: boolean;
}
interface AppConfigContextType {
@ -19,6 +21,8 @@ const defaultConfig: AppConfig = {
apiVersion: 'unknown',
deploymentMode: 'oss',
authProvider: 'local',
turnEnabled: false,
forceTurnRelay: false,
};
const AppConfigContext = createContext<AppConfigContextType>({
@ -39,6 +43,8 @@ export function AppConfigProvider({ children }: { children: ReactNode }) {
apiVersion: data.api || 'unknown',
deploymentMode: data.deploymentMode || 'oss',
authProvider: data.authProvider || 'local',
turnEnabled: Boolean(data.turnEnabled),
forceTurnRelay: Boolean(data.forceTurnRelay),
});
})
.catch(() => {