mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
feat: daily.co support as alternative to whereby (#691)
* llm instructions * vibe dailyco * vibe dailyco * doc update (vibe) * dont show recording ui on call * stub processor (vibe) * stub processor (vibe) self-review * stub processor (vibe) self-review * chore(main): release 0.14.0 (#670) * Add multitrack pipeline * Mixdown audio tracks * Mixdown with pyav filter graph * Trigger multitrack processing for daily recordings * apply platform from envs in priority: non-dry * Use explicit track keys for processing * Align tracks of a multitrack recording * Generate waveforms for the mixed audio * Emit multriack pipeline events * Fix multitrack pipeline track alignment * dailico docs * Enable multitrack reprocessing * modal temp files uniform names, cleanup. remove llm temporary docs * docs cleanup * dont proceed with raw recordings if any of the downloads fail * dry transcription pipelines * remove is_miltitrack * comments * explicit dailyco room name * docs * remove stub data/method * frontend daily/whereby code self-review (no-mistake) * frontend daily/whereby code self-review (no-mistakes) * frontend daily/whereby code self-review (no-mistakes) * consent cleanup for multitrack (no-mistakes) * llm fun * remove extra comments * fix tests * merge migrations * Store participant names * Get participants by meeting session id * pop back main branch migration * s3 paddington (no-mistakes) * comment * pr comments * pr comments * pr comments * platform / meeting cleanup * Use participant names in summary generation * platform assignment to meeting at controller level * pr comment * room playform properly default none * room playform properly default none * restore migration lost * streaming WIP * extract storage / use common storage / proper env vars for storage * fix mocks tests * remove fall back * streaming for multifile * cenrtal storage abstraction (no-mistakes) * remove dead code / vars * Set participant user id for authenticated users * whereby recording name parsing fix * whereby recording name parsing fix * more file stream * storage dry + tests * remove homemade boto3 streaming and use proper boto * update migration guide * webhook creation script - print uuid --------- Co-authored-by: Igor Loskutov <igor.loskutoff@gmail.com> Co-authored-by: Mathieu Virbel <mat@meltingrocks.com> Co-authored-by: Sergey Mankovsky <sergey@monadical.com>
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
import Room from "../room";
|
||||
import RoomContainer from "../components/RoomContainer";
|
||||
|
||||
export default Room;
|
||||
export default RoomContainer;
|
||||
|
||||
93
www/app/[roomName]/components/DailyRoom.tsx
Normal file
93
www/app/[roomName]/components/DailyRoom.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import DailyIframe, { DailyCall } from "@daily-co/daily-js";
|
||||
import type { components } from "../../reflector-api";
|
||||
import { useAuth } from "../../lib/AuthProvider";
|
||||
import {
|
||||
ConsentDialogButton,
|
||||
recordingTypeRequiresConsent,
|
||||
} from "../../lib/consent";
|
||||
|
||||
type Meeting = components["schemas"]["Meeting"];
|
||||
|
||||
interface DailyRoomProps {
|
||||
meeting: Meeting;
|
||||
}
|
||||
|
||||
export default function DailyRoom({ meeting }: DailyRoomProps) {
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const status = auth.status;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const roomUrl = meeting?.host_room_url || meeting?.room_url;
|
||||
|
||||
const isLoading = status === "loading";
|
||||
|
||||
const handleLeave = useCallback(() => {
|
||||
router.push("/browse");
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || !roomUrl || !containerRef.current) return;
|
||||
|
||||
let frame: DailyCall | null = null;
|
||||
let destroyed = false;
|
||||
|
||||
const createAndJoin = async () => {
|
||||
try {
|
||||
const existingFrame = DailyIframe.getCallInstance();
|
||||
if (existingFrame) {
|
||||
await existingFrame.destroy();
|
||||
}
|
||||
|
||||
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);
|
||||
await frame.join({ url: roomUrl });
|
||||
} catch (error) {
|
||||
console.error("Error creating Daily frame:", error);
|
||||
}
|
||||
};
|
||||
|
||||
createAndJoin();
|
||||
|
||||
return () => {
|
||||
destroyed = true;
|
||||
if (frame) {
|
||||
frame.destroy().catch((e) => {
|
||||
console.error("Error destroying frame:", e);
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [roomUrl, isLoading, handleLeave]);
|
||||
|
||||
if (!roomUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box position="relative" width="100vw" height="100vh">
|
||||
<div ref={containerRef} style={{ width: "100%", height: "100%" }} />
|
||||
{meeting.recording_type &&
|
||||
recordingTypeRequiresConsent(meeting.recording_type) &&
|
||||
meeting.id && <ConsentDialogButton meetingId={meeting.id} />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
214
www/app/[roomName]/components/RoomContainer.tsx
Normal file
214
www/app/[roomName]/components/RoomContainer.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
|
||||
import { roomMeetingUrl } from "../../lib/routes";
|
||||
import { useCallback, useEffect, useState, use } from "react";
|
||||
import { Box, Text, Spinner } from "@chakra-ui/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
useRoomGetByName,
|
||||
useRoomsCreateMeeting,
|
||||
useRoomGetMeeting,
|
||||
} from "../../lib/apiHooks";
|
||||
import type { components } from "../../reflector-api";
|
||||
import MeetingSelection from "../MeetingSelection";
|
||||
import useRoomDefaultMeeting from "../useRoomDefaultMeeting";
|
||||
import WherebyRoom from "./WherebyRoom";
|
||||
import DailyRoom from "./DailyRoom";
|
||||
import { useAuth } from "../../lib/AuthProvider";
|
||||
import { useError } from "../../(errors)/errorContext";
|
||||
import { parseNonEmptyString } from "../../lib/utils";
|
||||
import { printApiError } from "../../api/_error";
|
||||
|
||||
type Meeting = components["schemas"]["Meeting"];
|
||||
|
||||
export type RoomDetails = {
|
||||
params: Promise<{
|
||||
roomName: string;
|
||||
meetingId?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="100vh"
|
||||
bg="gray.50"
|
||||
p={4}
|
||||
>
|
||||
<Spinner color="blue.500" size="xl" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RoomContainer(details: RoomDetails) {
|
||||
const params = use(details.params);
|
||||
const roomName = parseNonEmptyString(
|
||||
params.roomName,
|
||||
true,
|
||||
"panic! params.roomName is required",
|
||||
);
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const status = auth.status;
|
||||
const isAuthenticated = status === "authenticated";
|
||||
const { setError } = useError();
|
||||
|
||||
const roomQuery = useRoomGetByName(roomName);
|
||||
const createMeetingMutation = useRoomsCreateMeeting();
|
||||
|
||||
const room = roomQuery.data;
|
||||
|
||||
const pageMeetingId = params.meetingId;
|
||||
|
||||
const defaultMeeting = useRoomDefaultMeeting(
|
||||
room && !room.ics_enabled && !pageMeetingId ? roomName : null,
|
||||
);
|
||||
|
||||
const explicitMeeting = useRoomGetMeeting(roomName, pageMeetingId || null);
|
||||
|
||||
const meeting = explicitMeeting.data || defaultMeeting.response;
|
||||
|
||||
const isLoading =
|
||||
status === "loading" ||
|
||||
roomQuery.isLoading ||
|
||||
defaultMeeting?.loading ||
|
||||
explicitMeeting.isLoading ||
|
||||
createMeetingMutation.isPending;
|
||||
|
||||
const errors = [
|
||||
explicitMeeting.error,
|
||||
defaultMeeting.error,
|
||||
roomQuery.error,
|
||||
createMeetingMutation.error,
|
||||
].filter(Boolean);
|
||||
|
||||
const isOwner =
|
||||
isAuthenticated && room ? auth.user?.id === room.user_id : false;
|
||||
|
||||
const handleMeetingSelect = (selectedMeeting: Meeting) => {
|
||||
router.push(
|
||||
roomMeetingUrl(
|
||||
roomName,
|
||||
parseNonEmptyString(
|
||||
selectedMeeting.id,
|
||||
true,
|
||||
"panic! selectedMeeting.id is required",
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleCreateUnscheduled = async () => {
|
||||
try {
|
||||
const newMeeting = await createMeetingMutation.mutateAsync({
|
||||
params: {
|
||||
path: { room_name: roomName },
|
||||
},
|
||||
body: {
|
||||
allow_duplicated: room ? room.ics_enabled : false,
|
||||
},
|
||||
});
|
||||
handleMeetingSelect(newMeeting);
|
||||
} catch (err) {
|
||||
console.error("Failed to create meeting:", err);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!room) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="100vh"
|
||||
bg="gray.50"
|
||||
p={4}
|
||||
>
|
||||
<Text fontSize="lg">Room not found</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (room.ics_enabled && !params.meetingId) {
|
||||
return (
|
||||
<MeetingSelection
|
||||
roomName={roomName}
|
||||
isOwner={isOwner}
|
||||
isSharedRoom={room?.is_shared || false}
|
||||
authLoading={["loading", "refreshing"].includes(auth.status)}
|
||||
onMeetingSelect={handleMeetingSelect}
|
||||
onCreateUnscheduled={handleCreateUnscheduled}
|
||||
isCreatingMeeting={createMeetingMutation.isPending}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="100vh"
|
||||
bg="gray.50"
|
||||
p={4}
|
||||
>
|
||||
{errors.map((error, i) => (
|
||||
<Text key={i} fontSize="lg">
|
||||
{printApiError(error)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!meeting) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
const platform = meeting.platform;
|
||||
|
||||
if (!platform) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="100vh"
|
||||
bg="gray.50"
|
||||
p={4}
|
||||
>
|
||||
<Text fontSize="lg">Meeting platform not configured</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case "daily":
|
||||
return <DailyRoom meeting={meeting} />;
|
||||
case "whereby":
|
||||
return <WherebyRoom meeting={meeting} />;
|
||||
default: {
|
||||
const _exhaustive: never = platform;
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
height="100vh"
|
||||
bg="gray.50"
|
||||
p={4}
|
||||
>
|
||||
<Text fontSize="lg">Unknown platform: {platform}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
www/app/[roomName]/components/WherebyRoom.tsx
Normal file
101
www/app/[roomName]/components/WherebyRoom.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, RefObject } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { components } from "../../reflector-api";
|
||||
import { useAuth } from "../../lib/AuthProvider";
|
||||
import { getWherebyUrl, useWhereby } from "../../lib/wherebyClient";
|
||||
import { assertExistsAndNonEmptyString, NonEmptyString } from "../../lib/utils";
|
||||
import {
|
||||
ConsentDialogButton as BaseConsentDialogButton,
|
||||
useConsentDialog,
|
||||
recordingTypeRequiresConsent,
|
||||
} from "../../lib/consent";
|
||||
|
||||
type Meeting = components["schemas"]["Meeting"];
|
||||
|
||||
interface WherebyRoomProps {
|
||||
meeting: Meeting;
|
||||
}
|
||||
|
||||
function WherebyConsentDialogButton({
|
||||
meetingId,
|
||||
wherebyRef,
|
||||
}: {
|
||||
meetingId: NonEmptyString;
|
||||
wherebyRef: React.RefObject<HTMLElement>;
|
||||
}) {
|
||||
const previousFocusRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const element = wherebyRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const handleWherebyReady = () => {
|
||||
previousFocusRef.current = document.activeElement as HTMLElement;
|
||||
};
|
||||
|
||||
element.addEventListener("ready", handleWherebyReady);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener("ready", handleWherebyReady);
|
||||
if (previousFocusRef.current && document.activeElement === element) {
|
||||
previousFocusRef.current.focus();
|
||||
}
|
||||
};
|
||||
}, [wherebyRef]);
|
||||
|
||||
return <BaseConsentDialogButton meetingId={meetingId} />;
|
||||
}
|
||||
|
||||
export default function WherebyRoom({ meeting }: WherebyRoomProps) {
|
||||
const wherebyLoaded = useWhereby();
|
||||
const wherebyRef = useRef<HTMLElement>(null);
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const status = auth.status;
|
||||
const isAuthenticated = status === "authenticated";
|
||||
|
||||
const wherebyRoomUrl = getWherebyUrl(meeting);
|
||||
const recordingType = meeting.recording_type;
|
||||
const meetingId = meeting.id;
|
||||
|
||||
const isLoading = status === "loading";
|
||||
|
||||
const handleLeave = useCallback(() => {
|
||||
router.push("/browse");
|
||||
}, [router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || !isAuthenticated || !wherebyRoomUrl || !wherebyLoaded)
|
||||
return;
|
||||
|
||||
wherebyRef.current?.addEventListener("leave", handleLeave);
|
||||
|
||||
return () => {
|
||||
wherebyRef.current?.removeEventListener("leave", handleLeave);
|
||||
};
|
||||
}, [handleLeave, wherebyRoomUrl, isLoading, isAuthenticated, wherebyLoaded]);
|
||||
|
||||
if (!wherebyRoomUrl || !wherebyLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<whereby-embed
|
||||
ref={wherebyRef}
|
||||
room={wherebyRoomUrl}
|
||||
style={{ width: "100vw", height: "100vh" }}
|
||||
/>
|
||||
{recordingType &&
|
||||
recordingTypeRequiresConsent(recordingType) &&
|
||||
meetingId && (
|
||||
<WherebyConsentDialogButton
|
||||
meetingId={assertExistsAndNonEmptyString(meetingId)}
|
||||
wherebyRef={wherebyRef}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
import Room from "./room";
|
||||
import RoomContainer from "./components/RoomContainer";
|
||||
|
||||
export default Room;
|
||||
export default RoomContainer;
|
||||
|
||||
Reference in New Issue
Block a user