Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -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}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user