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
|
scale: 0.5, // Reduced scale to make image smaller
|
||||||
background: true,
|
background: true,
|
||||||
padding: 20, // Increased padding to show full canvas
|
padding: 20, // Increased padding to show full canvas
|
||||||
preserveAspectRatio: "true",
|
preserveAspectRatio: "xMidYMid meet",
|
||||||
bounds: bounds, // Export the entire canvas bounds
|
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
|
// 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`, {
|
const response = await fetch(`${workerUrl}/daily/rooms`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -135,22 +142,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
enable_chat: true,
|
enable_chat: true,
|
||||||
enable_screenshare: true,
|
enable_screenshare: true,
|
||||||
start_video_off: true,
|
start_video_off: true,
|
||||||
start_audio_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"
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
@ -205,6 +197,12 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||||
|
|
||||||
try {
|
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`, {
|
const response = await fetch(`${workerUrl}/daily/recordings/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -212,7 +210,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
room_name: shape.id,
|
room_name: roomName,
|
||||||
layout: {
|
layout: {
|
||||||
preset: "active-speaker"
|
preset: "active-speaker"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const saveToPdf = async (editor: Editor) => {
|
||||||
scale: 2,
|
scale: 2,
|
||||||
background: true,
|
background: true,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
preserveAspectRatio: "true",
|
preserveAspectRatio: "xMidYMid meet",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,5 @@
|
||||||
/// <reference types="@cloudflare/workers-types" />
|
/// <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 { AutoRouter, IRequest, error } from "itty-router"
|
||||||
import throttle from "lodash.throttle"
|
import throttle from "lodash.throttle"
|
||||||
import { Environment } from "./types"
|
import { Environment } from "./types"
|
||||||
|
|
@ -21,45 +12,61 @@ import { SlideShape } from "@/shapes/SlideShapeUtil"
|
||||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||||
import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil"
|
import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil"
|
||||||
|
|
||||||
// add custom shapes and bindings here if needed:
|
// Lazy load TLDraw dependencies to avoid startup timeouts
|
||||||
export const customSchema = createTLSchema({
|
let customSchema: any = null
|
||||||
shapes: {
|
let TLSocketRoom: any = null
|
||||||
...defaultShapeSchemas,
|
|
||||||
ChatBox: {
|
async function getTldrawDependencies() {
|
||||||
props: ChatBoxShape.props,
|
if (!customSchema) {
|
||||||
migrations: ChatBoxShape.migrations,
|
const { createTLSchema, defaultBindingSchemas, defaultShapeSchemas } = await import("@tldraw/tlschema")
|
||||||
},
|
|
||||||
VideoChat: {
|
customSchema = createTLSchema({
|
||||||
props: VideoChatShape.props,
|
shapes: {
|
||||||
migrations: VideoChatShape.migrations,
|
...defaultShapeSchemas,
|
||||||
},
|
ChatBox: {
|
||||||
Embed: {
|
props: ChatBoxShape.props,
|
||||||
props: EmbedShape.props,
|
migrations: ChatBoxShape.migrations,
|
||||||
migrations: EmbedShape.migrations,
|
},
|
||||||
},
|
VideoChat: {
|
||||||
Markdown: {
|
props: VideoChatShape.props,
|
||||||
props: MarkdownShape.props,
|
migrations: VideoChatShape.migrations,
|
||||||
migrations: MarkdownShape.migrations,
|
},
|
||||||
},
|
Embed: {
|
||||||
MycrozineTemplate: {
|
props: EmbedShape.props,
|
||||||
props: MycrozineTemplateShape.props,
|
migrations: EmbedShape.migrations,
|
||||||
migrations: MycrozineTemplateShape.migrations,
|
},
|
||||||
},
|
Markdown: {
|
||||||
Slide: {
|
props: MarkdownShape.props,
|
||||||
props: SlideShape.props,
|
migrations: MarkdownShape.migrations,
|
||||||
migrations: SlideShape.migrations,
|
},
|
||||||
},
|
MycrozineTemplate: {
|
||||||
Prompt: {
|
props: MycrozineTemplateShape.props,
|
||||||
props: PromptShape.props,
|
migrations: MycrozineTemplateShape.migrations,
|
||||||
migrations: PromptShape.migrations,
|
},
|
||||||
},
|
Slide: {
|
||||||
SharedPiano: {
|
props: SlideShape.props,
|
||||||
props: SharedPianoShape.props,
|
migrations: SlideShape.migrations,
|
||||||
migrations: SharedPianoShape.migrations,
|
},
|
||||||
},
|
Prompt: {
|
||||||
},
|
props: PromptShape.props,
|
||||||
bindings: defaultBindingSchemas,
|
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:
|
// each whiteboard room is hosted in a DurableObject:
|
||||||
// https://developers.cloudflare.com/durable-objects/
|
// https://developers.cloudflare.com/durable-objects/
|
||||||
|
|
@ -72,7 +79,7 @@ export class TldrawDurableObject {
|
||||||
private roomId: string | null = null
|
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
|
// when we load the room from the R2 bucket, we keep it here. it's a promise so we only ever
|
||||||
// load it once.
|
// load it once.
|
||||||
private roomPromise: Promise<TLSocketRoom<TLRecord, void>> | null = null
|
private roomPromise: Promise<any> | null = null
|
||||||
|
|
||||||
constructor(private readonly ctx: DurableObjectState, env: Environment) {
|
constructor(private readonly ctx: DurableObjectState, env: Environment) {
|
||||||
this.r2 = env.TLDRAW_BUCKET
|
this.r2 = env.TLDRAW_BUCKET
|
||||||
|
|
@ -114,7 +121,7 @@ export class TldrawDurableObject {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.post("/room/:roomId", async (request) => {
|
.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)), {
|
return new Response(JSON.stringify(Array.from(records)), {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -206,29 +213,32 @@ export class TldrawDurableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoom() {
|
async getRoom() {
|
||||||
const roomId = this.roomId
|
const roomId = this.roomId
|
||||||
if (!roomId) throw new Error("Missing roomId")
|
if (!roomId) throw new Error("Missing roomId")
|
||||||
|
|
||||||
if (!this.roomPromise) {
|
if (!this.roomPromise) {
|
||||||
this.roomPromise = (async () => {
|
this.roomPromise = (async () => {
|
||||||
|
// Lazy load dependencies
|
||||||
|
const { customSchema, TLSocketRoom } = await getTldrawDependencies()
|
||||||
|
|
||||||
// fetch the room from R2
|
// fetch the room from R2
|
||||||
const roomFromBucket = await this.r2.get(`rooms/${roomId}`)
|
const roomFromBucket = await this.r2.get(`rooms/${roomId}`)
|
||||||
// if it doesn't exist, we'll just create a new empty room
|
// if it doesn't exist, we'll just create a new empty room
|
||||||
const initialSnapshot = roomFromBucket
|
const initialSnapshot = roomFromBucket
|
||||||
? ((await roomFromBucket.json()) as RoomSnapshot)
|
? ((await roomFromBucket.json()) as any)
|
||||||
: undefined
|
: undefined
|
||||||
if (initialSnapshot) {
|
if (initialSnapshot) {
|
||||||
initialSnapshot.documents = initialSnapshot.documents.filter(
|
initialSnapshot.documents = initialSnapshot.documents.filter(
|
||||||
(record) => {
|
(record: any) => {
|
||||||
const shape = record.state as TLShape
|
const shape = record.state as any
|
||||||
return shape.type !== "ChatBox"
|
return shape.type !== "ChatBox"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// create a new TLSocketRoom. This handles all the sync protocol & websocket connections.
|
// 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.
|
// 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,
|
schema: customSchema,
|
||||||
initialSnapshot,
|
initialSnapshot,
|
||||||
onDataChange: () => {
|
onDataChange: () => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { handleUnfurlRequest } from "cloudflare-workers-unfurl"
|
|
||||||
import { AutoRouter, cors, error, IRequest } from "itty-router"
|
import { AutoRouter, cors, error, IRequest } from "itty-router"
|
||||||
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
|
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
|
||||||
import { Environment } from "./types"
|
import { Environment } from "./types"
|
||||||
|
|
@ -6,6 +5,17 @@ import { Environment } from "./types"
|
||||||
// make sure our sync durable object is made available to cloudflare
|
// make sure our sync durable object is made available to cloudflare
|
||||||
export { TldrawDurableObject } from "./TldrawDurableObject"
|
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
|
// Define security headers
|
||||||
const securityHeaders = {
|
const securityHeaders = {
|
||||||
"Content-Security-Policy":
|
"Content-Security-Policy":
|
||||||
|
|
@ -27,6 +37,8 @@ const { preflight, corsify } = cors({
|
||||||
"https://www.jeffemmett.com",
|
"https://www.jeffemmett.com",
|
||||||
"https://jeffemmett-canvas.jeffemmett.workers.dev",
|
"https://jeffemmett-canvas.jeffemmett.workers.dev",
|
||||||
"https://jeffemmett.com/board/*",
|
"https://jeffemmett.com/board/*",
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://127.0.0.1:5173",
|
||||||
]
|
]
|
||||||
|
|
||||||
// Always allow if no origin (like from a local file)
|
// 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
|
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
||||||
.get("/connect/:roomId", (request, env) => {
|
.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 id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||||
return room.fetch(request.url, {
|
return room.fetch(request.url, {
|
||||||
|
|
@ -110,7 +135,10 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
.get("/uploads/:uploadId", handleAssetDownload)
|
.get("/uploads/:uploadId", handleAssetDownload)
|
||||||
|
|
||||||
// bookmarks need to extract metadata from pasted URLs:
|
// 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) => {
|
.get("/room/:roomId", (request, env) => {
|
||||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
|
|
@ -142,14 +170,26 @@ const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get the request body from the client
|
||||||
|
const body = await req.json()
|
||||||
|
|
||||||
const response = await fetch('https://api.daily.co/v1/rooms', {
|
const response = await fetch('https://api.daily.co/v1/rooms', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${apiKey}`
|
'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()
|
const data = await response.json()
|
||||||
return new Response(JSON.stringify(data), {
|
return new Response(JSON.stringify(data), {
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue