fix video chat in prod env vars
This commit is contained in:
parent
1d1b64fe7c
commit
9a9cab1b8e
|
|
@ -38,7 +38,7 @@ export const generateCanvasScreenshot = async (editor: Editor): Promise<string |
|
|||
scale: 0.5, // Reduced scale to make image smaller
|
||||
background: true,
|
||||
padding: 20, // Increased padding to show full canvas
|
||||
preserveAspectRatio: "true",
|
||||
preserveAspectRatio: "xMidYMid meet",
|
||||
bounds: bounds, // Export the entire canvas bounds
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -121,7 +121,14 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
}
|
||||
|
||||
// Create room name based on board ID and timestamp
|
||||
const roomName = `board_${boardId}_${Date.now()}`;
|
||||
// Sanitize boardId to only use valid Daily.co characters (A-Z, a-z, 0-9, '-', '_')
|
||||
const sanitizedBoardId = boardId.replace(/[^A-Za-z0-9\-_]/g, '_');
|
||||
const roomName = `board_${sanitizedBoardId}_${Date.now()}`;
|
||||
|
||||
console.log('🔧 Room name generation:');
|
||||
console.log('Original boardId:', boardId);
|
||||
console.log('Sanitized boardId:', sanitizedBoardId);
|
||||
console.log('Final roomName:', roomName);
|
||||
|
||||
const response = await fetch(`${workerUrl}/daily/rooms`, {
|
||||
method: 'POST',
|
||||
|
|
@ -135,22 +142,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
enable_chat: true,
|
||||
enable_screenshare: true,
|
||||
start_video_off: true,
|
||||
start_audio_off: true,
|
||||
enable_recording: "cloud",
|
||||
start_cloud_recording: true,
|
||||
start_cloud_recording_opts: {
|
||||
layout: {
|
||||
preset: "active-speaker"
|
||||
},
|
||||
format: "mp4",
|
||||
mode: "audio-only"
|
||||
},
|
||||
// Transcription settings
|
||||
transcription: {
|
||||
enabled: true,
|
||||
auto_start: false
|
||||
},
|
||||
recordings_template: "{room_name}/audio-{epoch_time}.mp4"
|
||||
start_audio_off: true
|
||||
}
|
||||
})
|
||||
});
|
||||
|
|
@ -205,6 +197,12 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||
|
||||
try {
|
||||
// Extract room name from URL (same as transcription methods)
|
||||
const roomName = shape.props.roomUrl.split('/').pop();
|
||||
if (!roomName) {
|
||||
throw new Error('Could not extract room name from URL');
|
||||
}
|
||||
|
||||
const response = await fetch(`${workerUrl}/daily/recordings/start`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -212,7 +210,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
|||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
room_name: shape.id,
|
||||
room_name: roomName,
|
||||
layout: {
|
||||
preset: "active-speaker"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const saveToPdf = async (editor: Editor) => {
|
|||
scale: 2,
|
||||
background: true,
|
||||
padding: 0,
|
||||
preserveAspectRatio: "true",
|
||||
preserveAspectRatio: "xMidYMid meet",
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,5 @@
|
|||
/// <reference types="@cloudflare/workers-types" />
|
||||
|
||||
import { RoomSnapshot, TLSocketRoom } from "@tldraw/sync-core"
|
||||
import {
|
||||
TLRecord,
|
||||
TLShape,
|
||||
createTLSchema,
|
||||
defaultBindingSchemas,
|
||||
defaultShapeSchemas,
|
||||
shapeIdValidator,
|
||||
} from "@tldraw/tlschema"
|
||||
import { AutoRouter, IRequest, error } from "itty-router"
|
||||
import throttle from "lodash.throttle"
|
||||
import { Environment } from "./types"
|
||||
|
|
@ -21,45 +12,61 @@ import { SlideShape } from "@/shapes/SlideShapeUtil"
|
|||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||
import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil"
|
||||
|
||||
// add custom shapes and bindings here if needed:
|
||||
export const customSchema = createTLSchema({
|
||||
shapes: {
|
||||
...defaultShapeSchemas,
|
||||
ChatBox: {
|
||||
props: ChatBoxShape.props,
|
||||
migrations: ChatBoxShape.migrations,
|
||||
},
|
||||
VideoChat: {
|
||||
props: VideoChatShape.props,
|
||||
migrations: VideoChatShape.migrations,
|
||||
},
|
||||
Embed: {
|
||||
props: EmbedShape.props,
|
||||
migrations: EmbedShape.migrations,
|
||||
},
|
||||
Markdown: {
|
||||
props: MarkdownShape.props,
|
||||
migrations: MarkdownShape.migrations,
|
||||
},
|
||||
MycrozineTemplate: {
|
||||
props: MycrozineTemplateShape.props,
|
||||
migrations: MycrozineTemplateShape.migrations,
|
||||
},
|
||||
Slide: {
|
||||
props: SlideShape.props,
|
||||
migrations: SlideShape.migrations,
|
||||
},
|
||||
Prompt: {
|
||||
props: PromptShape.props,
|
||||
migrations: PromptShape.migrations,
|
||||
},
|
||||
SharedPiano: {
|
||||
props: SharedPianoShape.props,
|
||||
migrations: SharedPianoShape.migrations,
|
||||
},
|
||||
},
|
||||
bindings: defaultBindingSchemas,
|
||||
})
|
||||
// Lazy load TLDraw dependencies to avoid startup timeouts
|
||||
let customSchema: any = null
|
||||
let TLSocketRoom: any = null
|
||||
|
||||
async function getTldrawDependencies() {
|
||||
if (!customSchema) {
|
||||
const { createTLSchema, defaultBindingSchemas, defaultShapeSchemas } = await import("@tldraw/tlschema")
|
||||
|
||||
customSchema = createTLSchema({
|
||||
shapes: {
|
||||
...defaultShapeSchemas,
|
||||
ChatBox: {
|
||||
props: ChatBoxShape.props,
|
||||
migrations: ChatBoxShape.migrations,
|
||||
},
|
||||
VideoChat: {
|
||||
props: VideoChatShape.props,
|
||||
migrations: VideoChatShape.migrations,
|
||||
},
|
||||
Embed: {
|
||||
props: EmbedShape.props,
|
||||
migrations: EmbedShape.migrations,
|
||||
},
|
||||
Markdown: {
|
||||
props: MarkdownShape.props,
|
||||
migrations: MarkdownShape.migrations,
|
||||
},
|
||||
MycrozineTemplate: {
|
||||
props: MycrozineTemplateShape.props,
|
||||
migrations: MycrozineTemplateShape.migrations,
|
||||
},
|
||||
Slide: {
|
||||
props: SlideShape.props,
|
||||
migrations: SlideShape.migrations,
|
||||
},
|
||||
Prompt: {
|
||||
props: PromptShape.props,
|
||||
migrations: PromptShape.migrations,
|
||||
},
|
||||
SharedPiano: {
|
||||
props: SharedPianoShape.props,
|
||||
migrations: SharedPianoShape.migrations,
|
||||
},
|
||||
},
|
||||
bindings: defaultBindingSchemas,
|
||||
})
|
||||
}
|
||||
|
||||
if (!TLSocketRoom) {
|
||||
const syncCore = await import("@tldraw/sync-core")
|
||||
TLSocketRoom = syncCore.TLSocketRoom
|
||||
}
|
||||
|
||||
return { customSchema, TLSocketRoom }
|
||||
}
|
||||
|
||||
// each whiteboard room is hosted in a DurableObject:
|
||||
// https://developers.cloudflare.com/durable-objects/
|
||||
|
|
@ -72,7 +79,7 @@ export class TldrawDurableObject {
|
|||
private roomId: string | null = null
|
||||
// when we load the room from the R2 bucket, we keep it here. it's a promise so we only ever
|
||||
// load it once.
|
||||
private roomPromise: Promise<TLSocketRoom<TLRecord, void>> | null = null
|
||||
private roomPromise: Promise<any> | null = null
|
||||
|
||||
constructor(private readonly ctx: DurableObjectState, env: Environment) {
|
||||
this.r2 = env.TLDRAW_BUCKET
|
||||
|
|
@ -114,7 +121,7 @@ export class TldrawDurableObject {
|
|||
})
|
||||
})
|
||||
.post("/room/:roomId", async (request) => {
|
||||
const records = (await request.json()) as TLRecord[]
|
||||
const records = (await request.json()) as any[]
|
||||
|
||||
return new Response(JSON.stringify(Array.from(records)), {
|
||||
headers: {
|
||||
|
|
@ -206,29 +213,32 @@ export class TldrawDurableObject {
|
|||
}
|
||||
}
|
||||
|
||||
getRoom() {
|
||||
async getRoom() {
|
||||
const roomId = this.roomId
|
||||
if (!roomId) throw new Error("Missing roomId")
|
||||
|
||||
if (!this.roomPromise) {
|
||||
this.roomPromise = (async () => {
|
||||
// Lazy load dependencies
|
||||
const { customSchema, TLSocketRoom } = await getTldrawDependencies()
|
||||
|
||||
// fetch the room from R2
|
||||
const roomFromBucket = await this.r2.get(`rooms/${roomId}`)
|
||||
// if it doesn't exist, we'll just create a new empty room
|
||||
const initialSnapshot = roomFromBucket
|
||||
? ((await roomFromBucket.json()) as RoomSnapshot)
|
||||
? ((await roomFromBucket.json()) as any)
|
||||
: undefined
|
||||
if (initialSnapshot) {
|
||||
initialSnapshot.documents = initialSnapshot.documents.filter(
|
||||
(record) => {
|
||||
const shape = record.state as TLShape
|
||||
(record: any) => {
|
||||
const shape = record.state as any
|
||||
return shape.type !== "ChatBox"
|
||||
},
|
||||
)
|
||||
}
|
||||
// create a new TLSocketRoom. This handles all the sync protocol & websocket connections.
|
||||
// it's up to us to persist the room state to R2 when needed though.
|
||||
return new TLSocketRoom<TLRecord, void>({
|
||||
return new TLSocketRoom({
|
||||
schema: customSchema,
|
||||
initialSnapshot,
|
||||
onDataChange: () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { handleUnfurlRequest } from "cloudflare-workers-unfurl"
|
||||
import { AutoRouter, cors, error, IRequest } from "itty-router"
|
||||
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
|
||||
import { Environment } from "./types"
|
||||
|
|
@ -6,6 +5,17 @@ import { Environment } from "./types"
|
|||
// make sure our sync durable object is made available to cloudflare
|
||||
export { TldrawDurableObject } from "./TldrawDurableObject"
|
||||
|
||||
// Lazy load heavy dependencies to avoid startup timeouts
|
||||
let handleUnfurlRequest: any = null
|
||||
|
||||
async function getUnfurlHandler() {
|
||||
if (!handleUnfurlRequest) {
|
||||
const unfurl = await import("cloudflare-workers-unfurl")
|
||||
handleUnfurlRequest = unfurl.handleUnfurlRequest
|
||||
}
|
||||
return handleUnfurlRequest
|
||||
}
|
||||
|
||||
// Define security headers
|
||||
const securityHeaders = {
|
||||
"Content-Security-Policy":
|
||||
|
|
@ -27,6 +37,8 @@ const { preflight, corsify } = cors({
|
|||
"https://www.jeffemmett.com",
|
||||
"https://jeffemmett-canvas.jeffemmett.workers.dev",
|
||||
"https://jeffemmett.com/board/*",
|
||||
"http://localhost:5173",
|
||||
"http://127.0.0.1:5173",
|
||||
]
|
||||
|
||||
// Always allow if no origin (like from a local file)
|
||||
|
|
@ -94,6 +106,19 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
|||
})
|
||||
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
||||
.get("/connect/:roomId", (request, env) => {
|
||||
// Check if this is a WebSocket upgrade request
|
||||
const upgradeHeader = request.headers.get("Upgrade")
|
||||
if (upgradeHeader === "websocket") {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// Handle regular GET requests
|
||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||
return room.fetch(request.url, {
|
||||
|
|
@ -110,7 +135,10 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
|||
.get("/uploads/:uploadId", handleAssetDownload)
|
||||
|
||||
// bookmarks need to extract metadata from pasted URLs:
|
||||
.get("/unfurl", handleUnfurlRequest)
|
||||
.get("/unfurl", async (request, env) => {
|
||||
const handler = await getUnfurlHandler()
|
||||
return handler(request, env)
|
||||
})
|
||||
|
||||
.get("/room/:roomId", (request, env) => {
|
||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||
|
|
@ -142,14 +170,26 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
|||
}
|
||||
|
||||
try {
|
||||
// Get the request body from the client
|
||||
const body = await req.json()
|
||||
|
||||
const response = await fetch('https://api.daily.co/v1/rooms', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
return new Response(JSON.stringify(error), {
|
||||
status: response.status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return new Response(JSON.stringify(data), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
|
|
|
|||
Loading…
Reference in New Issue