mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
daily backend code refactor
This commit is contained in:
@@ -571,10 +571,17 @@ async def rooms_join_meeting(
|
|||||||
|
|
||||||
if meeting.platform == "daily" and user_id is not None:
|
if meeting.platform == "daily" and user_id is not None:
|
||||||
client = create_platform_client(meeting.platform)
|
client = create_platform_client(meeting.platform)
|
||||||
|
# Show Daily's built-in recording UI when:
|
||||||
|
# - local recording (user controls when to record), OR
|
||||||
|
# - cloud recording with consent disabled (skip_consent=True)
|
||||||
|
# Hide it when cloud recording with consent enabled (we show custom consent UI)
|
||||||
|
enable_recording_ui = meeting.recording_type == "local" or (
|
||||||
|
meeting.recording_type == "cloud" and room.skip_consent
|
||||||
|
)
|
||||||
token = await client.create_meeting_token(
|
token = await client.create_meeting_token(
|
||||||
meeting.room_name,
|
meeting.room_name,
|
||||||
start_cloud_recording=meeting.recording_type == "cloud",
|
start_cloud_recording=meeting.recording_type == "cloud",
|
||||||
enable_recording_ui=meeting.recording_type == "local",
|
enable_recording_ui=enable_recording_ui,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
is_owner=user_id == room.user_id,
|
is_owner=user_id == room.user_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,38 +1,202 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import {
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { Box, Spinner, Center, Text } from "@chakra-ui/react";
|
import { Box, Spinner, Center, Text } from "@chakra-ui/react";
|
||||||
import { useRouter, useParams } from "next/navigation";
|
import { useRouter, useParams } from "next/navigation";
|
||||||
import DailyIframe, { DailyCall } from "@daily-co/daily-js";
|
import DailyIframe, {
|
||||||
|
DailyCall,
|
||||||
|
DailyCallOptions,
|
||||||
|
DailyCustomTrayButton,
|
||||||
|
DailyCustomTrayButtons,
|
||||||
|
DailyEventObjectCustomButtonClick,
|
||||||
|
DailyFactoryOptions,
|
||||||
|
DailyParticipantsObject,
|
||||||
|
} from "@daily-co/daily-js";
|
||||||
import type { components } from "../../reflector-api";
|
import type { components } from "../../reflector-api";
|
||||||
import { useAuth } from "../../lib/AuthProvider";
|
import { useAuth } from "../../lib/AuthProvider";
|
||||||
import {
|
import {
|
||||||
ConsentDialogButton,
|
|
||||||
RecordingIndicator,
|
|
||||||
recordingTypeRequiresConsent,
|
recordingTypeRequiresConsent,
|
||||||
|
useConsentDialog,
|
||||||
} from "../../lib/consent";
|
} from "../../lib/consent";
|
||||||
import { useRoomJoinMeeting } from "../../lib/apiHooks";
|
import { useRoomJoinMeeting } from "../../lib/apiHooks";
|
||||||
|
import { omit } from "remeda";
|
||||||
import { assertExists } from "../../lib/utils";
|
import { assertExists } from "../../lib/utils";
|
||||||
|
|
||||||
|
const CONSENT_BUTTON_ID = "recording-consent";
|
||||||
|
const RECORDING_INDICATOR_ID = "recording-indicator";
|
||||||
|
|
||||||
type Meeting = components["schemas"]["Meeting"];
|
type Meeting = components["schemas"]["Meeting"];
|
||||||
type Room = components["schemas"]["RoomDetails"];
|
type Room = components["schemas"]["RoomDetails"];
|
||||||
|
|
||||||
interface DailyRoomProps {
|
type DailyRoomProps = {
|
||||||
meeting: Meeting;
|
meeting: Meeting;
|
||||||
room: Room;
|
room: Room;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const useCustomTrayButtons = (
|
||||||
|
frame: {
|
||||||
|
updateCustomTrayButtons: (
|
||||||
|
customTrayButtons: DailyCustomTrayButtons,
|
||||||
|
) => void;
|
||||||
|
joined: boolean;
|
||||||
|
} | null,
|
||||||
|
) => {
|
||||||
|
const [, setCustomTrayButtons] = useState<DailyCustomTrayButtons>({});
|
||||||
|
return useCallback(
|
||||||
|
(id: string, button: DailyCustomTrayButton | null) => {
|
||||||
|
setCustomTrayButtons((prev) => {
|
||||||
|
// would blink state when frame blinks but it's ok here
|
||||||
|
const state =
|
||||||
|
button === null ? omit(prev, [id]) : { ...prev, [id]: button };
|
||||||
|
if (frame !== null && frame.joined)
|
||||||
|
frame.updateCustomTrayButtons(state);
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setCustomTrayButtons, frame],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const USE_FRAME_INIT_STATE = {
|
||||||
|
frame: null as DailyCall | null,
|
||||||
|
joined: false as boolean,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Daily js and not Daily react used right now because daily-js allows for prebuild interface vs. -react is customizable but has no nice defaults
|
||||||
|
const useFrame = (
|
||||||
|
container: HTMLDivElement | null,
|
||||||
|
cbs: {
|
||||||
|
onLeftMeeting: () => void;
|
||||||
|
onCustomButtonClick: (ev: DailyEventObjectCustomButtonClick) => void;
|
||||||
|
onJoinMeeting: (
|
||||||
|
startRecording: (args: { type: "raw-tracks" }) => void,
|
||||||
|
) => void;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const [{ frame, joined }, setState] = useState(USE_FRAME_INIT_STATE);
|
||||||
|
const setJoined = useCallback(
|
||||||
|
(joined: boolean) => setState((prev) => ({ ...prev, joined })),
|
||||||
|
[setState],
|
||||||
|
);
|
||||||
|
const setFrame = useCallback(
|
||||||
|
(frame: DailyCall | null) => setState((prev) => ({ ...prev, frame })),
|
||||||
|
[setState],
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!container) return;
|
||||||
|
const init = async () => {
|
||||||
|
const existingFrame = DailyIframe.getCallInstance();
|
||||||
|
if (existingFrame) {
|
||||||
|
console.error("existing daily frame present");
|
||||||
|
await existingFrame.destroy();
|
||||||
|
}
|
||||||
|
const frameOptions: DailyFactoryOptions = {
|
||||||
|
iframeStyle: {
|
||||||
|
width: "100vw",
|
||||||
|
height: "100vh",
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
|
showLeaveButton: true,
|
||||||
|
showFullscreenButton: true,
|
||||||
|
};
|
||||||
|
const frame = DailyIframe.createFrame(container, frameOptions);
|
||||||
|
setFrame(frame);
|
||||||
|
};
|
||||||
|
init().catch(
|
||||||
|
console.error.bind(console, "Failed to initialize daily frame:"),
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
frame
|
||||||
|
?.destroy()
|
||||||
|
.catch(console.error.bind(console, "Failed to destroy daily frame:"));
|
||||||
|
setState(USE_FRAME_INIT_STATE);
|
||||||
|
};
|
||||||
|
}, [container]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!frame) return;
|
||||||
|
frame.on("left-meeting", cbs.onLeftMeeting);
|
||||||
|
frame.on("custom-button-click", cbs.onCustomButtonClick);
|
||||||
|
const joinCb = () => {
|
||||||
|
if (!frame) {
|
||||||
|
console.error("frame is null in joined-meeting callback");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cbs.onJoinMeeting(frame.startRecording.bind(frame));
|
||||||
|
};
|
||||||
|
frame.on("joined-meeting", joinCb);
|
||||||
|
return () => {
|
||||||
|
frame.off("left-meeting", cbs.onLeftMeeting);
|
||||||
|
frame.off("custom-button-click", cbs.onCustomButtonClick);
|
||||||
|
frame.off("joined-meeting", joinCb);
|
||||||
|
};
|
||||||
|
}, [frame, cbs]);
|
||||||
|
const frame_ = useMemo(() => {
|
||||||
|
if (frame === null) return frame;
|
||||||
|
return {
|
||||||
|
join: async (
|
||||||
|
properties?: DailyCallOptions,
|
||||||
|
): Promise<DailyParticipantsObject | void> => {
|
||||||
|
await frame.join(properties);
|
||||||
|
setJoined(!frame.isDestroyed());
|
||||||
|
},
|
||||||
|
updateCustomTrayButtons: (
|
||||||
|
customTrayButtons: DailyCustomTrayButtons,
|
||||||
|
): DailyCall => frame.updateCustomTrayButtons(customTrayButtons),
|
||||||
|
};
|
||||||
|
}, [frame]);
|
||||||
|
const setCustomTrayButton = useCustomTrayButtons(
|
||||||
|
useMemo(() => {
|
||||||
|
if (frame_ === null) return null;
|
||||||
|
return {
|
||||||
|
updateCustomTrayButtons: frame_.updateCustomTrayButtons,
|
||||||
|
joined,
|
||||||
|
};
|
||||||
|
}, [frame_, joined]),
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
frame_,
|
||||||
|
setFrame,
|
||||||
|
{
|
||||||
|
joined,
|
||||||
|
setCustomTrayButton,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
};
|
||||||
|
|
||||||
export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const authLastUserId = auth.lastUserId;
|
const authLastUserId = auth.lastUserId;
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||||
const joinMutation = useRoomJoinMeeting();
|
const joinMutation = useRoomJoinMeeting();
|
||||||
const [joinedMeeting, setJoinedMeeting] = useState<Meeting | null>(null);
|
const [joinedMeeting, setJoinedMeeting] = useState<Meeting | null>(null);
|
||||||
|
|
||||||
const roomName = params?.roomName as string;
|
const roomName = params?.roomName as string;
|
||||||
|
|
||||||
|
const showRecordingInTray =
|
||||||
|
meeting.recording_type &&
|
||||||
|
recordingTypeRequiresConsent(meeting.recording_type) &&
|
||||||
|
// users know about recording in case of no-skip-consent from the consent dialog
|
||||||
|
room.skip_consent;
|
||||||
|
|
||||||
|
const needsConsent =
|
||||||
|
meeting.recording_type &&
|
||||||
|
recordingTypeRequiresConsent(meeting.recording_type) &&
|
||||||
|
!room.skip_consent;
|
||||||
|
const { showConsentModal, consentState, hasConsent } = useConsentDialog(
|
||||||
|
meeting.id,
|
||||||
|
);
|
||||||
|
const showConsentModalRef = useRef(showConsentModal);
|
||||||
|
showConsentModalRef.current = showConsentModal;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authLastUserId === undefined || !meeting?.id || !roomName) return;
|
if (authLastUserId === undefined || !meeting?.id || !roomName) return;
|
||||||
|
|
||||||
@@ -52,7 +216,7 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
join();
|
join().catch(console.error.bind(console, "Failed to join meeting:"));
|
||||||
}, [meeting?.id, roomName, authLastUserId]);
|
}, [meeting?.id, roomName, authLastUserId]);
|
||||||
|
|
||||||
const roomUrl = joinedMeeting?.room_url;
|
const roomUrl = joinedMeeting?.room_url;
|
||||||
@@ -61,84 +225,86 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
|||||||
router.push("/browse");
|
router.push("/browse");
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleCustomButtonClick = useCallback(
|
||||||
if (authLastUserId === undefined || !roomUrl || !containerRef.current)
|
(ev: DailyEventObjectCustomButtonClick) => {
|
||||||
return;
|
if (ev.button_id === CONSENT_BUTTON_ID) {
|
||||||
|
showConsentModalRef.current();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
/*keep static; iframe recreation depends on it*/
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
let frame: DailyCall | null = null;
|
const handleFrameJoinMeeting = useCallback(
|
||||||
let destroyed = false;
|
(startRecording: (args: { type: "raw-tracks" }) => void) => {
|
||||||
|
|
||||||
const createAndJoin = async () => {
|
|
||||||
try {
|
try {
|
||||||
const existingFrame = DailyIframe.getCallInstance();
|
if (meeting.recording_type === "cloud") {
|
||||||
if (existingFrame) {
|
console.log("Starting cloud recording");
|
||||||
await existingFrame.destroy();
|
startRecording({ type: "raw-tracks" });
|
||||||
}
|
}
|
||||||
|
|
||||||
frame = DailyIframe.createFrame(containerRef.current!, {
|
|
||||||
iframeStyle: {
|
|
||||||
width: "100vw",
|
|
||||||
height: "100vh",
|
|
||||||
border: "none",
|
|
||||||
},
|
|
||||||
showLeaveButton: true,
|
|
||||||
showFullscreenButton: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (destroyed) {
|
|
||||||
await frame.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame.on("left-meeting", handleLeave);
|
|
||||||
|
|
||||||
frame.on("joined-meeting", async () => {
|
|
||||||
try {
|
|
||||||
const frameInstance = assertExists(
|
|
||||||
frame,
|
|
||||||
"frame object got lost somewhere after frame.on was called",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (meeting.recording_type === "cloud") {
|
|
||||||
console.log("Starting cloud recording");
|
|
||||||
await frameInstance.startRecording({ type: "raw-tracks" });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to start recording:", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await frame.join({
|
|
||||||
url: roomUrl,
|
|
||||||
sendSettings: {
|
|
||||||
video: {
|
|
||||||
// Optimize bandwidth for camera video
|
|
||||||
// allowAdaptiveLayers automatically adjusts quality based on network conditions
|
|
||||||
allowAdaptiveLayers: true,
|
|
||||||
// Use bandwidth-optimized preset as fallback for browsers without adaptive support
|
|
||||||
maxQuality: "medium",
|
|
||||||
},
|
|
||||||
// Note: screenVideo intentionally not configured to preserve full quality for screen shares
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating Daily frame:", error);
|
console.error("Failed to start recording:", error);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[meeting.recording_type],
|
||||||
|
);
|
||||||
|
|
||||||
createAndJoin().catch((error) => {
|
const recordingIconUrl = useMemo(
|
||||||
console.error("Failed to create and join meeting:", error);
|
() => new URL("/recording-icon.svg", window.location.origin),
|
||||||
});
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
const [frame, setFrame, { setCustomTrayButton }] = useFrame(container, {
|
||||||
destroyed = true;
|
onLeftMeeting: handleLeave,
|
||||||
if (frame) {
|
onCustomButtonClick: handleCustomButtonClick,
|
||||||
frame.destroy().catch((e) => {
|
onJoinMeeting: handleFrameJoinMeeting,
|
||||||
console.error("Error destroying frame:", e);
|
});
|
||||||
});
|
|
||||||
}
|
useEffect(() => {
|
||||||
};
|
if (!frame || !roomUrl) return;
|
||||||
}, [roomUrl, authLastUserId, handleLeave]);
|
frame
|
||||||
|
.join({
|
||||||
|
url: roomUrl,
|
||||||
|
sendSettings: {
|
||||||
|
video: {
|
||||||
|
// Optimize bandwidth for camera video
|
||||||
|
// allowAdaptiveLayers automatically adjusts quality based on network conditions
|
||||||
|
allowAdaptiveLayers: true,
|
||||||
|
// Use bandwidth-optimized preset as fallback for browsers without adaptive support
|
||||||
|
maxQuality: "medium",
|
||||||
|
},
|
||||||
|
// Note: screenVideo intentionally not configured to preserve full quality for screen shares
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch(console.error.bind(console, "Failed to join daily room:"));
|
||||||
|
}, [frame, roomUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCustomTrayButton(
|
||||||
|
RECORDING_INDICATOR_ID,
|
||||||
|
showRecordingInTray
|
||||||
|
? {
|
||||||
|
iconPath: recordingIconUrl.href,
|
||||||
|
label: "Recording",
|
||||||
|
tooltip: "Recording in progress",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}, [showRecordingInTray, recordingIconUrl, setCustomTrayButton]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (needsConsent && !hasConsent(meeting.id)) {
|
||||||
|
const iconUrl = new URL("/consent-icon.svg", window.location.origin);
|
||||||
|
frameOptions.customTrayButtons = {
|
||||||
|
[CONSENT_BUTTON_ID]: {
|
||||||
|
iconPath: iconUrl.href,
|
||||||
|
label: "Consent",
|
||||||
|
tooltip: "Recording consent - click to respond",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if (authLastUserId === undefined) {
|
if (authLastUserId === undefined) {
|
||||||
return (
|
return (
|
||||||
@@ -162,15 +328,7 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position="relative" width="100vw" height="100vh">
|
<Box position="relative" width="100vw" height="100vh">
|
||||||
<div ref={containerRef} style={{ width: "100%", height: "100%" }} />
|
<div ref={setContainer} style={{ width: "100%", height: "100%" }} />
|
||||||
{meeting.recording_type &&
|
|
||||||
recordingTypeRequiresConsent(meeting.recording_type) &&
|
|
||||||
meeting.id &&
|
|
||||||
(room.skip_consent ? (
|
|
||||||
<RecordingIndicator />
|
|
||||||
) : (
|
|
||||||
<ConsentDialogButton meetingId={meeting.id} />
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user