Desplazar y acercar una pestaña capturada

François Beaufort
François Beaufort

Ya es posible compartir pestañas, ventanas y pantallas en la plataforma web con la API de Screen Capture. Cuando una app web llama a getDisplayMedia(), Chrome le solicita al usuario que comparta una pestaña, ventana o pantalla con la app web como un video de MediaStreamTrack.

Muchas apps web que usan getDisplayMedia() le muestran al usuario una vista previa de video de la superficie capturada. Por ejemplo, las apps de videoconferencia suelen transmitir este video a los usuarios remotos mientras lo renderizan en un HTMLVideoElement local para que el usuario local vea constantemente una vista previa de lo que comparten.

En esta documentación, se presenta la nueva API de Captured Surface Control en Chrome, que permite a tu aplicación web desplazarse por una pestaña capturada, así como leer y escribir el nivel de zoom de una pestaña capturada.

Un usuario se desplaza y acerca una pestaña capturada (demostración).

¿Por qué usar el control de superficie capturada?

Todas las apps de videoconferencia tienen el mismo inconveniente: si el usuario desea interactuar con una pestaña o ventana capturada, debe cambiar a esa plataforma, lo que lo quita de la app de videoconferencias. Esto presenta algunos desafíos:

  • El usuario no podrá ver la app capturada ni los videos de usuarios remotos al mismo tiempo, a menos que use la función Pantalla en pantalla o una ventana al lado de la otra para la pestaña de videoconferencia y la pestaña compartida. En una pantalla más pequeña, esto podría ser difícil.
  • El usuario se siente abrumado por la necesidad de saltar entre la app de videoconferencias y la superficie capturada.
  • El usuario pierde el acceso a los controles que expone la app de videoconferencias mientras está lejos de ella. Por ejemplo, una app de chat incorporada, reacciones con emojis, notificaciones sobre los usuarios que solicitan unirse a la llamada, controles multimedia y de diseño, y otras funciones útiles para videoconferencias.
  • El presentador no puede delegar el control a participantes remotos. Esto conduce a un escenario demasiado familiar en el que los usuarios remotos le piden al presentador que cambie la diapositiva, se desplace un poco hacia arriba y abajo o ajuste el nivel de zoom.

La API de Captured Surface Control soluciona estos problemas.

¿Cómo utilizo el control de superficie capturada?

El uso correcto del control de superficie capturada requiere algunos pasos, como capturar explícitamente una pestaña del navegador y obtener permiso del usuario antes de poder desplazar y acercar la pestaña capturada.

Cómo capturar una pestaña del navegador

Comienza por pedirle al usuario que elija una plataforma para compartir con getDisplayMedia() y, en el proceso, asocia un objeto CaptureController con la sesión de captura. Usaremos ese objeto para controlar la superficie capturada lo suficientemente pronto.

const controller = new CaptureController();
const stream = await navigator.mediaDevices.getDisplayMedia({ controller });

A continuación, produce una vista previa local de la superficie capturada en forma de un elemento <video>:

const previewTile = document.querySelector('video');
previewTile.srcObject = stream;

Si el usuario elige compartir una ventana o pantalla, está fuera del alcance por ahora, pero si eligió compartir una pestaña, podemos continuar.

const [track] = stream.getVideoTracks();

if (track.getSettings().displaySurface !== 'browser') {
  // Bail out early if the user didn't pick a tab.
  return;
}

Mensaje de permiso

La primera invocación de sendWheel() o setZoomLevel() en un objeto CaptureController determinado produce una solicitud de permiso. Si el usuario otorga permiso, se permitirán otras invocaciones de estos métodos en ese objeto CaptureController. Si el usuario rechaza el permiso, se rechaza la promesa que se muestra.

Ten en cuenta que los objetos CaptureController están asociados exclusivamente con una sesión de captura específica, no pueden asociarse con otra sesión de captura y no sobreviven a la navegación por la página en la que están definidos. Sin embargo, las sesiones de captura sobreviven a la navegación de la página capturada.

Se requiere un gesto del usuario para mostrarle una solicitud de permiso. Solo las llamadas a sendWheel() y setZoomLevel() requieren un gesto del usuario, y solo si se debe mostrar el mensaje. Si el usuario hace clic en un botón para acercar o alejar en la app web, ese gesto del usuario es un hecho. Sin embargo, si la app desea ofrecer primero control de desplazamiento, los desarrolladores deben tener en cuenta que el desplazamiento no constituye un gesto del usuario. Una posibilidad es primero ofrecer al usuario un botón de "iniciar desplazamiento", como se muestra en el siguiente ejemplo:

const startScrollingButton = document.querySelector('button');

startScrollingButton.addEventListener('click', async () => {
  try {
    const noOpWheelAction = {};

    await controller.sendWheel(noOpWheelAction);
    // The user approved the permission prompt.
    // You can now scroll and zoom the captured tab as shown later in the article.
  } catch (error) {
    return; // Permission denied. Bail.
  }
});

Desplázate

Con sendWheel(), una app de captura puede entregar eventos de la rueda de su magnitud elegida sobre las coordenadas que elija dentro del viewport de una pestaña. El evento no se puede distinguir de la aplicación capturada a partir de la interacción directa del usuario.

Si suponemos que la app de captura emplea un elemento <video> llamado "previewTile", en el siguiente código se muestra cómo retransmitir eventos de envío de la rueda a la pestaña capturada:

const previewTile = document.querySelector('video');

previewTile.addEventListener('wheel', async (event) => {
  // Translate the offsets into coordinates which sendWheel() can understand.
  // The implementation of this translation is further explained below.
  const [x, y] = translateCoordinates(event.offsetX, event.offsetY);
  const [wheelDeltaX, wheelDeltaY] = [-event.deltaX, -event.deltaY];

  try {
    // Relay the user's action to the captured tab.
    await controller.sendWheel({ x, y, wheelDeltaX, wheelDeltaY });
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

El método sendWheel() toma un diccionario con dos conjuntos de valores:

  • x y y: Son las coordenadas en las que se entregará el evento de rueda.
  • wheelDeltaX y wheelDeltaY: Son las magnitudes de los desplazamientos, en píxeles, para los desplazamientos horizontales y verticales, respectivamente. Ten en cuenta que estos valores se invierten en comparación con el evento original de la rueda.

Una posible implementación de translateCoordinates() es la siguiente:

function translateCoordinates(offsetX, offsetY) {
  const previewDimensions = previewTile.getBoundingClientRect();
  const trackSettings = previewTile.srcObject.getVideoTracks()[0].getSettings();

  const x = trackSettings.width * offsetX / previewDimensions.width;
  const y = trackSettings.height * offsetY / previewDimensions.height;

  return [Math.floor(x), Math.floor(y)];
}

Ten en cuenta que hay tres tamaños diferentes en juego anteriormente:

  • El tamaño del elemento <video>.
  • El tamaño de los fotogramas capturados (representados aquí como trackSettings.width y trackSettings.height).
  • El tamaño de la pestaña.

El tamaño del elemento <video> se encuentra dentro del dominio de la app de captura y el navegador lo desconoce. El tamaño de la pestaña está completamente dentro del dominio del navegador y es desconocido para la aplicación web.

La app web usa translateCoordinates() para traducir los desplazamientos relacionados con el elemento <video> en coordenadas dentro del espacio de coordenadas de la pista de video. Del mismo modo, el navegador traducirá entre el tamaño de los marcos capturados y el tamaño de la pestaña, y proporcionará el evento de desplazamiento en un desplazamiento correspondiente a la expectativa de la aplicación web.

La promesa que muestra sendWheel() se puede rechazar en los siguientes casos:

  • Si la sesión de captura aún no se inició o ya se detuvo, incluida la detención asíncrona mientras el navegador controla la acción sendWheel().
  • Si el usuario no otorgó permiso a la app para usar sendWheel()
  • Si la app de captura intenta entregar un evento de desplazamiento en coordenadas que están fuera de [trackSettings.width, trackSettings.height]. Ten en cuenta que estos valores pueden cambiar de forma asíncrona, por lo que es una buena idea detectar el error y, luego, ignorarlo. (Ten en cuenta que, en general, 0, 0 no estaría fuera de los límites, por lo que es seguro usarlos para solicitarle permiso al usuario).

Zoom

La interacción con el nivel de zoom de la pestaña capturada se realiza a través de las siguientes superficies de CaptureController:

  • getSupportedZoomLevels() muestra una lista de los niveles de zoom compatibles con el navegador, representados como porcentajes del "nivel de zoom predeterminado", que se define como 100%. Esta lista aumenta monótonamente y contiene el valor 100.
  • getZoomLevel(): Muestra el nivel de zoom actual de la pestaña.
  • setZoomLevel() establece el nivel de zoom de la pestaña en cualquier valor de número entero presente en getSupportedZoomLevels() y muestra una promesa cuando se realiza correctamente. Ten en cuenta que el nivel de zoom no se restablece al final de la sesión de captura.
  • oncapturedzoomlevelchange te permite escuchar los cambios en el nivel de zoom de una pestaña capturada, ya que los usuarios pueden cambiar ese nivel, ya sea desde la app de captura o mediante la interacción directa con la pestaña capturada.

Las llamadas a setZoomLevel() están restringidas por permiso. Las llamadas a los otros métodos de zoom de solo lectura son "gratuitas", al igual que la escucha de eventos.

En el siguiente ejemplo, se muestra cómo aumentar el nivel de zoom de una pestaña capturada en una sesión de captura existente:

const zoomIncreaseButton = document.getElementById('zoomInButton');

zoomIncreaseButton.addEventListener('click', async (event) => {
  const levels = CaptureController.getSupportedZoomLevels();
  const index = levels.indexOf(controller.getZoomLevel());
  const newZoomLevel = levels[Math.min(index + 1, levels.length - 1)];

  try {
    await controller.setZoomLevel(newZoomLevel);
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

En el siguiente ejemplo, se muestra cómo reaccionar ante los cambios en el nivel de zoom de una pestaña capturada:

controller.addEventListener('capturedzoomlevelchange', (event) => {
  const zoomLevel = controller.getZoomLevel();
  document.querySelector('#zoomLevelLabel').textContent = `${zoomLevel}%`;
});

Detección de funciones

Para verificar si se admite el envío de eventos de la rueda, usa lo siguiente:

if (!!window.CaptureController?.prototype.sendWheel) {
  // CaptureController sendWheel() is supported.
}

Para comprobar si se admite el control del zoom, usa lo siguiente:

if (!!window.CaptureController?.prototype.setZoomLevel) {
  // CaptureController setZoomLevel() is supported.
}

Habilitar el control de superficie capturada

La API de Captured Surface Control está disponible en Chrome para computadoras de escritorio detrás de la función experimental de Captured Surface Control y se puede habilitar en chrome://flags/#captured-surface-control.

Esta función también está ingresando a una prueba de origen a partir de Chrome 122 en computadoras de escritorio, lo que permite a los desarrolladores habilitar la función para que los visitantes de sus sitios recopilen datos de usuarios reales. Consulta Cómo comenzar a usar las pruebas de origen para obtener más información sobre las pruebas de origen y cómo funcionan.

Seguridad y privacidad

La política de permisos de "captured-surface-control" te permite administrar cómo tu app de captura y los iframes incorporados de terceros tienen acceso al control de superficie capturada. Para comprender las compensaciones de seguridad, consulta la sección Consideraciones de privacidad y seguridad de la explicación del control de superficie capturada.

Demostración

Puedes ejecutar la demostración en Glitch para jugar con el control de superficie capturada. Asegúrate de consultar el código fuente.

Cambios en versiones anteriores de Chrome

A continuación, se muestran algunas diferencias clave de comportamiento sobre el control de superficie capturada que debes tener en cuenta:

  • En Chrome 124 y versiones anteriores:
    • Si se otorga el permiso, se limita a la sesión de captura asociada con ese CaptureController, no al origen de captura.
  • En Chrome 122:
    • getZoomLevel() muestra una promesa con el nivel de zoom actual de la pestaña.
    • sendWheel() muestra una promesa rechazada con el mensaje de error "No permission." si el usuario no otorgó permiso para usar a la app. El tipo de error es "NotAllowedError" en Chrome 123 y versiones posteriores.
    • oncapturedzoomlevelchange no está disponible. Puedes polyfill esta función con setInterval().

Comentarios

El equipo de Chrome y la comunidad de estándares de la Web quieren conocer tu experiencia con el control de superficie capturada.

Cuéntanos sobre el diseño

¿Hay algo relacionado con la Captura en superficie capturada que no funciona como esperabas? ¿O faltan métodos o propiedades que necesitas para implementar tu idea? ¿Tienes alguna pregunta o comentario sobre el modelo de seguridad? Informa sobre un problema de especificaciones en el repositorio de GitHub o comenta un problema existente.

¿Tienes problemas con la implementación?

¿Encontraste un error en la implementación de Chrome? ¿O la implementación es diferente de la especificación? Informa el error en https://new.crbug.com. Asegúrate de incluir tantos detalles como puedas, así como instrucciones para reproducirlo. Glitch funciona muy bien para compartir errores reproducibles.