Extensiones de fuentes de medios

François Beaufort
François Beaufort
Joe Medley
Joe Medley

Media Source Extensions (MSE) es una API de JavaScript que te permite compilar transmisiones para la reproducción a partir de segmentos de audio o video. Aunque no se trata en este artículo, es necesario comprender el ECM si quieres incorporar videos en tu sitio que realicen acciones como las siguientes:

  • Transmisión adaptable, que es otra forma de decir que se adapta a las capacidades del dispositivo y a las condiciones de la red
  • Unión adaptativa, como la inserción de anuncios
  • Cambio de tiempo
  • Control del rendimiento y el tamaño de descarga
Flujo de datos básico de MSE
Figura 1: Flujo de datos básico de MSE

Puedes pensar en la MSE como una cadena. Como se ilustra en la figura, entre el archivo descargado y los elementos multimedia hay varias capas.

  • Un elemento <audio> o <video> para reproducir el contenido multimedia
  • Una instancia de MediaSource con un SourceBuffer para proporcionar el elemento multimedia.
  • Una llamada fetch() o XHR para recuperar datos multimedia en un objeto Response
  • Una llamada a Response.arrayBuffer() para alimentar a MediaSource.SourceBuffer.

En la práctica, la cadena se ve de la siguiente manera:

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

Si puedes ordenar todo a partir de las explicaciones hasta ahora, no dudes en dejar de leer ahora. Si quieres una explicación más detallada, sigue leyendo. Para explicar esta cadena, voy a crear un ejemplo básico de MSE. Cada uno de los pasos de compilación agregará código al paso anterior.

Una nota sobre la claridad

¿En este artículo encontrarás todo lo que necesitas saber sobre la reproducción de contenido multimedia en una página web? No, solo está diseñada para ayudarte a comprender código más complicado que podrías encontrar en otro lugar. Para mayor claridad, este documento simplifica y excluye muchos aspectos. Creemos que podemos permitirnos esto porque también recomendamos usar una biblioteca como Shaka Player de Google. A lo largo del artículo, señalaré dónde simplifico de forma deliberada.

Algunos aspectos que no se abordan

A continuación, en ningún orden en particular, se incluyen algunos temas que no abordaré.

  • Controles de reproducción Los obtenemos de forma gratuita gracias al uso de los elementos <audio> y <video> de HTML5.
  • Manejo de errores.

Para su uso en entornos de producción

A continuación, se incluyen algunas recomendaciones para el uso en producción de las APIs relacionadas con MSE:

  • Antes de realizar llamadas a estas APIs, controla los eventos de error o las excepciones de la API, y verifica HTMLMediaElement.readyState y MediaSource.readyState. Estos valores pueden cambiar antes de que se entreguen los eventos asociados.
  • Asegúrate de que las llamadas anteriores a appendBuffer() y remove() no estén en proceso. Para ello, verifica el valor booleano SourceBuffer.updating antes de actualizar mode, timestampOffset, appendWindowStart y appendWindowEnd de SourceBuffer, o de llamar a appendBuffer() o remove() en SourceBuffer.
  • Para todas las instancias de SourceBuffer agregadas a tu MediaSource, asegúrate de que ninguno de sus valores updating sea verdadero antes de llamar a MediaSource.endOfStream() o actualizar MediaSource.duration.
  • Si el valor de MediaSource.readyState es ended, las llamadas como appendBuffer() y remove(), o la configuración de SourceBuffer.mode o SourceBuffer.timestampOffset, harán que este valor cambie a open. Esto significa que debes estar preparado para controlar varios eventos sourceopen.
  • Cuando se manejan eventos HTMLMediaElement error, el contenido de MediaError.message puede ser útil para determinar la causa raíz de la falla, en especial para los errores que son difíciles de reproducir en entornos de prueba.

Adjunta una instancia de MediaSource a un elemento multimedia

Al igual que con muchas otras actividades del desarrollo web en la actualidad, comienza con la detección de funciones. A continuación, obtén un elemento multimedia, ya sea un elemento <audio> o <video>. Por último, crea una instancia de MediaSource. Se convierte en una URL y se pasa al atributo de fuente del elemento multimedia.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  // Is the MediaSource instance ready?
} else {
  console.log('The Media Source Extensions API is not supported.');
}
Un atributo fuente como un blob
Figura 1: Un atributo de origen como un blob

Que un objeto MediaSource se pueda pasar a un atributo src puede parecer un poco extraño. Por lo general, son cadenas, pero también pueden ser objetos blob. Si inspeccionas una página con contenido multimedia incorporado y examinas sus elementos multimedia, verás lo que quiero decir.

¿La instancia de MediaSource está lista?

URL.createObjectURL() es síncrono, pero procesa el archivo adjunto de forma asíncrona. Esto provoca una ligera demora antes de que puedas hacer algo con la instancia de MediaSource. Afortunadamente, existen formas de probarlo. La forma más sencilla es con una propiedad MediaSource llamada readyState. La propiedad readyState describe la relación entre una instancia de MediaSource y un elemento multimedia. Puede tener uno de los siguientes valores:

  • closed: La instancia de MediaSource no está adjunta a un elemento multimedia.
  • open: La instancia de MediaSource está adjunta a un elemento multimedia y está lista para recibir datos o está recibiendo datos.
  • ended: La instancia MediaSource se adjunta a un elemento multimedia y todos sus datos se pasaron a ese elemento.

Consultar directamente estas opciones puede afectar el rendimiento de forma negativa. Por fortuna, MediaSource también activa eventos cuando cambia readyState, específicamente sourceopen, sourceclosed y sourceended. En el ejemplo que estoy compilando, usaré el evento sourceopen para indicarme cuándo recuperar y almacenar en búfer el video.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  <strong>mediaSource.addEventListener('sourceopen', sourceOpen);</strong>
} else {
  console.log("The Media Source Extensions API is not supported.")
}

<strong>function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  // Create a SourceBuffer and get the media file.
}</strong>

Ten en cuenta que también llamé a revokeObjectURL(). Sé que esto parece prematuro, pero puedo hacerlo en cualquier momento después de que el atributo src del elemento multimedia esté conectado a una instancia de MediaSource. Llamar a este método no destruye ningún objeto. permite que la plataforma controle la recolección de elementos no utilizados en el momento adecuado, por lo que la llamo de inmediato.

Crea un SourceBuffer

Ahora es momento de crear el SourceBuffer, que es el objeto que realmente realiza el trabajo de transferir datos entre fuentes y elementos multimedia. Un SourceBuffer debe ser específico para el tipo de archivo multimedia que cargas.

En la práctica, puedes hacerlo llamando a addSourceBuffer() con el valor apropiado. Observa que, en el siguiente ejemplo, la cadena de tipo MIME contiene un tipo MIME y dos códecs. Esta es una cadena MIME para un archivo de video, pero usa códecs diferentes para las partes de video y audio del archivo.

La versión 1 de la especificación de ECM permite que los usuarios-agentes difieran en cuanto a si deben requerir un tipo de MIME y un códec. Algunos usuarios-agentes no lo requieren, pero permiten solo el tipo de MIME. Algunos usuarios-agentes, como Chrome, requieren un códec para los tipos de mime que no se autodescriben. En lugar de intentar ordenar todo esto, es mejor incluir ambos.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  <strong>
    var mime = 'video/webm; codecs="opus, vp09.00.10.08"'; // e.target refers to
    the mediaSource instance. // Store it in a variable so it can be used in a
    closure. var mediaSource = e.target; var sourceBuffer =
    mediaSource.addSourceBuffer(mime); // Fetch and process the video.
  </strong>;
}

Obtén el archivo multimedia

Si buscas ejemplos de MSE en Internet, encontrarás muchos que recuperan archivos multimedia con XHR. Para ser más vanguardista, usaré la API de Fetch y la promesa que muestra. Si intentas hacerlo en Safari, no funcionará sin un polyfill de fetch().

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  <strong>
    fetch(videoUrl) .then(function(response){' '}
    {
      // Process the response object.
    }
    );
  </strong>;
}

Un reproductor de calidad de producción tendría el mismo archivo en varias versiones para admitir diferentes navegadores. Podría usar archivos separados para audio y video para permitir que se seleccione el audio según la configuración de idioma.

El código del mundo real también tendría varias copias de archivos multimedia en diferentes resoluciones para que se pueda adaptar a las diferentes capacidades del dispositivo y las condiciones de la red. Dicha aplicación puede cargar y reproducir videos en fragmentos con solicitudes de rango o segmentos. Esto permite adaptarse a las condiciones de la red mientras se reproduce contenido multimedia. Es posible que hayas escuchado los términos DASH o HLS, que son dos métodos para lograrlo. El tratamiento completo de este tema está fuera del alcance de esta introducción.

Procesa el objeto de respuesta

El código parece estar casi listo, pero el contenido multimedia no se reproduce. Necesitamos obtener datos multimedia del objeto Response en SourceBuffer.

La forma típica de pasar datos del objeto de respuesta a la instancia de MediaSource es obtener un ArrayBuffer del objeto de respuesta y pasarlo a SourceBuffer. Para comenzar, llama a response.arrayBuffer(), que muestra una promesa al búfer. En mi código, pasé esta promesa a una segunda cláusula then() en la que la agrego a SourceBuffer.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      <strong>return response.arrayBuffer();</strong>
    })
    <strong>.then(function(arrayBuffer) {
      sourceBuffer.appendBuffer(arrayBuffer);
    });</strong>
}

Llama a endOfStream().

Después de que se adjunten todos los ArrayBuffers y no se esperen más datos multimedia, llama a MediaSource.endOfStream(). Esto cambiará MediaSource.readyState a ended y activará el evento sourceended.

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      <strong>sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });</strong>
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

La versión final

Este es el ejemplo de código completo. Espero que hayas aprendido algo sobre las extensiones de fuente de medios.

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log('The Media Source Extensions API is not supported.');
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp09.00.10.08"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function (response) {
      return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function (e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

Comentarios