Extensiones de fuentes de medios

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

Las extensiones de fuente de medios (MSE) son una API de JavaScript que te permite crear transmisiones para reproducir a partir de segmentos de audio o video. Aunque no se trata en este artículo, debes comprender el MSE si deseas incorporar videos en tu sitio que realicen las siguientes acciones:

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

Puedes pensar en ECM 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 alimentar 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 las explicaciones hasta ahora, no dudes en dejar de leer ahora. Si quieres obtener una explicación más detallada, sigue leyendo. Voy a explicar esta cadena para crear un ejemplo básico de ECM. Cada uno de los pasos de compilación agregará código al paso anterior.

Nota sobre la claridad

¿En este artículo encontrarás toda la información que necesitas para reproducir contenido multimedia en una página web? No, su único propósito es ayudarte a comprender código más complicado que puedas encontrar en otra parte. En este documento, se simplifican y excluyen muchos aspectos. Creemos que podemos solucionar este problema porque también recomendamos el uso de una biblioteca como Shaka Player de Google. Señalaré en todo dónde estoy simplificando deliberadamente.

Algunos aspectos que no se trataron

Aquí, sin un orden en particular, hay algunas cosas que no cubriré.

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

Para usar en entornos de producción

A continuación, se enumeran algunas sugerencias para realizar el uso de producción de las APIs relacionadas con ECM:

  • 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 publiquen los eventos asociados.
  • Asegúrate de que las llamadas anteriores a appendBuffer() y remove() no estén en curso. Para ello, verifica el valor booleano SourceBuffer.updating antes de actualizar mode, timestampOffset, appendWindowStart, appendWindowEnd de SourceBuffer o 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 de updating sea verdadero antes de llamar a MediaSource.endOfStream() o actualizar el 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 pase a open. Esto significa que debes estar preparado para manejar múltiples eventos sourceopen.
  • Cuando controlas eventos HTMLMediaElement error, el contenido de MediaError.message puede ser útil para determinar la causa raíz de la falla, en especial para errores que son difíciles de reproducir en entornos de pruebas.

Cómo adjuntar una instancia de MediaSource a un elemento multimedia

Al igual que con muchas otras cosas en la actualidad, el desarrollo web comienza con la detección de funciones. A continuación, obtén un elemento multimedia, ya sea <audio> o <video>. Por último, crea una instancia de MediaSource. Se convierte en una URL y se pasa al atributo 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 strings, pero también pueden ser BLOB. Si inspeccionas una página con contenido multimedia incorporado y examinas su elemento multimedia, verás a lo que me refiero.

¿Está lista la instancia de MediaSource?

URL.createObjectURL() es síncrono; sin embargo, procesa el adjunto de forma asíncrona. Esto provoca una leve demora antes de realizar cualquier acción con la instancia MediaSource. Afortunadamente, hay formas de probar esto. 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 se adjuntó a un elemento multimedia.
  • open: La instancia MediaSource se adjunta a un elemento multimedia y está lista para recibir datos o los está recibiendo.
  • ended: La instancia MediaSource se adjunta a un elemento multimedia y todos sus datos se pasaron a ese elemento.

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

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>

Observa que también llamé revokeObjectURL(). Sé que esto parece prematuro, pero puedo hacerlo en cualquier momento después de que el atributo src del elemento multimedia se conecte a una instancia MediaSource. Llamar a este método no destruye ningún objeto. permite que la plataforma maneje la recolección de elementos no utilizados en un momento adecuado, por eso la llamaré de inmediato.

Crea un SourceBuffer

Ahora es el momento de crear SourceBuffer, que es el objeto que realmente hace el trabajo de cerrar los datos entre las fuentes y los elementos multimedia. Un SourceBuffer debe ser específico para el tipo de archivo multimedia que estás cargando.

En la práctica, puedes hacerlo llamando a addSourceBuffer() con el valor apropiado. Ten en cuenta que, en el siguiente ejemplo, la string de tipo de MIME contiene un tipo de MIME y dos códecs. Se trata de una string MIME de un archivo de video, pero utiliza códecs diferentes para las partes de video y audio del archivo.

La versión 1 de las especificaciones de ECM permite que los usuarios-agentes difieran en cuanto a 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 describen sus códecs por su cuenta. En lugar de tratar de 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>;
}

Cómo obtener el archivo multimedia

Si realizas una búsqueda en Internet de ejemplos de ECM, encontrarás muchas opciones para recuperar archivos multimedia con XHR. Para ser más de vanguardia, usaré la API de Fetch y la Promise que muestra. Si intentas hacerlo en Safari, no funcionará sin un polyfill 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 distintos navegadores. Podría usar archivos separados para el audio y el video a fin de 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 los archivos multimedia en diferentes resoluciones para que pueda adaptarse a diferentes capacidades de dispositivos y condiciones de red. Esta aplicación puede cargar y reproducir videos en partes a través de solicitudes de rango o segmentos. Esto permite la adaptación 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 lograr esto. El análisis completo de este tema está más allá 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 al SourceBuffer.

La forma típica de pasar datos del objeto de respuesta a la instancia de MediaSource es obtener una ArrayBuffer del objeto de respuesta y pasarla al SourceBuffer. Comienza por llamar 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()

Una vez que se hayan agregado todos los elementos 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