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