372 lines
13 KiB
TypeScript
372 lines
13 KiB
TypeScript
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';
|
|
import { getActiveCallMembers } from './useRoomCallMembers';
|
|
|
|
/**
|
|
* Get room server from room ID (format: !roomid:server.com)
|
|
*/
|
|
function getRoomServer(roomId: string): string | undefined {
|
|
const server = getMxIdServer(roomId);
|
|
console.log(`[getRoomServer] Room ID: ${roomId} -> Server: ${server}`);
|
|
return server;
|
|
}
|
|
|
|
/**
|
|
* Determine which homeservers to try for LiveKit JWT, in priority order
|
|
* Prioritizes servers where active call members are located when joining existing calls
|
|
* @param room - Room to start/join 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;
|
|
|
|
console.log(`[getLiveKitHomeserverPriority] Room: ${room.roomId}`);
|
|
console.log(`[getLiveKitHomeserverPriority] Member count: ${memberCount}, isDM: ${isDM}`);
|
|
console.log(`[getLiveKitHomeserverPriority] User homeserver: ${userHomeserver}`);
|
|
|
|
// Check for active call members
|
|
const activeCallMembers = getActiveCallMembers(room);
|
|
const hasActiveCall = activeCallMembers.length > 0;
|
|
|
|
console.log(`[getLiveKitHomeserverPriority] Active call members: ${activeCallMembers.length}, hasActiveCall: ${hasActiveCall}`);
|
|
|
|
if (hasActiveCall) {
|
|
// Joining existing call - prioritize room's homeserver (where call is hosted)
|
|
const roomServer = getRoomServer(room.roomId);
|
|
console.log(`[getLiveKitHomeserverPriority] Room server (active call): ${roomServer}`);
|
|
if (roomServer) {
|
|
servers.push(`https://${roomServer}`);
|
|
}
|
|
|
|
// Add user's homeserver as fallback
|
|
const userDomain = userHomeserver.replace('https://', '').replace('http://', '');
|
|
if (!roomServer || roomServer !== userDomain) {
|
|
servers.push(userHomeserver);
|
|
}
|
|
|
|
// For DMs, also try other member's homeserver
|
|
if (isDM) {
|
|
const otherUserId = guessDmRoomUserId(room, myUserId);
|
|
if (otherUserId && otherUserId !== myUserId) {
|
|
const otherServer = getMxIdServer(otherUserId);
|
|
if (otherServer && otherServer !== userDomain && otherServer !== roomServer) {
|
|
servers.push(`https://${otherServer}`);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Starting new call - use original priority logic
|
|
if (isDM) {
|
|
console.log(`[getLiveKitHomeserverPriority] DM path - user homeserver first`);
|
|
// For DMs: user's homeserver first, then other member's homeserver
|
|
servers.push(userHomeserver);
|
|
|
|
const otherUserId = guessDmRoomUserId(room, myUserId);
|
|
console.log(`[getLiveKitHomeserverPriority] Other user ID: ${otherUserId}`);
|
|
if (otherUserId && otherUserId !== myUserId) {
|
|
const otherServer = getMxIdServer(otherUserId);
|
|
console.log(`[getLiveKitHomeserverPriority] Other user server: ${otherServer}`);
|
|
const userDomain = userHomeserver.replace('https://', '').replace('http://', '');
|
|
if (otherServer && otherServer !== userDomain) {
|
|
servers.push(`https://${otherServer}`);
|
|
}
|
|
}
|
|
} else {
|
|
console.log(`[getLiveKitHomeserverPriority] Channel path - room homeserver first`);
|
|
// For channels: room's homeserver first, then user's homeserver
|
|
const roomServer = getRoomServer(room.roomId);
|
|
console.log(`[getLiveKitHomeserverPriority] Room server from ID: ${roomServer}`);
|
|
if (roomServer) {
|
|
servers.push(`https://${roomServer}`);
|
|
}
|
|
|
|
// Fallback to user's homeserver if different
|
|
const userDomain = userHomeserver.replace('https://', '').replace('http://', '');
|
|
console.log(`[getLiveKitHomeserverPriority] User domain: ${userDomain}, room server: ${roomServer}`);
|
|
if (!roomServer || roomServer !== userDomain) {
|
|
servers.push(userHomeserver);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`[getLiveKitHomeserverPriority] Final server list:`, servers);
|
|
return servers;
|
|
}
|
|
|
|
/**
|
|
* Fetches the LiveKit focus configuration from well-known data
|
|
* @param wellKnown - The well-known response that may contain RTC foci
|
|
* @returns The LiveKit focus configuration if available
|
|
*/
|
|
export function getLiveKitFocus(wellKnown: WellKnownWithRTC): LiveKitFocus | undefined {
|
|
const rtcFoci = wellKnown['org.matrix.msc4143.rtc_foci'];
|
|
console.log(`[getLiveKitFocus] rtcFoci from well-known:`, rtcFoci);
|
|
|
|
if (!rtcFoci || !Array.isArray(rtcFoci)) {
|
|
console.log(`[getLiveKitFocus] No rtc_foci found or not an array`);
|
|
return undefined;
|
|
}
|
|
|
|
const livekitFocus = rtcFoci.find((focus): focus is LiveKitFocus => focus.type === 'livekit');
|
|
console.log(`[getLiveKitFocus] Found LiveKit focus:`, livekitFocus);
|
|
return livekitFocus;
|
|
}
|
|
|
|
/**
|
|
* Fetches the well-known client configuration from a homeserver
|
|
* @param baseUrl - The homeserver base URL
|
|
* @returns The well-known response with RTC configuration
|
|
*/
|
|
export async function fetchWellKnownWithRTC(baseUrl: string): Promise<WellKnownWithRTC> {
|
|
// In development mode (localhost), use CORS proxy to avoid CORS issues
|
|
const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
|
|
let wellKnownUrl: string;
|
|
if (isDev) {
|
|
// Extract hostname from baseUrl for proxy
|
|
const hostname = baseUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
|
|
wellKnownUrl = `/_matrix-cors-proxy/${hostname}/.well-known/matrix/client`;
|
|
} else {
|
|
wellKnownUrl = `${baseUrl}/.well-known/matrix/client`;
|
|
}
|
|
|
|
console.log(`[fetchWellKnownWithRTC] Fetching from: ${wellKnownUrl}`);
|
|
|
|
const response = await fetch(wellKnownUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch well-known: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log(`[fetchWellKnownWithRTC] Response data:`, data);
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Requests an OpenID token from the Matrix homeserver
|
|
* This token can be used to authenticate with third-party services
|
|
* @param homeserverBaseUrl - The Matrix homeserver base URL
|
|
* @param userId - The user's Matrix ID
|
|
* @param accessToken - The Matrix access token
|
|
* @returns The OpenID token response
|
|
*/
|
|
export async function requestOpenIdToken(
|
|
homeserverBaseUrl: string,
|
|
userId: string,
|
|
accessToken: string
|
|
): Promise<OpenIdToken> {
|
|
// In development mode (localhost), use CORS proxy to avoid CORS issues
|
|
const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
|
|
let url: string;
|
|
if (isDev) {
|
|
const hostname = homeserverBaseUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
|
|
url = `/_matrix-cors-proxy/${hostname}/_matrix/client/v3/user/${encodeURIComponent(userId)}/openid/request_token`;
|
|
} else {
|
|
url = `${homeserverBaseUrl}/_matrix/client/v3/user/${encodeURIComponent(userId)}/openid/request_token`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Failed to request OpenID token: ${response.status} - ${errorText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Fetches a LiveKit JWT for joining a call using OpenID token authentication
|
|
* @param livekitServiceUrl - The LiveKit service URL from Matrix homeserver well-known
|
|
* @param roomId - The Matrix room ID to join
|
|
* @param userId - The Matrix user ID
|
|
* @param deviceId - The Matrix device ID
|
|
* @param openIdToken - The OpenID token from the Matrix homeserver
|
|
* @returns The LiveKit JWT and server URL
|
|
*/
|
|
export async function fetchLiveKitJWT(
|
|
livekitServiceUrl: string,
|
|
roomId: string,
|
|
userId: string,
|
|
deviceId: string,
|
|
openIdToken: OpenIdToken
|
|
): Promise<LiveKitJWTResponse> {
|
|
// Generate a unique member ID for this participant (used for identity, not room assignment)
|
|
const memberId = `${deviceId}_${Date.now()}`;
|
|
|
|
// slot_id must be constant so all participants in the same Matrix room
|
|
// get assigned to the same LiveKit room. The JWT service hashes room_id + slot_id
|
|
// to create the LiveKit room name.
|
|
const slotId = 'm.call';
|
|
|
|
// In development mode (localhost), use CORS proxy to avoid CORS issues
|
|
const isDev = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
|
|
let url: string;
|
|
if (isDev) {
|
|
// Extract hostname and path from livekitServiceUrl
|
|
const urlObj = new URL(livekitServiceUrl);
|
|
const hostname = urlObj.hostname;
|
|
const pathname = urlObj.pathname;
|
|
url = `/_matrix-cors-proxy/${hostname}${pathname}`;
|
|
} else {
|
|
url = livekitServiceUrl;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
openid_token: openIdToken,
|
|
room_id: roomId,
|
|
slot_id: slotId,
|
|
member: {
|
|
id: memberId,
|
|
claimed_user_id: userId,
|
|
claimed_device_id: deviceId,
|
|
},
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Failed to fetch LiveKit JWT: ${response.status} - ${errorText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Try fetching LiveKit JWT from multiple homeservers in priority order
|
|
* @param homeservers - Array of homeserver base URLs to check for LiveKit config
|
|
* @param userHomeserver - The user's own homeserver URL (for OpenID token)
|
|
* @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[],
|
|
userHomeserver: 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;
|
|
}
|
|
|
|
// Always get OpenID token from user's own homeserver
|
|
// (Can't use access token on remote homeserver)
|
|
const openIdToken = await requestOpenIdToken(userHomeserver, 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
|
|
*/
|
|
export function useRtcConfig() {
|
|
const mx = useMatrixClient();
|
|
|
|
const getWellKnown = useCallback(async (): Promise<WellKnownWithRTC> => {
|
|
const baseUrl = mx.getHomeserverUrl();
|
|
return fetchWellKnownWithRTC(baseUrl);
|
|
}, [mx]);
|
|
|
|
const getLiveKitJWT = useCallback(
|
|
async (roomId: string): Promise<LiveKitJWTResponse | null> => {
|
|
const baseUrl = mx.getHomeserverUrl();
|
|
const accessToken = mx.getAccessToken();
|
|
const deviceId = mx.getDeviceId();
|
|
const userId = mx.getSafeUserId();
|
|
|
|
if (!accessToken || !deviceId) {
|
|
throw new Error('Missing access token or device ID');
|
|
}
|
|
|
|
const wellKnown = await fetchWellKnownWithRTC(baseUrl);
|
|
const livekitFocus = getLiveKitFocus(wellKnown);
|
|
|
|
if (!livekitFocus) {
|
|
return null;
|
|
}
|
|
|
|
// Get OpenID token from homeserver
|
|
const openIdToken = await requestOpenIdToken(baseUrl, userId, accessToken);
|
|
|
|
// Exchange for LiveKit JWT
|
|
return fetchLiveKitJWT(livekitFocus.livekit_service_url, roomId, userId, deviceId, openIdToken);
|
|
},
|
|
[mx]
|
|
);
|
|
|
|
const isCallSupported = useCallback(async (): Promise<boolean> => {
|
|
try {
|
|
const baseUrl = mx.getHomeserverUrl();
|
|
const wellKnown = await fetchWellKnownWithRTC(baseUrl);
|
|
return getLiveKitFocus(wellKnown) !== undefined;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}, [mx]);
|
|
|
|
return {
|
|
getWellKnown,
|
|
getLiveKitJWT,
|
|
isCallSupported,
|
|
};
|
|
}
|