feat: implement remote homeserver handling for LiveKit calls
This commit is contained in:
@@ -22,7 +22,14 @@ import {
|
|||||||
LiveKitJWTResponse,
|
LiveKitJWTResponse,
|
||||||
CallMemberEventContent,
|
CallMemberEventContent,
|
||||||
} from './types';
|
} 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 { getAudioSettings, SCREEN_SHARE_RESOLUTIONS, SCREEN_SHARE_BITRATES } from '../settings/audio/Audio';
|
||||||
import { getCallSounds, CallSoundType } from './CallSounds';
|
import { getCallSounds, CallSoundType } from './CallSounds';
|
||||||
|
|
||||||
@@ -293,23 +300,48 @@ export class CallService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Get an OpenID token from the Matrix homeserver
|
// Step 1: Get room and determine which homeservers to try
|
||||||
const openIdToken = await requestOpenIdToken(
|
const room = this.matrixClient.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
throw new Error(`Room ${roomId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const homeserverPriority = getLiveKitHomeserverPriority(
|
||||||
|
room,
|
||||||
this.config.homeserverBaseUrl,
|
this.config.homeserverBaseUrl,
|
||||||
this.config.userId,
|
this.config.userId
|
||||||
this.config.accessToken
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 2: Exchange OpenID token for LiveKit JWT
|
console.log('Trying homeservers in order:', homeserverPriority);
|
||||||
console.log('Requesting LiveKit JWT for Matrix room:', roomId);
|
|
||||||
this.livekitJwt = await fetchLiveKitJWT(
|
// Step 2: Try fetching LiveKit JWT from homeservers in priority order
|
||||||
this.config.livekitServiceUrl,
|
const result = await fetchLiveKitJWTFromServers(
|
||||||
|
homeserverPriority,
|
||||||
roomId,
|
roomId,
|
||||||
this.config.userId,
|
this.config.userId,
|
||||||
this.config.deviceId,
|
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('Got LiveKit JWT, connecting to:', this.livekitJwt.url);
|
||||||
console.log('LiveKit JWT response:', JSON.stringify(this.livekitJwt));
|
console.log('LiveKit JWT response:', JSON.stringify(this.livekitJwt));
|
||||||
|
|
||||||
@@ -918,6 +950,22 @@ export class CallService {
|
|||||||
return this.livekitRoom;
|
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
|
* Starts screen sharing with configurable resolution and bitrate
|
||||||
* @returns true if screen share started successfully
|
* @returns true if screen share started successfully
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ export interface ActiveCall {
|
|||||||
isDeafened: boolean;
|
isDeafened: boolean;
|
||||||
isVideoEnabled: boolean;
|
isVideoEnabled: boolean;
|
||||||
isScreenSharing: boolean;
|
isScreenSharing: boolean;
|
||||||
|
/** The homeserver being used for LiveKit JWT (if different from user's homeserver) */
|
||||||
|
livekitHomeserver?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createContext, useContext, useState, useCallback, useEffect, useMemo }
|
|||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { CallService } from './CallService';
|
import { CallService } from './CallService';
|
||||||
import { ActiveCall, CallEvent, CallEventType, CallState, CallType } from './types';
|
import { ActiveCall, CallEvent, CallEventType, CallState, CallType } from './types';
|
||||||
import { fetchWellKnownWithRTC, getLiveKitFocus } from './useRtcConfig';
|
import { fetchWellKnownWithRTC, getLiveKitFocus, getLiveKitHomeserverPriority } from './useRtcConfig';
|
||||||
|
|
||||||
interface CallContextValue {
|
interface CallContextValue {
|
||||||
callService: CallService | null;
|
callService: CallService | null;
|
||||||
@@ -245,10 +245,36 @@ export function useCallService(): CallContextValue {
|
|||||||
* Hook to check if a call is active in a specific room
|
* Hook to check if a call is active in a specific room
|
||||||
*/
|
*/
|
||||||
export function useRoomCall(roomId: string) {
|
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 isInCall = activeCall?.roomId === roomId;
|
||||||
const callState = isInCall ? activeCall.state : CallState.Idle;
|
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 startVoiceCall = useCallback(() => startCall(roomId, CallType.Voice), [roomId, startCall]);
|
||||||
const startVideoCall = useCallback(() => startCall(roomId, CallType.Video), [roomId, startCall]);
|
const startVideoCall = useCallback(() => startCall(roomId, CallType.Video), [roomId, startCall]);
|
||||||
@@ -257,6 +283,7 @@ export function useRoomCall(roomId: string) {
|
|||||||
isInCall,
|
isInCall,
|
||||||
callState,
|
callState,
|
||||||
activeCall: isInCall ? activeCall : null,
|
activeCall: isInCall ? activeCall : null,
|
||||||
|
isUsingRemoteHomeserver: wouldUseRemoteHomeserver,
|
||||||
startVoiceCall,
|
startVoiceCall,
|
||||||
startVideoCall,
|
startVideoCall,
|
||||||
endCall,
|
endCall,
|
||||||
|
|||||||
@@ -1,6 +1,59 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { Room } from 'matrix-js-sdk';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { LiveKitFocus, LiveKitJWTResponse, OpenIdToken, WellKnownWithRTC } from './types';
|
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
|
* Fetches the LiveKit focus configuration from well-known data
|
||||||
@@ -113,6 +166,59 @@ export async function fetchLiveKitJWT(
|
|||||||
return response.json();
|
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
|
* Hook to get RTC configuration functions
|
||||||
* Provides utilities for fetching well-known data and LiveKit JWTs
|
* Provides utilities for fetching well-known data and LiveKit JWTs
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
|
|||||||
isInCall,
|
isInCall,
|
||||||
callState,
|
callState,
|
||||||
activeCall,
|
activeCall,
|
||||||
|
isUsingRemoteHomeserver,
|
||||||
startVoiceCall,
|
startVoiceCall,
|
||||||
startVideoCall,
|
startVideoCall,
|
||||||
endCall,
|
endCall,
|
||||||
@@ -433,7 +434,10 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
|
|||||||
offset={4}
|
offset={4}
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text>{isInCall ? 'End Call' : 'Voice Call'}</Text>
|
<Text>
|
||||||
|
{isInCall ? 'End Call' : 'Voice Call'}
|
||||||
|
{isUsingRemoteHomeserver && !isInCall && ' (Remote Server)'}
|
||||||
|
</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -446,7 +450,12 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
|
|||||||
{isConnecting ? (
|
{isConnecting ? (
|
||||||
<Spinner size="100" variant="Secondary" />
|
<Spinner size="100" variant="Secondary" />
|
||||||
) : (
|
) : (
|
||||||
<Icon size="400" src={Icons.Phone} filled={isActive && activeCall?.callType === CallType.Voice} />
|
<Box direction="Row" gap="100" alignItems="Center">
|
||||||
|
<Icon size="400" src={Icons.Phone} filled={isActive && activeCall?.callType === CallType.Voice} />
|
||||||
|
{isUsingRemoteHomeserver && (
|
||||||
|
<Icon size="50" src={Icons.HashGlobe} style={{ opacity: 0.7 }} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
@@ -456,7 +465,10 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
|
|||||||
offset={4}
|
offset={4}
|
||||||
tooltip={
|
tooltip={
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Text>{isInCall ? 'End Call' : 'Video Call'}</Text>
|
<Text>
|
||||||
|
{isInCall ? 'End Call' : 'Video Call'}
|
||||||
|
{isUsingRemoteHomeserver && !isInCall && ' (Remote Server)'}
|
||||||
|
</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -469,7 +481,12 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
|
|||||||
{isConnecting ? (
|
{isConnecting ? (
|
||||||
<Spinner size="100" variant="Secondary" />
|
<Spinner size="100" variant="Secondary" />
|
||||||
) : (
|
) : (
|
||||||
<Icon size="400" src={Icons.VideoCamera} filled={isActive && activeCall?.callType === CallType.Video} />
|
<Box direction="Row" gap="100" alignItems="Center">
|
||||||
|
<Icon size="400" src={Icons.VideoCamera} filled={isActive && activeCall?.callType === CallType.Video} />
|
||||||
|
{isUsingRemoteHomeserver && (
|
||||||
|
<Icon size="50" src={Icons.HashGlobe} style={{ opacity: 0.7 }} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user