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 permiten crear transmisiones para la reproducción desde segmentos de audio o video. Aunque no se explica en este artículo, es necesario comprender la MSE 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
  • Empalmes adaptables, como 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 básico de datos de ECM

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 alimentar el elemento multimedia
  • Una llamada fetch() o XHR para recuperar datos multimedia en un objeto Response
  • Llamada a Response.arrayBuffer() para alimentar 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, continúa 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 tiene como objetivo ayudarte a comprender un 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 incluyen

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

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

Para usar 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 appendBuffer() y remove() anteriores aún no estén en curso verificando el valor booleano SourceBuffer.updating antes de actualizar mode, timestampOffset, appendWindowStart o appendWindowEnd de SourceBuffer, o de llamar a appendBuffer() o remove() en SourceBuffer.
  • Para todas las instancias de SourceBuffer que se agregaron a tu MediaSource, asegúrate de que ninguno de sus valores de 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 controlas 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 pruebas.

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

Al igual que con muchas cosas en el desarrollo web en la actualidad, comienzas con la detección de atributos. 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 de origen 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 su elemento multimedia, verás a qué me refiero.

¿La instancia de MediaSource está lista?

URL.createObjectURL() es síncrono, pero procesa el archivo adjunto de forma asíncrona. Esto provoca un leve retraso antes de que puedas hacer algo con la instancia 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 estas opciones directamente puede afectar negativamente el rendimiento. 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 basura en un 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 separados para las partes de video y audio del archivo.

La versión 1 de la especificación de MSE permite que los usuarios-agentes difieran en cuanto a si requieren un tipo mime y un códec. Algunos usuarios-agentes no requieren, pero sí permiten solo el tipo mime. Algunos usuarios-agentes, por ejemplo, Chrome, requieren un códec para tipos MIME que no autodescriben sus códecs. 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 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 el audio se seleccione 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 al 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. 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>
}

Llamar 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