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,45 +19,206 @@
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) {
// 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();
}
return;
}
try {
stream = await navigator.mediaDevices.getUserMedia({
// 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
}
];
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;
}
console.log('Webcam acessada com sucesso');
webcamDisponivel = true;
// Atribuir stream ao elemento de vídeo (o $effect também fará isso, mas garantimos aqui)
if (videoElement) {
videoElement.srcObject = stream;
await videoElement.play();
// 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 segundo para o usuário se posicionar
// Aguardar 1.5 segundos para o vídeo estabilizar
setTimeout(() => {
if (videoElement && canvasElement && !capturando && !previewUrl) {
if (videoElement && canvasElement && !capturando && !previewUrl && webcamDisponivel) {
capturar();
}
}, 1000);
}, 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);
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);
// 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">

View File

@@ -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,19 +242,58 @@ async function obterLocalizacao(): Promise<{
pais?: string;
}> {
if (typeof navigator === 'undefined' || !navigator.geolocation) {
console.warn('Geolocalização não suportada');
return {};
}
return new Promise((resolve) => {
// 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
}
];
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({});
}, 5000); // Timeout de 5 segundos (reduzido para não bloquear)
}, options.timeout + 1000);
navigator.geolocation.getCurrentPosition(
async (position) => {
clearTimeout(timeout);
const { latitude, longitude, accuracy } = position.coords;
// Validar coordenadas
if (isNaN(latitude) || isNaN(longitude) || latitude === 0 || longitude === 0) {
resolve({});
return;
}
// Tentar obter endereço via reverse geocoding
let endereco = '';
let cidade = '';
@@ -263,7 +302,12 @@ async function obterLocalizacao(): Promise<{
try {
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`
`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 {
@@ -302,16 +346,27 @@ async function obterLocalizacao(): Promise<{
},
(error) => {
clearTimeout(timeout);
console.warn('Erro ao obter localização:', error);
console.warn('Erro ao obter localização:', error.code, error.message);
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
}
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 {};
}
/**