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:
@@ -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">
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user