feat: add call management features with useCall and useRoomCallMembers hooks

- Implemented useCall hook for managing call state and actions.
- Created useRoomCallMembers hook to track active call members in a room.
- Added useRtcConfig for fetching RTC configurations and LiveKit JWT.
- Developed Audio settings component for managing audio devices and settings.
- Introduced device selection and screen sharing options in the Audio settings.
- Persisted audio settings in localStorage for user preferences.
This commit is contained in:
2026-01-23 19:35:25 +11:00
parent c88cb4bca9
commit 94f8466d1c
19 changed files with 3270 additions and 14 deletions

View File

@@ -0,0 +1,170 @@
import { useCallback } from 'react';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { LiveKitFocus, LiveKitJWTResponse, OpenIdToken, WellKnownWithRTC } from './types';
/**
* 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'];
if (!rtcFoci || !Array.isArray(rtcFoci)) {
return undefined;
}
return rtcFoci.find((focus): focus is LiveKitFocus => focus.type === 'livekit');
}
/**
* 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> {
const wellKnownUrl = `${baseUrl}/.well-known/matrix/client`;
const response = await fetch(wellKnownUrl);
if (!response.ok) {
throw new Error(`Failed to fetch well-known: ${response.status}`);
}
return response.json();
}
/**
* 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> {
const 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 well-known or config
* @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';
const response = await fetch(livekitServiceUrl, {
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();
}
/**
* 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,
};
}