APIs mobiles dans une PWA : GPS, caméra, scan QR, micro et torche
Une PWA installée accède aux mêmes capacités matérielles qu'une application native sur Android et iOS : géolocalisation, caméra, microphone, torche, scan de codes-barres. Ces fonctionnalités sont exposées par les APIs du navigateur et ne nécessitent pas de développement natif.
Nous documentons ici les composables Vue.js que nous avons développés et testés dans nos projets de PWA terrain et de visite guidée, avec les détails qui comptent en production : gestion des permissions par OS, HTTPS requis, limites réelles par appareil.
APIS DISPONIBLES
Ce que la PWA peut faire sur l'appareil du technicien
Toutes ces APIs sont exposées par le navigateur et ne nécessitent pas de SDK natif. Elles fonctionnent dans la PWA exactement comme dans une application iOS ou Android.
GÉOLOCALISATION
Geolocation API pour horodater les actions terrain
Latitude, longitude, précision en mètres. Capture ponctuelle (getCurrentPosition) pour horodater une saisie ou suivi continu (watchPosition) pour afficher la position sur une carte Leaflet. HTTPS requis sur mobile.
La précision varie selon le contexte : 3 à 10 mètres en extérieur, dégradée en intérieur où la PWA bascule sur la géolocalisation par réseau (Wi-Fi, antennes relais).
CAMÉRA ET COMPRESSION
Capture photo et compression côté client
Capture via <input type=file capture=environment> ou flux vidéo direct. Compression avec browser-image-compression avant stockage dans IndexedDB ou upload : 3-5 Mo vers 150-300 Ko (1200px, JPEG 75%).
Sur réseau lent ou en zone dégradée, la compression est la différence entre une synchronisation réussie en quelques secondes et un timeout. Le paramètre de qualité est ajustable par projet.
Compression de photo côté client dans une PWA
SCAN QR ET CODES-BARRES
ZXing BrowserMultiFormatReader pour 12+ formats
ZXing (BrowserMultiFormatReader) : QR, EAN-13, Code 128, Code 39, PDF417, Aztec, Data Matrix, ITF... La reconnaissance est automatique, sans choisir le format. Cooldown anti-spam, retour visuel en temps réel sur le flux vidéo.
Le scan fonctionne entièrement côté client, sans serveur : résultat instantané même sans réseau. Sur iOS Safari, le scan est possible mais légèrement moins fluide qu'un input natif.
Scanner un QR code depuis une PWA sur mobile
ENREGISTREMENT AUDIO
MediaRecorder pour les notes vocales terrain
Notes vocales avec pause/reprise, format audio adapté à l'appareil (WebM Opus, MP4 selon support). HTTPS requis. Nettoyage du flux au unmount via onUnmounted pour éviter les fuites mémoire.
Un technicien qui manipule des équipements peut dicter un commentaire plutôt que le saisir. L'enregistrement est stocké localement et synchronisé au retour du réseau.
Enregistrement audio dans une PWA mobile
TORCHE
API torch sur Android uniquement
Allumage/extinction via MediaTrackCapabilities.torch + applyConstraints. Disponible uniquement sur Android Chrome : iOS Safari n'expose pas cette capacité dans les APIs web.
Le composable useFlash vérifie isAndroid au montage et teste capabilities.torch après l'ouverture du flux caméra. Il expose isSupported pour que le composant parent masque le bouton automatiquement sur iPhone, sans afficher d'erreur.
Android Chrome
isSupported = true si capabilities.torch est exposé par l'appareil.
iPhone / iPad
isSupported = false, le bouton torche est masqué automatiquement.
PERMISSIONS
Messages d'erreur adaptés à chaque navigateur
L'API Permissions (navigator.permissions.query) permet de connaître l'état (granted/denied/prompt) avant de demander l'accès. Si l'état est 'denied', l'utilisateur est guidé vers les paramètres avec un message adapté à son OS.
Pattern device-aware implémenté dans tous les composables (GPS, caméra, micro) : chaque navigateur a un chemin différent pour réactiver une permission, et les messages le reflètent.
iOS Safari
Réglages > Confidentialité > Caméra > Safari
Samsung Internet
Cadenas dans la barre d'adresse > Permissions du site
Firefox Android
Cadenas > Connexion sécurisée > Permissions
PRÉREQUIS
HTTPS et permissions : ce qui bloque en développement
Les deux contraintes les plus fréquentes lors de l'intégration des APIs mobiles ne sont pas des bugs : elles sont documentées et ont des solutions connues.
HTTPS requis pour toutes les APIs sensibles
GPS, caméra et micro nécessitent window.isSecureContext = true.
Sur localhost, Chrome autorise ces APIs sans HTTPS.
Pour tester sur un vrai mobile depuis le dev local, nous utilisons ngrok :
un tunnel HTTPS vers localhost:5173 avec un certificat reconnu par
Chrome Android et Safari iOS.
ngrok http 5173
vite.config.ts expose server.allowedHosts: true pour accepter les
requêtes depuis le tunnel ngrok sans whitelist explicite.
Paramètres de permission par navigateur
Quand un utilisateur refuse ou révoque une permission, le chemin pour la réactiver
est différent sur iOS, Android natif, Samsung Internet et Firefox.
Afficher un message générique "permission refusée" oblige l'utilisateur à chercher
par lui-même. Nos composables exposent un message ciblé selon le device.
Cas particulier : la torche est Android uniquement
L'API MediaTrackCapabilities.torch + applyConstraints
est supportée sur Android Chrome mais pas sur iOS Safari. Apple n'expose pas cette
capacité dans les APIs web. Notre composable useFlash vérifie
isAndroid au montage et teste capabilities.torch après
l'ouverture du flux caméra. Il expose isSupported pour que le composant
parent cache le bouton torche automatiquement sur iPhone, sans afficher d'erreur.
Usage terrain : le technicien inspecte un équipement dans un local
sombre (armoire électrique, comble, sous-sol) et allume la torche depuis l'interface
sans changer d'application.
Documentation développeurs
Composables Vue.js : APIs mobiles de nos PWA
Chaque composable est autonome et réutilisable entre projets
useGeolocation : GPS avec messages d'erreur device-aware
Précision mesurée, messages différenciés par OS
Le composable capture la position GPS avec enableHighAccuracy: true et signale quand la précision dépasse 50 mètres. Les messages d'erreur sont différents selon le navigateur et l'OS pour guider l'utilisateur vers le bon menu de paramètres.
const ACCURACY_THRESHOLD = 50 // mètres, position signalée comme impréciseconst TIMEOUT = 10000 // msfunction getPermissionDeniedMessage(): string { if (isIOS) { return "Accès bloqué. Sur iOS : Réglages → Confidentialité et sécurité → " + "Service de localisation → Safari → \"Lors de l'utilisation\"." } if (isAndroid && isSamsung) { return "Sur Samsung Internet : cadenas dans la barre d'adresse → " + "Autorisations → Localisation → Autoriser." } if (isAndroid) { return 'Sur Android : Paramètres → Localisation → Utiliser la localisation.' } return "Cliquez sur l'icône cadenas → Autorisations → Localisation → Autoriser."}async function capture(): Promise<GeoPosition | null> { return new Promise((resolve) => { navigator.geolocation.getCurrentPosition( (pos) => { position.value = { lat: pos.coords.latitude, lng: pos.coords.longitude, accuracy: Math.round(pos.coords.accuracy), capturedAt: new Date().toISOString(), } isImprecise.value = pos.coords.accuracy > ACCURACY_THRESHOLD resolve(position.value) }, (err) => { if (err.code === err.PERMISSION_DENIED) { error.value = getPermissionDeniedMessage() } else if (err.code === err.TIMEOUT) { error.value = 'Délai dépassé. Déplacez-vous vers une zone dégagée.' } else { error.value = 'Position indisponible. Réessayez dans un moment.' } resolve(null) }, { enableHighAccuracy: true, timeout: TIMEOUT, maximumAge: 0 } ) })}
Cas d'usage terrain : les coordonnées GPS sont attachées à l'intervention au moment de la création, avec le champ geoAccuracy persisté dans Dexie. Si la précision dépasse le seuil, une alerte visuelle invite le technicien à se déplacer avant de valider la saisie.
useCodeScanner : ZXing avec 12+ formats de codes
composable partagé entre projets
Le scan démarre le flux caméra arrière et décode en continu. Un cooldown de 1 seconde évite de déclencher plusieurs fois sur le même code. Les formats supportés incluent QR, EAN-13, EAN-8, Code 128, Code 39, PDF417, Aztec, Data Matrix et plus.
import { BrowserMultiFormatReader } from '@zxing/browser'import { NotFoundException, BarcodeFormat } from '@zxing/library'const SCAN_COOLDOWN_MS = 1000let lastScanAt = 0async function startScan(): Promise<void> { const codeReader = new BrowserMultiFormatReader() controls = await codeReader.decodeFromConstraints( { video: { facingMode: 'environment' } }, videoRef.value, (scanResult, scanError) => { if (scanResult) { isCodeVisible.value = true const now = Date.now() if (now - lastScanAt > SCAN_COOLDOWN_MS) { lastScanAt = now result.value = { code: scanResult.getText(), format: BarcodeFormat[scanResult.getBarcodeFormat()], capturedAt: new Date().toISOString(), } } return } isCodeVisible.value = false // NotFoundException levée à chaque frame sans code : comportement normal, // on ne l'affiche pas comme une erreur. if (scanError && !(scanError instanceof NotFoundException)) { error.value = 'Erreur pendant le scan. Réessayez.' stopScan() } } ) isScanning.value = true}
Cas d'usage : dans notre Démo PWA terrain, le technicien scanne le QR code de l'équipement (extincteur, alarme, dérat) pour préremplir la référence dans le formulaire d'intervention. Dans la Démo PWA parcours touristique, le visiteur scanne le panneau pour ouvrir la fiche du point d'intérêt.
useCamera : capture photo avec compression automatique
browser-image-compression en Web Worker
La capture utilise un <input type="file" capture="environment"> caché, ce qui délègue au navigateur la gestion du flux caméra et du stockage temporaire. La compression se fait côté client dans un Web Worker pour ne pas bloquer le thread principal.
async function capturePhoto(file: File): Promise<void> { const originalSize = file.size const compressed = await imageCompression(file, { maxWidthOrHeight: 1200, initialQuality: 0.75, useWebWorker: true, // compression hors thread principal }) // Si la compression n'apporte pas de gain (photo déjà compressée), // on conserve l'original : évite d'aggraver la taille. const finalBlob = compressed.size < file.size ? compressed : file const url = URL.createObjectURL(finalBlob) const label = `Photo du ${now.toLocaleDateString('fr-FR')} à ${now.toLocaleTimeString('fr-FR')}` photos.value.unshift({ id: uuidv7(), url, originalSize, compressedSize: finalBlob.size, label })}// Révocation des object URLs au unmount : évite les fuites mémoire sur les blobs DexieonUnmounted(() => { photos.value.forEach(p => URL.revokeObjectURL(p.url))})
Révocation des object URLs : chaque photo crée une URL blob temporaire (createObjectURL) qui référence un blob en mémoire. Sans revokeObjectURL au unmount, ces blobs restent en mémoire indéfiniment. Sur une application terrain avec 50 photos par intervention, c'est une fuite mémoire significative.
useFlash : torche Android uniquement
torch via MediaTrackCapabilities
La torche utilise MediaTrackCapabilities.torch via applyConstraints. L'API n'est disponible que sur Android Chrome. Le composable détecte le device au montage et expose isSupported pour que l'interface adapte son affichage sans erreur.
import { isAndroid } from '@/utils/deviceDetect.js'// Détection au montage : iOS ne supporte pas torch, on coupe court.function checkSupport(): void { if (!isAndroid) { isSupported.value = false }}async function acquireStream(): Promise<boolean> { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, }) track = stream.getVideoTracks()[0] const capabilities = track.getCapabilities() as MediaTrackCapabilities & { torch?: boolean } if (!capabilities.torch) { isSupported.value = false releaseStream() return false } isSupported.value = true return true}async function toggle(): Promise<void> { if (isOn.value) { await track?.applyConstraints({ advanced: [{ torch: false }] }) releaseStream() isOn.value = false return } const ok = await acquireStream() if (!ok) { error.value = 'Flash non disponible sur cet appareil.'; return } await track.applyConstraints({ advanced: [{ torch: true }] }) isOn.value = true}// Extinction automatique si l'app passe en arrière-plan (onglet caché)function handleVisibilityChange(): void { if (document.hidden && isOn.value) { releaseStream(); isOn.value = false }}
Cas d'usage terrain : le technicien inspecte un équipement dans un local technique sombre (armoire électrique, local technique, comble). Il allume la torche depuis l'app sans sortir de l'interface d'intervention. L'extinction automatique au changement d'onglet évite de laisser la torche allumée en arrière-plan.
useRecorder : notes vocales avec MediaRecorder
format audio adaptatif + gestion permissions
MediaRecorder sélectionne automatiquement le meilleur format audio disponible sur l'appareil (WebM Opus en priorité, MP4 en fallback). HTTPS requis : une erreur explicite est retournée en HTTP pour guider le développeur en local.
// Sélection du format selon le support de l'appareilfunction getSupportedMimeType(): string { const candidates = [ 'audio/webm;codecs=opus', // Chrome / Firefox desktop + Android 'audio/webm', // fallback WebM sans codec explicite 'audio/ogg;codecs=opus', // Firefox 'audio/mp4', // Safari iOS ] return candidates.find(t => MediaRecorder.isTypeSupported(t)) ?? ''}async function startRecording(): Promise<void> { // HTTPS requis : en HTTP, getUserMedia() est bloqué par le navigateur. // En dev local, utiliser ngrok ou 'pnpm dev:https'. if (!window.isSecureContext) { error.value = "L'enregistrement nécessite HTTPS. Utilisez ngrok ou pnpm dev:https." return } stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const mimeType = getSupportedMimeType() mediaRecorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream) // fallback navigateur mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data) } mediaRecorder.onstop = () => { const blob = new Blob(chunks, { type: mediaRecorder?.mimeType ?? 'audio/webm' }) recordings.value.unshift({ id: uuidv7(), url: URL.createObjectURL(blob), duration, label }) } mediaRecorder.start(100) // chunk toutes les 100ms}
Alternative au clavier sur le terrain : la saisie vocale permet au technicien de dicter une observation sans enlever ses gants. La note est horodatée automatiquement et stockée en attente de synchronisation avec le rapport d'intervention.
FAQ
Les réponses à vos questions
Et si vous ne trouvez pas ce que vous cherchez, nous serons ravis de vous répondre en direct lors d'un rendez-vous entre humains !
Oui, avec des nuances. Geolocation API et MediaDevices (caméra) sont supportés sur iOS depuis Safari 11.1. Les notifications push nécessitent iOS 16.4 et l'installation sur l'écran d'accueil. La torche (torch API) n'est pas supportée sur iOS : Apple n'expose pas MediaTrackCapabilities.torch dans Safari. On affiche simplement le bouton torche uniquement quand isSupported est vrai, ce qui le cache automatiquement sur iPhone.
Oui. ZXing BrowserMultiFormatReader supporte 12+ formats : QR Code, EAN-13, EAN-8, Code 128, Code 39, PDF417, Aztec, Data Matrix, ITF, RSS-14 et plus. La reconnaissance est automatique : pas besoin de choisir le format à l'avance. En pratique, le scan fonctionne bien en flux vidéo continu sur Android. Sur iOS Safari, le scan via flux vidéo est possible mais moins fluide qu'un scan via input natif.
L'API Permissions (navigator.permissions.query) permet de connaître l'état avant de demander l'accès : 'granted', 'denied' ou 'prompt'. Si l'état est 'denied', on n'affiche plus de bouton 'Redemander' et on guide l'utilisateur vers les paramètres du navigateur avec un message adapté à son OS. Ce pattern device-aware est implémenté dans tous nos composables (GPS, caméra, micro) avec des messages différents pour iOS, Android natif, Samsung Internet et Firefox.
Oui, toutes les APIs sensibles (GPS, caméra, micro) nécessitent un contexte sécurisé (window.isSecureContext = true). En production sur un domaine avec certificat valide, tout fonctionne. En développement local sur localhost, Chrome autorise ces APIs sans HTTPS. Pour tester sur un vrai mobile depuis le serveur de dev local, nous utilisons ngrok : il crée un tunnel HTTPS vers localhost avec un certificat reconnu par Chrome Android et Safari iOS.
Non. La torch API via applyConstraints est disponible sur Android Chrome mais pas sur iOS Safari. Sur Android, certains appareils d'entrée de gamme ne l'exposent pas non plus dans MediaTrackCapabilities. Notre composable useFlash teste explicitement isAndroid au montage et vérifie capabilities.torch après l'ouverture du flux caméra. isSupported est exposé pour que le composant parent cache ou désactive le bouton selon le résultat de la détection.
Pour les usages terrain avec 150-300 Ko cible, oui. Une photo smartphone à pleine résolution fait 3-5 Mo. Compressée à 1200px / JPEG 75% avec browser-image-compression, elle descend à 150-300 Ko. La différence en upload sur réseau mobile dégradé est significative : 300 Ko s'uploadent en quelques secondes là où 4 Mo peuvent provoquer un timeout. Si la compression n'apporte pas de gain (fichier déjà compressé), l'original est conservé tel quel.
Votre projet
Votre cas d'usage dépasse les formulaires classiques ?
GPS, scan, photos, notes vocales : ces fonctionnalités sont disponibles dans une PWA sans développement natif. Décrivez vos besoins terrain et nous évaluons la faisabilité.