From 86f5cced03e7ae2b11dce050ef4c977082afe2d3 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 20:57:54 +0000 Subject: [PATCH] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/hooks/useCanvasRecorder.ts | 149 +++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 src/hooks/useCanvasRecorder.ts diff --git a/src/hooks/useCanvasRecorder.ts b/src/hooks/useCanvasRecorder.ts new file mode 100644 index 0000000..8a76ccc --- /dev/null +++ b/src/hooks/useCanvasRecorder.ts @@ -0,0 +1,149 @@ +import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; +import { toast } from 'sonner'; + +export type RecorderStatus = 'idle' | 'recording' | 'paused'; + +// Module-level singleton so the recording state can be shared between +// the CaptureTab (inside the 3D HUD) and a floating indicator. +type State = { status: RecorderStatus; elapsedMs: number }; +let state: State = { status: 'idle', elapsedMs: 0 }; +const listeners = new Set<() => void>(); +function setState(next: Partial) { + state = { ...state, ...next }; + listeners.forEach((l) => l()); +} +function subscribe(l: () => void) { listeners.add(l); return () => { listeners.delete(l); }; } +function getSnapshot() { return state; } + +let recorder: MediaRecorder | null = null; +let chunks: Blob[] = []; +let startedAt = 0; +let accumulatedMs = 0; +let pausedAt = 0; +let tickTimer: number | null = null; + +function pickMimeType(): string | undefined { + const candidates = [ + 'video/webm;codecs=vp9,opus', + 'video/webm;codecs=vp9', + 'video/webm;codecs=vp8', + 'video/webm', + 'video/mp4', + ]; + for (const t of candidates) { + if (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported?.(t)) return t; + } + return undefined; +} + +function startTick() { + if (tickTimer != null) return; + tickTimer = window.setInterval(() => { + if (state.status === 'recording') { + setState({ elapsedMs: accumulatedMs + (performance.now() - startedAt) }); + } + }, 250); +} +function stopTick() { + if (tickTimer != null) { clearInterval(tickTimer); tickTimer = null; } +} + +function getCanvas(): HTMLCanvasElement | null { + return document.querySelector('canvas'); +} + +export function startRecording() { + if (state.status !== 'idle') return; + const canvas = getCanvas(); + if (!canvas) { toast.error('Canvas não encontrado'); return; } + const stream = (canvas as any).captureStream?.(30) as MediaStream | undefined; + if (!stream) { toast.error('captureStream não suportado neste browser'); return; } + const mimeType = pickMimeType(); + try { + recorder = new MediaRecorder(stream, mimeType ? { mimeType, videoBitsPerSecond: 8_000_000 } : undefined); + } catch (e) { + console.error(e); + toast.error('MediaRecorder falhou ao iniciar'); + return; + } + chunks = []; + recorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) chunks.push(e.data); }; + recorder.onstop = () => { + const type = recorder?.mimeType || 'video/webm'; + const blob = new Blob(chunks, { type }); + const ext = type.includes('mp4') ? 'mp4' : 'webm'; + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `tracksteel-AR-${new Date().toISOString().replace(/[:.]/g, '-')}.${ext}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 5000); + toast.success(`Vídeo salvo (${(blob.size / 1_000_000).toFixed(1)} MB)`); + recorder = null; + chunks = []; + accumulatedMs = 0; + stopTick(); + setState({ status: 'idle', elapsedMs: 0 }); + }; + recorder.start(1000); + startedAt = performance.now(); + accumulatedMs = 0; + setState({ status: 'recording', elapsedMs: 0 }); + startTick(); + toast.success('Gravação iniciada'); +} + +export function pauseRecording() { + if (!recorder || state.status !== 'recording') return; + recorder.pause(); + accumulatedMs += performance.now() - startedAt; + pausedAt = performance.now(); + setState({ status: 'paused', elapsedMs: accumulatedMs }); +} + +export function resumeRecording() { + if (!recorder || state.status !== 'paused') return; + recorder.resume(); + startedAt = performance.now(); + setState({ status: 'recording' }); +} + +export function stopRecording() { + if (!recorder) return; + try { recorder.stop(); } catch (e) { console.error(e); } +} + +export function captureScreenshot(): string | null { + const canvas = getCanvas(); + if (!canvas) { toast.error('Canvas não encontrado'); return null; } + try { + const dataUrl = canvas.toDataURL('image/png'); + // Trigger download + const a = document.createElement('a'); + a.href = dataUrl; + a.download = `tracksteel-AR-${new Date().toISOString().replace(/[:.]/g, '-')}.png`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + toast.success('Foto capturada'); + return dataUrl; + } catch (e) { + console.error(e); + toast.error('Falha ao capturar foto'); + return null; + } +} + +export function useCanvasRecorder() { + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +} + +export function formatElapsed(ms: number): string { + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + const ss = (s % 60).toString().padStart(2, '0'); + const mm = m.toString().padStart(2, '0'); + return `${mm}:${ss}`; +}