videochat working
This commit is contained in:
parent
baf1efce43
commit
2d763c669a
|
|
@ -15,5 +15,5 @@ R2_BUCKET_NAME='your_bucket_name'
|
||||||
R2_PREVIEW_BUCKET_NAME='your_preview_bucket_name'
|
R2_PREVIEW_BUCKET_NAME='your_preview_bucket_name'
|
||||||
|
|
||||||
# Daily.co Configuration
|
# Daily.co Configuration
|
||||||
DAILY_API_KEY='your_daily_api_key'
|
VITE_DAILY_API_KEY=your_daily_api_key_here
|
||||||
DAILY_DOMAIN='your_daily_domain'
|
VITE_DAILY_DOMAIN='your_daily_domain'
|
||||||
|
|
@ -175,7 +175,5 @@ dist
|
||||||
.env.*.local
|
.env.*.local
|
||||||
.dev.vars
|
.dev.vars
|
||||||
|
|
||||||
# Keep example file
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
22
src/App.tsx
22
src/App.tsx
|
|
@ -19,6 +19,8 @@ import { MarkdownShape } from "./shapes/MarkdownShapeUtil"
|
||||||
import { MarkdownTool } from "./tools/MarkdownTool"
|
import { MarkdownTool } from "./tools/MarkdownTool"
|
||||||
import { createRoot } from "react-dom/client"
|
import { createRoot } from "react-dom/client"
|
||||||
import { handleInitialPageLoad } from "./utils/handleInitialPageLoad"
|
import { handleInitialPageLoad } from "./utils/handleInitialPageLoad"
|
||||||
|
import { DailyProvider } from "@daily-co/daily-react"
|
||||||
|
import Daily from "@daily-co/daily-js"
|
||||||
|
|
||||||
inject()
|
inject()
|
||||||
|
|
||||||
|
|
@ -30,6 +32,8 @@ const customShapeUtils = [
|
||||||
]
|
]
|
||||||
const customTools = [ChatBoxTool, VideoChatTool, EmbedTool, MarkdownTool]
|
const customTools = [ChatBoxTool, VideoChatTool, EmbedTool, MarkdownTool]
|
||||||
|
|
||||||
|
const callObject = Daily.createCallObject()
|
||||||
|
|
||||||
export default function InteractiveShapeExample() {
|
export default function InteractiveShapeExample() {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
|
|
@ -64,14 +68,16 @@ function App() {
|
||||||
console.log("App initialized, NODE_ENV:", process.env.NODE_ENV)
|
console.log("App initialized, NODE_ENV:", process.env.NODE_ENV)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<DailyProvider callObject={callObject}>
|
||||||
<Routes>
|
<BrowserRouter>
|
||||||
<Route path="/" element={<Default />} />
|
<Routes>
|
||||||
<Route path="/contact" element={<Contact />} />
|
<Route path="/" element={<Default />} />
|
||||||
<Route path="/board/:slug" element={<Board />} />
|
<Route path="/contact" element={<Contact />} />
|
||||||
<Route path="/inbox" element={<Inbox />} />
|
<Route path="/board/:slug" element={<Board />} />
|
||||||
</Routes>
|
<Route path="/inbox" element={<Inbox />} />
|
||||||
</BrowserRouter>
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</DailyProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
||||||
import { useEffect, useState, useRef } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { WORKER_URL } from "../routes/Board"
|
|
||||||
import DailyIframe from "@daily-co/daily-js"
|
|
||||||
|
|
||||||
interface Window {
|
|
||||||
DailyIframe: {
|
|
||||||
setLogLevel(level: "error" | "warn" | "info" | "debug"): void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IVideoChatShape = TLBaseShape<
|
export type IVideoChatShape = TLBaseShape<
|
||||||
"VideoChat",
|
"VideoChat",
|
||||||
|
|
@ -15,63 +7,11 @@ export type IVideoChatShape = TLBaseShape<
|
||||||
w: number
|
w: number
|
||||||
h: number
|
h: number
|
||||||
roomUrl: string | null
|
roomUrl: string | null
|
||||||
userName: string
|
allowCamera: boolean
|
||||||
|
allowMicrophone: boolean
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
interface DailyApiError {
|
|
||||||
error: string
|
|
||||||
info?: string
|
|
||||||
message?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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> {
|
export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
static override type = "VideoChat"
|
static override type = "VideoChat"
|
||||||
|
|
||||||
|
|
@ -84,86 +24,45 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
roomUrl: null,
|
roomUrl: null,
|
||||||
w: 640,
|
w: 640,
|
||||||
h: 480,
|
h: 480,
|
||||||
userName: "",
|
allowCamera: false,
|
||||||
|
allowMicrophone: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureRoomExists(shape: IVideoChatShape) {
|
async ensureRoomExists(shape: IVideoChatShape) {
|
||||||
console.log("Environment variables:", {
|
if (shape.props.roomUrl !== null) return
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
|
||||||
allEnvVars: import.meta.env,
|
|
||||||
dailyApiKey: import.meta.env.VITE_DAILY_API_KEY,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (shape.props.roomUrl !== null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure API key exists and is properly formatted
|
const apiKey = import.meta.env["VITE_DAILY_API_KEY"]
|
||||||
const apiKey = import.meta.env.VITE_DAILY_API_KEY?.trim()
|
if (!apiKey) throw new Error("Daily API key is missing")
|
||||||
console.log("API Key exists:", !!apiKey)
|
|
||||||
console.log(
|
|
||||||
"API Key format:",
|
|
||||||
apiKey?.substring(0, 4) === "key_" ? "correct" : "incorrect",
|
|
||||||
)
|
|
||||||
console.log("Available env vars:", import.meta.env)
|
|
||||||
|
|
||||||
if (!apiKey) {
|
const response = await fetch("https://api.daily.co/v1/rooms", {
|
||||||
throw new Error("Daily API key is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create room using Daily.co API directly
|
|
||||||
const response = await fetch(`https://api.daily.co/v1/rooms`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
// Ensure no extra spaces in the Authorization header
|
Authorization: `Bearer ${apiKey.trim()}`,
|
||||||
Authorization: `Bearer ${apiKey}`.trim(),
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
properties: {
|
properties: {
|
||||||
enable_chat: true,
|
enable_chat: true,
|
||||||
start_audio_off: true,
|
start_audio_off: true,
|
||||||
start_video_off: true,
|
start_video_off: true,
|
||||||
enable_screenshare: true,
|
|
||||||
enable_recording: true,
|
|
||||||
max_participants: 8,
|
|
||||||
enable_network_ui: true,
|
|
||||||
enable_prejoin_ui: true,
|
|
||||||
enable_people_ui: true,
|
|
||||||
enable_pip_ui: true,
|
|
||||||
enable_emoji_reactions: true,
|
|
||||||
enable_hand_raising: true,
|
|
||||||
enable_noise_cancellation_ui: true,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok)
|
||||||
const errorData = (await response.json()) as DailyApiError
|
throw new Error(`Failed to create room (${response.status})`)
|
||||||
console.error("Daily API Error:", {
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
errorData,
|
|
||||||
authHeader: `Bearer ${apiKey.substring(0, 5)}...`, // Log first 5 chars for debugging
|
|
||||||
})
|
|
||||||
throw new Error(
|
|
||||||
errorData.message ||
|
|
||||||
errorData.info ||
|
|
||||||
`Failed to create room (${response.status})`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url } = (await response.json()) as { url: string }
|
const responseData = (await response.json()) as { url: string }
|
||||||
|
const url = responseData.url
|
||||||
|
|
||||||
|
if (!url) throw new Error("Room URL is missing")
|
||||||
|
|
||||||
this.editor.updateShape<IVideoChatShape>({
|
this.editor.updateShape<IVideoChatShape>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: "VideoChat",
|
type: "VideoChat",
|
||||||
props: {
|
props: { ...shape.props, roomUrl: url },
|
||||||
...shape.props,
|
|
||||||
roomUrl: url,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create Daily room:", error)
|
console.error("Failed to create Daily room:", error)
|
||||||
|
|
@ -172,48 +71,39 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: IVideoChatShape) {
|
component(shape: IVideoChatShape) {
|
||||||
const [isInRoom, setIsInRoom] = useState(false)
|
const [hasPermissions, setHasPermissions] = useState(false)
|
||||||
const [error, setError] = useState("")
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true)
|
this.ensureRoomExists(shape).catch(console.error)
|
||||||
this.ensureRoomExists(shape)
|
|
||||||
.catch((err) => setError(err.message))
|
|
||||||
.finally(() => setIsLoading(false))
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// // Disable Daily.co debug logs
|
// Request permissions when needed
|
||||||
// if (window.DailyIframe) {
|
const requestPermissions = async () => {
|
||||||
// window.DailyIframe.setLogLevel("error")
|
try {
|
||||||
// }
|
if (shape.props.allowCamera || shape.props.allowMicrophone) {
|
||||||
// }, [])
|
const constraints = {
|
||||||
|
video: shape.props.allowCamera,
|
||||||
|
audio: shape.props.allowMicrophone,
|
||||||
|
}
|
||||||
|
await navigator.mediaDevices.getUserMedia(constraints)
|
||||||
|
setHasPermissions(true)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Permission request failed:", error)
|
||||||
|
setHasPermissions(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
requestPermissions()
|
||||||
return (
|
}, [shape.props.allowCamera, shape.props.allowMicrophone])
|
||||||
<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) {
|
if (!shape.props.roomUrl) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: `${shape.props.w}px`,
|
width: shape.props.w,
|
||||||
height: `${shape.props.h}px`,
|
height: shape.props.h,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
|
@ -226,49 +116,54 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct URL with permission parameters
|
||||||
|
const roomUrlWithParams = new URL(shape.props.roomUrl)
|
||||||
|
roomUrlWithParams.searchParams.set(
|
||||||
|
"allow_camera",
|
||||||
|
String(shape.props.allowCamera),
|
||||||
|
)
|
||||||
|
roomUrlWithParams.searchParams.set(
|
||||||
|
"allow_mic",
|
||||||
|
String(shape.props.allowMicrophone),
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(shape.props.roomUrl)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: `${shape.props.w}px`,
|
width: `${shape.props.w}px`,
|
||||||
height: `${shape.props.h}px`,
|
height: `${shape.props.h}px`,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
pointerEvents: "all",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isInRoom ? (
|
<iframe
|
||||||
<button
|
src={roomUrlWithParams.toString()}
|
||||||
onClick={() => setIsInRoom(true)}
|
width="100%"
|
||||||
style={{
|
height="100%"
|
||||||
position: "absolute",
|
style={{ border: "none" }}
|
||||||
top: "50%",
|
allow={`camera ${shape.props.allowCamera ? "self" : ""}; microphone ${
|
||||||
left: "50%",
|
shape.props.allowMicrophone ? "self" : ""
|
||||||
transform: "translate(-50%, -50%)",
|
}`}
|
||||||
padding: "12px 24px",
|
></iframe>
|
||||||
backgroundColor: "#2563eb",
|
<p
|
||||||
color: "white",
|
style={{
|
||||||
border: "none",
|
position: "absolute",
|
||||||
borderRadius: "4px",
|
bottom: 0,
|
||||||
cursor: "pointer",
|
left: 0,
|
||||||
}}
|
margin: "8px",
|
||||||
>
|
padding: "4px 8px",
|
||||||
Join Video Chat
|
background: "rgba(255, 255, 255, 0.9)",
|
||||||
</button>
|
borderRadius: "4px",
|
||||||
) : (
|
fontSize: "12px",
|
||||||
<VideoChatComponent roomUrl={shape.props.roomUrl} />
|
pointerEvents: "all",
|
||||||
)}
|
cursor: "text",
|
||||||
{error && (
|
userSelect: "text",
|
||||||
<div
|
}}
|
||||||
style={{
|
>
|
||||||
position: "absolute",
|
url: {shape.props.roomUrl}
|
||||||
bottom: 10,
|
</p>
|
||||||
left: 10,
|
|
||||||
right: 10,
|
|
||||||
color: "red",
|
|
||||||
textAlign: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,5 @@ export default defineConfig({
|
||||||
"import.meta.env.VITE_WORKER_URL": JSON.stringify(
|
"import.meta.env.VITE_WORKER_URL": JSON.stringify(
|
||||||
process.env.VITE_WORKER_URL,
|
process.env.VITE_WORKER_URL,
|
||||||
),
|
),
|
||||||
"import.meta.env.VITE_DAILY_DOMAIN": JSON.stringify(
|
|
||||||
"https://mycopunks.daily.co",
|
|
||||||
),
|
|
||||||
"import.meta.env.VITE_DAILY_API_KEY": JSON.stringify(
|
|
||||||
process.env.VITE_DAILY_API_KEY,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue