Skip to content

Commit f9ab5d8

Browse files
committed
Refactor useLivekit to only ever connect to one room.
This change also tries to make the code more explicit so that we only do the things we really need to do and rely less on react updating everything correctly. It also surfaces, that we are currently implementing useLivekit in a way, so that we can change the encryption system on the fly and recreate the room. I am not sure this is a case we need to support?
1 parent 93dfb08 commit f9ab5d8

File tree

1 file changed

+95
-56
lines changed

1 file changed

+95
-56
lines changed

src/livekit/useLivekit.ts

Lines changed: 95 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
type RoomOptions,
1515
Track,
1616
} from "livekit-client";
17-
import { useEffect, useMemo, useRef } from "react";
17+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1818
import E2EEWorker from "livekit-client/e2ee-worker?worker";
1919
import { logger } from "matrix-js-sdk/lib/logger";
2020
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
@@ -40,7 +40,6 @@ import {
4040
useTrackProcessor,
4141
useTrackProcessorSync,
4242
} from "./TrackProcessorContext";
43-
import { useInitial } from "../useInitial";
4443
import { observeTrackReference$ } from "../state/MediaViewModel";
4544
import { useUrlParams } from "../UrlParams";
4645

@@ -57,87 +56,127 @@ export function useLivekit(
5756
): UseLivekitResult {
5857
const { controlledAudioDevices } = useUrlParams();
5958

60-
const e2eeOptions = useMemo((): E2EEManagerOptions | undefined => {
61-
if (e2eeSystem.kind === E2eeType.NONE) return undefined;
59+
const initialMuteStates = useRef<MuteStates>(muteStates);
60+
const devices = useMediaDevices();
61+
const initialDevices = useRef<MediaDevices>(devices);
6262

63-
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
63+
// Store if audio/video are currently updating. If to prohibit unnecessary calls
64+
// to setMicrophoneEnabled/setCameraEnabled
65+
const audioMuteUpdating = useRef(false);
66+
const videoMuteUpdating = useRef(false);
67+
// Store the current button mute state that gets passed to this hook via props.
68+
// We need to store it for awaited code that relies on the current value.
69+
const buttonEnabled = useRef({
70+
audio: initialMuteStates.current.audio.enabled,
71+
video: initialMuteStates.current.video.enabled,
72+
});
73+
74+
const { processor } = useTrackProcessor();
75+
76+
const createRoom = (opt: RoomOptions, e2ee: EncryptionSystem): Room => {
77+
logger.info("[LivekitRoom] Create LiveKit room with options", opt);
78+
// We have to create the room manually here due to a bug inside
79+
// @livekit/components-react. JSON.stringify() is used in deps of a
80+
// useEffect() with an argument that references itself, if E2EE is enabled
81+
let newE2eeOptions: E2EEManagerOptions | undefined;
82+
if (e2ee.kind === E2eeType.PER_PARTICIPANT) {
6483
logger.info("Created MatrixKeyProvider (per participant)");
65-
return {
84+
newE2eeOptions = {
6685
keyProvider: new MatrixKeyProvider(),
6786
worker: new E2EEWorker(),
6887
};
69-
} else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) {
88+
} else if (e2ee.kind === E2eeType.SHARED_KEY && e2ee.secret) {
7089
logger.info("Created ExternalE2EEKeyProvider (shared key)");
71-
72-
return {
90+
newE2eeOptions = {
7391
keyProvider: new ExternalE2EEKeyProvider(),
7492
worker: new E2EEWorker(),
7593
};
7694
}
77-
}, [e2eeSystem]);
78-
79-
useEffect(() => {
80-
if (e2eeSystem.kind === E2eeType.NONE || !e2eeOptions) return;
81-
82-
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
83-
(e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession);
84-
} else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) {
85-
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider)
86-
.setKey(e2eeSystem.secret)
87-
.catch((e) => {
88-
logger.error("Failed to set shared key for E2EE", e);
89-
});
90-
}
91-
}, [e2eeOptions, e2eeSystem, rtcSession]);
95+
const r = new Room({ ...opt, e2ee: newE2eeOptions });
96+
r.setE2EEEnabled(e2ee.kind !== E2eeType.NONE).catch((e) => {
97+
logger.error("Failed to set E2EE enabled on room", e);
98+
});
9299

93-
const initialMuteStates = useRef<MuteStates>(muteStates);
94-
const devices = useMediaDevices();
95-
const initialDevices = useRef<MediaDevices>(devices);
100+
return r;
101+
};
96102

97-
const { processor } = useTrackProcessor();
98-
const initialProcessor = useInitial(() => processor);
103+
// Track the current room options in case we need to recreate the room if the encryption system changes
104+
// Only needed because we allow swapping the room in case the e2ee system changes.
105+
// otherwise this could become part of: `createRoom`
99106
const roomOptions = useMemo(
100107
(): RoomOptions => ({
101108
...defaultLiveKitOptions,
102109
videoCaptureDefaults: {
103110
...defaultLiveKitOptions.videoCaptureDefaults,
104-
deviceId: initialDevices.current.videoInput.selectedId,
105-
processor: initialProcessor,
111+
deviceId: devices.videoInput.selectedId,
112+
processor: processor,
106113
},
107114
audioCaptureDefaults: {
108115
...defaultLiveKitOptions.audioCaptureDefaults,
109-
deviceId: initialDevices.current.audioInput.selectedId,
116+
deviceId: devices.audioInput.selectedId,
110117
},
111118
audioOutput: {
112-
deviceId: initialDevices.current.audioOutput.selectedId,
119+
deviceId: devices.audioOutput.selectedId,
113120
},
114-
e2ee: e2eeOptions,
115121
}),
116-
[e2eeOptions, initialProcessor],
122+
[processor, devices],
117123
);
124+
const [room, setRoom] = useState(() => createRoom(roomOptions, e2eeSystem));
118125

119-
// Store if audio/video are currently updating. If to prohibit unnecessary calls
120-
// to setMicrophoneEnabled/setCameraEnabled
121-
const audioMuteUpdating = useRef(false);
122-
const videoMuteUpdating = useRef(false);
123-
// Store the current button mute state that gets passed to this hook via props.
124-
// We need to store it for awaited code that relies on the current value.
125-
const buttonEnabled = useRef({
126-
audio: initialMuteStates.current.audio.enabled,
127-
video: initialMuteStates.current.video.enabled,
128-
});
126+
// Setup and update the already existing keyProvider
127+
useEffect(() => {
128+
const e2eeOptions = room.options.e2ee;
129+
if (
130+
e2eeSystem.kind === E2eeType.NONE ||
131+
!(e2eeOptions && "keyProvider" in e2eeOptions)
132+
)
133+
return;
129134

130-
// We have to create the room manually here due to a bug inside
131-
// @livekit/components-react. JSON.stringify() is used in deps of a
132-
// useEffect() with an argument that references itself, if E2EE is enabled
133-
const room = useMemo(() => {
134-
logger.info("[LivekitRooms] Create LiveKit room with options", roomOptions);
135-
const r = new Room(roomOptions);
136-
r.setE2EEEnabled(e2eeSystem.kind !== E2eeType.NONE).catch((e) => {
137-
logger.error("Failed to set E2EE enabled on room", e);
138-
});
139-
return r;
140-
}, [roomOptions, e2eeSystem]);
135+
if (e2eeSystem.kind === E2eeType.PER_PARTICIPANT) {
136+
(e2eeOptions.keyProvider as MatrixKeyProvider).setRTCSession(rtcSession);
137+
} else if (e2eeSystem.kind === E2eeType.SHARED_KEY && e2eeSystem.secret) {
138+
(e2eeOptions.keyProvider as ExternalE2EEKeyProvider)
139+
.setKey(e2eeSystem.secret)
140+
.catch((e) => {
141+
logger.error("Failed to set shared key for E2EE", e);
142+
});
143+
}
144+
}, [room.options.e2ee, e2eeSystem, rtcSession]);
145+
146+
// Do we really allow hot swapping the e2ee system?
147+
// Will we ever reach this code?
148+
useEffect(() => {
149+
const e2eeOptions = room.options.e2ee;
150+
// Only do sth if our e2eeSystem has changed.
151+
if (
152+
// from non to sth else.
153+
(e2eeSystem.kind === E2eeType.NONE && e2eeOptions !== undefined) ||
154+
// from MatrixKeyProvider to sth else
155+
(e2eeSystem.kind === E2eeType.PER_PARTICIPANT &&
156+
!(
157+
e2eeOptions &&
158+
"keyProvider" in e2eeOptions &&
159+
e2eeOptions.keyProvider instanceof MatrixKeyProvider
160+
)) ||
161+
// from ExternalE2EEKeyProvider to sth else
162+
(e2eeSystem.kind === E2eeType.SHARED_KEY &&
163+
!(
164+
e2eeOptions &&
165+
"keyProvider" in e2eeOptions &&
166+
e2eeOptions.keyProvider instanceof ExternalE2EEKeyProvider
167+
))
168+
) {
169+
logger.warn(
170+
"[LivekitRoom] we cannot change the key provider after the room has been created, disconnecting and creating a new room",
171+
);
172+
const resetRoom = async (): Promise<void> => {
173+
await room.disconnect();
174+
const newRoom = createRoom(roomOptions, e2eeSystem);
175+
setRoom(newRoom);
176+
};
177+
void resetRoom();
178+
}
179+
}, [room, e2eeSystem, roomOptions, createRoom]);
141180

142181
// Sync the requested track processors with LiveKit
143182
useTrackProcessorSync(

0 commit comments

Comments
 (0)