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
2 changes: 1 addition & 1 deletion app/src/components/call/window/CallWindow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
<j-text size="400" nomargin color="warning-500"> This is a beta feature </j-text>
</j-flex>
<j-text size="300" nomargin color="warning-500">
We use external STUN servers to establish the connection. Any further communication is peer-to-peer.
Connection is established via Iroh transport. All further communication is peer-to-peer.
</j-text>
</div>
</div>
Expand Down
95 changes: 60 additions & 35 deletions app/src/stores/webrtcStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,6 @@ export const WEBRTC_EMOJI = 'webrtc/emoji';
export const WEBRTC_MEDIA_SETTINGS_CHANGED = 'webrtc/media-settings-changed';
export const WEBRTC_LEAVING_CALL = 'webrtc/leaving-call';
const MAX_RECONNECTION_ATTEMPTS = 3;
const defaultIceServers = [
{
urls: 'stun:relay.ad4m.dev:3478',
username: 'openrelay',
credential: 'openrelay',
},
{
urls: 'turn:relay.ad4m.dev:443',
username: 'openrelay',
credential: 'openrelay',
},
{
urls: 'stun:stun.l.google.com:19302',
},
{
urls: 'stun:global.stun.twilio.com:3478',
},
] as IceServer[];

export type IceServer = { urls: string; username?: string; credential?: string };
export type MediaState = 'on' | 'off' | 'loading';
export type PeerConnection = {
did: string;
Expand Down Expand Up @@ -73,6 +53,59 @@ export const useWebrtcStore = defineStore(
const { stream: localStream, mediaSettings } = storeToRefs(mediaDevicesStore);
const { getCommunityService } = communityServiceStore;

// --- Iroh-ICE: Dynamic STUN server discovery from executor ---
const executorIceServers = ref<RTCIceServer[]>([]);
let iceRefreshInterval: NodeJS.Timeout | null = null;

/**
* Format executor ICE candidate addresses as STUN server URLs.
* Each candidate's address:port becomes a `stun:` URL for RTCPeerConnection.
*/
function formatStunServers(candidates: Array<{ address: string; port: number }>): RTCIceServer[] {
return candidates
.filter((c) => c.address && c.port)
.map((c) => ({ urls: `stun:${c.address}:${c.port}` }));
}

/**
* Fetch ICE candidates from the executor and update iceServers.
* Falls back to empty list if executor is unavailable.
*/
async function refreshIceServers() {
try {
const wsUrl = localStorage.getItem('ad4m-url') || 'ws://localhost:12000/graphql';
const httpUrl = wsUrl.replace('ws://', 'http://').replace('wss://', 'https://');
const token = localStorage.getItem('ad4m-token') || '';
const response = await fetch(httpUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(token ? { authorization: token } : {}) },
body: JSON.stringify({ query: '{ runtimeIceCandidates { address port } }' }),
});
const json = await response.json();
const candidates = json?.data?.runtimeIceCandidates || [];
executorIceServers.value = formatStunServers(candidates);
} catch (e) {
console.warn('iroh-ice: failed to fetch ICE candidates from executor:', e);
// Keep existing servers on failure; fall back to empty on first failure
}
}

/** Start periodic ICE server refresh (every 60s). */
function startIceRefresh() {
if (iceRefreshInterval) return;
refreshIceServers(); // immediate first fetch
iceRefreshInterval = setInterval(refreshIceServers, 60_000);
}
Comment on lines +93 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition: ICE servers may not be available when peers connect.

startIceRefresh() calls refreshIceServers() without awaiting it. In joinRoom(), peer connections are created immediately after calling startIceRefresh() (lines 629-636). Since refreshIceServers() is async and involves a network fetch, executorIceServers.value will likely still be empty when createPeerConnection() runs.

Consider awaiting the initial fetch before creating peer connections:

🔧 Proposed fix
-    /** Start periodic ICE server refresh (every 60s). */
-    function startIceRefresh() {
+    /** Start periodic ICE server refresh (every 60s). Returns promise for initial fetch. */
+    async function startIceRefresh() {
       if (iceRefreshInterval) return;
-      refreshIceServers(); // immediate first fetch
+      await refreshIceServers(); // await initial fetch before connections
       iceRefreshInterval = setInterval(refreshIceServers, 60_000);
     }

Then in joinRoom():

       // Start fetching STUN servers from executor
-      startIceRefresh();
+      await startIceRefresh();

       try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/stores/webrtcStore.ts` around lines 93 - 98, The initial ICE fetch is
started but not awaited, causing executorIceServers.value to be empty when peers
are created; modify startIceRefresh/refreshIceServers so the initial fetch is
awaited (e.g., have startIceRefresh return a Promise that awaits
refreshIceServers for the first call and then starts the interval) and update
joinRoom to await startIceRefresh (or directly await refreshIceServers) before
calling createPeerConnection so executorIceServers.value is populated when peer
connections are constructed.


/** Stop periodic ICE server refresh. */
function stopIceRefresh() {
if (iceRefreshInterval) {
clearInterval(iceRefreshInterval);
iceRefreshInterval = null;
}
}
// --- End Iroh-ICE ---

const popSound = new Howl({ src: [popWav] });
const guitarSound = new Howl({ src: [guitarWav] });
const kissSound = new Howl({ src: [kissWav] });
Expand All @@ -88,7 +121,6 @@ export const useWebrtcStore = defineStore(
const myAgentStatus = ref<AgentStatus>('active');
const reconnectionAttempts = ref<Record<string, number>>({});
const reconnectionTimeouts = ref<Record<string, NodeJS.Timeout>>({});
const iceServers = ref(defaultIceServers);
const disconnectedAgents = ref<string[]>([]);
const hasCopiedLink = ref(false);
let copyLinkTimer: ReturnType<typeof setTimeout> | null = null;
Expand Down Expand Up @@ -207,7 +239,7 @@ export const useWebrtcStore = defineStore(
const peer = new SimplePeer({
initiator,
stream: localStream.value || undefined,
config: { iceServers: iceServers.value },
config: { iceServers: executorIceServers.value }, // Dynamic STUN servers from Iroh transport
trickle: true,
}) as Instance;

Expand Down Expand Up @@ -577,21 +609,15 @@ export const useWebrtcStore = defineStore(
}
}

function addIceServer(newIceServer: IceServer) {
iceServers.value = [...iceServers.value, newIceServer];
}

function removeIceServer(url: string) {
iceServers.value = iceServers.value.filter((server) => server.urls !== url);
}

function resetIceServers() {
iceServers.value = defaultIceServers;
}

async function joinRoom() {
joiningCall.value = true;

// Start fetching STUN servers from executor
startIceRefresh();

try {
// Update the call route
callRoute.value = route.params;
Expand Down Expand Up @@ -626,6 +652,9 @@ export const useWebrtcStore = defineStore(

async function leaveRoom() {
try {
// Stop STUN server refresh
stopIceRefresh();

// Signal all peers that we're leaving the call
signalPeers(WEBRTC_LEAVING_CALL);

Expand Down Expand Up @@ -779,16 +808,12 @@ export const useWebrtcStore = defineStore(
communityService,
peerConnections,
joiningCall,
iceServers,
disconnectedAgents,
hasCopiedLink,
addTrack,
removeTrack,
replaceAudioTrack,
replaceVideoTrack,
addIceServer,
removeIceServer,
resetIceServers,
joinRoom,
leaveRoom,
signalAgent,
Expand Down
136 changes: 0 additions & 136 deletions app/src/views/main/modals/webrtc-settings/Connection.vue

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@
<j-menu-item :selected="currentTab === 'transcription'" @click="currentTab = 'transcription'">
Transcription
</j-menu-item>
<j-menu-item :selected="currentTab === 'connection'" @click="currentTab = 'connection'">
Connection
</j-menu-item>
<!-- <j-menu-item :selected="currentTab === 'debug'" @click="currentTab = 'debug'"> Debug </j-menu-item> -->
</j-menu-group-item>
</div>

<div class="contents">
<VoiceVideo v-if="currentTab === 'voice-video'" />
<Transcription v-if="currentTab === 'transcription'" />
<Connection v-if="currentTab === 'connection'" />
<Debug v-if="currentTab === 'debug'" />
</div>
</div>
Expand All @@ -33,7 +29,6 @@
<script setup lang="ts">
import { useModalStore } from '@/stores';
import { ref } from 'vue';
import Connection from './Connection.vue';
import Debug from './Debug.vue';
import Transcription from './Transcription.vue';
import VoiceVideo from './VoiceVideo.vue';
Expand Down
20 changes: 0 additions & 20 deletions packages/constants/src/videoSettings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { IceServer, Settings } from 'utils/helpers/WebRTCManager';

const frameRate = {
min: 5,
Expand Down Expand Up @@ -33,22 +32,3 @@ export const defaultSettings = {
messageTimeout: 5,
},
} as Settings;

export const defaultIceServers = [
{
urls: 'stun:relay.ad4m.dev:3478',
username: 'openrelay',
credential: 'openrelay',
},
{
urls: 'turn:relay.ad4m.dev:443',
username: 'openrelay',
credential: 'openrelay',
},
{
urls: 'stun:stun.l.google.com:19302',
},
{
urls: 'stun:global.stun.twilio.com:3478',
},
] as IceServer[];
Loading
Loading