From 67d6b3ec729063200235df098fb9b3ce9574773b Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Tue, 18 Nov 2025 16:20:38 -0300 Subject: [PATCH] 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. --- .../lib/components/ponto/WebcamCapture.svelte | 274 ++++++++++++++++-- apps/web/src/lib/utils/deviceInfo.ts | 179 ++++++++---- 2 files changed, 360 insertions(+), 93 deletions(-) diff --git a/apps/web/src/lib/components/ponto/WebcamCapture.svelte b/apps/web/src/lib/components/ponto/WebcamCapture.svelte index ff983ec..99e55e1 100644 --- a/apps/web/src/lib/components/ponto/WebcamCapture.svelte +++ b/apps/web/src/lib/components/ponto/WebcamCapture.svelte @@ -19,11 +19,56 @@ let capturando = $state(false); let erro = $state(null); let previewUrl = $state(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((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((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((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;" > + {#if !videoReady && webcamDisponivel} +
+ +
+ {/if} {#if erro}
diff --git a/apps/web/src/lib/utils/deviceInfo.ts b/apps/web/src/lib/utils/deviceInfo.ts index c91d645..f0f2c0f 100644 --- a/apps/web/src/lib/utils/deviceInfo.ts +++ b/apps/web/src/lib/utils/deviceInfo.ts @@ -230,7 +230,7 @@ function obterInformacoesMemoria(): string { } /** - * Obtém localização via GPS + * Obtém localização via GPS com múltiplas tentativas */ async function obterLocalizacao(): Promise<{ latitude?: number; @@ -242,76 +242,131 @@ async function obterLocalizacao(): Promise<{ pais?: string; }> { if (typeof navigator === 'undefined' || !navigator.geolocation) { + console.warn('Geolocalização não suportada'); return {}; } - return new Promise((resolve) => { - const timeout = setTimeout(() => { - resolve({}); - }, 5000); // Timeout de 5 segundos (reduzido para não bloquear) + // Tentar múltiplas estratégias + const estrategias = [ + // Estratégia 1: Alta precisão (mais lento, mas mais preciso) + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 0 + }, + // Estratégia 2: Precisão média (balanceado) + { + enableHighAccuracy: false, + timeout: 8000, + maximumAge: 30000 + }, + // Estratégia 3: Rápido (usa cache) + { + enableHighAccuracy: false, + timeout: 5000, + maximumAge: 60000 + } + ]; - navigator.geolocation.getCurrentPosition( - async (position) => { - clearTimeout(timeout); - const { latitude, longitude, accuracy } = position.coords; + for (const options of estrategias) { + try { + const resultado = await new Promise<{ + latitude?: number; + longitude?: number; + precisao?: number; + endereco?: string; + cidade?: string; + estado?: string; + pais?: string; + }>((resolve) => { + const timeout = setTimeout(() => { + resolve({}); + }, options.timeout + 1000); - // Tentar obter endereço via reverse geocoding - let endereco = ''; - let cidade = ''; - let estado = ''; - let pais = ''; + navigator.geolocation.getCurrentPosition( + async (position) => { + clearTimeout(timeout); + const { latitude, longitude, accuracy } = position.coords; - try { - const response = await fetch( - `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1` - ); - if (response.ok) { - const data = (await response.json()) as { - address?: { - road?: string; - house_number?: string; - city?: string; - town?: string; - state?: string; - country?: string; - }; - }; - if (data.address) { - const addr = data.address; - if (addr.road) { - endereco = `${addr.road}${addr.house_number ? `, ${addr.house_number}` : ''}`; - } - cidade = addr.city || addr.town || ''; - estado = addr.state || ''; - pais = addr.country || ''; + // Validar coordenadas + if (isNaN(latitude) || isNaN(longitude) || latitude === 0 || longitude === 0) { + resolve({}); + return; } - } - } catch (error) { - console.warn('Erro ao obter endereço:', error); - } - resolve({ - latitude, - longitude, - precisao: accuracy, - endereco, - cidade, - estado, - pais, - }); - }, - (error) => { - clearTimeout(timeout); - console.warn('Erro ao obter localização:', error); - resolve({}); - }, - { - enableHighAccuracy: false, // false para ser mais rápido - timeout: 5000, // Timeout reduzido para 5 segundos - maximumAge: 60000, // Aceitar localização de até 1 minuto atrás + // Tentar obter endereço via reverse geocoding + let endereco = ''; + let cidade = ''; + let estado = ''; + let pais = ''; + + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`, + { + headers: { + 'User-Agent': 'SGSE-App/1.0' + } + } + ); + if (response.ok) { + const data = (await response.json()) as { + address?: { + road?: string; + house_number?: string; + city?: string; + town?: string; + state?: string; + country?: string; + }; + }; + if (data.address) { + const addr = data.address; + if (addr.road) { + endereco = `${addr.road}${addr.house_number ? `, ${addr.house_number}` : ''}`; + } + cidade = addr.city || addr.town || ''; + estado = addr.state || ''; + pais = addr.country || ''; + } + } + } catch (error) { + console.warn('Erro ao obter endereço:', error); + } + + resolve({ + latitude, + longitude, + precisao: accuracy, + endereco, + cidade, + estado, + pais, + }); + }, + (error) => { + clearTimeout(timeout); + console.warn('Erro ao obter localização:', error.code, error.message); + resolve({}); + }, + options + ); + }); + + // Se obteve localização, retornar + if (resultado.latitude && resultado.longitude) { + console.log('Localização obtida com sucesso:', resultado); + return resultado; } - ); - }); + } catch (error) { + console.warn('Erro na estratégia de geolocalização:', error); + continue; + } + } + + // Se todas as estratégias falharam, retornar vazio + console.warn('Não foi possível obter localização após todas as tentativas'); + return {}; } /**