diff --git a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts index 80f229843d..ee640cce9b 100644 --- a/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/dailyvideo/lib/VideoApiAdapter.ts @@ -42,6 +42,35 @@ export interface DailyVideoCallData { url: string; } +// Regions available to create DailyVideo Rooms in. +const REGION_CODES = [ + "af-south-1", + "ap-northeast-2", + "ap-southeast-1", + "ap-southeast-2", + "ap-south-1", + "eu-central-1", + "eu-west-2", + "sa-east-1", + "us-east-1", + "us-west-2", +] as const; + +type RoomGeo = (typeof REGION_CODES)[number]; + +function getDailyVideoRegionFromEnv(): RoomGeo | undefined { + if (!process?.env?.DAILY_VIDEO_REGION) return; + const isRoomGeo = (value: string): value is RoomGeo => REGION_CODES.includes(value as RoomGeo); + function assertIsDailyVideoRegion(value: string): asserts value is RoomGeo { + if (!isRoomGeo(value)) { + throw new Error(`Invalid region code: ${value}. Must be one of: ${REGION_CODES.join(", ")}`); + } + } + const region = process.env.DAILY_VIDEO_REGION; + assertIsDailyVideoRegion(region); + return region; +} + const isS3StorageEnabled = process.env.CAL_VIDEO_BUCKET_NAME && process.env.CAL_VIDEO_BUCKET_REGION && @@ -203,11 +232,15 @@ export const setEnableRecordingUIAndUserIdForOrganizer = async ( }; const DailyVideoApiAdapter = (): VideoApiAdapter => { - async function createOrUpdateMeeting(endpoint: string, event: CalendarEvent): Promise { + async function createOrUpdateMeeting( + endpoint: string, + event: CalendarEvent, + region?: RoomGeo + ): Promise { if (!event.uid) { throw new Error("We need need the booking uid to create the Daily reference in DB"); } - const body = await translateEvent(event); + const body = await translateEvent(event, region); const dailyEvent = await postToDailyAPI(endpoint, body).then(dailyReturnTypeSchema.parse); const meetingToken = await postToDailyAPI("/meeting-tokens", { properties: { @@ -226,7 +259,7 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => { }); } - const translateEvent = async (event: CalendarEvent) => { + const translateEvent = async (event: CalendarEvent, region?: RoomGeo) => { // Documentation at: https://docs.daily.co/reference#list-rooms // Adds 14 days from the end of the booking as the expiration date const exp = Math.round(new Date(event.endTime).getTime() / 1000) + 60 * 60 * 24 * 14; @@ -248,6 +281,7 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => { return { privacy: "public", properties: { + ...(region ? { geo: region } : {}), enable_prejoin_ui: true, enable_knocking: true, enable_screenshare: true, @@ -284,7 +318,7 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => { }; }; - async function createInstantMeeting(endTime: string) { + async function createInstantMeeting(endTime: string, region?: RoomGeo) { // added a 1 hour buffer for room expiration const exp = Math.round(new Date(endTime).getTime() / 1000) + 60 * 60; const { scale_plan: scalePlan } = await getDailyAppKeys(); @@ -296,6 +330,7 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => { const body = { privacy: "public", properties: { + ...(region ? { geo: region } : {}), enable_prejoin_ui: true, enable_knocking: true, enable_screenshare: true, @@ -347,20 +382,22 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => { url: dailyEvent.url, }); } - + // Region on which the DailyVideo room is created can be controlled by ENV var + // undefined region leaves the choice to DailyVideo + const region = getDailyVideoRegionFromEnv(); return { /** Daily doesn't need to return busy times, so we return empty */ getAvailability: () => { return Promise.resolve([]); }, createMeeting: async (event: CalendarEvent): Promise => - createOrUpdateMeeting("/rooms", event), + createOrUpdateMeeting("/rooms", event, region), deleteMeeting: async (uid: string): Promise => { await fetcher(`/rooms/${uid}`, { method: "DELETE" }); return Promise.resolve(); }, updateMeeting: (bookingRef: PartialReference, event: CalendarEvent): Promise => - createOrUpdateMeeting(`/rooms/${bookingRef.uid}`, event), + createOrUpdateMeeting(`/rooms/${bookingRef.uid}`, event, region), getRecordings: async (roomName: string): Promise => { try { const res = await fetcher(`/recordings?room_name=${roomName}`).then( @@ -371,7 +408,7 @@ const DailyVideoApiAdapter = (): VideoApiAdapter => { throw new Error("Something went wrong! Unable to get recording"); } }, - createInstantCalVideoRoom: (endTime: string) => createInstantMeeting(endTime), + createInstantCalVideoRoom: (endTime: string) => createInstantMeeting(endTime, region), getRecordingDownloadLink: async (recordingId: string): Promise => { try { const res = await fetcher(`/recordings/${recordingId}/access-link?valid_for_secs=43200`).then( diff --git a/turbo.json b/turbo.json index 6188cd0742..2497c466ef 100644 --- a/turbo.json +++ b/turbo.json @@ -281,6 +281,7 @@ "DAILY_SCALE_PLAN", "DAILY_WEBHOOK_SECRET", "DAILY_MEETING_ENDED_WEBHOOK_SECRET", + "DAILY_VIDEO_REGION", "DATABASE_CHUNK_SIZE", "DATABASE_DIRECT_URL", "DATABASE_URL",