Procesamiento de videos con WebCodecs

Manipular los componentes de transmisión de video por Internet

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

Las tecnologías web modernas proporcionan amplias formas de trabajar con videos. API de Media Stream, API de Media Recording, API de Media Source, y la API de WebRTC se suman en un conjunto de herramientas completo para grabar, transferir y reproducir transmisiones de video. A pesar de que resuelven ciertas tareas de alto nivel, estas APIs no permiten que la Web los programadores trabajan con componentes individuales de una transmisión de video por Internet como fotogramas y fragmentos de audio o video codificados sin combinar. Para obtener acceso de bajo nivel a estos componentes básicos, los desarrolladores usan WebAssembly para incorporar códecs de audio y video al navegador. Pero dado que los navegadores modernos ya incluyen una variedad de códecs (que suelen ser acelerado por hardware), reempaquetarlas como WebAssembly parece un desperdicio de recursos humanos y informáticos.

La API de WebCodecs elimina esta ineficiencia. ya que ofrece a los programadores una forma de usar componentes multimedia que ya están presentes en el navegador. En particular, haz lo siguiente:

  • Decodificadores de audio y video
  • Codificadores de audio y video
  • Fotogramas de video sin procesar
  • Decodificadores de imágenes

La API de WebCodecs es útil para las aplicaciones web que requieren un control total del forma en que se procesa el contenido multimedia, como editores de video, videoconferencias, transmisión, etc.

Flujo de trabajo del procesamiento de videos

Los fotogramas son el elemento central del procesamiento de video. Por lo tanto, en WebCodecs, la mayoría de las clases consumen o producen fotogramas. Los codificadores de video convierten fotogramas en bloques en bloques. Los decodificadores de video hacen lo contrario.

Además, VideoFrame funciona bien con otras APIs web, ya que es un CanvasImageSource y tiene un constructor que acepta CanvasImageSource. Por lo tanto, se puede usar en funciones como drawImage() y texImage2D(). También se puede construir a partir de lienzos, mapas de bits, elementos de video y otros marcos de video.

La API de WebCodecs funciona bien en conjunto con las clases de la API de Insertable Streams. que conectan WebCodecs a pistas de transmisión multimedia.

  • MediaStreamTrackProcessor divide las pistas de contenido multimedia en fotogramas individuales.
  • MediaStreamTrackGenerator crea una pista multimedia a partir de una transmisión de fotogramas.

WebCodecs y web workers

Por diseño, la API de WebCodecs hace todo el trabajo pesado de forma asíncrona y fuera del subproceso principal. Pero dado que las devoluciones de llamada de marco y fragmento a menudo se pueden llamar varias veces por segundo, pueden desordenar el subproceso principal y, por lo tanto, hacer que el sitio web sea menos responsivo. Por lo tanto, es preferible mover el manejo de marcos individuales y fragmentos codificados a un trabajador web.

Para ayudar con eso, ReadableStream. proporciona una forma conveniente de transferir automáticamente todos los fotogramas provenientes de un contenido multimedia seguimiento al trabajador. Por ejemplo, se puede usar MediaStreamTrackProcessor para obtener un ReadableStream para una pista de transmisión de contenido multimedia que proviene de la cámara web. Después La transmisión se transfiere a un trabajador web, en el que las tramas se leen una por una y se ponen en cola. en un VideoEncoder.

Con HTMLCanvasElement.transferControlToOffscreen, incluso la renderización se puede realizar fuera del subproceso principal. Pero si todas las herramientas de alto nivel se no es conveniente, el VideoFrame en sí es transferible y se puede se mueva entre los trabajadores.

WebCodecs en acción

Codificación

La ruta desde Canvas o ImageBitmap a la red o al almacenamiento
La ruta desde un Canvas o un ImageBitmap a la red o al almacenamiento

Todo comienza con un VideoFrame. Existen tres formas de construir fotogramas.

  • Desde una fuente de imagen, como un lienzo, un mapa de bits de imagen o un elemento de video

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • Usa MediaStreamTrackProcessor para extraer fotogramas de un MediaStreamTrack.

    const stream = await navigator.mediaDevices.getUserMedia({});
    const track = stream.getTracks()[0];
    
    const trackProcessor = new MediaStreamTrackProcessor(track);
    
    const reader = trackProcessor.readable.getReader();
    while (true) {
      const result = await reader.read();
      if (result.done) break;
      const frameFromCamera = result.value;
    }
    
  • Crea un fotograma a partir de su representación de píxeles binarios en un BufferSource.

    const pixelSize = 4;
    const init = {
      timestamp: 0,
      codedWidth: 320,
      codedHeight: 200,
      format: "RGBA",
    };
    const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
    for (let x = 0; x < init.codedWidth; x++) {
      for (let y = 0; y < init.codedHeight; y++) {
        const offset = (y * init.codedWidth + x) * pixelSize;
        data[offset] = 0x7f;      // Red
        data[offset + 1] = 0xff;  // Green
        data[offset + 2] = 0xd4;  // Blue
        data[offset + 3] = 0x0ff; // Alpha
      }
    }
    const frame = new VideoFrame(data, init);
    

Más allá de su origen, los fotogramas se pueden codificar Objetos EncodedVideoChunk con VideoEncoder

Antes de la codificación, VideoEncoder debe tener dos objetos JavaScript:

  • Init diccionario con dos funciones para manejar fragmentos codificados y errores. El desarrollador define estas funciones y no se pueden cambiar después se pasan al constructor VideoEncoder.
  • Objeto de configuración del codificador, que contiene parámetros para la salida transmisión de video por Internet. Puedes cambiar estos parámetros más adelante llamando a configure().

El método configure() arrojará una NotSupportedError si la configuración no está compatibles con el navegador. Se recomienda que llames al método estático VideoEncoder.isConfigSupported() con la configuración para verificar de antemano si que la configuración sea compatible y esperará a que se cumpla su promesa.

const init = {
  output: handleChunk,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  width: 640,
  height: 480,
  bitrate: 2_000_000, // 2 Mbps
  framerate: 30,
};

const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
  const encoder = new VideoEncoder(init);
  encoder.configure(config);
} else {
  // Try another config.
}

Después de configurar el codificador, estará listo para aceptar fotogramas a través del método encode(). Tanto configure() como encode() se muestran de inmediato sin esperar a que el trabajo real a completar. Permite que varios marcos se pongan en cola para codificar en el al mismo tiempo, mientras que encodeQueueSize muestra cuántas solicitudes hay en la cola. para que finalicen las codificaciones anteriores. Los errores se informan arrojando inmediatamente una excepción, en caso de que los argumentos o el orden de las llamadas a los métodos infringe el contrato de la API, o si llama a error() para problemas encontrados en la implementación del códec. Si la codificación se completa correctamente, output() se llama a la devolución de llamada con un nuevo fragmento codificado como argumento. Otro detalle importante aquí es que los marcos deben contarse cuando no están más tiempo llamando a close().

let frameCounter = 0;

const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);

const reader = trackProcessor.readable.getReader();
while (true) {
  const result = await reader.read();
  if (result.done) break;

  const frame = result.value;
  if (encoder.encodeQueueSize > 2) {
    // Too many frames in flight, encoder is overwhelmed
    // let's drop this frame.
    frame.close();
  } else {
    frameCounter++;
    const keyFrame = frameCounter % 150 == 0;
    encoder.encode(frame, { keyFrame });
    frame.close();
  }
}

Por último, es momento de terminar de codificar el código escribiendo una función que se encargue fragmentos de video codificados que salen del codificador. Por lo general, esta función envía fragmentos de datos a través de la red o los múltiples en un contenido multimedia. para su almacenamiento.

function handleChunk(chunk, metadata) {
  if (metadata.decoderConfig) {
    // Decoder needs to be configured (or reconfigured) with new parameters
    // when metadata has a new decoderConfig.
    // Usually it happens in the beginning or when the encoder has a new
    // codec specific binary configuration. (VideoDecoderConfig.description).
    fetch("/upload_extra_data", {
      method: "POST",
      headers: { "Content-Type": "application/octet-stream" },
      body: metadata.decoderConfig.description,
    });
  }

  // actual bytes of encoded data
  const chunkData = new Uint8Array(chunk.byteLength);
  chunk.copyTo(chunkData);

  fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream" },
    body: chunkData,
  });
}

Si en algún momento necesitas asegurarte de que todas las solicitudes de codificación pendientes se completó, puedes llamar a flush() y esperar a que se prometa.

await encoder.flush();

Decodificación

Es la ruta de acceso desde la red o el almacenamiento a un lienzo o un ImageBitmap.
Es la ruta de acceso desde la red o el almacenamiento a un Canvas o ImageBitmap.

La configuración de un VideoDecoder es similar a lo que se hizo para VideoEncoder: se pasan dos funciones cuando se crea el decodificador y el códec. se proporcionan los parámetros a configure().

El conjunto de parámetros de códecs varía de códec a códec. Por ejemplo, códec H.264 Es posible que necesites un BLOB binario de AVCC, a menos que esté codificado en el formato de Anexo B (encoderConfig.avc = { format: "annexb" }).

const init = {
  output: handleFrame,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  codedWidth: 640,
  codedHeight: 480,
};

const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
  const decoder = new VideoDecoder(init);
  decoder.configure(config);
} else {
  // Try another config.
}

Una vez que se inicialice el decodificador, puedes comenzar a alimentarlo con objetos EncodedVideoChunk. Para crear un fragmento, necesitarás lo siguiente:

  • Un BufferSource de datos de video codificados
  • la marca de tiempo de inicio del fragmento en microsegundos (tiempo multimedia del primer fotograma codificado en el fragmento)
  • el tipo de bloque, uno de los siguientes:
    • key si el fragmento se puede decodificar independientemente de los fragmentos anteriores.
    • delta si el fragmento solo se puede decodificar después de que se decodificaron uno o más fragmentos anteriores.

Además, cualquier fragmento emitido por el codificador está listo para el decodificador sin modificaciones. Todo lo dicho sobre los informes de errores y la naturaleza asíncrona de los métodos del codificador también es verdadero para los decodificadores.

const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
  const chunk = new EncodedVideoChunk({
    timestamp: responses[i].timestamp,
    type: responses[i].key ? "key" : "delta",
    data: new Uint8Array(responses[i].body),
  });
  decoder.decode(chunk);
}
await decoder.flush();

Ahora es el momento de mostrar cómo se puede mostrar un marco recién decodificado en la página. Es Asegúrate de que la devolución de llamada de salida del decodificador (handleFrame()) regresa con rapidez. En el siguiente ejemplo, solo agrega un fotograma a la cola de ya que están listos para la renderización. La renderización se realiza por separado y consta de dos pasos:

  1. Espera el momento adecuado para mostrar el cuadro.
  2. Dibujando el marco en el lienzo.

Cuando ya no se necesite un fotograma, llama a close() para liberar la memoria subyacente. antes de que el recolector de elementos no utilizados llegue a él, se reducirá la cantidad promedio de la memoria usada por la aplicación web.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;

function handleFrame(frame) {
  pendingFrames.push(frame);
  if (underflow) setTimeout(renderFrame, 0);
}

function calculateTimeUntilNextFrame(timestamp) {
  if (baseTime == 0) baseTime = performance.now();
  let mediaTime = performance.now() - baseTime;
  return Math.max(0, timestamp / 1000 - mediaTime);
}

async function renderFrame() {
  underflow = pendingFrames.length == 0;
  if (underflow) return;

  const frame = pendingFrames.shift();

  // Based on the frame's timestamp calculate how much of real time waiting
  // is needed before showing the next frame.
  const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
  await new Promise((r) => {
    setTimeout(r, timeUntilNextFrame);
  });
  ctx.drawImage(frame, 0, 0);
  frame.close();

  // Immediately schedule rendering of the next frame
  setTimeout(renderFrame, 0);
}

Sugerencias para desarrolladores

Usa el Panel multimedia. en las Herramientas para desarrolladores de Chrome para ver los registros de contenido multimedia y depurar WebCodecs.

Captura de pantalla del panel Media Panel para depurar WebCodecs
Panel multimedia en las Herramientas para desarrolladores de Chrome para depurar WebCodecs.

Demostración

La siguiente demostración muestra cómo son los fotogramas de animación de un lienzo:

  • capturada a 25 FPS en una ReadableStream por MediaStreamTrackProcessor
  • transferidos a un trabajador web
  • codificada en formato de video H.264
  • se decodifican nuevamente en una secuencia de fotogramas de video
  • y se renderiza en el segundo lienzo con transferControlToOffscreen().

Otras demostraciones

Echa un vistazo también a nuestras otras demostraciones:

Usa la API de WebCodecs

Detección de funciones

Para verificar la compatibilidad con WebCodecs, sigue estos pasos:

if ('VideoEncoder' in window) {
  // WebCodecs API is supported.
}

Ten en cuenta que la API de WebCodecs solo está disponible en contextos seguros, por lo que la detección fallará si self.isSecureContext es falso.

Comentarios

El equipo de Chrome quiere conocer tu experiencia con la API de WebCodecs.

Cuéntanos sobre el diseño de la API

¿Algo en la API no funciona como esperabas? O son ¿faltan métodos o propiedades que necesitas para implementar tu idea? Tener un pregunta o comentario sobre el modelo de seguridad? Presenta un problema de especificaciones en el repositorio de GitHub correspondiente o agrega tus ideas a un problema existente.

Informar un problema con la implementación

¿Encontraste un error en la implementación de Chrome? ¿O la implementación diferente de la especificación? Informa un error en new.crbug.com. Asegúrate de incluir tantos detalles como puedas e instrucciones sencillas para e ingresa Blink>Media>WebCodecs en el cuadro Componentes. Glitch funciona muy bien para compartir repros rápidos y fáciles.

Demuestra compatibilidad con la API

¿Planeas usar la API de WebCodecs? Tu apoyo público ayuda a El equipo de Chrome priorizará funciones y mostrará a otros proveedores de navegadores la importancia es respaldarlos.

Envía correos electrónicos a media-dev@chromium.org o un tweet a @ChromiumDev con el hashtag #WebCodecs y cuéntanos dónde y cómo la utilizas.

Hero image de Daniela Hernández en Unsplash.