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 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 videoconferencias suelen transmitir este video a usuarios remotos y, al mismo tiempo, renderizarlo en un HTMLVideoElement local, de modo que el usuario local vea constantemente una vista previa de lo que está compartiendo.

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

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

¿Por qué usar el Control de superficie capturado?

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 y alejarlo de la app. Esto presenta algunos desafíos:

  • El usuario no puede ver la app capturada y los videos de usuarios remotos al mismo tiempo, a menos que use la función Pantalla en pantalla o ventanas en paralelo separadas para las pestañas de videoconferencia y la pestaña Compartidos. En una pantalla más pequeña, esto podría ser difícil.
  • El usuario se siente abrumado por la necesidad de pasar de la app de videoconferencia a la superficie capturada.
  • El usuario pierde el acceso a los controles que expone la app de videoconferencia mientras está lejos de ella. 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 participantes remotos. Esto conduce a un escenario demasiado familiar en el que los usuarios remotos piden al presentador que cambie la diapositiva, se desplace un poco hacia arriba y hacia abajo, o ajuste el nivel de zoom.

La API de Captured Surface Control soluciona estos problemas.

¿Cómo uso el Control de superficie capturado?

El uso correcto de Captured Surface Control requiere algunos pasos, como capturar explícitamente una pestaña del navegador y obtener permiso del usuario antes de poder desplazarse y hacer zoom en la pestaña capturada.

Cómo capturar una pestaña del navegador

Primero, pídele 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 pronto.

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

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

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

Si el usuario elige compartir una ventana o una pantalla, está fuera del 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;
}

Solicitud de permiso

La primera invocación de sendWheel() o setZoomLevel() en un objeto CaptureController determinado produce una solicitud de permiso. Si el usuario otorga el permiso, se permitirán invocaciones adicionales 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 de manera única con una capture-session específica, no pueden asociarse con otra sesión de captura y no sobreviven a la navegación de la página donde 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 una solicitud de permiso. Solo las llamadas a sendWheel() y setZoomLevel() requieren un gesto del usuario y únicamente si se debe mostrar el mensaje. Si el usuario hace clic en un botón de acercamiento o alejamiento en la aplicación web, ese gesto del usuario es un determinado. pero 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, en primer lugar, ofrecer al usuario la opción "comenzar el 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.
  }
});

Desplazamiento

Con sendWheel(), una app de captura puede entregar eventos de rueda de la magnitud elegida en las coordenadas que elija dentro del viewport de una pestaña. Para la app capturada, el evento no puede distinguirse 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 el envío de eventos de 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: Las coordenadas en las que se entregará el evento de la rueda.
  • wheelDeltaX y wheelDeltaY: 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 la 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:

  • 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 no lo conoce. El tamaño de la pestaña se encuentra dentro del dominio del navegador y la aplicación web lo desconoce.

La app web usa translateCoordinates() para traducir los desplazamientos relativos al elemento <video> en coordenadas dentro del propio espacio de coordenadas de la pista de video. El navegador también traducirá entre el tamaño de los marcos capturados y el tamaño de la pestaña, y entregará el evento de desplazamiento en un desplazamiento correspondiente a las expectativas de la aplicación web.

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

  • Si la sesión de captura aún no comenzó o ya se detuvo, incluida la detención asíncrona mientras el navegador controle 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 e ignorarlo. (Ten en cuenta que, en 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 niveles de zoom admitidos por el navegador, representados como porcentajes del "nivel de zoom predeterminado", que se define como 100%. Esta lista aumenta de forma monótona y contiene el valor 100.
  • getZoomLevel(): Devuelve 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 tiene éxito. 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, ya sea a través de la app de captura o de la interacción directa con la pestaña capturada.

Las llamadas a setZoomLevel() están restringidas por permiso. las llamadas al otro, los métodos de zoom de solo lectura son "gratuitos", al igual que la detección 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 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 funciones

Para comprobar 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 marca de Captured Surface Control y se puede habilitar en chrome://flags/#captured-surface-control.

Esta función también ingresará a 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 al respecto y su funcionamiento.

Seguridad y privacidad

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

Demostración

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

Cambios en versiones anteriores de Chrome

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

  • En Chrome 124 y versiones anteriores:
    • El permiso, si se otorga, 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ó el permiso de uso a la app. El tipo de error es "NotAllowedError" en Chrome 123 y versiones posteriores.
    • oncapturedzoomlevelchange no está disponible. Puedes aplicar polyfills en esta función con setInterval().

Comentarios

El equipo de Chrome y la comunidad de estándares web quieren escuchar sobre tus experiencias con Captured Surface Control.

Cuéntanos sobre el diseño

¿Hay algún aspecto de Captured Surface Capture que no funcione como esperabas? ¿O faltan métodos o propiedades que necesites para implementar tu idea? ¿Tienes alguna pregunta o comentario sobre el modelo de seguridad? Informa un problema de especificaciones en el repositorio de GitHub o agrega tus ideas sobre un problema existente.

¿Tiene 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 la reproducción. Glitch funciona muy bien para compartir errores reproducibles.