diff --git a/client/src/hooks/useSimpleCamera.tsx b/client/src/hooks/useSimpleCamera.tsx index fb38a40..e1e0e40 100644 --- a/client/src/hooks/useSimpleCamera.tsx +++ b/client/src/hooks/useSimpleCamera.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useCallback } from 'react'; import { PhotoType } from '@/types/sale'; +import { getCameraCapabilities, logCameraCapabilities } from '@/utils/cameraDebug'; export function useSimpleCamera() { const [isOpen, setIsOpen] = useState(false); @@ -13,9 +14,14 @@ export function useSimpleCamera() { const startCamera = useCallback(async (photoType: PhotoType): Promise => { console.log('=== SIMPLE CAMERA START ==='); + // Diagnostic complet des capacités caméra + const capabilities = await getCameraCapabilities(); + logCameraCapabilities(capabilities); + try { // Nettoyer le stream précédent s'il existe if (stream) { + console.log('Cleaning up previous stream...'); stream.getTracks().forEach(track => track.stop()); setStream(null); } @@ -25,6 +31,11 @@ export function useSimpleCamera() { throw new Error('Votre navigateur ne supporte pas l\'accès à la caméra. Utilisez un navigateur moderne comme Chrome, Firefox ou Safari.'); } + // HTTPS check for production + if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') { + console.warn('Camera access may be restricted on non-HTTPS connections'); + } + // État initial setCurrentPhotoType(photoType); setIsOpen(true); @@ -32,45 +43,60 @@ export function useSimpleCamera() { let mediaStream: MediaStream; try { - // Essayer d'abord avec des contraintes simples - console.log('Trying basic video constraints...'); + // Essayer d'abord avec des contraintes optimisées pour production + console.log('Trying production-optimized video constraints...'); mediaStream = await navigator.mediaDevices.getUserMedia({ video: { - width: { ideal: 1280, max: 1920 }, - height: { ideal: 720, max: 1080 } + width: { ideal: 1280, min: 640, max: 1920 }, + height: { ideal: 720, min: 480, max: 1080 }, + facingMode: 'environment' // Préférer la caméra arrière } }); - console.log('Basic video constraints successful'); + console.log('Production video constraints successful'); } catch (basicError) { - console.warn('Basic constraints failed, trying minimal video:', basicError); + console.warn('Production constraints failed, trying user-facing camera:', basicError); try { - // Fallback ultra-simple + // Fallback avec caméra avant mediaStream = await navigator.mediaDevices.getUserMedia({ - video: true - }); - console.log('Minimal video constraints successful'); - } catch (minimalError) { - console.error('All camera access failed:', minimalError); - - // Fermer la modal en cas d'erreur - setIsOpen(false); - setCurrentPhotoType(null); - - // Gestion des erreurs spécifiques - if (minimalError instanceof Error) { - if (minimalError.name === 'NotAllowedError') { - throw new Error('Permission d\'accès à la caméra refusée. Veuillez autoriser l\'accès dans votre navigateur et actualiser la page.'); - } else if (minimalError.name === 'NotFoundError') { - throw new Error('Aucune caméra trouvée sur cet appareil. Veuillez connecter une caméra ou utiliser l\'option d\'importation de fichier.'); - } else if (minimalError.name === 'NotReadableError') { - throw new Error('Caméra déjà utilisée par une autre application. Fermez les autres applications utilisant la caméra.'); - } else if (minimalError.name === 'OverconstrainedError') { - throw new Error('Contraintes de caméra non supportées par votre appareil.'); + video: { + width: { ideal: 1280, min: 640 }, + height: { ideal: 720, min: 480 }, + facingMode: 'user' } - } + }); + console.log('User-facing camera successful'); + } catch (userError) { + console.warn('User-facing camera failed, trying basic constraints:', userError); - throw new Error('Erreur d\'accès à la caméra. Utilisez l\'option d\'importation de fichier comme alternative.'); + try { + // Fallback ultra-simple + mediaStream = await navigator.mediaDevices.getUserMedia({ + video: true + }); + console.log('Basic video constraints successful'); + } catch (minimalError) { + console.error('All camera access attempts failed:', minimalError); + + // Fermer la modal en cas d'erreur + setIsOpen(false); + setCurrentPhotoType(null); + + // Gestion des erreurs spécifiques + if (minimalError instanceof Error) { + if (minimalError.name === 'NotAllowedError') { + throw new Error('Permission d\'accès à la caméra refusée. Veuillez autoriser l\'accès dans votre navigateur et actualiser la page.'); + } else if (minimalError.name === 'NotFoundError') { + throw new Error('Aucune caméra trouvée sur cet appareil. Veuillez connecter une caméra ou utiliser l\'option d\'importation de fichier.'); + } else if (minimalError.name === 'NotReadableError') { + throw new Error('Caméra déjà utilisée par une autre application. Fermez les autres applications utilisant la caméra.'); + } else if (minimalError.name === 'OverconstrainedError') { + throw new Error('Contraintes de caméra non supportées par votre appareil.'); + } + } + + throw new Error('Erreur d\'accès à la caméra. Utilisez l\'option d\'importation de fichier comme alternative.'); + } } } @@ -78,27 +104,50 @@ export function useSimpleCamera() { setStream(mediaStream); // Attendre que la modal soit rendue avant d'assigner le stream - setTimeout(() => { - if (videoRef.current && mediaStream && mediaStream.active) { - const video = videoRef.current; - console.log('Assigning stream to video element'); - - video.srcObject = mediaStream; - - // Forcer le play avec gestion d'erreur - video.play().then(() => { - console.log('Video playing successfully'); - }).catch(playError => { - console.error('Video play failed:', playError); - // Réessayer après un délai - setTimeout(() => { - video.play().catch(e => console.error('Retry video play failed:', e)); - }, 200); - }); - } else { - console.error('Video element not ready or stream inactive'); - } - }, 200); + await new Promise((resolve) => { + setTimeout(() => { + if (videoRef.current && mediaStream && mediaStream.active) { + const video = videoRef.current; + console.log('Assigning stream to video element, tracks:', mediaStream.getVideoTracks().length); + + video.srcObject = mediaStream; + + // Configuration vidéo pour production + video.setAttribute('autoplay', 'true'); + video.setAttribute('playsinline', 'true'); + video.setAttribute('muted', 'true'); + + // Événements de diagnostic + video.onloadedmetadata = () => { + console.log('Video metadata loaded:', video.videoWidth, 'x', video.videoHeight); + resolve(void 0); + }; + + video.onerror = (e) => { + console.error('Video element error:', e); + resolve(void 0); + }; + + // Forcer le play avec gestion d'erreur robuste + video.play().then(() => { + console.log('Video playing successfully'); + resolve(void 0); + }).catch(playError => { + console.error('Video play failed:', playError); + // Réessayer après un délai + setTimeout(() => { + video.play().catch(e => { + console.error('Retry video play failed:', e); + resolve(void 0); + }); + }, 500); + }); + } else { + console.error('Video element not ready or stream inactive'); + resolve(void 0); + } + }, 300); + }); } catch (error) { console.error('Camera start failed:', error); diff --git a/client/src/utils/cameraDebug.ts b/client/src/utils/cameraDebug.ts new file mode 100644 index 0000000..eb06c98 --- /dev/null +++ b/client/src/utils/cameraDebug.ts @@ -0,0 +1,106 @@ +// Utilitaires de diagnostic caméra pour production +export interface CameraCapabilities { + hasMediaDevices: boolean; + hasGetUserMedia: boolean; + isHTTPS: boolean; + userAgent: string; + availableDevices: MediaDeviceInfo[]; + constraints: { + video: boolean; + audio: boolean; + }; +} + +export async function getCameraCapabilities(): Promise { + const capabilities: CameraCapabilities = { + hasMediaDevices: !!navigator.mediaDevices, + hasGetUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia), + isHTTPS: location.protocol === 'https:' || location.hostname === 'localhost', + userAgent: navigator.userAgent, + availableDevices: [], + constraints: { + video: false, + audio: false + } + }; + + // Énumérer les appareils disponibles si possible + if (capabilities.hasMediaDevices) { + try { + capabilities.availableDevices = await navigator.mediaDevices.enumerateDevices(); + } catch (error) { + console.warn('Cannot enumerate devices:', error); + } + + // Tester les contraintes supportées + if (capabilities.hasGetUserMedia) { + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true }); + capabilities.constraints.video = true; + stream.getTracks().forEach(track => track.stop()); + } catch (error) { + console.warn('Video not supported:', error); + } + + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + capabilities.constraints.audio = true; + stream.getTracks().forEach(track => track.stop()); + } catch (error) { + console.warn('Audio not supported:', error); + } + } + } + + return capabilities; +} + +export function logCameraCapabilities(capabilities: CameraCapabilities): void { + console.group('🔍 Camera Capabilities Report'); + console.log('Media Devices API:', capabilities.hasMediaDevices ? '✅' : '❌'); + console.log('getUserMedia API:', capabilities.hasGetUserMedia ? '✅' : '❌'); + console.log('HTTPS Connection:', capabilities.isHTTPS ? '✅' : '⚠️'); + console.log('User Agent:', capabilities.userAgent); + console.log('Video Support:', capabilities.constraints.video ? '✅' : '❌'); + console.log('Audio Support:', capabilities.constraints.audio ? '✅' : '❌'); + + if (capabilities.availableDevices.length > 0) { + console.log('Available Devices:'); + capabilities.availableDevices.forEach((device, index) => { + console.log(` ${index + 1}. ${device.kind}: ${device.label || 'Unknown'}`); + }); + } else { + console.log('Available Devices: None found or permission denied'); + } + + console.groupEnd(); +} + +export function generateCameraReport(capabilities: CameraCapabilities): string { + const deviceCount = capabilities.availableDevices.filter(d => d.kind === 'videoinput').length; + + let report = `=== Rapport Caméra Production ===\n`; + report += `Navigateur: ${capabilities.userAgent.split(' ')[0]}\n`; + report += `HTTPS: ${capabilities.isHTTPS ? 'Oui' : 'Non'}\n`; + report += `API Média: ${capabilities.hasMediaDevices ? 'Disponible' : 'Indisponible'}\n`; + report += `getUserMedia: ${capabilities.hasGetUserMedia ? 'Disponible' : 'Indisponible'}\n`; + report += `Support Vidéo: ${capabilities.constraints.video ? 'Oui' : 'Non'}\n`; + report += `Caméras détectées: ${deviceCount}\n`; + + if (!capabilities.isHTTPS && location.hostname !== 'localhost') { + report += `\n⚠️ ATTENTION: Connexion non sécurisée détectée.\n`; + report += `La caméra peut ne pas fonctionner sans HTTPS.\n`; + } + + if (!capabilities.hasGetUserMedia) { + report += `\n❌ ERREUR: API getUserMedia non disponible.\n`; + report += `Utilisez un navigateur moderne (Chrome, Firefox, Safari).\n`; + } + + if (deviceCount === 0) { + report += `\n⚠️ ATTENTION: Aucune caméra détectée.\n`; + report += `Vérifiez les permissions ou connectez une caméra.\n`; + } + + return report; +} \ No newline at end of file