Improve photo capture reliability and address camera access issues

Adds enhanced camera setup, error handling, and canvas drawing in `useCamera.tsx`, `CameraModal.tsx`, and `NewSaleForm.tsx`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: dec54137-d5a2-4406-8ad4-5f2f9fb110bf
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/e6a2493b-9ed8-4bd8-be32-29049cc60fa3.jpg
This commit is contained in:
michaelschal committed 2025-07-11 16:55:20 +00:00
1 parent 77d77ab658
commit fbbe1375ab
3 files changed
+119 -26

No files matched your search

+31 -4
View File
@@ -26,7 +26,31 @@ export default function CameraModal({
useEffect(() => { useEffect(() => {
if (isOpen && videoRef.current) { if (isOpen && videoRef.current) {
// Ensure video starts playing when modal opens // Ensure video starts playing when modal opens
videoRef.current.play().catch(console.error); const video = videoRef.current;
const handleLoadedMetadata = () => {
console.log('Video loaded metadata:', video.videoWidth, 'x', video.videoHeight);
};
const handleCanPlay = () => {
console.log('Video can play');
};
const handleError = (error: Event) => {
console.error('Video error:', error);
};
video.addEventListener('loadedmetadata', handleLoadedMetadata);
video.addEventListener('canplay', handleCanPlay);
video.addEventListener('error', handleError);
video.play().catch(console.error);
return () => {
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
video.removeEventListener('canplay', handleCanPlay);
video.removeEventListener('error', handleError);
};
} }
}, [isOpen, videoRef]); }, [isOpen, videoRef]);
@@ -57,10 +81,13 @@ export default function CameraModal({
{/* Overlay pour aider à cadrer la pièce d'identité */} {/* Overlay pour aider à cadrer la pièce d'identité */}
<div className="absolute inset-0 pointer-events-none"> <div className="absolute inset-0 pointer-events-none">
<div className="absolute inset-4 border-2 border-white border-dashed rounded-lg opacity-50"></div> <div className="absolute inset-4 border-2 border-white border-dashed rounded-lg opacity-70"></div>
<div className="absolute bottom-2 left-2 text-white text-sm bg-black bg-opacity-50 px-2 py-1 rounded"> <div className="absolute bottom-2 left-2 text-white text-sm bg-black bg-opacity-70 px-2 py-1 rounded">
Cadrez la pièce d'identité dans le rectangle Cadrez la pièce d'identité dans le rectangle
</div> </div>
<div className="absolute top-2 right-2 text-white text-xs bg-black bg-opacity-70 px-2 py-1 rounded">
{photoType === 'recto' ? 'RECTO' : 'VERSO'}
</div>
</div> </div>
</div> </div>
@@ -72,7 +99,7 @@ export default function CameraModal({
<Button <Button
onClick={onCapture} onClick={onCapture}
disabled={isCapturing} disabled={isCapturing}
className="bg-primary hover:bg-primary/90 flex-1" className="bg-green-600 hover:bg-green-700 text-white flex-1"
> >
<Camera className="h-4 w-4 mr-2" /> <Camera className="h-4 w-4 mr-2" />
{isCapturing ? 'Capture...' : 'Prendre la photo'} {isCapturing ? 'Capture...' : 'Prendre la photo'}
+4
View File
@@ -205,7 +205,10 @@ export default function NewSaleForm() {
if (!currentPhotoType) return; if (!currentPhotoType) return;
try { try {
console.log('Attempting to capture photo...');
const photoData = await capturePhoto(); const photoData = await capturePhoto();
console.log('Photo captured successfully, data length:', photoData.length);
form.setValue(currentPhotoType === 'recto' ? 'photoRecto' : 'photoVerso', photoData); form.setValue(currentPhotoType === 'recto' ? 'photoRecto' : 'photoVerso', photoData);
toast({ toast({
@@ -216,6 +219,7 @@ export default function NewSaleForm() {
stopCamera(); stopCamera();
} catch (error) { } catch (error) {
console.error('Photo capture error:', error);
toast({ toast({
title: "Erreur de capture", title: "Erreur de capture",
description: error instanceof Error ? error.message : "Erreur lors de la capture", description: error instanceof Error ? error.message : "Erreur lors de la capture",
+84 -22
View File
@@ -12,6 +12,15 @@ export function useCamera() {
const startCamera = useCallback(async (photoType: PhotoType) => { const startCamera = useCallback(async (photoType: PhotoType) => {
try { try {
// Vérification des permissions avant d'essayer
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter(device => device.kind === 'videoinput');
console.log('Available video devices:', videoDevices.length);
if (videoDevices.length === 0) {
throw new Error('Aucune caméra détectée sur cet appareil');
}
// Configuration avancée pour tablettes et appareils mobiles // Configuration avancée pour tablettes et appareils mobiles
const constraints = { const constraints = {
video: { video: {
@@ -28,6 +37,7 @@ export function useCamera() {
try { try {
// Essai avec caméra arrière // Essai avec caméra arrière
stream = await navigator.mediaDevices.getUserMedia(constraints); stream = await navigator.mediaDevices.getUserMedia(constraints);
console.log('Back camera started successfully');
} catch (backCameraError) { } catch (backCameraError) {
console.warn('Caméra arrière non disponible, essai avec caméra avant:', backCameraError); console.warn('Caméra arrière non disponible, essai avec caméra avant:', backCameraError);
@@ -44,6 +54,7 @@ export function useCamera() {
try { try {
stream = await navigator.mediaDevices.getUserMedia(frontConstraints); stream = await navigator.mediaDevices.getUserMedia(frontConstraints);
console.log('Front camera started successfully');
} catch (frontCameraError) { } catch (frontCameraError) {
console.warn('Caméra avant non disponible, essai basique:', frontCameraError); console.warn('Caméra avant non disponible, essai basique:', frontCameraError);
@@ -51,6 +62,7 @@ export function useCamera() {
stream = await navigator.mediaDevices.getUserMedia({ stream = await navigator.mediaDevices.getUserMedia({
video: true video: true
}); });
console.log('Basic camera started successfully');
} }
} }
@@ -60,6 +72,14 @@ export function useCamera() {
if (videoRef.current) { if (videoRef.current) {
videoRef.current.srcObject = stream; videoRef.current.srcObject = stream;
// Attendre que la vidéo soit prête
await new Promise<void>((resolve) => {
videoRef.current!.onloadedmetadata = () => {
console.log('Video metadata loaded');
resolve();
};
});
} }
return true; return true;
@@ -83,36 +103,78 @@ export function useCamera() {
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
if (!context) { if (!context) {
setIsCapturing(false);
reject(new Error('Canvas context not available')); reject(new Error('Canvas context not available'));
return; return;
} }
// Vérifier que la vidéo a des dimensions valides
if (video.videoWidth === 0 || video.videoHeight === 0) {
console.error('Video dimensions:', video.videoWidth, 'x', video.videoHeight);
setIsCapturing(false);
reject(new Error('Video not ready - no dimensions'));
return;
}
console.log('Video dimensions:', video.videoWidth, 'x', video.videoHeight);
// Définir les dimensions du canvas
canvas.width = video.videoWidth; canvas.width = video.videoWidth;
canvas.height = video.videoHeight; canvas.height = video.videoHeight;
context.drawImage(video, 0, 0); // Dessiner l'image de la vidéo sur le canvas
try {
// Effacer le canvas avant de dessiner
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(video, 0, 0, canvas.width, canvas.height);
console.log('Image drawn on canvas successfully');
} catch (error) {
console.error('Failed to draw image on canvas:', error);
setIsCapturing(false);
reject(new Error('Failed to draw image on canvas'));
return;
}
canvas.toBlob( // Méthode alternative plus robuste - utiliser toDataURL directement
(blob) => { try {
if (!blob) { const dataURL = canvas.toDataURL('image/jpeg', 0.8);
reject(new Error('Failed to create image blob')); console.log('Canvas dataURL generated, length:', dataURL.length);
return;
} if (!dataURL || dataURL === 'data:,') {
setIsCapturing(false);
const reader = new FileReader(); reject(new Error('Failed to create image data'));
reader.onload = (e) => { return;
setIsCapturing(false); }
resolve(e.target?.result as string);
}; setIsCapturing(false);
reader.onerror = () => { resolve(dataURL);
setIsCapturing(false); } catch (error) {
reject(new Error('Failed to read image')); console.error('Canvas toDataURL error:', error);
};
reader.readAsDataURL(blob); // Fallback avec toBlob si toDataURL échoue
}, canvas.toBlob(
'image/jpeg', (blob) => {
0.8 if (!blob) {
); setIsCapturing(false);
reject(new Error('Failed to create image blob'));
return;
}
const reader = new FileReader();
reader.onload = (e) => {
setIsCapturing(false);
resolve(e.target?.result as string);
};
reader.onerror = () => {
setIsCapturing(false);
reject(new Error('Failed to read image'));
};
reader.readAsDataURL(blob);
},
'image/jpeg',
0.8
);
}
}); });
}, []); }, []);