Capturer une image auprès de l'utilisateur

La plupart des navigateurs peuvent accéder à l'appareil photo de l'utilisateur.

Balances à palettes

De nombreux navigateurs peuvent désormais accéder aux entrées vidéo et audio de l'utilisateur. Toutefois, selon le navigateur, il peut s'agir d'une expérience entièrement dynamique et intégrée, ou d'une délégation à une autre application sur l'appareil de l'utilisateur. En plus de cela, tous les appareils ne sont même pas équipés d’une caméra. Alors, comment créer une expérience qui utilise une image générée par l'utilisateur qui fonctionne bien partout ?

Procédez de façon simple et progressive

Si vous souhaitez améliorer progressivement votre expérience, vous devez commencer par quelque chose qui fonctionne partout. Le plus simple est de demander simplement à l'utilisateur un fichier préenregistré.

Demander une URL

Il s'agit de l'option la plus acceptée, mais la moins satisfaisante. Demandez à l'utilisateur de vous fournir une URL, puis utilisez-la. Pour n'afficher que l'image, cela fonctionne partout. Créez un élément img, définissez src, et vous avez terminé.

Toutefois, si vous voulez manipuler l'image de quelque manière que ce soit, les choses sont un peu plus compliquées. CORS vous empêche d'accéder aux pixels réels, sauf si le serveur définit les en-têtes appropriés et que vous marquez l'image comme multi-origine. La seule solution pratique consiste à exécuter un serveur proxy.

Entrée de fichier

Vous pouvez également utiliser un élément de saisie de fichier simple, y compris un filtre accept qui indique que vous ne voulez que des fichiers image.

<input type="file" accept="image/*" />

Cette méthode fonctionne sur toutes les plates-formes. Sur ordinateur, l'utilisateur est invité à importer un fichier image à partir du système de fichiers. Dans Chrome et Safari sur iOS et Android, cette méthode permettra à l'utilisateur de choisir l'application à utiliser pour capturer l'image, y compris de prendre une photo directement avec l'appareil photo ou de choisir un fichier image existant.

Un menu Android proposant deux options : &quot;Capturer une image&quot; et &quot;Fichiers&quot; Un menu iOS avec trois options: prendre une photo, bibliothèque, iCloud

Vous pouvez ensuite associer les données à un <form> ou les manipuler avec JavaScript en écoutant un événement onchange sur l'élément d'entrée, puis en lisant la propriété files de l'événement target.

<input type="file" accept="image/*" id="file-input" />
<script>
  const fileInput = document.getElementById('file-input');

  fileInput.addEventListener('change', (e) =>
    doSomethingWithFiles(e.target.files),
  );
</script>

La propriété files est un objet FileList, dont je parlerai plus en détail.

Vous pouvez également ajouter l'attribut capture à l'élément pour indiquer au navigateur que vous préférez obtenir une image de l'appareil photo.

<input type="file" accept="image/*" capture />
<input type="file" accept="image/*" capture="user" />
<input type="file" accept="image/*" capture="environment" />

L'ajout de l'attribut capture sans valeur permet au navigateur de décider quelle caméra utiliser, tandis que les valeurs "user" et "environment" indiquent au navigateur de privilégier les caméras avant et arrière, respectivement.

L'attribut capture fonctionne sur Android et iOS, mais est ignoré sur ordinateur. Toutefois, sachez que sur Android, cela signifie que l'utilisateur ne pourra plus choisir une image existante. L'application de la caméra système démarrera directement à la place.

Glisser-déposer

Si vous ajoutez déjà la possibilité d'importer un fichier, vous pouvez enrichir l'expérience utilisateur de deux façons simples.

La première consiste à ajouter sur votre page une cible de dépôt qui permet à l'utilisateur de faire glisser un fichier à partir du bureau ou d'une autre application.

<div id="target">You can drag an image file here</div>
<script>
  const target = document.getElementById('target');

  target.addEventListener('drop', (e) => {
    e.stopPropagation();
    e.preventDefault();

    doSomethingWithFiles(e.dataTransfer.files);
  });

  target.addEventListener('dragover', (e) => {
    e.stopPropagation();
    e.preventDefault();

    e.dataTransfer.dropEffect = 'copy';
  });
</script>

Comme pour l'entrée de fichier, vous pouvez obtenir un objet FileList à partir de la propriété dataTransfer.files de l'événement drop.

Le gestionnaire d'événements dragover vous permet de signaler à l'utilisateur ce qui se passera lorsqu'il supprimera le fichier à l'aide de la propriété dropEffect.

Le glisser-déposer existe depuis longtemps et est bien pris en charge par les principaux navigateurs.

Coller depuis le presse-papiers

La dernière méthode pour récupérer un fichier image existant consiste à utiliser le presse-papiers. Le code pour cela est très simple, mais l'expérience utilisateur est un peu plus difficile à obtenir correctement.

<textarea id="target">Paste an image here</textarea>
<script>
  const target = document.getElementById('target');

  target.addEventListener('paste', (e) => {
    e.preventDefault();
    doSomethingWithFiles(e.clipboardData.files);
  });
</script>

(e.clipboardData.files est un autre objet FileList).

La difficulté de l'API du presse-papiers est que, pour une compatibilité complète entre les navigateurs, l'élément cible doit être à la fois sélectionnable et modifiable. <textarea> et <input type="text">, tout comme les éléments avec l'attribut contenteditable, correspondent à la facture ici. Ils sont aussi conçus pour modifier du texte.

Il peut être difficile d'assurer le bon fonctionnement de ce processus si vous ne souhaitez pas que l'utilisateur puisse saisir du texte. Des astuces comme le fait d'avoir une entrée masquée sélectionnée lorsque vous cliquez sur un autre élément peuvent compliquer le maintien de l'accessibilité.

Traiter un objet FileList

Étant donné que la plupart des méthodes ci-dessus génèrent une FileList, je vais vous expliquer brièvement de quoi il s'agit.

Un FileList est semblable à un Array. Il comporte des clés numériques et une propriété length, mais il ne s'agit pas en réalité d'un tableau. Il n'existe aucune méthode de tableau, comme forEach() ou pop(), et n'est pas itérable. Bien sûr, vous pouvez obtenir un vrai tableau en utilisant Array.from(fileList).

Les entrées de FileList sont des objets File. Ils sont exactement identiques aux objets Blob, sauf qu'ils comportent des propriétés de lecture seule supplémentaires name et lastModified.

<img id="output" />
<script>
  const output = document.getElementById('output');

  function doSomethingWithFiles(fileList) {
    let file = null;

    for (let i = 0; i < fileList.length; i++) {
      if (fileList[i].type.match(/^image\//)) {
        file = fileList[i];
        break;
      }
    }

    if (file !== null) {
      output.src = URL.createObjectURL(file);
    }
  }
</script>

Cet exemple trouve le premier fichier ayant un type MIME d'image, mais il peut également gérer la sélection, le collage ou la suppression de plusieurs images à la fois.

Une fois que vous avez accès au fichier, vous pouvez le modifier comme bon vous semble. Par exemple, vous pouvez :

  • Le dessinez dans un élément <canvas> afin de pouvoir le manipuler.
  • Téléchargez-le sur l'appareil de l'utilisateur.
  • Importez-le sur un serveur avec fetch().

Accéder à l'appareil photo de manière interactive

Maintenant que vous avez couvert vos bases, il est temps de vous améliorer progressivement !

Les navigateurs récents peuvent accéder directement aux appareils photo. Vous pouvez ainsi créer des expériences entièrement intégrées à la page Web, de sorte que l'utilisateur n'ait jamais besoin de quitter son navigateur.

Obtenir l'accès à l'appareil photo

Vous pouvez accéder directement à une caméra et à un micro à l'aide d'une API appelée getUserMedia() dans la spécification WebRTC. L'utilisateur est alors invité à accéder à ses micros et caméras connectés.

La compatibilité avec getUserMedia() est plutôt bonne, mais elle n'est pas encore partout. En particulier, elle n'est pas disponible dans Safari 10 ou une version antérieure, qui, au moment de la rédaction de ce document, correspond toujours à la dernière version stable. Cependant, Apple a annoncé qu'elle serait disponible dans Safari 11.

La prise en charge est cependant très simple.

const supported = 'mediaDevices' in navigator;

Lorsque vous appelez getUserMedia(), vous devez transmettre un objet décrivant le type de contenu multimédia souhaité. Ces choix sont appelés contraintes. Il existe plusieurs contraintes possibles, par exemple si vous préférez une caméra avant ou arrière, si vous souhaitez du son et votre résolution préférée pour le flux.

Toutefois, pour obtenir des données de la caméra, vous n'avez besoin que d'une seule contrainte : video: true.

Si l'opération réussit, l'API renvoie un MediaStream contenant les données de l'appareil photo. Vous pouvez ensuite l'associer à un élément <video> et le lire pour afficher un aperçu en temps réel, ou l'associer à un <canvas> pour obtenir un instantané.

<video id="player" controls autoplay></video>
<script>
  const player = document.getElementById('player');

  const constraints = {
    video: true,
  };

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

En soi, ce n’est pas très utile. Tout ce que vous pouvez faire est de lire les données de la vidéo. Si vous voulez obtenir une image, vous devez effectuer un peu plus de travail.

Prenez un instantané

La meilleure solution pour obtenir une image est de dessiner un cadre de la vidéo sur un canevas.

Contrairement à l'API Web Audio, il n'existe pas d'API de traitement par flux dédiée pour la vidéo sur le Web. Vous devez donc recourir à un petit piratage pour capturer un instantané à partir de la caméra de l'utilisateur.

Le processus est le suivant :

  1. Créez un objet canevas qui tiendra le cadre de l'appareil photo
  2. Accéder au flux de la caméra
  3. L'associer à un élément vidéo
  4. Lorsque vous souhaitez capturer une image précise, ajoutez les données de l'élément vidéo à un objet canevas à l'aide de drawImage().
<video id="player" controls autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    // Draw the video frame to the canvas.
    context.drawImage(player, 0, 0, canvas.width, canvas.height);
  });

  // Attach the video stream to the video element and autoplay.
  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    player.srcObject = stream;
  });
</script>

Une fois que les données de l'appareil photo sont stockées dans la toile, vous pouvez l'utiliser pour effectuer de nombreuses actions. Vous pouvez :

  • Importez-le directement sur le serveur.
  • Stockez-les localement
  • Appliquer des effets originaux à l'image

Conseils

Arrêtez le flux vidéo de la caméra lorsque vous n'en avez pas besoin

Il est recommandé d'arrêter d'utiliser la caméra lorsque vous n'en avez plus besoin. Cela permettra non seulement d'économiser la batterie et la puissance de traitement, mais aussi de rassurer les utilisateurs vis-à-vis de votre application.

Pour empêcher l'accès à la caméra, vous pouvez simplement appeler stop() sur chaque piste vidéo du flux renvoyé par getUserMedia().

<video id="player" controls autoplay></video>
<button id="capture">Capture</button>
<canvas id="canvas" width="320" height="240"></canvas>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    context.drawImage(player, 0, 0, canvas.width, canvas.height);

    // Stop all video streams.
    player.srcObject.getVideoTracks().forEach(track => track.stop());
  });

  navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
    // Attach the video stream to the video element and autoplay.
    player.srcObject = stream;
  });
</script>

Demander l'autorisation d'utiliser l'appareil photo de manière responsable

Si l'utilisateur n'a pas déjà autorisé votre site à accéder à l'appareil photo, dès que vous appelez getUserMedia(), le navigateur l'invite à autoriser votre site à accéder à l'appareil photo.

Les utilisateurs détestent être invités à accéder à des appareils puissants sur leur machine et ils bloquent fréquemment la requête, ou l'ignoreront s'ils ne comprennent pas le contexte pour lequel l'invite a été créée. Il est recommandé de ne demander à accéder à l'appareil photo qu'en cas de besoin. Une fois que l'utilisateur a accordé l'accès, il n'est plus invité à le faire. Toutefois, si l'utilisateur refuse l'accès, vous ne pouvez pas y accéder à nouveau, sauf s'il modifie manuellement les paramètres d'autorisation de l'appareil photo.

Compatibilité

Informations supplémentaires sur la mise en œuvre des navigateurs pour mobile et pour ordinateur:

Nous vous recommandons également d'utiliser le shim adapter.js pour protéger les applications contre les modifications des spécifications WebRTC et les différences de préfixe.

Commentaires