feat: enhance webcam capture and geolocation functionality

- Improved webcam capture process with multiple constraint strategies for better compatibility and error handling.
- Added loading state management for video readiness, enhancing user experience during webcam access.
- Refactored geolocation retrieval to implement multiple strategies, improving accuracy and reliability in obtaining user location.
- Enhanced error handling for both webcam and geolocation features, providing clearer feedback to users.
This commit is contained in:
2025-11-18 16:20:38 -03:00
parent b01d2d6786
commit 67d6b3ec72
2 changed files with 360 additions and 93 deletions

View File

@@ -19,11 +19,56 @@
let capturando = $state(false);
let erro = $state<string | null>(null);
let previewUrl = $state<string | null>(null);
let videoReady = $state(false);
// Efeito para garantir que o vídeo seja exibido quando o stream for atribuído
$effect(() => {
if (stream && videoElement && !videoReady && videoElement.srcObject !== stream) {
// Apenas atribuir se ainda não foi atribuído
videoElement.srcObject = stream;
videoElement.play()
.then(() => {
if (videoElement && videoElement.readyState >= 2) {
videoReady = true;
}
})
.catch((err) => {
console.warn('Erro ao reproduzir vídeo no effect:', err);
});
}
});
onMount(async () => {
// Tentar obter permissão de webcam automaticamente
// Aguardar um pouco para garantir que os elementos estejam no DOM
await new Promise(resolve => setTimeout(resolve, 100));
// Verificar suporte
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
erro = 'Webcam não suportada';
// Tentar método alternativo (navegadores antigos)
const getUserMedia =
navigator.getUserMedia ||
(navigator as any).webkitGetUserMedia ||
(navigator as any).mozGetUserMedia ||
(navigator as any).msGetUserMedia;
if (!getUserMedia) {
erro = 'Webcam não suportada';
if (autoCapture && onError) {
onError();
}
return;
}
}
// Aguardar videoElement estar disponível
let tentativas = 0;
while (!videoElement && tentativas < 10) {
await new Promise(resolve => setTimeout(resolve, 100));
tentativas++;
}
if (!videoElement) {
erro = 'Elemento de vídeo não encontrado';
if (autoCapture && onError) {
onError();
}
@@ -31,33 +76,149 @@
}
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
// Tentar diferentes configurações de webcam
const constraints = [
{
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user'
}
},
{
video: {
width: { ideal: 640 },
height: { ideal: 480 },
facingMode: 'user'
}
},
{
video: {
facingMode: 'user'
}
},
{
video: true
}
});
];
webcamDisponivel = true;
let ultimoErro: Error | null = null;
for (const constraint of constraints) {
try {
console.log('Tentando acessar webcam com constraint:', constraint);
stream = await navigator.mediaDevices.getUserMedia(constraint);
// Verificar se o stream tem tracks de vídeo
if (stream.getVideoTracks().length === 0) {
stream.getTracks().forEach(track => track.stop());
stream = null;
continue;
}
if (videoElement) {
videoElement.srcObject = stream;
await videoElement.play();
console.log('Webcam acessada com sucesso');
webcamDisponivel = true;
// Se for captura automática, aguardar um pouco e capturar
if (autoCapture) {
// Aguardar 1 segundo para o usuário se posicionar
setTimeout(() => {
if (videoElement && canvasElement && !capturando && !previewUrl) {
capturar();
}
}, 1000);
// Atribuir stream ao elemento de vídeo (o $effect também fará isso, mas garantimos aqui)
if (videoElement) {
videoElement.srcObject = stream;
// Aguardar o vídeo estar pronto
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timeout ao carregar vídeo'));
}, 10000);
const onLoadedMetadata = () => {
clearTimeout(timeout);
videoElement?.removeEventListener('loadedmetadata', onLoadedMetadata);
videoElement?.removeEventListener('playing', onPlaying);
videoElement?.removeEventListener('error', onError);
videoReady = true;
resolve();
};
const onPlaying = () => {
clearTimeout(timeout);
videoElement?.removeEventListener('loadedmetadata', onLoadedMetadata);
videoElement?.removeEventListener('playing', onPlaying);
videoElement?.removeEventListener('error', onError);
videoReady = true;
resolve();
};
const onError = () => {
clearTimeout(timeout);
videoElement?.removeEventListener('loadedmetadata', onLoadedMetadata);
videoElement?.removeEventListener('playing', onPlaying);
videoElement?.removeEventListener('error', onError);
reject(new Error('Erro ao carregar vídeo'));
};
videoElement.addEventListener('loadedmetadata', onLoadedMetadata);
videoElement.addEventListener('playing', onPlaying);
videoElement.addEventListener('error', onError);
// Tentar reproduzir
videoElement.play()
.then(() => {
console.log('Vídeo iniciado, readyState:', videoElement?.readyState);
// Se já tiver metadata, resolver imediatamente
if (videoElement && videoElement.readyState >= 2) {
onLoadedMetadata();
}
})
.catch((err) => {
console.warn('Erro ao reproduzir vídeo:', err);
// Continuar mesmo assim se já tiver metadata
if (videoElement && videoElement.readyState >= 2) {
onLoadedMetadata();
} else {
onError();
}
});
});
console.log('Vídeo pronto, dimensões:', videoElement.videoWidth, 'x', videoElement.videoHeight);
}
// Se for captura automática, aguardar um pouco e capturar
if (autoCapture) {
// Aguardar 1.5 segundos para o vídeo estabilizar
setTimeout(() => {
if (videoElement && canvasElement && !capturando && !previewUrl && webcamDisponivel) {
capturar();
}
}, 1500);
}
// Sucesso, sair do loop
return;
} catch (err) {
console.warn('Falha ao acessar webcam com constraint:', constraint, err);
ultimoErro = err instanceof Error ? err : new Error(String(err));
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = null;
}
continue;
}
}
// Se chegou aqui, todas as tentativas falharam
throw ultimoErro || new Error('Não foi possível acessar a webcam');
} catch (error) {
console.error('Erro ao acessar webcam:', error);
erro = 'Erro ao acessar webcam. Continuando sem foto.';
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('Permission denied') || errorMessage.includes('NotAllowedError')) {
erro = 'Permissão de webcam negada. Continuando sem foto.';
} else if (errorMessage.includes('NotFoundError') || errorMessage.includes('DevicesNotFoundError')) {
erro = 'Nenhuma webcam encontrada. Continuando sem foto.';
} else {
erro = 'Erro ao acessar webcam. Continuando sem foto.';
}
webcamDisponivel = false;
// Se for captura automática e houver erro, chamar onError para continuar sem foto
if (autoCapture && onError) {
@@ -79,19 +240,69 @@
async function capturar() {
if (!videoElement || !canvasElement) {
console.error('Elementos de vídeo ou canvas não disponíveis');
if (autoCapture && onError) {
onError();
}
return;
}
// Verificar se o vídeo está pronto
if (videoElement.readyState < 2) {
console.warn('Vídeo ainda não está pronto, aguardando...');
await new Promise<void>((resolve) => {
const checkReady = () => {
if (videoElement && videoElement.readyState >= 2) {
resolve();
} else {
setTimeout(checkReady, 100);
}
};
checkReady();
});
}
capturando = true;
erro = null;
try {
const blob = await capturarWebcamComPreview(videoElement, canvasElement);
if (blob) {
// Verificar dimensões do vídeo
if (!videoElement.videoWidth || !videoElement.videoHeight) {
throw new Error('Dimensões do vídeo não disponíveis');
}
// Configurar canvas com as dimensões do vídeo
canvasElement.width = videoElement.videoWidth;
canvasElement.height = videoElement.videoHeight;
// Obter contexto do canvas
const ctx = canvasElement.getContext('2d');
if (!ctx) {
throw new Error('Não foi possível obter contexto do canvas');
}
// Desenhar frame atual do vídeo no canvas
ctx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
// Converter para blob
const blob = await new Promise<Blob | null>((resolve, reject) => {
canvasElement.toBlob(
(blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Falha ao converter canvas para blob'));
}
},
'image/jpeg',
0.92 // Qualidade ligeiramente reduzida para melhor compatibilidade
);
});
if (blob && blob.size > 0) {
previewUrl = URL.createObjectURL(blob);
console.log('Imagem capturada com sucesso, tamanho:', blob.size, 'bytes');
// Parar stream para mostrar preview
if (stream) {
stream.getTracks().forEach((track) => track.stop());
@@ -105,13 +316,7 @@
}, 500);
}
} else {
erro = 'Falha ao capturar imagem';
// Se for captura automática e falhar, continuar sem foto
if (autoCapture && onError) {
setTimeout(() => {
onError();
}, 500);
}
throw new Error('Blob vazio ou inválido');
}
} catch (error) {
console.error('Erro ao capturar:', error);
@@ -246,9 +451,16 @@
bind:this={videoElement}
autoplay
playsinline
class="border-primary max-h-[60vh] max-w-full rounded-lg border-2 object-contain"
muted
class="border-primary max-h-[60vh] max-w-full rounded-lg border-2 object-contain bg-black"
style="min-width: 320px; min-height: 240px;"
></video>
<canvas bind:this={canvasElement} class="hidden"></canvas>
{#if !videoReady && webcamDisponivel}
<div class="absolute inset-0 flex items-center justify-center bg-black/50 rounded-lg">
<span class="loading loading-spinner loading-lg text-white"></span>
</div>
{/if}
</div>
{#if erro}
<div class="alert alert-error max-w-md">