PWA : Progressive Web App / Capacités matérielles

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).

Géolocalisation GPS dans une PWA sur mobile

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 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 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 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.

  • iOS : Réglages → Confidentialité → Caméra → Safari
  • Samsung Internet : cadenas dans la barre d'adresse
  • Firefox : cadenas → Connexion sécurisée → Permissions

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écise
const TIMEOUT = 10000          // ms

function 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 = 1000
let lastScanAt = 0

async 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 Dexie
onUnmounted(() => {
  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'appareil
function 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é.

POUR ALLER PLUS LOIN

Approfondir la stack PWA

Mode hors ligne : Service Worker + IndexedDB

L'architecture offline-first que nous utilisons en production : Workbox, Dexie.js, queue de synchronisation FIFO et compression photos côté client.

Manifest et installation A2HS

Configurer le manifest, gérer le prompt d'installation sur Android et iOS, détecter si l'app est déjà installée en mode standalone.

Application de visite guidée

Notre exemple en production : carte Leaflet, scan QR, audio, comparatif avant/après et points d'intérêt géolocalisés.

Application mobile terrain sur mesure

GPS, photos, formulaires hors ligne : notre approche pour les équipes terrain qui travaillent en zone blanche.

PWA : présentation générale

La page hub : comparatif PWA vs natif, cas d'usage, APIs mobiles et librairies clés de notre stack.

Vous avez un projet ?

Contactez-nous pour savoir comment nous pouvons vous aider.