diff --git a/server/reflector/dailyco_api/requests.py b/server/reflector/dailyco_api/requests.py index e943b90f..54b25697 100644 --- a/server/reflector/dailyco_api/requests.py +++ b/server/reflector/dailyco_api/requests.py @@ -40,6 +40,10 @@ class RoomProperties(BaseModel): ) enable_chat: bool = Field(default=True, description="Enable in-meeting chat") enable_screenshare: bool = Field(default=True, description="Enable screen sharing") + enable_knocking: bool = Field( + default=False, + description="Enable knocking for private rooms (allows participants to request access)", + ) start_video_off: bool = Field( default=False, description="Start with video off for all participants" ) diff --git a/server/reflector/video_platforms/daily.py b/server/reflector/video_platforms/daily.py index f7782ca9..7695b745 100644 --- a/server/reflector/video_platforms/daily.py +++ b/server/reflector/video_platforms/daily.py @@ -31,6 +31,7 @@ class DailyClient(VideoPlatformClient): PLATFORM_NAME: Platform = "daily" TIMESTAMP_FORMAT = "%Y%m%d%H%M%S" RECORDING_NONE: RecordingType = "none" + RECORDING_LOCAL: RecordingType = "local" RECORDING_CLOUD: RecordingType = "cloud" def __init__(self, config: VideoPlatformConfig): @@ -54,19 +55,23 @@ class DailyClient(VideoPlatformClient): timestamp = datetime.now().strftime(self.TIMESTAMP_FORMAT) room_name = f"{room_name_prefix}{ROOM_PREFIX_SEPARATOR}{timestamp}" + enable_recording = None + if room.recording_type == self.RECORDING_LOCAL: + enable_recording = "local" + elif room.recording_type == self.RECORDING_CLOUD: + enable_recording = "raw-tracks" + properties = RoomProperties( - enable_recording="raw-tracks" - if room.recording_type != self.RECORDING_NONE - else False, + enable_recording=enable_recording, enable_chat=True, enable_screenshare=True, + enable_knocking=room.is_locked, start_video_off=False, start_audio_off=False, exp=int(end_date.timestamp()), ) - # Only configure recordings_bucket if recording is enabled - if room.recording_type != self.RECORDING_NONE: + if room.recording_type == self.RECORDING_CLOUD: daily_storage = get_dailyco_storage() assert daily_storage.bucket_name, "S3 bucket must be configured" properties.recordings_bucket = RecordingsBucketConfig( @@ -172,15 +177,16 @@ class DailyClient(VideoPlatformClient): async def create_meeting_token( self, room_name: DailyRoomName, - enable_recording: bool, + start_cloud_recording: bool, + enable_recording_ui: bool, user_id: NonEmptyString | None = None, is_owner: bool = False, ) -> NonEmptyString: properties = MeetingTokenProperties( room_name=room_name, user_id=user_id, - start_cloud_recording=enable_recording, - enable_recording_ui=False, + start_cloud_recording=start_cloud_recording, + enable_recording_ui=enable_recording_ui, is_owner=is_owner, ) request = CreateMeetingTokenRequest(properties=properties) diff --git a/server/reflector/views/rooms.py b/server/reflector/views/rooms.py index da5db1e8..5b218cb4 100644 --- a/server/reflector/views/rooms.py +++ b/server/reflector/views/rooms.py @@ -89,7 +89,7 @@ class CreateRoom(BaseModel): ics_url: Optional[str] = None ics_fetch_interval: int = 300 ics_enabled: bool = False - platform: Optional[Platform] = None + platform: Platform class UpdateRoom(BaseModel): @@ -248,7 +248,7 @@ async def rooms_create( ics_url=room.ics_url, ics_fetch_interval=room.ics_fetch_interval, ics_enabled=room.ics_enabled, - platform=room.platform or settings.DEFAULT_VIDEO_PLATFORM, + platform=room.platform, ) @@ -310,6 +310,22 @@ async def rooms_create_meeting( room=room, current_time=current_time ) + if meeting is not None: + settings_match = ( + meeting.is_locked == room.is_locked + and meeting.room_mode == room.room_mode + and meeting.recording_type == room.recording_type + and meeting.recording_trigger == room.recording_trigger + and meeting.platform == room.platform + ) + if not settings_match: + logger.info( + f"Room settings changed for {room_name}, creating new meeting", + room_id=room.id, + old_meeting_id=meeting.id, + ) + meeting = None + if meeting is None: end_date = current_time + timedelta(hours=8) @@ -549,21 +565,16 @@ async def rooms_join_meeting( if meeting.end_date <= current_time: raise HTTPException(status_code=400, detail="Meeting has ended") - if meeting.platform == "daily": + if meeting.platform == "daily" and user_id is not None: client = create_platform_client(meeting.platform) - enable_recording = room.recording_trigger != "none" token = await client.create_meeting_token( meeting.room_name, - enable_recording=enable_recording, + start_cloud_recording=meeting.recording_type == "cloud", + enable_recording_ui=meeting.recording_type == "local", user_id=user_id, is_owner=user_id == room.user_id, ) meeting = meeting.model_copy() meeting.room_url = add_query_param(meeting.room_url, "t", token) - if meeting.host_room_url: - meeting.host_room_url = add_query_param(meeting.host_room_url, "t", token) - - if user_id != room.user_id and meeting.platform == "whereby": - meeting.host_room_url = "" return meeting diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx index a7a68d2f..198df774 100644 --- a/www/app/(app)/rooms/page.tsx +++ b/www/app/(app)/rooms/page.tsx @@ -67,6 +67,11 @@ const recordingTypeOptions: SelectOption[] = [ { label: "Cloud", value: "cloud" }, ]; +const platformOptions: SelectOption[] = [ + { label: "Whereby", value: "whereby" }, + { label: "Daily", value: "daily" }, +]; + const roomInitialState = { name: "", zulipAutoPost: false, @@ -82,6 +87,7 @@ const roomInitialState = { icsUrl: "", icsEnabled: false, icsFetchInterval: 5, + platform: "whereby", }; export default function RoomsList() { @@ -99,6 +105,11 @@ export default function RoomsList() { const recordingTypeCollection = createListCollection({ items: recordingTypeOptions, }); + + const platformCollection = createListCollection({ + items: platformOptions, + }); + const [roomInput, setRoomInput] = useState( null, ); @@ -143,15 +154,24 @@ export default function RoomsList() { zulipStream: detailedEditedRoom.zulip_stream, zulipTopic: detailedEditedRoom.zulip_topic, isLocked: detailedEditedRoom.is_locked, - roomMode: detailedEditedRoom.room_mode, + roomMode: + detailedEditedRoom.platform === "daily" + ? "group" + : detailedEditedRoom.room_mode, recordingType: detailedEditedRoom.recording_type, - recordingTrigger: detailedEditedRoom.recording_trigger, + recordingTrigger: + detailedEditedRoom.platform === "daily" + ? detailedEditedRoom.recording_type === "cloud" + ? "automatic-2nd-participant" + : "none" + : detailedEditedRoom.recording_trigger, isShared: detailedEditedRoom.is_shared, webhookUrl: detailedEditedRoom.webhook_url || "", webhookSecret: detailedEditedRoom.webhook_secret || "", icsUrl: detailedEditedRoom.ics_url || "", icsEnabled: detailedEditedRoom.ics_enabled || false, icsFetchInterval: detailedEditedRoom.ics_fetch_interval || 5, + platform: detailedEditedRoom.platform, } : null, [detailedEditedRoom], @@ -277,21 +297,32 @@ export default function RoomsList() { return; } + const platform: "whereby" | "daily" | null = + room.platform === "whereby" || room.platform === "daily" + ? room.platform + : null; + const roomData = { name: room.name, zulip_auto_post: room.zulipAutoPost, zulip_stream: room.zulipStream, zulip_topic: room.zulipTopic, is_locked: room.isLocked, - room_mode: room.roomMode, + room_mode: platform === "daily" ? "group" : room.roomMode, recording_type: room.recordingType, - recording_trigger: room.recordingTrigger, + recording_trigger: + platform === "daily" + ? room.recordingType === "cloud" + ? "automatic-2nd-participant" + : "none" + : room.recordingTrigger, is_shared: room.isShared, webhook_url: room.webhookUrl, webhook_secret: room.webhookSecret, ics_url: room.icsUrl, ics_enabled: room.icsEnabled, ics_fetch_interval: room.icsFetchInterval, + platform, }; if (isEditing) { @@ -339,15 +370,21 @@ export default function RoomsList() { zulipStream: roomData.zulip_stream, zulipTopic: roomData.zulip_topic, isLocked: roomData.is_locked, - roomMode: roomData.room_mode, + roomMode: roomData.platform === "daily" ? "group" : roomData.room_mode, // Daily always uses 2-200 recordingType: roomData.recording_type, - recordingTrigger: roomData.recording_trigger, + recordingTrigger: + roomData.platform === "daily" + ? roomData.recording_type === "cloud" + ? "automatic-2nd-participant" + : "none" + : roomData.recording_trigger, isShared: roomData.is_shared, webhookUrl: roomData.webhook_url || "", webhookSecret: roomData.webhook_secret || "", icsUrl: roomData.ics_url || "", icsEnabled: roomData.ics_enabled || false, icsFetchInterval: roomData.ics_fetch_interval || 5, + platform: roomData.platform, }); setEditRoomId(roomId); setIsEditing(true); @@ -482,6 +519,48 @@ export default function RoomsList() { )} + + Platform + { + const newPlatform = e.value[0] as "whereby" | "daily"; + const updates: Partial = { + platform: newPlatform, + }; + if (newPlatform === "daily") { + updates.roomMode = "group"; + updates.recordingTrigger = + room.recordingType === "cloud" + ? "automatic-2nd-participant" + : "none"; + } + setRoomInput({ ...room, ...updates }); + }} + collection={platformCollection} + > + + + + + + + + + + + + {platformOptions.map((option) => ( + + {option.label} + + + ))} + + + + + @@ -538,16 +618,26 @@ export default function RoomsList() { Recording type - setRoomInput({ - ...room, - recordingType: e.value[0], - recordingTrigger: - e.value[0] !== "cloud" + onValueChange={(e) => { + const newRecordingType = e.value[0]; + const updates: Partial = { + recordingType: newRecordingType, + }; + // For Daily: if cloud, use automatic; otherwise none + if (room.platform === "daily") { + updates.recordingTrigger = + newRecordingType === "cloud" + ? "automatic-2nd-participant" + : "none"; + } else { + // For Whereby: if not cloud, set to none + updates.recordingTrigger = + newRecordingType !== "cloud" ? "none" - : room.recordingTrigger, - }) - } + : room.recordingTrigger; + } + setRoomInput({ ...room, ...updates }); + }} collection={recordingTypeCollection} > @@ -572,7 +662,7 @@ export default function RoomsList() { - Cloud recording start trigger + Recording start trigger @@ -582,7 +672,11 @@ export default function RoomsList() { }) } collection={recordingTriggerCollection} - disabled={room.recordingType !== "cloud"} + disabled={ + room.recordingType !== "cloud" || + (room.platform === "daily" && + room.recordingType === "cloud") + } > diff --git a/www/app/[roomName]/components/DailyRoom.tsx b/www/app/[roomName]/components/DailyRoom.tsx index 8c0302e7..45f2dad2 100644 --- a/www/app/[roomName]/components/DailyRoom.tsx +++ b/www/app/[roomName]/components/DailyRoom.tsx @@ -52,7 +52,7 @@ export default function DailyRoom({ meeting }: DailyRoomProps) { join(); }, [meeting?.id, roomName, authLastUserId]); - const roomUrl = joinedMeeting?.host_room_url || joinedMeeting?.room_url; + const roomUrl = joinedMeeting?.room_url; const handleLeave = useCallback(() => { router.push("/browse"); @@ -89,14 +89,17 @@ export default function DailyRoom({ meeting }: DailyRoomProps) { frame.on("left-meeting", handleLeave); - // TODO this method must not ignore no-recording option - // TODO this method is here to make dev environments work in some cases (not examined which) frame.on("joined-meeting", async () => { try { - assertExists( + const frameInstance = assertExists( frame, "frame object got lost somewhere after frame.on was called", - ).startRecording({ type: "raw-tracks" }); + ); + + 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); }