mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-02-06 18:56:48 +00:00
fix: prevent presence race condition during WebRTC handshake
Add /joining and /joined endpoints to track user join intent before WebRTC handshake completes. This prevents meetings from being deactivated while users are still connecting. - Add pending_joins Redis module with 30s TTL - Add /joining endpoint (called before WebRTC handshake) - Add /joined endpoint (called after connection established) - Check for pending joins before deactivating meetings in worker - Frontend integration with connectionId per browser tab
This commit is contained in:
@@ -25,6 +25,8 @@ import { useConsentDialog } from "../../lib/consent";
|
||||
import {
|
||||
useRoomJoinMeeting,
|
||||
useMeetingStartRecording,
|
||||
useMeetingJoining,
|
||||
useMeetingJoined,
|
||||
} from "../../lib/apiHooks";
|
||||
import { omit } from "remeda";
|
||||
import {
|
||||
@@ -187,8 +189,14 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
const joinMutation = useRoomJoinMeeting();
|
||||
const startRecordingMutation = useMeetingStartRecording();
|
||||
const joiningMutation = useMeetingJoining();
|
||||
const joinedMutation = useMeetingJoined();
|
||||
const [joinedMeeting, setJoinedMeeting] = useState<Meeting | null>(null);
|
||||
|
||||
// Generate a stable connection ID for this component instance
|
||||
// Used to track pending joins per browser tab (prevents key collision for anonymous users)
|
||||
const connectionId = useMemo(() => crypto.randomUUID(), []);
|
||||
|
||||
// Generate deterministic instanceIds so all participants use SAME IDs
|
||||
const cloudInstanceId = parseNonEmptyString(meeting.id);
|
||||
const rawTracksInstanceId = parseNonEmptyString(
|
||||
@@ -249,6 +257,28 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
||||
);
|
||||
|
||||
const handleFrameJoinMeeting = useCallback(() => {
|
||||
// Signal that WebRTC connection is established
|
||||
// This clears the pending join intent, confirming successful connection
|
||||
joinedMutation.mutate(
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
room_name: roomName,
|
||||
meeting_id: meeting.id,
|
||||
},
|
||||
},
|
||||
body: {
|
||||
connection_id: connectionId,
|
||||
},
|
||||
},
|
||||
{
|
||||
onError: (error: unknown) => {
|
||||
// Non-blocking: log but don't fail - this is cleanup, not critical
|
||||
console.warn("Failed to signal joined:", error);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (meeting.recording_type === "cloud") {
|
||||
console.log("Starting dual recording via REST API", {
|
||||
cloudInstanceId,
|
||||
@@ -310,6 +340,9 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
||||
}, [
|
||||
meeting.recording_type,
|
||||
meeting.id,
|
||||
roomName,
|
||||
connectionId,
|
||||
joinedMutation,
|
||||
startRecordingMutation,
|
||||
cloudInstanceId,
|
||||
rawTracksInstanceId,
|
||||
@@ -328,8 +361,28 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!frame || !roomUrl) return;
|
||||
frame
|
||||
.join({
|
||||
|
||||
const joinRoom = async () => {
|
||||
// Signal intent to join before WebRTC handshake starts
|
||||
// This prevents race condition where meeting is deactivated during handshake
|
||||
try {
|
||||
await joiningMutation.mutateAsync({
|
||||
params: {
|
||||
path: {
|
||||
room_name: roomName,
|
||||
meeting_id: meeting.id,
|
||||
},
|
||||
},
|
||||
body: {
|
||||
connection_id: connectionId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Non-blocking: log but continue with join
|
||||
console.warn("Failed to signal joining intent:", error);
|
||||
}
|
||||
|
||||
await frame.join({
|
||||
url: roomUrl,
|
||||
sendSettings: {
|
||||
video: {
|
||||
@@ -341,9 +394,13 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
||||
},
|
||||
// Note: screenVideo intentionally not configured to preserve full quality for screen shares
|
||||
},
|
||||
})
|
||||
.catch(console.error.bind(console, "Failed to join daily room:"));
|
||||
}, [frame, roomUrl]);
|
||||
});
|
||||
};
|
||||
|
||||
joinRoom().catch(console.error.bind(console, "Failed to join daily room:"));
|
||||
// joiningMutation excluded from deps - it's a stable hook reference
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [frame, roomUrl, roomName, meeting.id, connectionId]);
|
||||
|
||||
useEffect(() => {
|
||||
setCustomTrayButton(
|
||||
|
||||
@@ -807,6 +807,26 @@ export function useRoomJoinMeeting() {
|
||||
);
|
||||
}
|
||||
|
||||
// Presence race fix endpoints (not yet in OpenAPI spec)
|
||||
// These signal join intent to prevent race conditions during WebRTC handshake
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useMeetingJoining(): any {
|
||||
return ($api as any).useMutation(
|
||||
"post",
|
||||
"/v1/rooms/{room_name}/meetings/{meeting_id}/joining",
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useMeetingJoined(): any {
|
||||
return ($api as any).useMutation(
|
||||
"post",
|
||||
"/v1/rooms/{room_name}/meetings/{meeting_id}/joined",
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
export function useRoomIcsSync() {
|
||||
const { setError } = useError();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user