video fix
This commit is contained in:
parent
4380a7bdd6
commit
f22f5b1a6c
|
|
@ -5,6 +5,10 @@ interface DailyApiResponse {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DailyRecordingResponse {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type IVideoChatShape = TLBaseShape<
|
export type IVideoChatShape = TLBaseShape<
|
||||||
"VideoChat",
|
"VideoChat",
|
||||||
{
|
{
|
||||||
|
|
@ -13,6 +17,8 @@ export type IVideoChatShape = TLBaseShape<
|
||||||
roomUrl: string | null
|
roomUrl: string | null
|
||||||
allowCamera: boolean
|
allowCamera: boolean
|
||||||
allowMicrophone: boolean
|
allowMicrophone: boolean
|
||||||
|
enableRecording: boolean
|
||||||
|
recordingId: string | null // Track active recording
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|
@ -26,69 +32,185 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
getDefaultProps(): IVideoChatShape["props"] {
|
getDefaultProps(): IVideoChatShape["props"] {
|
||||||
return {
|
return {
|
||||||
roomUrl: null,
|
roomUrl: null,
|
||||||
w: 640,
|
w: 800,
|
||||||
h: 480,
|
h: 600,
|
||||||
allowCamera: false,
|
allowCamera: false,
|
||||||
allowMicrophone: false,
|
allowMicrophone: false,
|
||||||
|
enableRecording: true,
|
||||||
|
recordingId: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureRoomExists(shape: IVideoChatShape) {
|
async ensureRoomExists(shape: IVideoChatShape) {
|
||||||
if (shape.props.roomUrl !== null) {
|
const boardId = this.editor.getCurrentPageId();
|
||||||
console.log("Room already exists:", shape.props.roomUrl)
|
if (!boardId) {
|
||||||
return
|
throw new Error('Board ID is undefined');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get existing room URL from localStorage first
|
||||||
|
const storageKey = `videoChat_room_${boardId}`;
|
||||||
|
const existingRoomUrl = localStorage.getItem(storageKey);
|
||||||
|
|
||||||
|
if (existingRoomUrl && existingRoomUrl !== 'undefined') {
|
||||||
|
console.log("Using existing room from storage:", existingRoomUrl);
|
||||||
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
roomUrl: existingRoomUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape.props.roomUrl !== null && shape.props.roomUrl !== 'undefined') {
|
||||||
|
console.log("Room already exists:", shape.props.roomUrl);
|
||||||
|
localStorage.setItem(storageKey, shape.props.roomUrl);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL
|
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
||||||
const apiKey = import.meta.env.VITE_DAILY_API_KEY
|
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error('Daily.co API key not configured')
|
throw new Error('Daily.co API key not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${workerUrl}/daily/rooms`, {
|
if (!workerUrl) {
|
||||||
|
throw new Error('Worker URL is not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create room name based on board ID and timestamp
|
||||||
|
const roomName = `board_${boardId}_${Date.now()}`;
|
||||||
|
|
||||||
|
const response = await fetch(`${workerUrl}/daily/rooms`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: roomName,
|
||||||
|
properties: {
|
||||||
|
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"
|
||||||
|
},
|
||||||
|
auto_start_transcription: true,
|
||||||
|
recordings_template: "{room_name}/audio-{epoch_time}.mp4"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json()
|
||||||
|
throw new Error(`Failed to create room (${response.status}): ${JSON.stringify(error)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as DailyApiResponse;
|
||||||
|
const url = data.url;
|
||||||
|
|
||||||
|
if (!url) throw new Error("Room URL is missing")
|
||||||
|
|
||||||
|
// Store the room URL in localStorage
|
||||||
|
localStorage.setItem(storageKey, url);
|
||||||
|
|
||||||
|
console.log("Room created successfully:", url)
|
||||||
|
console.log("Updating shape with new URL")
|
||||||
|
|
||||||
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
roomUrl: url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log("Shape updated:", this.editor.getShape(shape.id))
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in ensureRoomExists:", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startRecording(shape: IVideoChatShape) {
|
||||||
|
if (!shape.props.roomUrl) return;
|
||||||
|
|
||||||
|
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
||||||
|
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${workerUrl}/daily/recordings/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
'Authorization': `Bearer ${apiKey}`
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
properties: {
|
room_name: shape.id,
|
||||||
enable_chat: true,
|
layout: {
|
||||||
enable_screenshare: true,
|
preset: "active-speaker"
|
||||||
start_video_off: true,
|
|
||||||
start_audio_off: true
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error('Failed to start recording');
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(`Failed to create room (${response.status}): ${JSON.stringify(error)}`)
|
const data = await response.json() as DailyRecordingResponse;
|
||||||
}
|
|
||||||
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: shape.type,
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
recordingId: data.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const data = (await response.json()) as DailyApiResponse;
|
} catch (error) {
|
||||||
const url = data.url;
|
console.error('Error starting recording:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!url) throw new Error("Room URL is missing")
|
async stopRecording(shape: IVideoChatShape) {
|
||||||
|
if (!shape.props.recordingId) return;
|
||||||
|
|
||||||
console.log("Room created successfully:", url)
|
const workerUrl = import.meta.env.VITE_TLDRAW_WORKER_URL;
|
||||||
console.log("Updating shape with new URL")
|
const apiKey = import.meta.env.VITE_DAILY_API_KEY;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`${workerUrl}/daily/recordings/${shape.props.recordingId}/stop`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await this.editor.updateShape<IVideoChatShape>({
|
await this.editor.updateShape<IVideoChatShape>({
|
||||||
id: shape.id,
|
id: shape.id,
|
||||||
type: shape.type,
|
type: shape.type,
|
||||||
props: {
|
props: {
|
||||||
...shape.props,
|
...shape.props,
|
||||||
roomUrl: url,
|
recordingId: null
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
console.log("Shape updated:", this.editor.getShape(shape.id))
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in ensureRoomExists:", error)
|
console.error('Error stopping recording:', error);
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,16 +218,43 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
const [hasPermissions, setHasPermissions] = useState(false)
|
const [hasPermissions, setHasPermissions] = useState(false)
|
||||||
const [error, setError] = useState<Error | null>(null)
|
const [error, setError] = useState<Error | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [roomUrl, setRoomUrl] = useState<string | null>(shape.props.roomUrl)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(true)
|
let mounted = true;
|
||||||
this.ensureRoomExists(shape)
|
|
||||||
.catch(console.error)
|
const createRoom = async () => {
|
||||||
.finally(() => setIsLoading(false))
|
try {
|
||||||
}, [])
|
setIsLoading(true);
|
||||||
|
await this.ensureRoomExists(shape);
|
||||||
|
|
||||||
|
// Get the updated shape after room creation
|
||||||
|
const updatedShape = this.editor.getShape(shape.id);
|
||||||
|
if (mounted && updatedShape) {
|
||||||
|
setRoomUrl((updatedShape as IVideoChatShape).props.roomUrl);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (mounted) {
|
||||||
|
console.error("Error creating room:", err);
|
||||||
|
setError(err as Error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createRoom();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, [shape.id]); // Only re-run if shape.id changes
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Request permissions when needed
|
let mounted = true;
|
||||||
|
|
||||||
const requestPermissions = async () => {
|
const requestPermissions = async () => {
|
||||||
try {
|
try {
|
||||||
if (shape.props.allowCamera || shape.props.allowMicrophone) {
|
if (shape.props.allowCamera || shape.props.allowMicrophone) {
|
||||||
|
|
@ -114,41 +263,49 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
audio: shape.props.allowMicrophone,
|
audio: shape.props.allowMicrophone,
|
||||||
}
|
}
|
||||||
await navigator.mediaDevices.getUserMedia(constraints)
|
await navigator.mediaDevices.getUserMedia(constraints)
|
||||||
setHasPermissions(true)
|
if (mounted) {
|
||||||
|
setHasPermissions(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Permission request failed:", err)
|
||||||
|
if (mounted) {
|
||||||
|
setHasPermissions(false)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error("Permission request failed:", error)
|
|
||||||
setHasPermissions(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPermissions()
|
requestPermissions()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
}
|
||||||
}, [shape.props.allowCamera, shape.props.allowMicrophone])
|
}, [shape.props.allowCamera, shape.props.allowMicrophone])
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div>Error creating room: {error.message}</div>
|
return <div>Error creating room: {error.message}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shape.props.roomUrl) {
|
if (isLoading || !roomUrl || roomUrl === 'undefined') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: shape.props.w,
|
width: shape.props.w,
|
||||||
height: shape.props.h,
|
height: shape.props.h,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
backgroundColor: "#f0f0f0",
|
backgroundColor: "#f0f0f0",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading ? "Creating room... Please wait" : "Creating room..."}
|
{isLoading ? "Creating room... Please wait" : "Error: No room URL available"}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct URL with permission parameters
|
// Construct URL with permission parameters
|
||||||
const roomUrlWithParams = new URL(shape.props.roomUrl)
|
const roomUrlWithParams = new URL(roomUrl)
|
||||||
roomUrlWithParams.searchParams.set(
|
roomUrlWithParams.searchParams.set(
|
||||||
"allow_camera",
|
"allow_camera",
|
||||||
String(shape.props.allowCamera),
|
String(shape.props.allowCamera),
|
||||||
|
|
@ -158,7 +315,7 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
String(shape.props.allowMicrophone),
|
String(shape.props.allowMicrophone),
|
||||||
)
|
)
|
||||||
|
|
||||||
console.log(shape.props.roomUrl)
|
console.log(roomUrl)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -167,17 +324,55 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
height: `${shape.props.h}px`,
|
height: `${shape.props.h}px`,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
pointerEvents: "all",
|
pointerEvents: "all",
|
||||||
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
src={roomUrlWithParams.toString()}
|
src={roomUrlWithParams.toString()}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
style={{ border: "none" }}
|
style={{
|
||||||
|
border: "none",
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
}}
|
||||||
allow={`camera ${shape.props.allowCamera ? "self" : ""}; microphone ${
|
allow={`camera ${shape.props.allowCamera ? "self" : ""}; microphone ${
|
||||||
shape.props.allowMicrophone ? "self" : ""
|
shape.props.allowMicrophone ? "self" : ""
|
||||||
}`}
|
}`}
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|
||||||
|
{shape.props.enableRecording && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (shape.props.recordingId) {
|
||||||
|
await this.stopRecording(shape);
|
||||||
|
} else {
|
||||||
|
await this.startRecording(shape);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Recording error:', err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "8px",
|
||||||
|
right: "8px",
|
||||||
|
padding: "4px 8px",
|
||||||
|
background: shape.props.recordingId ? "#ff4444" : "#ffffff",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{shape.props.recordingId ? "Stop Recording" : "Start Recording"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
|
@ -191,9 +386,10 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
pointerEvents: "all",
|
pointerEvents: "all",
|
||||||
cursor: "text",
|
cursor: "text",
|
||||||
userSelect: "text",
|
userSelect: "text",
|
||||||
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
url: {shape.props.roomUrl}
|
url: {roomUrl}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue