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,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">
|
||||
|
||||
Reference in New Issue
Block a user