From 713966ae75c6f17aae8268306997feb135f07922 Mon Sep 17 00:00:00 2001 From: Hoang Pham Date: Fri, 29 Nov 2024 18:47:34 +0700 Subject: [PATCH] Canvas, UI stuffs, ... Signed-off-by: Hoang Pham --- src/App.tsx | 27 ++------- src/components/RecordingControls.tsx | 86 ++++++++++++++-------------- src/hooks/useWhiteboardRecording.ts | 61 ++++++++++++++------ 3 files changed, 95 insertions(+), 79 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1370f32..a53c111 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -248,36 +248,21 @@ export default function App({ downloadRecording, } = useWhiteboardRecording() - const handleStartRecording = useCallback(async () => { + const handleStartRecording = useCallback(() => { if (!excalidrawAPIRef.current) { showError(t('whiteboard', 'Could not access whiteboard')) return } - // Wait for a short moment to ensure canvas is ready - await new Promise(resolve => setTimeout(resolve, 100)) + const staticCanvas = document.querySelector('.excalidraw__canvas.static') as HTMLCanvasElement + const interactiveCanvas = document.querySelector('.excalidraw__canvas.interactive') as HTMLCanvasElement - // Get all canvases and find the main one - const allCanvases = document.querySelectorAll('.excalidraw canvas') - console.log('Found canvases:', allCanvases) - - // Convert NodeList to array for easier filtering - const canvasArray = Array.from(allCanvases) - - // Find the main canvas - it should be visible and have dimensions - const mainCanvas = canvasArray.find(canvas => { - const rect = canvas.getBoundingClientRect() - const style = window.getComputedStyle(canvas) - return rect.width > 0 && rect.height > 0 && style.display !== 'none' && !canvas.classList.contains('reset-zoom') - }) as HTMLCanvasElement - - if (!mainCanvas) { - showError(t('whiteboard', 'Could not find main canvas element')) + if (!staticCanvas || !interactiveCanvas) { + showError(t('whiteboard', 'Could not find canvases to record')) return } - console.log('Using canvas for recording:', mainCanvas) - startRecording(mainCanvas) + startRecording({ staticCanvas, interactiveCanvas }) }, [startRecording]) return ( diff --git a/src/components/RecordingControls.tsx b/src/components/RecordingControls.tsx index d03b1e5..39d22ed 100644 --- a/src/components/RecordingControls.tsx +++ b/src/components/RecordingControls.tsx @@ -4,8 +4,8 @@ */ import { useCallback } from 'react' +import type { RecordingState } from '../hooks/useWhiteboardRecording' import { translate as t } from '@nextcloud/l10n' -import { RecordingState } from '../hooks/useWhiteboardRecording' interface RecordingControlsProps { recordingState: RecordingState @@ -15,47 +15,49 @@ interface RecordingControlsProps { } export function RecordingControls({ - recordingState, - onStartRecording, - onStopRecording, - onDownloadRecording, + recordingState, + onStartRecording, + onStopRecording, + onDownloadRecording, }: RecordingControlsProps) { - const formatDuration = useCallback((seconds: number) => { - const minutes = Math.floor(seconds / 60) - const remainingSeconds = seconds % 60 - return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}` - }, []) + const formatDuration = useCallback((seconds: number) => { + const minutes = Math.floor(seconds / 60) + const remainingSeconds = seconds % 60 + return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}` + }, []) - return ( -
- {!recordingState.isRecording ? ( - - ) : ( - <> - - - )} - {!recordingState.isRecording && recordingState.frames.length > 0 && ( - - )} -
- ) + return ( +
+ {!recordingState.isRecording + ? ( + + ) + : ( + <> + + + )} + {!recordingState.isRecording && recordingState.frames.length > 0 && ( + + )} +
+ ) } diff --git a/src/hooks/useWhiteboardRecording.ts b/src/hooks/useWhiteboardRecording.ts index 210dd6f..26ac7c7 100644 --- a/src/hooks/useWhiteboardRecording.ts +++ b/src/hooks/useWhiteboardRecording.ts @@ -7,12 +7,21 @@ import { useState, useRef, useCallback } from 'react' import { showError, showSuccess } from '@nextcloud/dialogs' import { translate as t } from '@nextcloud/l10n' +interface ExtendedMediaRecorder extends MediaRecorder { + animationFrameId?: number +} + export interface RecordingState { isRecording: boolean duration: number frames: Blob[] } +interface CanvasParams { + staticCanvas: HTMLCanvasElement + interactiveCanvas: HTMLCanvasElement +} + export function useWhiteboardRecording() { const [recordingState, setRecordingState] = useState({ isRecording: false, @@ -20,23 +29,45 @@ export function useWhiteboardRecording() { frames: [], }) - const mediaRecorderRef = useRef(null) + const mediaRecorderRef = useRef(null) const streamRef = useRef(null) const durationIntervalRef = useRef(null) + const animationFrameIdRef = useRef(null) - const startRecording = useCallback((canvas: HTMLCanvasElement) => { + const startRecording = useCallback(({ staticCanvas, interactiveCanvas }: CanvasParams) => { try { - // Get canvas stream at 60 FPS for smoother recording - const stream = canvas.captureStream(60) + const combinedCanvas = document.createElement('canvas') + const ctx = combinedCanvas.getContext('2d') + if (!ctx) { + throw new Error('Failed to get canvas context') + } + + combinedCanvas.width = staticCanvas.width + combinedCanvas.height = staticCanvas.height + + const drawFrame = () => { + + ctx.clearRect(0, 0, combinedCanvas.width, combinedCanvas.height) + + ctx.drawImage(staticCanvas, 0, 0) + + ctx.drawImage(interactiveCanvas, 0, 0) + } + + const stream = combinedCanvas.captureStream(60) streamRef.current = stream - // Use WebM with VP8 for recording (better browser support) + const updateCanvas = () => { + drawFrame() + animationFrameIdRef.current = requestAnimationFrame(updateCanvas) + } + updateCanvas() + const mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp8', - videoBitsPerSecond: 8000000, // 8 Mbps for better quality - }) + videoBitsPerSecond: 8000000, + }) as ExtendedMediaRecorder - // Handle data available event mediaRecorder.ondataavailable = (event) => { setRecordingState((prev) => ({ ...prev, @@ -44,11 +75,9 @@ export function useWhiteboardRecording() { })) } - // Start recording with smaller chunks for better handling - mediaRecorder.start(500) // Capture chunks every 500ms + mediaRecorder.start(500) mediaRecorderRef.current = mediaRecorder - // Start duration counter durationIntervalRef.current = window.setInterval(() => { setRecordingState((prev) => ({ ...prev, @@ -76,13 +105,15 @@ export function useWhiteboardRecording() { return } - // Stop media recorder + if (animationFrameIdRef.current !== null) { + cancelAnimationFrame(animationFrameIdRef.current) + animationFrameIdRef.current = null + } + mediaRecorderRef.current.stop() - // Stop all tracks streamRef.current.getTracks().forEach((track) => track.stop()) - // Clear duration interval if (durationIntervalRef.current) { clearInterval(durationIntervalRef.current) } @@ -101,11 +132,9 @@ export function useWhiteboardRecording() { } try { - // Create WebM blob const blob = new Blob(recordingState.frames, { type: 'video/webm' }) const url = URL.createObjectURL(blob) - // Create download link const a = document.createElement('a') a.href = url a.download = `whiteboard-recording-${Date.now()}.webm`