Files
mabibli/MaBibli.Client/wwwroot/js/scanner-camera.js
T
mathieuandClaude Opus 5 f7ad9582f6 Ajouter le scan camera du code-barres ISBN
Le decodage EAN-13/EAN-8 est fait en C# par ZXing.Net (IsbnScanner), conformement
a la decision actee dans CLAUDE.md : le JavaScript ne fournit que les pixels de la
camera, aucune logique de decodage ne lui est confiee.

Le scan se greffe en amont du flux d'ajout par ISBN existant : des qu'un code est
lu et reconnu comme ISBN-13 valide, la recherche de notices est declenchee sans
que l'utilisateur ait a retaper quoi que ce soit. La saisie manuelle reste
accessible en permanence, y compris quand la camera echoue.

Points traites explicitement :
- camera arriere par defaut (facingMode "environment", en "ideal" pour ne pas
  echouer sur un PC sans camera arriere) ;
- liberation de la camera a l'annulation, au succes et au demontage du composant ;
- cadence limitee a ~12 images/s et decodage restreint a la bande centrale
  reduite a 640 px de large : inutile de decoder chaque frame en pleine taille ;
- messages distincts pour permission refusee, absence de camera, camera occupee
  et contexte non securise (getUserMedia exige HTTPS ou localhost : une IP de
  reseau local en http ne marchera jamais, c'est une source de confusion garantie).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:32:05 +02:00

62 lines
2.7 KiB
JavaScript

// Rôle unique de ce fichier : ouvrir la caméra et fournir des pixels à C#.
// AUCUN décodage ici — il est fait par ZXing.Net côté C# (décision actée dans CLAUDE.md).
// getUserMedia et canvas/getImageData n'ont pas d'équivalent C#, d'où ces quelques lignes.
let flux = null;
let video = null;
let canvas = null;
let ctx = null;
let derniersPixels = null;
// getUserMedia exige un contexte sécurisé : HTTPS, ou localhost.
// Une IP de réseau local (192.168.x.x) en http NE l'est PAS — piège classique en dev.
export function diagnostic() {
if (!window.isSecureContext) return 'contexte-non-securise';
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) return 'api-absente';
return 'ok';
}
export async function demarrer(element) {
video = element;
// facingMode "environment" = caméra arrière : on scanne un livre tenu devant soi.
// "ideal" et non "exact" pour ne pas échouer sur un PC qui n'a qu'une webcam frontale.
flux = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false,
});
video.srcObject = flux;
video.setAttribute('playsinline', '');
await video.play();
canvas = document.createElement('canvas');
ctx = canvas.getContext('2d', { willReadFrequently: true });
}
// Capture une frame et la garde en mémoire. Renvoie [largeur, hauteur], ou null si
// la vidéo n'est pas encore prête. Les pixels se récupèrent ensuite via pixels().
export function capturer(largeurMax, partHauteur) {
if (!video || !ctx || video.readyState < 2 || !video.videoWidth) return null;
const echelle = Math.min(1, largeurMax / video.videoWidth);
const l = Math.round(video.videoWidth * echelle);
const h = Math.round(video.videoHeight * echelle);
if (canvas.width !== l || canvas.height !== h) { canvas.width = l; canvas.height = h; }
ctx.drawImage(video, 0, 0, l, h);
// On ne renvoie que la bande centrale, celle que l'utilisateur vise : moins de pixels
// à transférer et à décoder, donc moins de travail par frame côté C#.
const hb = Math.max(1, Math.round(h * partHauteur));
const y = Math.round((h - hb) / 2);
derniersPixels = new Uint8Array(ctx.getImageData(0, y, l, hb).data.buffer);
return [l, hb];
}
export function pixels() {
return derniersPixels;
}
// Libérer la caméra : sinon le voyant reste allumé et la batterie se vide,
// ce que l'utilisateur perçoit — à raison — comme un dysfonctionnement.
export function arreter() {
if (flux) { flux.getTracks().forEach(t => t.stop()); flux = null; }
if (video) { video.srcObject = null; video = null; }
canvas = null; ctx = null; derniersPixels = null;
}