Skip to content

Commit

Permalink
Canvas, UI stuffs, ...
Browse files Browse the repository at this point in the history
Signed-off-by: Hoang Pham <[email protected]>
  • Loading branch information
hweihwang committed Nov 29, 2024
1 parent b2518da commit 713966a
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 79 deletions.
27 changes: 6 additions & 21 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
86 changes: 44 additions & 42 deletions src/components/RecordingControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
<div className="whiteboard-recording-controls">
{!recordingState.isRecording ? (
<button
className="recording-button start"
onClick={onStartRecording}
title={t('whiteboard', 'Start recording')}>
<span className="recording-icon"></span>
{t('whiteboard', 'Record')}
</button>
) : (
<>
<button
className="recording-button stop"
onClick={onStopRecording}
title={t('whiteboard', 'Stop recording')}>
<span className="recording-icon"></span>
{formatDuration(recordingState.duration)}
</button>
</>
)}
{!recordingState.isRecording && recordingState.frames.length > 0 && (
<button
className="recording-button download"
onClick={onDownloadRecording}
title={t('whiteboard', 'Download recording')}>
<span className="recording-icon"></span>
{t('whiteboard', 'Download')}
</button>
)}
</div>
)
return (
<div className="whiteboard-recording-controls">
{!recordingState.isRecording
? (
<button
className="recording-button start"
onClick={onStartRecording}
title={t('whiteboard', 'Start recording')}>
<span className="recording-icon"></span>
{t('whiteboard', 'Record')}
</button>
)
: (
<>
<button
className="recording-button stop"
onClick={onStopRecording}
title={t('whiteboard', 'Stop recording')}>
<span className="recording-icon"></span>
{formatDuration(recordingState.duration)}
</button>
</>
)}
{!recordingState.isRecording && recordingState.frames.length > 0 && (
<button
className="recording-button download"
onClick={onDownloadRecording}
title={t('whiteboard', 'Download recording')}>
<span className="recording-icon"></span>
{t('whiteboard', 'Download')}
</button>
)}
</div>
)
}
61 changes: 45 additions & 16 deletions src/hooks/useWhiteboardRecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,77 @@ 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<RecordingState>({
isRecording: false,
duration: 0,
frames: [],
})

const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const mediaRecorderRef = useRef<ExtendedMediaRecorder | null>(null)
const streamRef = useRef<MediaStream | null>(null)
const durationIntervalRef = useRef<number | null>(null)
const animationFrameIdRef = useRef<number | null>(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,
frames: [...prev.frames, event.data],
}))
}

// 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,
Expand Down Expand Up @@ -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)
}
Expand All @@ -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`
Expand Down

0 comments on commit 713966a

Please sign in to comment.