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, una ventana o una pantalla con la app web como un video MediaStreamTrack.

Muchas apps web que usan getDisplayMedia() le muestran al usuario una vista previa en video de la superficie capturada. Por ejemplo, las apps de videoconferencias suelen transmitir este video a usuarios remotos y, al mismo tiempo, renderizarlo en un HTMLVideoElement local para que el usuario local vea constantemente una vista previa de lo que comparte.

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

Un usuario desplaza y acerca una pestaña capturada (demo).

¿Por qué usar el control de superficie capturada?

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

  • El usuario no puede ver la app capturada y los videos de los usuarios remotos al mismo tiempo, a menos que use la función Pantalla en pantalla o ventanas separadas 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 ve abrumado por la necesidad de alternar entre la app de videoconferencia y la superficie capturada.
  • El usuario pierde el acceso a los controles que expone la app de videoconferencia mientras no la está usando, por ejemplo, una app de chat incorporada, reacciones con emojis, notificaciones sobre usuarios que solicitan unirse a la llamada, controles multimedia y de diseño, y otras funciones útiles de videoconferencia.
  • El presentador no puede delegar el control a los participantes remotos. Esto genera la situación tan conocida en la que los usuarios remotos le piden al presentador que cambie la diapositiva, que se desplace un poco hacia arriba y abajo, o que ajuste el nivel de zoom.

La API de Captured Surface Control aborda estos problemas.

¿Cómo uso el control de superficie capturada?

Para usar el control de superficie capturada de forma correcta, debes seguir algunos pasos, como capturar explícitamente una pestaña del navegador y obtener el permiso del usuario antes de poder desplazarte y acercar la pestaña capturada.

Captura una pestaña del navegador

Para comenzar, pídele al usuario que elija una superficie para compartir con getDisplayMedia() y, en el proceso, asocia un objeto CaptureController con la sesión de captura. Pronto usaremos ese objeto para controlar la superficie capturada.

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

A continuación, crea 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 una pantalla, eso está fuera de alcance por el momento, pero si elige 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 genera un mensaje de permiso. Si el usuario otorga permiso, se permiten más 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 se asocian de forma exclusiva con una captura-sesión específica, no se pueden asociar con otra captura-sesión y no sobreviven a la navegación de la página en la que se definen. Sin embargo, las sesiones de captura sobreviven a la navegación de la página capturada.

Se requiere un gesto del usuario para mostrarle un mensaje 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 la imagen en la app web, ese gesto del usuario es un hecho. Sin embargo, si la app desea ofrecer primero el control de desplazamiento, los desarrolladores deben tener en cuenta que el desplazamiento no constituye un gesto del usuario. Una posibilidad es ofrecer primero al usuario un botón para comenzar a desplazarse, como 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.
  }
});

Desplazamiento

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

Suponiendo que la app de captura emplee un elemento <video> llamado "previewTile", el siguiente código muestra cómo retransmitir eventos 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 explained further 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 debe 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 de rueda original.

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 en el código anterior:

  • Es el tamaño del elemento <video>.
  • Es el tamaño de los fotogramas capturados (representados aquí como trackSettings.width y trackSettings.height).
  • Es el tamaño de la tabulación.

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

La app web usa translateCoordinates() para traducir los desplazamientos en relación con el elemento <video> en coordenadas dentro del espacio de coordenadas de la pista de video. El navegador también traducirá entre el tamaño de los fotogramas capturados y el tamaño de la pestaña, y entregará el evento de desplazamiento en un desplazamiento correspondiente a la expectativa de la app 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 de forma 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 recomendable detectar el error y, luego, ignorarlo. (Ten en cuenta que, por lo general, 0, 0 no estaría fuera de los límites, por lo que es seguro usarlo 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 plataformas 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 es monótonamente creciente 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 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 el nivel de zoom a través de la app de captura o mediante la interacción directa con la pestaña capturada.

Las llamadas a setZoomLevel() están restringidas por permisos. Las llamadas a los otros métodos de zoom de solo lectura son "gratuitas", al igual que escuchar 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 a los cambios de nivel de zoom de una pestaña capturada:

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

Detección de atributos

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

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

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

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

Habilita el control de superficie capturada

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

Esta función también ingresará en una prueba de origen a partir de Chrome 122 para computadoras, lo que permitirá a los desarrolladores habilitar la función para que los visitantes de sus sitios recopilen datos de usuarios reales. Consulta Comienza 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 de terceros incorporados tienen acceso al Control de superficie capturada. Para comprender las compensaciones de seguridad, consulta la sección Consideraciones sobre la privacidad y la seguridad de la explicación del control de la superficie capturada.

Demostración

Para jugar con el control de superficie capturado, ejecuta la demo en Glitch. Asegúrate de consultar el código fuente.

Cambios en versiones anteriores de Chrome

Estas son algunas diferencias de comportamiento clave sobre el control de la superficie capturada que debes tener en cuenta:

  • En Chrome 124 y versiones anteriores:
    • El permiso, si se otorga, se aplica a la sesión de captura asociada con ese CaptureController, no al origen de captura.
  • En Chrome 122, haz lo siguiente:
    • 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 a la app para usarla. El tipo de error es "NotAllowedError" en Chrome 123 y versiones posteriores.
    • oncapturedzoomlevelchange no está disponible. Puedes usar setInterval() para realizar el polifil de esta función.

Comentarios

El equipo de Chrome y la comunidad de estándares web quieren conocer tus experiencias con el control de superficie capturada.

Cuéntanos sobre el diseño

¿Hay algo relacionado con la Captura de 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 un problema de especificación en el repositorio de GitHub o agrega tus comentarios a 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 un error en https://new.crbug.com. Asegúrate de incluir tantos detalles como sea posible, así como instrucciones para reproducirlo. Glitch es ideal para compartir errores reproducibles.