Introducción a la API de captura de pantalla: escanee códigos QR en un navegador

Introduccion


En este artículo, hemos adivinado que hablaremos sobre la API de captura de pantalla. Esta API nació en 2014, y es difícil llamarla nueva, pero el soporte del navegador aún es bastante débil. Sin embargo, puede usarse para proyectos personales o donde este apoyo no es tan importante.


Algunos enlaces para comenzar:



En caso de que el enlace con la demostración se caiga (o si eres demasiado vago para ir allí), así es como se ve la demostración terminada:



Empecemos


Motivación


Recientemente se me ocurrió la idea de una aplicación web que utiliza códigos QR en su trabajo. Y aunque generalmente son convenientes para enviar, por ejemplo, enlaces largos en el mundo real, donde puedes apuntar el teléfono hacia ellos, en el escritorio es un poco más complicado. Si el código QR está en la pantalla del mismo dispositivo en el que necesita leerlo, debe meterse con los servicios para reconocimiento o reconocimiento desde el teléfono y transferir los datos nuevamente a la PC. Inconveniente


Algunos productos, como 1Password , incluyen una solución interesante para esta situación. Si necesita configurar una cuenta desde un código QR, abren una ventana translúcida que puede arrastrar sobre la imagen con el código, y se reconoce automáticamente. Así es como se ve:



Sería ideal si pudiéramos implementar algo similar para nuestra aplicación. Pero probablemente no funcionará en el navegador ...


Conoce - getDisplayMedia


Pues casi. Aquí la API de captura de pantalla con su único método getDisplayMedia nos getDisplayMedia . getDisplayMedia es como getUserMedia , solo para la pantalla del dispositivo, en lugar de su cámara. Desafortunadamente, el soporte del navegador, como se mencionó anteriormente, está lejos de ser tan extendido como el acceso a la cámara. Según MDN, se puede usar en Firefox, Chrome, Edge (aunque está en el lugar equivocado, directamente en el navigator , y no en navigator.mediaDevices ) + Edge Mobile y ... Opera para Android.


Una selección bastante curiosa de navegadores móviles junto a los Big Two esperados.


La API en sí es extremadamente simple. Funciona igual que getUserMedia , pero le permite capturar una transmisión de video desde una de las superficies de visualización definidas:


  • desde el monitor (pantalla completa),
  • desde una ventana o todas las ventanas de una determinada aplicación,
  • desde un navegador , o más bien desde un documento específico. En Chrome, este documento es una pestaña separada, pero en FF no existe tal opción.

API del navegador, que le permite mirar más allá del navegador ... Suena familiar y generalmente se reduce a algunos problemas, pero en este caso puede ser bastante conveniente. Puede capturar una imagen desde otras ventanas y, por ejemplo, reconocer y traducir texto en tiempo real, como Google Translate Camera. Bueno, y probablemente hay muchos más usos interesantes.


Recogemos


Entonces, descubrimos las capacidades que la API nos brinda. Que sigue


Y luego necesitamos adelantar esta transmisión de video a imágenes en las que podamos trabajar. Para hacer esto, usamos los elementos <video> , <canvas> y algunos más JS.


Un primer plano del proceso se parece a esto:


  • Transmisión directa a <video> ;
  • Con cierta frecuencia, dibuje el contenido de <video> en <canvas> ;
  • Recopile un objeto ImageData de <canvas> utilizando el método de contexto getImageData 2D.

Todo este procedimiento puede sonar un poco extraño debido a una tubería tan larga, pero este método es bastante popular y se utilizó para capturar datos de cámaras web en getUserMedia .


Omitiendo todo lo irrelevante, para iniciar la secuencia y extraer el marco de ella, necesitamos el siguiente código:


 async function run() { const video = document.createElement('video'); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const displayMediaOptions = { video: { cursor: "never" }, audio: false } video.srcObject = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions); const videoTrack = video.srcObject.getVideoTracks()[0]; const { height, width } = videoTrack.getSettings(); context.drawImage(video, 0, 0, width, height); return context.getImageData(0, 0, width, height); } await run(); 

Como se mencionó anteriormente: primero creamos los elementos <video> y <canvas> y le pedimos al lienzo un contexto 2D ( CanvasRenderingContext2D ).


Luego definimos restricciones / condiciones de flujo. A diferencia de las transmisiones de la cámara, hay pocas. Decimos que no queremos ver el cursor y que no necesitamos audio. Aunque en el momento de escribir esto, la captura de audio todavía no es compatible con nadie.


Después de eso, conectamos la secuencia recibida de tipo MediaStream al elemento <video> . Tenga en cuenta que getDisplayMedia devuelve una promesa.


Finalmente, de los datos recibidos sobre la transmisión, recordamos la resolución del video para dibujarlo correctamente en el lienzo, dibujar el marco y extraer el objeto ImageData del ImageData .


Para un uso completo, lo más probable es que desee procesar marcos en un bucle en lugar de una vez. Por ejemplo, mientras espera cuando la imagen deseada aparece en el cuadro. Y aquí hay que decir algunas palabras.


Cuando se trata de "manejar algo en el DOM en un bucle constante", lo primero que viene a la mente es, probablemente, requestAnimationFrame . Sin embargo, en nuestro caso, usarlo no funcionará. La cuestión es que cuando la pestaña deja de estar activa, los navegadores pausan el procesamiento del bucle rAF. En nuestro caso, es en este momento que vamos a querer procesar las imágenes.


En este sentido, en lugar de rAF, usaremos el viejo setInterval . Pero las cosas no son tan fáciles con él. En una pestaña inactiva, el intervalo entre las operaciones de devolución de llamada es de al menos 1 segundo . Sin embargo, esto es suficiente para nosotros.


Finalmente, cuando llegamos a los marcos, podemos procesarlos como queramos. Para los fines de esta demostración, utilizaremos la biblioteca jsQR . Es extremadamente simple: la entrada acepta ImageData , el ancho y la altura de la imagen. Si la imagen recibida tiene un código QR, obtendrá un objeto JS con datos reconocidos.
Complementemos nuestro ejemplo anterior con solo un par de líneas de código más:


 const imageData = await run(); const code = jsQR(imageData.data, streamWidth, streamHeight); 

Hecho


NPM


Pensé que el código principal detrás de este ejemplo podría empaquetarse en una biblioteca npm y ahorrar algo de tiempo para la configuración inicial en un uso posterior. La biblioteca es muy simple, en esta etapa solo acepta la devolución de llamada a la que se enviará ImageData , y un parámetro adicional es la frecuencia de envío de datos. Todo el procesamiento que necesita para traer el suyo. Pensaré si tiene sentido expandir su funcionalidad.


La biblioteca se llama stream-display : NPM | Github


Su uso se reduce literalmente a tres líneas de código y una devolución de llamada:


 const callback = imageData => {...} // do whatever with those images const capture = new StreamDisplay(callback); // specify where the ImageData will go await capture.startCapture(); // when ready capture.stopCapture(); // when done 

La demostración se puede ver aquí . También hay una versión de CodePen para experimentos rápidos. Ambos ejemplos usan el paquete NPM anterior.


Un poco sobre pruebas


Al empacar este código en la biblioteca, tuve que pensar en cómo probarlo. Absolutamente no quería arrastrar 50 MB de Chrome sin cabeza para ejecutar algunas pequeñas pruebas en él. Y aunque la idea de escribir talones para todos los componentes parecía demasiado dolorosa, al final lo hice.
Como corredor de prueba, se seleccionó la tape . Esto es lo que finalmente tuve que simular:


  • objeto de document y elementos DOM. Para esto, tomé jsdom ;
  • algunos métodos jsdom que carecen de implementación: HTMLMediaElement#play , HTMLCanvasElement#getContext y navigator.mediaDevices#getDisplayMedia ;
  • tiempo Para hacer esto, utilicé useFakeTimers biblioteca de useFakeTimers , que bajo el capó llama lolex . Establece sus reemplazos en setInterval , requestAnimationFrame y muchas otras funciones que funcionan con el tiempo, y también le permite controlar el flujo de este tiempo falso. Pero tenga cuidado: jsdom en un lugar de su proceso de inicialización utiliza el paso del tiempo, y si enciende el sinon primero, todo se congelará.

También utilicé sinon para todos los apéndices de función que debían monitorearse. El resto fue implementado por funciones JS vacías.


Por supuesto, puede elegir las herramientas con las que ya está familiarizado. Pero, espero que esta lista le permita prepararla con anticipación, ya que ahora sabe con qué tiene que lidiar.


El resultado final se puede ver en el repositorio de la biblioteca. No se ve muy bonito, pero funciona.


Conclusión


La solución resultó no ser tan elegante como la ventana transparente mencionada al comienzo del artículo, pero tal vez la web llegue a esto algún día. Uno solo puede esperar que cuando los navegadores aprendan a ver a través de sus ventanas, estas capacidades serán estrictamente controladas por nosotros. Mientras tanto, recuerde que cuando manipula la pantalla en Chrome, se puede analizar, grabar, etc. ¡Así que no hurgue más de lo necesario!


Espero que alguien después de este artículo haya aprendido un nuevo truco para ellos. Si tiene ideas sobre qué más se puede usar, escriba en los comentarios. Y hasta pronto.

Source: https://habr.com/ru/post/460825/


All Articles