Extensões de origem de mídia

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

As extensões de origem de mídia (MSE, na sigla em inglês) são uma API JavaScript que permite criar transmissões para reprodução de segmentos de áudio ou vídeo. Embora não seja abordado neste artigo, é necessário entender o MSE se você quiser incorporar vídeos no seu site que façam coisas como:

  • Streaming adaptável, que é outra forma de dizer que se adapta aos recursos do dispositivo e às condições da rede
  • Empilhamento adaptativo, como inserção de anúncios
  • Mudança de horário
  • Controle de desempenho e tamanho do download
Fluxo de dados básico da MSE
Figura 1: fluxo de dados básico da MSE

Você pode pensar na MSE como uma cadeia. Como ilustrado na figura, entre o arquivo transferido por download e os elementos de mídia há várias camadas.

  • Um elemento <audio> ou <video> para reproduzir a mídia.
  • Uma instância MediaSource com um SourceBuffer para alimentar o elemento de mídia.
  • Uma chamada fetch() ou XHR para extrair dados de mídia em um objeto Response.
  • Uma chamada para Response.arrayBuffer() para alimentar MediaSource.SourceBuffer.

Na prática, a cadeia fica assim:

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);
    });
}

Se você conseguir resolver as coisas com as explicações até agora, pode parar de ler agora. Se quiser uma explicação mais detalhada, continue lendo. Vou mostrar essa cadeia criando um exemplo básico de MSE. Cada uma das etapas de build vai adicionar código à etapa anterior.

Observação sobre clareza

Este artigo vai explicar tudo o que você precisa saber sobre a reprodução de mídia em uma página da Web? Não, ele só ajuda a entender códigos mais complicados que você pode encontrar em outros lugares. Para maior clareza, este documento simplifica e exclui muitas coisas. Achamos que podemos fazer isso porque também recomendamos o uso de uma biblioteca, como o Shaka Player do Google. Vou anotar onde estou simplificando deliberadamente.

Algumas coisas não estão incluídas

Confira, sem ordem específica, algumas coisas que não vou abordar.

  • Controles de mídia. Elas são sem custo financeiro por causa do uso dos elementos HTML5 <audio> e <video>.
  • Tratamento de erros.

Para uso em ambientes de produção

Confira algumas recomendações para o uso em produção de APIs relacionadas ao MSE:

  • Antes de fazer chamadas nessas APIs, processe todos os eventos de erro ou exceções da API e verifique HTMLMediaElement.readyState e MediaSource.readyState. Esses valores podem mudar antes que os eventos associados sejam enviados.
  • Verifique se as chamadas appendBuffer() e remove() anteriores não estão em andamento. Para isso, verifique o valor booleano SourceBuffer.updating antes de atualizar o mode, timestampOffset, appendWindowStart, appendWindowEnd do SourceBuffer ou chamar appendBuffer() ou remove() no SourceBuffer.
  • Para todas as instâncias de SourceBuffer adicionadas ao MediaSource, verifique se nenhum dos valores de updating é verdadeiro antes de chamar MediaSource.endOfStream() ou atualizar o MediaSource.duration.
  • Se o valor de MediaSource.readyState for ended, chamadas como appendBuffer() e remove() ou a configuração SourceBuffer.mode ou SourceBuffer.timestampOffset fará com que esse valor mude para open. Isso significa que você precisa estar preparado para processar vários eventos sourceopen.
  • Ao processar eventos HTMLMediaElement error, o conteúdo de MediaError.message pode ser útil para determinar a causa raiz da falha, especialmente para erros difíceis de reproduzir em ambientes de teste.

Anexar uma instância do MediaSource a um elemento de mídia

Como acontece com muitas coisas no desenvolvimento da Web atualmente, você começa com a detecção de recursos. Em seguida, extraia um elemento de mídia, <audio> ou <video>. Por fim, crie uma instância de MediaSource. Ele é transformado em um URL e transmitido ao atributo de origem do elemento de mídia.

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.');
}
Um atributo de origem como um blob
Figura 1: um atributo de origem como um blob

O fato de um objeto MediaSource poder ser transmitido para um atributo src pode parecer um pouco estranho. Geralmente, são strings, mas também podem ser blobs. Se você inspecionar uma página com mídia incorporada e examinar o elemento de mídia, vai entender o que quero dizer.

A instância do MediaSource está pronta?

O URL.createObjectURL() é síncrono, mas processa o anexo de maneira assíncrona. Isso causa um pequeno atraso antes que você possa fazer qualquer coisa com a instância MediaSource. Felizmente, há maneiras de testar isso. A maneira mais simples é com uma propriedade MediaSource chamada readyState. A propriedade readyState descreve a relação entre uma instância MediaSource e um elemento de mídia. Pode ter um destes valores:

  • closed: a instância MediaSource não está anexada a um elemento de mídia.
  • open: a instância MediaSource está anexada a um elemento de mídia e está pronta para receber dados ou está recebendo dados.
  • ended: a instância MediaSource é anexada a um elemento de mídia, e todos os dados dela foram transmitidos a esse elemento.

Consultar essas opções diretamente pode afetar negativamente a performance. Felizmente, MediaSource também dispara eventos quando readyState muda, especificamente sourceopen, sourceclosed e sourceended. Para o exemplo que estou criando, vou usar o evento sourceopen para informar quando buscar e armazenar em buffer o vídeo.

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>

Observe que também chamei revokeObjectURL(). Sei que isso parece prematuro, mas posso fazer isso a qualquer momento depois que o atributo src do elemento de mídia for conectado a uma instância MediaSource. Chamar esse método não destrói nenhum objeto. Ele permite que a plataforma processe a coleta de lixo no momento adequado. Por isso, estou chamando-o imediatamente.

Criar um SourceBuffer

Agora é hora de criar o SourceBuffer, que é o objeto que realmente faz o trabalho de transferir dados entre fontes e elementos de mídia. Um SourceBuffer precisa ser específico para o tipo de arquivo de mídia que você está carregando.

Na prática, é possível fazer isso chamando addSourceBuffer() com o valor adequado. Observe que, no exemplo abaixo, a string de tipo MIME contém um tipo MIME e dois codecs. Essa é uma string MIME para um arquivo de vídeo, mas ela usa codecs separados para as partes de vídeo e áudio do arquivo.

A versão 1 da especificação MSE permite que os agentes do usuário sejam diferentes para exigir um tipo mime e um codec. Alguns agentes do usuário não exigem, mas permitem apenas o tipo mime. Alguns user agents, como o Chrome, exigem um codec para tipos MIME que não se autodescrevem. Em vez de tentar resolver tudo isso, é melhor incluir os dois.

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>;
}

Receber o arquivo de mídia

Se você fizer uma pesquisa na Internet para encontrar exemplos de MSE, vai encontrar muitos que recuperam arquivos de mídia usando XHR. Para ser mais avançado, vou usar a API Fetch e a Promise que ela retorna. Se você estiver tentando fazer isso no Safari, ele não vai funcionar sem um 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>;
}

Um player de qualidade de produção teria o mesmo arquivo em várias versões para oferecer suporte a diferentes navegadores. Ele pode usar arquivos separados para áudio e vídeo para permitir que o áudio seja selecionado com base nas configurações de idioma.

O código do mundo real também teria várias cópias de arquivos de mídia em diferentes resoluções para se adaptar a diferentes recursos de dispositivo e condições de rede. Esse aplicativo pode carregar e reproduzir vídeos em fragmentos usando solicitações de intervalo ou segmentos. Isso permite a adaptação às condições de rede enquanto a mídia é reproduzida. Você provavelmente já ouviu os termos DASH ou HLS, que são dois métodos para fazer isso. Uma discussão completa deste tópico está fora do escopo desta introdução.

Processar o objeto de resposta

O código parece quase pronto, mas a mídia não é reproduzida. Precisamos extrair dados de mídia do objeto Response para o SourceBuffer.

A maneira típica de transmitir dados do objeto de resposta para a instância MediaSource é receber um ArrayBuffer do objeto de resposta e transmiti-lo ao SourceBuffer. Comece chamando response.arrayBuffer(), que retorna uma promessa para o buffer. No meu código, transmiti essa promessa para uma segunda cláusula then(), em que a adiciono ao 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>
}

Chamar endOfStream()

Depois que todos os ArrayBuffers forem anexados e nenhum outro dado de mídia for esperado, chame MediaSource.endOfStream(). Isso vai mudar MediaSource.readyState para ended e disparar o 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);
    });
}

A versão final

Confira o exemplo de código completo. Esperamos que você tenha aprendido algo sobre as extensões de fonte de mídia.

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);
    });
}

Feedback