fix: align daily room settings (#759)

* Switch platform ui

* Update room settings based on platform

* Add local and none recording options to daily

* Don't create tokens for unauthentikated users

* Enable knocking for private rooms

* Create new meeting on room settings change

* Always use 2-200 option for daily

* Show recording start trigger for daily

* Fix broken test
This commit is contained in:
2025-12-02 09:06:36 +01:00
committed by GitHub
parent dabf7251db
commit 28f87c09dc
5 changed files with 158 additions and 40 deletions

View File

@@ -40,6 +40,10 @@ class RoomProperties(BaseModel):
) )
enable_chat: bool = Field(default=True, description="Enable in-meeting chat") enable_chat: bool = Field(default=True, description="Enable in-meeting chat")
enable_screenshare: bool = Field(default=True, description="Enable screen sharing") 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( start_video_off: bool = Field(
default=False, description="Start with video off for all participants" default=False, description="Start with video off for all participants"
) )

View File

@@ -31,6 +31,7 @@ class DailyClient(VideoPlatformClient):
PLATFORM_NAME: Platform = "daily" PLATFORM_NAME: Platform = "daily"
TIMESTAMP_FORMAT = "%Y%m%d%H%M%S" TIMESTAMP_FORMAT = "%Y%m%d%H%M%S"
RECORDING_NONE: RecordingType = "none" RECORDING_NONE: RecordingType = "none"
RECORDING_LOCAL: RecordingType = "local"
RECORDING_CLOUD: RecordingType = "cloud" RECORDING_CLOUD: RecordingType = "cloud"
def __init__(self, config: VideoPlatformConfig): def __init__(self, config: VideoPlatformConfig):
@@ -54,19 +55,23 @@ class DailyClient(VideoPlatformClient):
timestamp = datetime.now().strftime(self.TIMESTAMP_FORMAT) timestamp = datetime.now().strftime(self.TIMESTAMP_FORMAT)
room_name = f"{room_name_prefix}{ROOM_PREFIX_SEPARATOR}{timestamp}" 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( properties = RoomProperties(
enable_recording="raw-tracks" enable_recording=enable_recording,
if room.recording_type != self.RECORDING_NONE
else False,
enable_chat=True, enable_chat=True,
enable_screenshare=True, enable_screenshare=True,
enable_knocking=room.is_locked,
start_video_off=False, start_video_off=False,
start_audio_off=False, start_audio_off=False,
exp=int(end_date.timestamp()), exp=int(end_date.timestamp()),
) )
# Only configure recordings_bucket if recording is enabled if room.recording_type == self.RECORDING_CLOUD:
if room.recording_type != self.RECORDING_NONE:
daily_storage = get_dailyco_storage() daily_storage = get_dailyco_storage()
assert daily_storage.bucket_name, "S3 bucket must be configured" assert daily_storage.bucket_name, "S3 bucket must be configured"
properties.recordings_bucket = RecordingsBucketConfig( properties.recordings_bucket = RecordingsBucketConfig(
@@ -172,15 +177,16 @@ class DailyClient(VideoPlatformClient):
async def create_meeting_token( async def create_meeting_token(
self, self,
room_name: DailyRoomName, room_name: DailyRoomName,
enable_recording: bool, start_cloud_recording: bool,
enable_recording_ui: bool,
user_id: NonEmptyString | None = None, user_id: NonEmptyString | None = None,
is_owner: bool = False, is_owner: bool = False,
) -> NonEmptyString: ) -> NonEmptyString:
properties = MeetingTokenProperties( properties = MeetingTokenProperties(
room_name=room_name, room_name=room_name,
user_id=user_id, user_id=user_id,
start_cloud_recording=enable_recording, start_cloud_recording=start_cloud_recording,
enable_recording_ui=False, enable_recording_ui=enable_recording_ui,
is_owner=is_owner, is_owner=is_owner,
) )
request = CreateMeetingTokenRequest(properties=properties) request = CreateMeetingTokenRequest(properties=properties)

View File

@@ -89,7 +89,7 @@ class CreateRoom(BaseModel):
ics_url: Optional[str] = None ics_url: Optional[str] = None
ics_fetch_interval: int = 300 ics_fetch_interval: int = 300
ics_enabled: bool = False ics_enabled: bool = False
platform: Optional[Platform] = None platform: Platform
class UpdateRoom(BaseModel): class UpdateRoom(BaseModel):
@@ -248,7 +248,7 @@ async def rooms_create(
ics_url=room.ics_url, ics_url=room.ics_url,
ics_fetch_interval=room.ics_fetch_interval, ics_fetch_interval=room.ics_fetch_interval,
ics_enabled=room.ics_enabled, 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 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: if meeting is None:
end_date = current_time + timedelta(hours=8) end_date = current_time + timedelta(hours=8)
@@ -549,21 +565,16 @@ async def rooms_join_meeting(
if meeting.end_date <= current_time: if meeting.end_date <= current_time:
raise HTTPException(status_code=400, detail="Meeting has ended") 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) client = create_platform_client(meeting.platform)
enable_recording = room.recording_trigger != "none"
token = await client.create_meeting_token( token = await client.create_meeting_token(
meeting.room_name, 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, user_id=user_id,
is_owner=user_id == room.user_id, is_owner=user_id == room.user_id,
) )
meeting = meeting.model_copy() meeting = meeting.model_copy()
meeting.room_url = add_query_param(meeting.room_url, "t", token) 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 return meeting

View File

@@ -67,6 +67,11 @@ const recordingTypeOptions: SelectOption[] = [
{ label: "Cloud", value: "cloud" }, { label: "Cloud", value: "cloud" },
]; ];
const platformOptions: SelectOption[] = [
{ label: "Whereby", value: "whereby" },
{ label: "Daily", value: "daily" },
];
const roomInitialState = { const roomInitialState = {
name: "", name: "",
zulipAutoPost: false, zulipAutoPost: false,
@@ -82,6 +87,7 @@ const roomInitialState = {
icsUrl: "", icsUrl: "",
icsEnabled: false, icsEnabled: false,
icsFetchInterval: 5, icsFetchInterval: 5,
platform: "whereby",
}; };
export default function RoomsList() { export default function RoomsList() {
@@ -99,6 +105,11 @@ export default function RoomsList() {
const recordingTypeCollection = createListCollection({ const recordingTypeCollection = createListCollection({
items: recordingTypeOptions, items: recordingTypeOptions,
}); });
const platformCollection = createListCollection({
items: platformOptions,
});
const [roomInput, setRoomInput] = useState<null | typeof roomInitialState>( const [roomInput, setRoomInput] = useState<null | typeof roomInitialState>(
null, null,
); );
@@ -143,15 +154,24 @@ export default function RoomsList() {
zulipStream: detailedEditedRoom.zulip_stream, zulipStream: detailedEditedRoom.zulip_stream,
zulipTopic: detailedEditedRoom.zulip_topic, zulipTopic: detailedEditedRoom.zulip_topic,
isLocked: detailedEditedRoom.is_locked, isLocked: detailedEditedRoom.is_locked,
roomMode: detailedEditedRoom.room_mode, roomMode:
detailedEditedRoom.platform === "daily"
? "group"
: detailedEditedRoom.room_mode,
recordingType: detailedEditedRoom.recording_type, 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, isShared: detailedEditedRoom.is_shared,
webhookUrl: detailedEditedRoom.webhook_url || "", webhookUrl: detailedEditedRoom.webhook_url || "",
webhookSecret: detailedEditedRoom.webhook_secret || "", webhookSecret: detailedEditedRoom.webhook_secret || "",
icsUrl: detailedEditedRoom.ics_url || "", icsUrl: detailedEditedRoom.ics_url || "",
icsEnabled: detailedEditedRoom.ics_enabled || false, icsEnabled: detailedEditedRoom.ics_enabled || false,
icsFetchInterval: detailedEditedRoom.ics_fetch_interval || 5, icsFetchInterval: detailedEditedRoom.ics_fetch_interval || 5,
platform: detailedEditedRoom.platform,
} }
: null, : null,
[detailedEditedRoom], [detailedEditedRoom],
@@ -277,21 +297,32 @@ export default function RoomsList() {
return; return;
} }
const platform: "whereby" | "daily" | null =
room.platform === "whereby" || room.platform === "daily"
? room.platform
: null;
const roomData = { const roomData = {
name: room.name, name: room.name,
zulip_auto_post: room.zulipAutoPost, zulip_auto_post: room.zulipAutoPost,
zulip_stream: room.zulipStream, zulip_stream: room.zulipStream,
zulip_topic: room.zulipTopic, zulip_topic: room.zulipTopic,
is_locked: room.isLocked, is_locked: room.isLocked,
room_mode: room.roomMode, room_mode: platform === "daily" ? "group" : room.roomMode,
recording_type: room.recordingType, 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, is_shared: room.isShared,
webhook_url: room.webhookUrl, webhook_url: room.webhookUrl,
webhook_secret: room.webhookSecret, webhook_secret: room.webhookSecret,
ics_url: room.icsUrl, ics_url: room.icsUrl,
ics_enabled: room.icsEnabled, ics_enabled: room.icsEnabled,
ics_fetch_interval: room.icsFetchInterval, ics_fetch_interval: room.icsFetchInterval,
platform,
}; };
if (isEditing) { if (isEditing) {
@@ -339,15 +370,21 @@ export default function RoomsList() {
zulipStream: roomData.zulip_stream, zulipStream: roomData.zulip_stream,
zulipTopic: roomData.zulip_topic, zulipTopic: roomData.zulip_topic,
isLocked: roomData.is_locked, 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, 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, isShared: roomData.is_shared,
webhookUrl: roomData.webhook_url || "", webhookUrl: roomData.webhook_url || "",
webhookSecret: roomData.webhook_secret || "", webhookSecret: roomData.webhook_secret || "",
icsUrl: roomData.ics_url || "", icsUrl: roomData.ics_url || "",
icsEnabled: roomData.ics_enabled || false, icsEnabled: roomData.ics_enabled || false,
icsFetchInterval: roomData.ics_fetch_interval || 5, icsFetchInterval: roomData.ics_fetch_interval || 5,
platform: roomData.platform,
}); });
setEditRoomId(roomId); setEditRoomId(roomId);
setIsEditing(true); setIsEditing(true);
@@ -482,6 +519,48 @@ export default function RoomsList() {
)} )}
</Field.Root> </Field.Root>
<Field.Root mt={4}>
<Field.Label>Platform</Field.Label>
<Select.Root
value={[room.platform]}
onValueChange={(e) => {
const newPlatform = e.value[0] as "whereby" | "daily";
const updates: Partial<typeof room> = {
platform: newPlatform,
};
if (newPlatform === "daily") {
updates.roomMode = "group";
updates.recordingTrigger =
room.recordingType === "cloud"
? "automatic-2nd-participant"
: "none";
}
setRoomInput({ ...room, ...updates });
}}
collection={platformCollection}
>
<Select.HiddenSelect />
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select platform" />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
</Select.IndicatorGroup>
</Select.Control>
<Select.Positioner>
<Select.Content>
{platformOptions.map((option) => (
<Select.Item key={option.value} item={option}>
{option.label}
<Select.ItemIndicator />
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Select.Root>
</Field.Root>
<Field.Root mt={4}> <Field.Root mt={4}>
<Checkbox.Root <Checkbox.Root
name="isLocked" name="isLocked"
@@ -512,6 +591,7 @@ export default function RoomsList() {
setRoomInput({ ...room, roomMode: e.value[0] }) setRoomInput({ ...room, roomMode: e.value[0] })
} }
collection={roomModeCollection} collection={roomModeCollection}
disabled={room.platform === "daily"}
> >
<Select.HiddenSelect /> <Select.HiddenSelect />
<Select.Control> <Select.Control>
@@ -538,16 +618,26 @@ export default function RoomsList() {
<Field.Label>Recording type</Field.Label> <Field.Label>Recording type</Field.Label>
<Select.Root <Select.Root
value={[room.recordingType]} value={[room.recordingType]}
onValueChange={(e) => onValueChange={(e) => {
setRoomInput({ const newRecordingType = e.value[0];
...room, const updates: Partial<typeof room> = {
recordingType: e.value[0], recordingType: newRecordingType,
recordingTrigger: };
e.value[0] !== "cloud" // 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" ? "none"
: room.recordingTrigger, : room.recordingTrigger;
})
} }
setRoomInput({ ...room, ...updates });
}}
collection={recordingTypeCollection} collection={recordingTypeCollection}
> >
<Select.HiddenSelect /> <Select.HiddenSelect />
@@ -572,7 +662,7 @@ export default function RoomsList() {
</Select.Root> </Select.Root>
</Field.Root> </Field.Root>
<Field.Root mt={4}> <Field.Root mt={4}>
<Field.Label>Cloud recording start trigger</Field.Label> <Field.Label>Recording start trigger</Field.Label>
<Select.Root <Select.Root
value={[room.recordingTrigger]} value={[room.recordingTrigger]}
onValueChange={(e) => onValueChange={(e) =>
@@ -582,7 +672,11 @@ export default function RoomsList() {
}) })
} }
collection={recordingTriggerCollection} collection={recordingTriggerCollection}
disabled={room.recordingType !== "cloud"} disabled={
room.recordingType !== "cloud" ||
(room.platform === "daily" &&
room.recordingType === "cloud")
}
> >
<Select.HiddenSelect /> <Select.HiddenSelect />
<Select.Control> <Select.Control>

View File

@@ -52,7 +52,7 @@ export default function DailyRoom({ meeting }: DailyRoomProps) {
join(); join();
}, [meeting?.id, roomName, authLastUserId]); }, [meeting?.id, roomName, authLastUserId]);
const roomUrl = joinedMeeting?.host_room_url || joinedMeeting?.room_url; const roomUrl = joinedMeeting?.room_url;
const handleLeave = useCallback(() => { const handleLeave = useCallback(() => {
router.push("/browse"); router.push("/browse");
@@ -89,14 +89,17 @@ export default function DailyRoom({ meeting }: DailyRoomProps) {
frame.on("left-meeting", handleLeave); 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 () => { frame.on("joined-meeting", async () => {
try { try {
assertExists( const frameInstance = assertExists(
frame, frame,
"frame object got lost somewhere after frame.on was called", "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) { } catch (error) {
console.error("Failed to start recording:", error); console.error("Failed to start recording:", error);
} }