Captura una imagen del usuario

La mayoría de los navegadores pueden acceder a la cámara del usuario.

Ahora muchos navegadores tienen la habilidad de acceder a la entrada de audio y video desde el usuario. Sin embargo, según el navegador puede ser una experiencia en línea y totalmente dinámica o puede ser delegada a otra app en el dispositivo del usuario. Además, no todos los dispositivos tienen cámara. Entonces, ¿cómo puedes crear una experiencia que use una imagen generada por el usuario que funcione bien en todas partes?

Comienza de forma simple y progresiva

Si deseas mejorar tu experiencia de manera progresiva, debes comenzar con algo que funcione en todas partes. Lo más fácil es simplemente pedirle al usuario un archivo grabado previamente.

Cómo solicitar una URL

Esta es la mejor opción compatible, pero menos satisfactoria. Pídele al usuario que te proporcione una URL y, luego, úsala. Para solo mostrar la imagen, funciona en todas partes. Crea un elemento img, establece el src y listo.

Sin embargo, si quieres manipular la imagen de alguna manera, las cosas se complican un poco. CORS evita que accedas a los píxeles reales, a menos que el servidor establezca los encabezados adecuados y marques la imagen como origen cruzado; la única forma práctica de hacerlo es ejecutar un servidor proxy.

Entrada de archivos

También puedes usar un elemento de entrada de archivo simple, incluido un filtro accept que indique que solo quieres archivos de imagen.

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

Este método funciona en todas las plataformas. En el escritorio, se le solicitará al usuario que cargue un archivo de imagen desde el sistema de archivos. En Chrome y Safari para iOS y Android, este método le permite al usuario elegir qué app usar para capturar la imagen, incluida la opción de tomar una foto directamente con la cámara o elegir un archivo de imagen existente.

Un menú de Android, con dos opciones: capturar imágenes y archivos Un menú de iOS con tres opciones: tomar foto, biblioteca de fotos y iCloud

Los datos se pueden adjuntar a un <form> o manipular con JavaScript. Para ello, debes detectar un evento onchange en el elemento de entrada y, luego, leer la propiedad files del evento 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 propiedad files es un objeto FileList, del que hablaré más adelante.

De manera opcional, también puedes agregar el atributo capture al elemento, que le indica al navegador que prefieres obtener una imagen de la cámara.

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

Agregar el atributo capture sin un valor permite que el navegador decida qué cámara usar, mientras que los valores "user" y "environment" le indican al navegador que prefiera las cámaras frontal y posterior, respectivamente.

El atributo capture funciona en iOS y Android, pero se ignora en computadoras. Sin embargo, ten en cuenta que, en Android, esto significa que el usuario ya no tendrá la opción de elegir una foto existente. En su lugar, se iniciará directamente la app de la cámara del sistema.

Arrastrar y soltar

Si ya agregas la capacidad de subir un archivo, hay algunas maneras sencillas de enriquecer la experiencia del usuario.

La primera es agregar un destino de caída a tu página que le permita al usuario arrastrar un archivo desde la computadora de escritorio o desde otra aplicación.

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

Al igual que con la entrada de archivo, puedes obtener un objeto FileList de la propiedad dataTransfer.files del evento drop.

El controlador de eventos dragover te permite indicarle al usuario lo que sucederá cuando suelte el archivo con la propiedad dropEffect.

La función de arrastrar y soltar existe desde hace mucho tiempo y es compatible con los principales navegadores.

Cómo pegar desde el portapapeles

La última forma de obtener un archivo de imagen existente es desde el portapapeles. El código es muy simple, pero la experiencia del usuario es un poco más difícil de hacer bien.

<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 es otro objeto FileList).

La parte complicada de la API del portapapeles es que, para la compatibilidad total con varios navegadores, el elemento de destino debe poder seleccionarse y editarse. Tanto <textarea> como <input type="text"> cumplen con los requisitos aquí, al igual que los elementos con el atributo contenteditable. Pero, obviamente, también están diseñados para editar texto.

Puede ser difícil hacer que esto funcione sin problemas si no quieres que el usuario pueda ingresar texto. Trucos como tener una entrada oculta que se selecciona cuando haces clic en algún otro elemento pueden dificultar el mantenimiento de la accesibilidad.

Controla un objeto FileList

Dado que la mayoría de los métodos anteriores producen un FileList, debo hablar un poco sobre qué es.

Un FileList es similar a un Array. Tiene claves numéricas y una propiedad length, pero en realidad no es un array. No hay métodos de array, como forEach() o pop(), y no es iterable. Por supuesto, puedes obtener un Array real con Array.from(fileList).

Las entradas de FileList son objetos File. Son exactamente iguales a los objetos Blob, excepto que tienen propiedades adicionales de solo lectura name y 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>

En este ejemplo, se encuentra el primer archivo que tiene un tipo MIME de imagen, pero también podría controlar varias imágenes que se seleccionan, pegan o dejan caer a la vez.

Una vez que obtienes acceso al archivo, puedes hacer lo que quieras con él. Por ejemplo, puedes hacer lo siguiente:

  • Dibujarlo en un elemento <canvas> para que puedas manipularlo
  • Descárgalo en el dispositivo del usuario
  • Sube el archivo a un servidor con fetch()

Acceder a la cámara de manera interactiva

Ahora que ya conoces los aspectos básicos, es hora de mejorar de forma progresiva.

Los navegadores modernos pueden obtener acceso directo a las cámaras, lo que te permite compilar experiencias que estén totalmente integradas con la página web, de modo que el usuario nunca tenga que abandonar el navegador.

Adquirir acceso a la cámara

Puedes acceder de forma directa a una cámara y a un micrófono usando una API en la especificación WebRTC llamada getUserMedia(). Con esto se le solicita al usuario acceso a sus micrófonos y cámaras conectados.

La compatibilidad con getUserMedia() es bastante buena, pero aún no está disponible en todas partes. En particular, no están disponibles en Safari 10 ni versiones anteriores, que al momento de escribir este código siguen siendo la versión estable más reciente. Sin embargo, Apple anunció que estará disponible en Safari 11.

Sin embargo, es muy fácil detectar la compatibilidad.

const supported = 'mediaDevices' in navigator;

Cuando llames a getUserMedia(), debes pasar un objeto que describa el tipo de contenido multimedia que deseas. Estas opciones se denominan restricciones. Existen varias restricciones posibles, como si prefieres una cámara frontal o posterior, si quieres audio y la resolución que prefieres para la transmisión.

Sin embargo, para obtener datos de la cámara, solo necesitas una restricción, que es video: true.

Si se ejecuta de forma correcta, la API mostrará un MediaStream que contiene datos de la cámara. Luego, podrás adjuntarlo a un elemento <video> y reproducirlo para mostrar una vista previa en tiempo real, o bien adjuntarlo a una <canvas> para obtener una instantánea.

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

  const constraints = {
    video: true,
  };

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

Por sí solo, no es tan útil. Todo lo que puedes hacer es tomar los datos del video y reproducirlos. Si quieres obtener una imagen, debes hacer un poco de trabajo adicional.

Toma una instantánea

La mejor opción admitida para obtener una imagen es dibujar un fotograma del video en un lienzo.

A diferencia de la API de Web Audio, no hay una API de procesamiento de transmisión exclusiva para videos en la Web, por lo que debes recurrir a un poco de hackeo para capturar una instantánea desde la cámara del usuario.

El proceso es el siguiente:

  1. Crea un objeto de lienzo que contenga el marco de la cámara
  2. Obtén acceso a la transmisión de la cámara
  3. Adjúntalo a un elemento de video
  4. Cuando quieras capturar un fotograma preciso, agrega los datos del elemento de video a un objeto de lienzo con drawImage().
<video id="player" controls playsinline 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>

Una vez que tienes los datos de la cámara almacenados en el lienzo, puedes hacer muchas cosas con ellos. Intenta realizar lo siguiente:

  • Sube los archivos directamente al servidor
  • Almacenarlos localmente
  • Aplica efectos extravagantes a la imagen

Sugerencias

Detener la transmisión desde la cámara cuando no sea necesaria

Es una buena práctica dejar de usar la cámara cuando ya no la necesites. Esto no solo ahorrará batería y potencia de procesamiento, sino que también hará que los usuarios confíen en tu aplicación.

Para detener el acceso a la cámara, puedes simplemente llamar a stop() en cada pista de video para la transmisión que devuelve getUserMedia().

<video id="player" controls playsinline 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>

Solicitar permiso para usar la cámara de forma responsable

Si el usuario no le otorgó acceso a la cámara a tu sitio anteriormente, en el momento en que llames a getUserMedia(), el navegador le solicitará al usuario que le otorgue permiso a tu sitio para acceder a la cámara.

A los usuarios no les gusta que se les solicite acceso a los dispositivos potentes en su máquina y suelen bloquear la solicitud o la ignoran si no comprenden el contexto para el que se creó la instrucción. La práctica recomendada es solicitar acceso a la cámara solo la primera vez que se necesita. Una vez que el usuario haya otorgado el acceso, no se le volverá a preguntar. Sin embargo, si el usuario rechaza el acceso, no podrás volver a obtener acceso, a menos que cambie de forma manual la configuración de permisos de la cámara.

Compatibilidad

Más información acerca de la implementación de navegadores para dispositivos móviles y computadoras de escritorio:

También recomendamos usar la corrección adapter.js para proteger las apps de los cambios de especificación WebRTC y las diferencias de prefijos.

Comentarios