videochat tool update
This commit is contained in:
parent
a9a23e27e3
commit
2d562b3e4c
|
|
@ -15,6 +15,8 @@
|
|||
"author": "Jeff Emmett",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@daily-co/daily-js": "^0.60.0",
|
||||
"@daily-co/daily-react": "^0.20.0",
|
||||
"@tldraw/assets": "^3.6.0",
|
||||
"@tldraw/sync": "^3.6.0",
|
||||
"@tldraw/sync-core": "^3.6.0",
|
||||
|
|
@ -26,11 +28,13 @@
|
|||
"gray-matter": "^4.0.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"itty-router": "^5.0.17",
|
||||
"jotai": "^2.6.0",
|
||||
"jspdf": "^2.5.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^7.0.2",
|
||||
"recoil": "^0.7.7",
|
||||
"tldraw": "^3.6.0",
|
||||
"vercel": "^39.1.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -53,19 +53,25 @@ export function useCameraControls(editor: Editor | null) {
|
|||
const frameId = searchParams.get("frameId")
|
||||
const isLocked = searchParams.get("isLocked") === "true"
|
||||
|
||||
console.log("Setting camera:", { x, y, zoom })
|
||||
console.log("Setting camera:", { x, y, zoom, frameId, isLocked })
|
||||
|
||||
// Set camera position if coordinates exist
|
||||
if (x && y && zoom) {
|
||||
const position = {
|
||||
x: parseFloat(x),
|
||||
y: parseFloat(y),
|
||||
z: parseFloat(zoom),
|
||||
x: Math.round(parseFloat(x)),
|
||||
y: Math.round(parseFloat(y)),
|
||||
z: Math.round(parseFloat(zoom)),
|
||||
}
|
||||
console.log("Camera position:", position)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
editor.setCamera(position, { animation: { duration: 0 } })
|
||||
|
||||
// Apply camera lock immediately after setting position if needed
|
||||
if (isLocked) {
|
||||
editor.setCameraOptions({ isLocked: true })
|
||||
}
|
||||
|
||||
console.log("Current camera:", editor.getCamera())
|
||||
})
|
||||
}
|
||||
|
|
@ -78,9 +84,17 @@ export function useCameraControls(editor: Editor | null) {
|
|||
|
||||
// If x/y/zoom are not provided in URL, zoom to frame bounds
|
||||
if (!x || !y || !zoom) {
|
||||
editor.zoomToBounds(editor.getShapePageBounds(frame)!, {
|
||||
const bounds = editor.getShapePageBounds(frame)!
|
||||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
const targetZoom = Math.min(
|
||||
viewportPageBounds.width / bounds.width,
|
||||
viewportPageBounds.height / bounds.height,
|
||||
1, // Cap at 1x zoom, matching lockCameraToFrame
|
||||
)
|
||||
|
||||
editor.zoomToBounds(bounds, {
|
||||
animation: { duration: 0 },
|
||||
targetZoom: 1,
|
||||
targetZoom,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -129,9 +143,9 @@ export function useCameraControls(editor: Editor | null) {
|
|||
if (!editor) return
|
||||
const camera = editor.getCamera()
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set("x", camera.x.toString())
|
||||
url.searchParams.set("y", camera.y.toString())
|
||||
url.searchParams.set("zoom", camera.z.toString())
|
||||
url.searchParams.set("x", Math.round(camera.x).toString())
|
||||
url.searchParams.set("y", Math.round(camera.y).toString())
|
||||
url.searchParams.set("zoom", Math.round(camera.z).toString())
|
||||
navigator.clipboard.writeText(url.toString())
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useState, useRef } from "react"
|
||||
import { WORKER_URL } from "../routes/Board"
|
||||
import DailyIframe from "@daily-co/daily-js"
|
||||
|
||||
export type IVideoChatShape = TLBaseShape<
|
||||
"VideoChat",
|
||||
|
|
@ -12,6 +13,53 @@ export type IVideoChatShape = TLBaseShape<
|
|||
}
|
||||
>
|
||||
|
||||
// Simplified component using Daily Prebuilt
|
||||
const VideoChatComponent = ({ roomUrl }: { roomUrl: string }) => {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
const callFrameRef = useRef<ReturnType<
|
||||
typeof DailyIframe.createFrame
|
||||
> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!wrapperRef.current || !roomUrl) return
|
||||
|
||||
// Create and configure the Daily call frame
|
||||
callFrameRef.current = DailyIframe.createFrame(wrapperRef.current, {
|
||||
iframeStyle: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "0",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
showLeaveButton: true,
|
||||
showFullscreenButton: true,
|
||||
})
|
||||
|
||||
// Join the room
|
||||
callFrameRef.current.join({ url: roomUrl })
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (callFrameRef.current) {
|
||||
callFrameRef.current.destroy()
|
||||
}
|
||||
}
|
||||
}, [roomUrl])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "#f0f0f0",
|
||||
borderRadius: "4px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||
static override type = "VideoChat"
|
||||
|
||||
|
|
@ -33,29 +81,37 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
return
|
||||
}
|
||||
|
||||
const response = await fetch(`${WORKER_URL}/daily/rooms`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
properties: {
|
||||
enable_recording: true,
|
||||
max_participants: 8,
|
||||
try {
|
||||
const response = await fetch(`${WORKER_URL}/daily/rooms`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
})
|
||||
body: JSON.stringify({
|
||||
properties: {
|
||||
enable_recording: true,
|
||||
max_participants: 8,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create room")
|
||||
}
|
||||
|
||||
this.editor.updateShape<IVideoChatShape>({
|
||||
id: shape.id,
|
||||
type: "VideoChat",
|
||||
props: {
|
||||
...shape.props,
|
||||
roomUrl: (data as any).url,
|
||||
},
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
this.editor.updateShape<IVideoChatShape>({
|
||||
id: shape.id,
|
||||
type: "VideoChat",
|
||||
props: {
|
||||
...shape.props,
|
||||
roomUrl: (data as any).url,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to create Daily room:", error)
|
||||
}
|
||||
}
|
||||
|
||||
component(shape: IVideoChatShape) {
|
||||
|
|
@ -64,60 +120,91 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isInRoom && shape.props.roomUrl) {
|
||||
const script = document.createElement("script")
|
||||
script.src = "https://www.daily.co/static/call-machine.js"
|
||||
document.body.appendChild(script)
|
||||
setIsLoading(true)
|
||||
this.ensureRoomExists(shape)
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setIsLoading(false))
|
||||
}, [])
|
||||
|
||||
script.onload = () => {
|
||||
// @ts-ignore
|
||||
window.DailyIframe.createFrame({
|
||||
iframeStyle: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "0",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
showLeaveButton: true,
|
||||
showFullscreenButton: true,
|
||||
}).join({ url: shape.props.roomUrl })
|
||||
}
|
||||
}
|
||||
}, [isInRoom, shape.props.roomUrl])
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: `${shape.props.w}px`,
|
||||
height: `${shape.props.h}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#f0f0f0",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<div>Initializing video chat...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!shape.props.roomUrl) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: `${shape.props.w}px`,
|
||||
height: `${shape.props.h}px`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#f0f0f0",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
Creating room...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
pointerEvents: "all",
|
||||
width: `${shape.props.w}px`,
|
||||
height: `${shape.props.h}px`,
|
||||
position: "absolute",
|
||||
top: "10px",
|
||||
left: "10px",
|
||||
zIndex: 9999,
|
||||
padding: "15px",
|
||||
backgroundColor: "#F0F0F0",
|
||||
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||
borderRadius: "4px",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{!isInRoom ? (
|
||||
<button
|
||||
onClick={() => setIsInRoom(true)}
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
padding: "12px 24px",
|
||||
backgroundColor: "#2563eb",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Join Room
|
||||
Join Video Chat
|
||||
</button>
|
||||
) : (
|
||||
<div
|
||||
id="daily-call-iframe-container"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
<VideoChatComponent roomUrl={shape.props.roomUrl} />
|
||||
)}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
right: 10,
|
||||
color: "red",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{error && <p className="text-red-500 mt-2">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export const lockCameraToFrame = async (editor: Editor) => {
|
|||
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
||||
const url = new URL(baseUrl)
|
||||
|
||||
// Calculate zoom level to fit the frame
|
||||
// Calculate zoom level to fit the frame (for URL only)
|
||||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
const targetZoom = Math.min(
|
||||
viewportPageBounds.width / bounds.width,
|
||||
|
|
@ -194,17 +194,19 @@ export const lockCameraToFrame = async (editor: Editor) => {
|
|||
1, // Cap at 1x zoom
|
||||
)
|
||||
|
||||
// Set camera parameters first
|
||||
// Set URL parameters without affecting the current view
|
||||
url.searchParams.set("x", Math.round(bounds.x).toString())
|
||||
url.searchParams.set("y", Math.round(bounds.y).toString())
|
||||
url.searchParams.set("zoom", targetZoom.toString())
|
||||
|
||||
// Add frame-specific parameters last
|
||||
url.searchParams.set("isLocked", "true")
|
||||
url.searchParams.set(
|
||||
"zoom",
|
||||
(Math.round(targetZoom * 100) / 100).toString(),
|
||||
)
|
||||
url.searchParams.set("frameId", selectedShape.id)
|
||||
url.searchParams.set("isLocked", "true")
|
||||
|
||||
const finalUrl = url.toString()
|
||||
|
||||
// Copy URL to clipboard without modifying the current view
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(finalUrl)
|
||||
} else {
|
||||
|
|
|
|||
263
worker/worker.ts
263
worker/worker.ts
|
|
@ -1,135 +1,182 @@
|
|||
import { handleUnfurlRequest } from 'cloudflare-workers-unfurl'
|
||||
import { AutoRouter, cors, error, IRequest } from 'itty-router'
|
||||
import { handleAssetDownload, handleAssetUpload } from './assetUploads'
|
||||
import { Environment } from './types'
|
||||
import { handleUnfurlRequest } from "cloudflare-workers-unfurl"
|
||||
import { AutoRouter, cors, error, IRequest } from "itty-router"
|
||||
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
|
||||
import { Environment } from "./types"
|
||||
|
||||
// make sure our sync durable object is made available to cloudflare
|
||||
export { TldrawDurableObject } from './TldrawDurableObject'
|
||||
export { TldrawDurableObject } from "./TldrawDurableObject"
|
||||
|
||||
// Define security headers
|
||||
const securityHeaders = {
|
||||
'Content-Security-Policy': "default-src 'self'; connect-src 'self' wss: https:; img-src 'self' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
'X-XSS-Protection': '1; mode=block',
|
||||
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()'
|
||||
"Content-Security-Policy":
|
||||
"default-src 'self'; connect-src 'self' wss: https:; img-src 'self' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-XSS-Protection": "1; mode=block",
|
||||
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
||||
}
|
||||
|
||||
// we use itty-router (https://itty.dev/) to handle routing. in this example we turn on CORS because
|
||||
// we're hosting the worker separately to the client. you should restrict this to your own domain.
|
||||
const { preflight, corsify } = cors({
|
||||
origin: (origin) => {
|
||||
const allowedOrigins = [
|
||||
'https://jeffemmett.com',
|
||||
'https://www.jeffemmett.com',
|
||||
'https://jeffemmett-canvas.jeffemmett.workers.dev',
|
||||
'https://jeffemmett.com/board/*',
|
||||
];
|
||||
origin: (origin) => {
|
||||
const allowedOrigins = [
|
||||
"https://jeffemmett.com",
|
||||
"https://www.jeffemmett.com",
|
||||
"https://jeffemmett-canvas.jeffemmett.workers.dev",
|
||||
"https://jeffemmett.com/board/*",
|
||||
]
|
||||
|
||||
// Always allow if no origin (like from a local file)
|
||||
if (!origin) return '*';
|
||||
// Always allow if no origin (like from a local file)
|
||||
if (!origin) return "*"
|
||||
|
||||
// Check exact matches
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return origin;
|
||||
}
|
||||
// Check exact matches
|
||||
if (allowedOrigins.includes(origin)) {
|
||||
return origin
|
||||
}
|
||||
|
||||
// For development - check if it's a localhost or local IP
|
||||
if (origin.match(/^http:\/\/(localhost|127\.0\.0\.192\.168\.|169\.254\.|10\.)/)) {
|
||||
return origin;
|
||||
}
|
||||
// For development - check if it's a localhost or local IP
|
||||
if (
|
||||
origin.match(
|
||||
/^http:\/\/(localhost|127\.0\.0\.192\.168\.|169\.254\.|10\.)/,
|
||||
)
|
||||
) {
|
||||
return origin
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
allowMethods: ['GET', 'POST', 'OPTIONS', 'UPGRADE'],
|
||||
allowHeaders: [
|
||||
'Content-Type',
|
||||
'Authorization',
|
||||
'Upgrade',
|
||||
'Connection',
|
||||
'Sec-WebSocket-Key',
|
||||
'Sec-WebSocket-Version',
|
||||
'Sec-WebSocket-Extensions',
|
||||
'Sec-WebSocket-Protocol'
|
||||
],
|
||||
maxAge: 86400,
|
||||
credentials: true
|
||||
return undefined
|
||||
},
|
||||
allowMethods: ["GET", "POST", "OPTIONS", "UPGRADE"],
|
||||
allowHeaders: [
|
||||
"Content-Type",
|
||||
"Authorization",
|
||||
"Upgrade",
|
||||
"Connection",
|
||||
"Sec-WebSocket-Key",
|
||||
"Sec-WebSocket-Version",
|
||||
"Sec-WebSocket-Extensions",
|
||||
"Sec-WebSocket-Protocol",
|
||||
],
|
||||
maxAge: 86400,
|
||||
credentials: true,
|
||||
})
|
||||
const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||
before: [preflight],
|
||||
finally: [(response) => {
|
||||
// Add security headers to all responses except WebSocket upgrades
|
||||
if (response.status !== 101) {
|
||||
Object.entries(securityHeaders).forEach(([key, value]) => {
|
||||
response.headers.set(key, value)
|
||||
})
|
||||
}
|
||||
return corsify(response)
|
||||
}],
|
||||
catch: (e) => {
|
||||
console.error(e)
|
||||
return error(e)
|
||||
},
|
||||
before: [preflight],
|
||||
finally: [
|
||||
(response) => {
|
||||
// Add security headers to all responses except WebSocket upgrades
|
||||
if (response.status !== 101) {
|
||||
Object.entries(securityHeaders).forEach(([key, value]) => {
|
||||
response.headers.set(key, value)
|
||||
})
|
||||
}
|
||||
return corsify(response)
|
||||
},
|
||||
],
|
||||
catch: (e) => {
|
||||
console.error(e)
|
||||
return error(e)
|
||||
},
|
||||
})
|
||||
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
||||
.get('/connect/:roomId', (request, env) => {
|
||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||
return room.fetch(request.url, {
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
method: request.method
|
||||
})
|
||||
})
|
||||
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
||||
.get("/connect/:roomId", (request, env) => {
|
||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||
return room.fetch(request.url, {
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
method: request.method,
|
||||
})
|
||||
})
|
||||
|
||||
// assets can be uploaded to the bucket under /uploads:
|
||||
.post('/uploads/:uploadId', handleAssetUpload)
|
||||
// assets can be uploaded to the bucket under /uploads:
|
||||
.post("/uploads/:uploadId", handleAssetUpload)
|
||||
|
||||
// they can be retrieved from the bucket too:
|
||||
.get('/uploads/:uploadId', handleAssetDownload)
|
||||
// they can be retrieved from the bucket too:
|
||||
.get("/uploads/:uploadId", handleAssetDownload)
|
||||
|
||||
// bookmarks need to extract metadata from pasted URLs:
|
||||
.get('/unfurl', handleUnfurlRequest)
|
||||
// bookmarks need to extract metadata from pasted URLs:
|
||||
.get("/unfurl", handleUnfurlRequest)
|
||||
|
||||
.get('/room/:roomId', (request, env) => {
|
||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||
return room.fetch(request.url, {
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
method: request.method
|
||||
})
|
||||
})
|
||||
.get("/room/:roomId", (request, env) => {
|
||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||
return room.fetch(request.url, {
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
method: request.method,
|
||||
})
|
||||
})
|
||||
|
||||
.post('/room/:roomId', async (request, env) => {
|
||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||
return room.fetch(request.url, {
|
||||
method: 'POST',
|
||||
body: request.body
|
||||
})
|
||||
})
|
||||
.post("/room/:roomId", async (request, env) => {
|
||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||
return room.fetch(request.url, {
|
||||
method: "POST",
|
||||
body: request.body,
|
||||
})
|
||||
})
|
||||
|
||||
.post('/daily/rooms', async (request, env) => {
|
||||
const response = await fetch('https://api.daily.co/v1/rooms', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${env.DAILY_API_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: await request.text()
|
||||
});
|
||||
.post("/daily/rooms", async (request, env) => {
|
||||
try {
|
||||
// Log the request for debugging
|
||||
console.log(
|
||||
"Creating Daily room with API key:",
|
||||
env.DAILY_API_KEY ? "present" : "missing",
|
||||
)
|
||||
|
||||
const data = await response.json() as Record<string, unknown>;
|
||||
return new Response(JSON.stringify({
|
||||
...data,
|
||||
url: `https://${env.DAILY_DOMAIN}/${data.name}`
|
||||
}), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
})
|
||||
const response = await fetch("https://api.daily.co/v1/rooms", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.DAILY_API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: await request.text(),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("Daily API error:", errorText)
|
||||
return new Response(`Daily API error: ${errorText}`, {
|
||||
status: response.status,
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Log successful response
|
||||
console.log("Daily room created:", data)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
...(data as Record<string, unknown>),
|
||||
url: `https://${env.DAILY_DOMAIN}/${
|
||||
(data as Record<string, unknown>).name
|
||||
}`,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// Add CORS headers specifically for this endpoint if needed
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
},
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error creating Daily room:", error)
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Failed to create Daily room" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// export our router for cloudflare
|
||||
export default router
|
||||
|
|
|
|||
Loading…
Reference in New Issue