¿Compartes la pantalla de una pestaña del navegador en HTML5?

En los últimos años, ayudé a diferentes empresas a lograr una funcionalidad similar a la de compartir pantalla usando solo tecnologías de navegador. Según mi experiencia, la implementación de VNC únicamente en las tecnologías de las plataformas web (es decir, sin complementos) es un problema difícil. Hay muchas cosas que considerar y muchos desafíos que superar. La retransmisión de la posición del puntero del mouse, el adelanto de las pulsaciones de teclas y la reprocesamiento de colores completos de 24 bits a 60 fps son solo algunos de los problemas.

Captura el contenido de la pestaña

Si eliminamos las complejidades de compartir pantalla tradicional y nos enfocamos en compartir el contenido de una pestaña del navegador, el problema se simplifica mucho a.) capturar la pestaña visible en su estado actual y b. enviar ese “marco” a través del cable. Básicamente, necesitamos una forma de tomar una instantánea del DOM y compartirlo.

La parte para compartir es fácil. Los WebSockets son muy capaces de enviar datos en diferentes formatos (string, JSON, binario). La parte de la creación de instantáneas es un problema mucho más complicado. Proyectos como html2canvas han abordado la captura de pantalla de HTML mediante la reimplementación del motor de renderización del navegador... en JavaScript. Otro ejemplo es Google Feedback, aunque no es de código abierto. Estos tipos de proyectos son muy geniales, pero también son terriblemente lentos. Tendrías suerte si tuvieras una capacidad de procesamiento de 1 FPS y mucho menos que 60 FPS.

En este artículo se analizan algunas de mis soluciones de prueba de concepto favoritas para "compartir pantalla" en una pestaña.

Método 1: Observadores de mutaciones + WebSocket

A principios de este año, +Rafael Weinstein demostró un enfoque para duplicar una pestaña. Su técnica utiliza Mutation Observers y un WebSocket.

En esencia, la pestaña que el presentador comparte observa los cambios en la página y envía diferencias al usuario mediante un websocket. A medida que el usuario se desplaza por la página o interactúa con ella, los observadores detectan estos cambios y los informan al usuario mediante la biblioteca de resumen de mutaciones de Rafael. Esto mantiene el rendimiento. No se envía la página completa en todos los marcos.

Como señala Rafael en el video, esto es solo una prueba de concepto. Aun así, creo que es una excelente forma de combinar una nueva función de la plataforma, como Mutation Observers, con una más antigua, como Websockets.

Método 2: BLOB desde un HTMLDocument + WebSocket binario

El próximo método es uno que descubrí recientemente. Es similar al enfoque de Mutation Observers, pero en lugar de enviar diferencias de resumen, crea un clon de Blob de todo el HTMLDocument y lo envía a través de un websocket binario. Esta es la configuración por configuración:

  1. Volver a escribir todas las URLs de la página para que sean absolutas De esta manera, se evita que los recursos CSS y de imágenes estáticas contengan vínculos rotos.
  2. Clona el elemento del documento de la página: document.documentElement.cloneNode(true);
  3. Haz que la clonación sea de solo lectura, no pueda seleccionarse y evita el desplazamiento con CSS pointer-events: 'none';user-select:'none';overflow:hidden;.
  4. Captura la posición de desplazamiento actual de la página y agrégalos como atributos data-* en el duplicado.
  5. Crea un new Blob() a partir del .outerHTML del duplicado.

El código debería ser similar al siguiente (hice simplificaciones a partir de la fuente completa):

function screenshotPage() {
    // 1. Rewrite current doc's imgs, css, and script URLs to be absolute before
    // we duplicate. This ensures no broken links when viewing the duplicate.
    urlsToAbsolute(document.images);
    urlsToAbsolute(document.querySelectorAll("link[rel='stylesheet']"));
    urlsToAbsolute(document.scripts);

    // 2. Duplicate entire document tree.
    var screenshot = document.documentElement.cloneNode(true);

    // 3. Screenshot should be readyonly, no scrolling, and no selections.
    screenshot.style.pointerEvents = 'none';
    screenshot.style.overflow = 'hidden';
    screenshot.style.userSelect = 'none'; // Note: need vendor prefixes

    // 4. … read on …

    // 5. Create a new .html file from the cloned content.
    var blob = new Blob([screenshot.outerHTML], {type: 'text/html'});

    // Open a popup to new file by creating a blob URL.
    window.open(window.URL.createObjectURL(blob));
}

urlsToAbsolute() contiene regex simples para reescribir URLs relativas o sin esquemas en valores absolutas. Esto es necesario para que las imágenes, el CSS, las fuentes y las secuencias de comandos no fallen cuando se visualizan en el contexto de una URL de BLOB (p.ej., de otro origen).

Un último retoque que hice fue agregar compatibilidad con el desplazamiento. Cuando el presentador se desplace por la página, el usuario debería seguirlo. Para ello, guardo las posiciones actuales scrollX y scrollY como atributos data-* en el HTMLDocument duplicado. Antes de crear el BLOB final, se inyecta un poco de JS que se activa cuando se carga la página:

// 4. Preserve current x,y scroll position of this page. See addOnPageLoad().
screenshot.dataset.scrollX = window.scrollX;
screenshot.dataset.scrollY = window.scrollY;

// 4.5. When screenshot loads (e.g. in blob URL), scroll it to the same location
// of this page. Do this by appending a window.onDOMContentLoaded listener
// which pulls out the screenshot (dupe's) saved scrollX/Y state on the DOM.
var script = document.createElement('script');
script.textContent = '(' + addOnPageLoad_.toString() + ')();'; // self calling.
screenshot.querySelector('body').appendChild(script);

// NOTE: Not to be invoked directly. When the screenshot loads, scroll it
// to the same x,y location of original page.
function addOnPageLoad() {
    window.addEventListener('DOMContentLoaded', function(e) {
    var scrollX = document.documentElement.dataset.scrollX || 0;
    var scrollY = document.documentElement.dataset.scrollY || 0;
    window.scrollTo(scrollX, scrollY);
    });

Falsificar el desplazamiento da la impresión de que tomamos capturas de pantalla de una parte de la página original, cuando, en realidad, duplicamos todo y solo lo reposicionamos. #clever

Demostración

Sin embargo, para compartir pestañas, debemos capturar la pestaña de forma continua y enviársela a los usuarios. Para ello, escribí un pequeño servidor websocket de Node, una app y un favoritolet que demuestra el flujo. Si no te interesa el código, aquí tienes un video breve de cómo funciona:

Mejoras futuras

Una optimización consiste en no duplicar todo el documento en cada fotograma. Eso es un desperdicio y algo en lo que el ejemplo de Mutation Observer hace muy bien. Otra mejora es controlar las imágenes de fondo de CSS relativas en urlsToAbsolute(). Eso es algo que el script actual no tiene en cuenta.

Método 3: API de Chrome Extension + WebSocket binario

En Google I/O 2012, demostré otro enfoque para compartir la pantalla del contenido de una pestaña del navegador. Sin embargo, esta es una trampa. Requiere una API de extensión de Chrome: no la magia de HTML5 pura.

La fuente de esta también está en GitHub, pero lo esencial es la siguiente:

  1. Captura la pestaña actual como una dataURL en formato .png. Las extensiones de Chrome tienen una API para ese chrome.tabs.captureVisibleTab().
  2. Convierte la dataURL en una Blob. Consulta el ayudante convertDataURIToBlob().
  3. Para enviar cada BLOB (marco) al visualizador mediante un websocket binario, configura socket.responseType='blob'.

Ejemplo

Este es el código para tomar una captura de pantalla de la pestaña actual en formato png y enviar el marco a través de un WebSocket:

var IMG_MIMETYPE = 'images/jpeg'; // Update to image/webp when crbug.com/112957 is fixed.
var IMG_QUALITY = 80; // [0-100]
var SEND_INTERVAL = 250; // ms

var ws = new WebSocket('ws://…', 'dumby-protocol');
ws.binaryType = 'blob';

function captureAndSendTab() {
    var opts = {format: IMG_MIMETYPE, quality: IMG_QUALITY};
    chrome.tabs.captureVisibleTab(null, opts, function(dataUrl) {
    // captureVisibleTab returns a dataURL. Decode it -> convert to blob -> send.
    ws.send(convertDataURIToBlob(dataUrl, IMG_MIMETYPE));
    });
}

var intervalId = setInterval(function() {
    if (ws.bufferedAmount == 0) {
    captureAndSendTab();
    }
}, SEND_INTERVAL);

Mejoras futuras

La velocidad de fotogramas es sorprendentemente buena para este, pero podría ser aún mejor. Una mejora sería quitar la sobrecarga que genera convertir dataURL en un BLOB. Por desgracia, chrome.tabs.captureVisibleTab() solo nos proporciona una dataURL. Si devolviera un BLOB o un array escrito, podríamos enviarlo directamente a través del websocket en lugar de hacer la conversión a un BLOB. Destaca crbug.com/32498 para hacerlo.

Método 4: WebRTC: el futuro real

Por último, pero no menos importante.

El futuro del uso compartido de la pantalla en el navegador se desarrollará a través de WebRTC. El 14 de agosto de 2012, el equipo propuso una API de captura de contenido de pestaña de WebRTC para compartir el contenido de las pestañas:

Hasta que este tipo esté listo, nos quedarán con los métodos 1 a 3.

Conclusión

Por lo tanto, la tecnología web actual permite compartir pestañas del navegador.

Pero esa afirmación debe tomarse con cautela. Si bien son claras, las técnicas de este artículo no permiten compartir una UX excelente de una forma u otra. Todo eso cambiará con el esfuerzo de captura de contenido de pestaña de WebRTC. Sin embargo, hasta que sea una realidad, solo nos quedan complementos para el navegador o soluciones limitadas como los que se abordan aquí.

¿Tienes más técnicas? Publicar un comentario