Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-19 20:57:54 +00:00
parent b52bf3605a
commit 86f5cced03
+149
View File
@@ -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 = { ...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}`;
}