diff --git a/src/app/features/call/CallService.ts b/src/app/features/call/CallService.ts
index d66d50a..a323981 100644
--- a/src/app/features/call/CallService.ts
+++ b/src/app/features/call/CallService.ts
@@ -22,7 +22,14 @@ import {
LiveKitJWTResponse,
CallMemberEventContent,
} from './types';
-import { fetchLiveKitJWT, fetchWellKnownWithRTC, getLiveKitFocus, requestOpenIdToken } from './useRtcConfig';
+import {
+ fetchLiveKitJWT,
+ fetchWellKnownWithRTC,
+ getLiveKitFocus,
+ requestOpenIdToken,
+ fetchLiveKitJWTFromServers,
+ getLiveKitHomeserverPriority,
+} from './useRtcConfig';
import { getAudioSettings, SCREEN_SHARE_RESOLUTIONS, SCREEN_SHARE_BITRATES } from '../settings/audio/Audio';
import { getCallSounds, CallSoundType } from './CallSounds';
@@ -293,23 +300,48 @@ export class CallService {
});
try {
- // Step 1: Get an OpenID token from the Matrix homeserver
- const openIdToken = await requestOpenIdToken(
+ // Step 1: Get room and determine which homeservers to try
+ const room = this.matrixClient.getRoom(roomId);
+ if (!room) {
+ throw new Error(`Room ${roomId} not found`);
+ }
+
+ const homeserverPriority = getLiveKitHomeserverPriority(
+ room,
this.config.homeserverBaseUrl,
- this.config.userId,
- this.config.accessToken
+ this.config.userId
);
- // Step 2: Exchange OpenID token for LiveKit JWT
- console.log('Requesting LiveKit JWT for Matrix room:', roomId);
- this.livekitJwt = await fetchLiveKitJWT(
- this.config.livekitServiceUrl,
+ console.log('Trying homeservers in order:', homeserverPriority);
+
+ // Step 2: Try fetching LiveKit JWT from homeservers in priority order
+ const result = await fetchLiveKitJWTFromServers(
+ homeserverPriority,
roomId,
this.config.userId,
this.config.deviceId,
- openIdToken
+ this.config.accessToken
);
+ if (!result) {
+ throw new Error('Failed to get LiveKit JWT from any homeserver');
+ }
+
+ this.livekitJwt = result.jwt;
+
+ // Track if using remote homeserver
+ const userHomeserverDomain = this.config.homeserverBaseUrl
+ .replace('https://', '')
+ .replace('http://', '');
+ const usedHomeserverDomain = result.homeserver
+ .replace('https://', '')
+ .replace('http://', '');
+
+ if (usedHomeserverDomain !== userHomeserverDomain) {
+ this.activeCall!.livekitHomeserver = result.homeserver;
+ console.log(`Using remote homeserver for LiveKit: ${result.homeserver}`);
+ }
+
console.log('Got LiveKit JWT, connecting to:', this.livekitJwt.url);
console.log('LiveKit JWT response:', JSON.stringify(this.livekitJwt));
@@ -918,6 +950,22 @@ export class CallService {
return this.livekitRoom;
}
+ /**
+ * Checks if the current call is using a remote homeserver for LiveKit
+ * @returns true if using a different homeserver than the user's
+ */
+ isUsingRemoteHomeserver(): boolean {
+ return !!this.activeCall?.livekitHomeserver;
+ }
+
+ /**
+ * Gets the homeserver being used for LiveKit (if remote)
+ * @returns The remote homeserver URL, or undefined if using user's homeserver
+ */
+ getLiveKitHomeserver(): string | undefined {
+ return this.activeCall?.livekitHomeserver;
+ }
+
/**
* Starts screen sharing with configurable resolution and bitrate
* @returns true if screen share started successfully
diff --git a/src/app/features/call/types.ts b/src/app/features/call/types.ts
index f3b2e82..17097f4 100644
--- a/src/app/features/call/types.ts
+++ b/src/app/features/call/types.ts
@@ -119,6 +119,8 @@ export interface ActiveCall {
isDeafened: boolean;
isVideoEnabled: boolean;
isScreenSharing: boolean;
+ /** The homeserver being used for LiveKit JWT (if different from user's homeserver) */
+ livekitHomeserver?: string;
}
/**
diff --git a/src/app/features/call/useCall.ts b/src/app/features/call/useCall.ts
index c79a2ff..271d883 100644
--- a/src/app/features/call/useCall.ts
+++ b/src/app/features/call/useCall.ts
@@ -2,7 +2,7 @@ import { createContext, useContext, useState, useCallback, useEffect, useMemo }
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { CallService } from './CallService';
import { ActiveCall, CallEvent, CallEventType, CallState, CallType } from './types';
-import { fetchWellKnownWithRTC, getLiveKitFocus } from './useRtcConfig';
+import { fetchWellKnownWithRTC, getLiveKitFocus, getLiveKitHomeserverPriority } from './useRtcConfig';
interface CallContextValue {
callService: CallService | null;
@@ -245,10 +245,36 @@ export function useCallService(): CallContextValue {
* Hook to check if a call is active in a specific room
*/
export function useRoomCall(roomId: string) {
- const { activeCall, startCall, endCall, callSupported, callSupportLoading } = useCall();
+ const { activeCall, startCall, endCall, callSupported, callSupportLoading, callService } = useCall();
+ const mx = useMatrixClient();
const isInCall = activeCall?.roomId === roomId;
const callState = isInCall ? activeCall.state : CallState.Idle;
+ const isUsingRemoteHomeserver = isInCall ? callService?.isUsingRemoteHomeserver() ?? false : false;
+
+ // Check if a remote homeserver WOULD be used (pre-call check)
+ const wouldUseRemoteHomeserver = useMemo(() => {
+ if (isInCall) return isUsingRemoteHomeserver;
+
+ const room = mx.getRoom(roomId);
+ if (!room) return false;
+
+ const homeserverPriority = getLiveKitHomeserverPriority(
+ room,
+ mx.getHomeserverUrl(),
+ mx.getSafeUserId()
+ );
+
+ // Check if first priority is different from user's homeserver
+ const userHomeserverDomain = mx.getHomeserverUrl()
+ .replace('https://', '')
+ .replace('http://', '');
+ const firstPriorityDomain = homeserverPriority[0]
+ ?.replace('https://', '')
+ .replace('http://', '');
+
+ return firstPriorityDomain !== userHomeserverDomain;
+ }, [isInCall, isUsingRemoteHomeserver, mx, roomId]);
const startVoiceCall = useCallback(() => startCall(roomId, CallType.Voice), [roomId, startCall]);
const startVideoCall = useCallback(() => startCall(roomId, CallType.Video), [roomId, startCall]);
@@ -257,6 +283,7 @@ export function useRoomCall(roomId: string) {
isInCall,
callState,
activeCall: isInCall ? activeCall : null,
+ isUsingRemoteHomeserver: wouldUseRemoteHomeserver,
startVoiceCall,
startVideoCall,
endCall,
diff --git a/src/app/features/call/useRtcConfig.ts b/src/app/features/call/useRtcConfig.ts
index 88a0d58..ab5abfd 100644
--- a/src/app/features/call/useRtcConfig.ts
+++ b/src/app/features/call/useRtcConfig.ts
@@ -1,6 +1,59 @@
import { useCallback } from 'react';
+import { Room } from 'matrix-js-sdk';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { LiveKitFocus, LiveKitJWTResponse, OpenIdToken, WellKnownWithRTC } from './types';
+import { getMxIdServer } from '../../utils/matrix';
+import { guessDmRoomUserId } from '../../utils/matrix';
+
+/**
+ * Get room server from room ID (format: !roomid:server.com)
+ */
+function getRoomServer(roomId: string): string | undefined {
+ return getMxIdServer(roomId);
+}
+
+/**
+ * Determine which homeservers to try for LiveKit JWT, in priority order
+ * @param room - Room to start call in
+ * @param userHomeserver - User's homeserver
+ * @param myUserId - Current user's Matrix ID
+ * @returns Array of homeserver base URLs to try in order
+ */
+export function getLiveKitHomeserverPriority(
+ room: Room,
+ userHomeserver: string,
+ myUserId: string
+): string[] {
+ const servers: string[] = [];
+ const memberCount = room.getMembers().length;
+ const isDM = memberCount <= 2;
+
+ if (isDM) {
+ // For DMs: user's homeserver first, then other member's homeserver
+ servers.push(userHomeserver);
+
+ const otherUserId = guessDmRoomUserId(room, myUserId);
+ if (otherUserId && otherUserId !== myUserId) {
+ const otherServer = getMxIdServer(otherUserId);
+ if (otherServer && otherServer !== userHomeserver) {
+ servers.push(`https://${otherServer}`);
+ }
+ }
+ } else {
+ // For channels: room's homeserver first, then user's homeserver
+ const roomServer = getRoomServer(room.roomId);
+ if (roomServer) {
+ servers.push(`https://${roomServer}`);
+ }
+
+ // Fallback to user's homeserver if different
+ if (!roomServer || roomServer !== userHomeserver.replace('https://', '').replace('http://', '')) {
+ servers.push(userHomeserver);
+ }
+ }
+
+ return servers;
+}
/**
* Fetches the LiveKit focus configuration from well-known data
@@ -113,6 +166,59 @@ export async function fetchLiveKitJWT(
return response.json();
}
+/**
+ * Try fetching LiveKit JWT from multiple homeservers in priority order
+ * @param homeservers - Array of homeserver base URLs to try
+ * @param roomId - The Matrix room ID
+ * @param userId - The Matrix user ID
+ * @param deviceId - The Matrix device ID
+ * @param accessToken - The Matrix access token
+ * @returns LiveKit JWT response and the homeserver that provided it, or null
+ */
+export async function fetchLiveKitJWTFromServers(
+ homeservers: string[],
+ roomId: string,
+ userId: string,
+ deviceId: string,
+ accessToken: string
+): Promise<{ jwt: LiveKitJWTResponse; homeserver: string } | null> {
+ for (const homeserver of homeservers) {
+ try {
+ console.log(`Trying LiveKit JWT from homeserver: ${homeserver}`);
+
+ // Fetch well-known from this homeserver
+ const wellKnown = await fetchWellKnownWithRTC(homeserver);
+ const livekitFocus = getLiveKitFocus(wellKnown);
+
+ if (!livekitFocus) {
+ console.log(`No LiveKit config on ${homeserver}, trying next...`);
+ continue;
+ }
+
+ // Get OpenID token from this homeserver
+ const openIdToken = await requestOpenIdToken(homeserver, userId, accessToken);
+
+ // Exchange for LiveKit JWT
+ const jwt = await fetchLiveKitJWT(
+ livekitFocus.livekit_service_url,
+ roomId,
+ userId,
+ deviceId,
+ openIdToken
+ );
+
+ console.log(`Successfully got LiveKit JWT from ${homeserver}`);
+ return { jwt, homeserver };
+ } catch (error) {
+ console.warn(`Failed to get LiveKit JWT from ${homeserver}:`, error);
+ // Continue to next homeserver
+ }
+ }
+
+ console.error('Failed to get LiveKit JWT from any homeserver');
+ return null;
+}
+
/**
* Hook to get RTC configuration functions
* Provides utilities for fetching well-known data and LiveKit JWTs
diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx
index f8b3b97..e608c74 100644
--- a/src/app/features/room/RoomViewHeader.tsx
+++ b/src/app/features/room/RoomViewHeader.tsx
@@ -381,6 +381,7 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
isInCall,
callState,
activeCall,
+ isUsingRemoteHomeserver,
startVoiceCall,
startVideoCall,
endCall,
@@ -433,7 +434,10 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
offset={4}
tooltip={
- {isInCall ? 'End Call' : 'Voice Call'}
+
+ {isInCall ? 'End Call' : 'Voice Call'}
+ {isUsingRemoteHomeserver && !isInCall && ' (Remote Server)'}
+
}
>
@@ -446,7 +450,12 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
{isConnecting ? (
) : (
-
+
+
+ {isUsingRemoteHomeserver && (
+
+ )}
+
)}
)}
@@ -456,7 +465,10 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
offset={4}
tooltip={
- {isInCall ? 'End Call' : 'Video Call'}
+
+ {isInCall ? 'End Call' : 'Video Call'}
+ {isUsingRemoteHomeserver && !isInCall && ' (Remote Server)'}
+
}
>
@@ -469,7 +481,12 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
{isConnecting ? (
) : (
-
+
+
+ {isUsingRemoteHomeserver && (
+
+ )}
+
)}
)}