A API Media Source Extensions (MSE) é uma API JavaScript que permite criar streams 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 ao seu site vídeos que façam coisas como:
- Streaming adaptável, que é outra maneira de dizer adaptação aos recursos do dispositivo e às condições da rede
- Junção adaptável, como a inserção de anúncios
- Mudança de horário
- Controle do desempenho e do tamanho do download
É quase como uma rede. Como ilustrado na figura, há várias camadas entre o arquivo transferido por download e os elementos de mídia.
- Um elemento
<audio>
ou<video>
para reproduzir a mídia. - Uma instância de
MediaSource
com umSourceBuffer
para alimentar o elemento de mídia. - Uma chamada
fetch()
ou XHR para recuperar dados de mídia em um objetoResponse
. - Uma chamada para
Response.arrayBuffer()
para alimentarMediaSource.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 tirar tudo das explicações até agora, fique à vontade para parar de ler agora. Se quiser uma explicação mais detalhada, continue lendo. Vou acompanhar essa cadeia criando um exemplo básico de MSE. Cada uma das etapas de criação adicionará código à etapa anterior.
Uma observação sobre clareza
Este artigo explica tudo o que você precisa saber sobre como reproduzir mídia em uma página da Web? Não, ele serve apenas para ajudar você a entender códigos mais complicados que pode encontrar em outro lugar. Para maior clareza, este documento simplifica e exclui muitas coisas. Acreditamos que é possível fazer isso porque também recomendamos o uso de uma biblioteca como o Shaka Player do Google. vou anotar onde estou simplificando deliberadamente.
Alguns detalhes não abordados
Aqui, em uma ordem específica, não vou falar sobre algumas coisas.
- Controles de mídia. Elas são recebidas sem custo financeiro pelo uso dos elementos
<audio>
e<video>
do HTML5. - Tratamento de erros.
Para uso em ambientes de produção
Aqui estão algumas coisas que eu recomendaria ao usar em produção APIs relacionadas ao MSE:
- Antes de fazer chamadas nessas APIs, processe todos os eventos de erro ou exceções
de API e verifique
HTMLMediaElement.readyState
eMediaSource.readyState
. Esses valores podem mudar antes que os eventos associados sejam entregues. - Verifique se as chamadas
appendBuffer()
eremove()
anteriores ainda não estão em andamento verificando o valor booleanoSourceBuffer.updating
antes de atualizarmode
,timestampOffset
,appendWindowStart
,appendWindowEnd
doSourceBuffer
ou chamarappendBuffer()
ouremove()
noSourceBuffer
. - Para todas as instâncias de
SourceBuffer
adicionadas aoMediaSource
, verifique se nenhum dos valores deupdating
é verdadeiro antes de chamarMediaSource.endOfStream()
ou atualizar oMediaSource.duration
. - Se o valor de
MediaSource.readyState
forended
, chamadas comoappendBuffer()
eremove()
ou a definição deSourceBuffer.mode
ouSourceBuffer.timestampOffset
farão a transição desse valor paraopen
. Isso significa que você precisa se preparar para processar vários eventossourceopen
. - Ao processar eventos
HTMLMediaElement error
, o conteúdo deMediaError.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 MediaSource a um elemento de mídia
Assim como em muitos aspectos do desenvolvimento da Web atualmente, você começa com a detecção de recursos. Em seguida, acesse um elemento de mídia, <audio>
ou <video>
.
Por fim, crie uma instância de MediaSource
. Ela é transformada em um URL e transmitida
para o 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.');
}
O fato de um objeto MediaSource
poder ser transmitido para um atributo src
pode parecer
um pouco estranho. Elas 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, entenderá o que quero dizer.
A instância MediaSource está pronta?
URL.createObjectURL()
é síncrono, mas processa o anexo de forma assíncrona. Isso causa um pequeno atraso antes que você possa fazer qualquer coisa
com a instância MediaSource
. Felizmente, há maneiras de fazer isso.
A maneira mais simples é com uma propriedade MediaSource
chamada readyState
. A propriedade
readyState
descreve a relação entre uma instância de MediaSource
e
um elemento de mídia. Pode ter um dos seguintes valores:
closed
: a instância deMediaSource
não está anexada a um elemento de mídia.open
: a instânciaMediaSource
está anexada a um elemento de mídia e está pronta para receber ou receber dados.ended
: a instânciaMediaSource
está anexada a um elemento de mídia e todos os dados foram transmitidos a ele.
Consultar essas opções diretamente pode afetar negativamente o desempenho. Felizmente,
MediaSource
também dispara eventos quando readyState
muda, especificamente
sourceopen
, sourceclosed
, sourceended
. No exemplo que estou criando, vou usar o evento sourceopen
para me dizer 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 eu também chamei revokeObjectURL()
. Sei que parece prematuro,
mas posso fazer isso a qualquer momento depois que o atributo src
do elemento de mídia estiver
conectado a uma instância MediaSource
. Chamar esse método não destrói nenhum
objeto. Ele permite que a plataforma lide com a coleta de lixo em um
momento adequado, e é por isso que a chamei imediatamente.
Criar um SourceBuffer
Agora é hora de criar o SourceBuffer
, que é o objeto que realmente
faz o trabalho de bloquear 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 apropriado. No exemplo abaixo, a string do tipo MIME contém um tipo MIME e dois codecs. Essa é uma string MIME para um arquivo de vídeo, mas usa codecs separados para as partes de vídeo e áudio do arquivo.
A versão 1 da especificação do MSE permite que os user agents sejam diferentes quanto à necessidade de um tipo MIME e um codec. Alguns user agents não exigem, mas permitem apenas o tipo MIME. Alguns user agents, como o Chrome, exigem um codec para tipos MIME que não autodescrevem os codecs. Em vez de tentar classificar tudo isso, é melhor apenas 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>;
}
Acessar o arquivo de mídia
Se você fizer uma pesquisa na Internet por exemplos de MSE, vai encontrar muitos recursos para recuperar
arquivos de mídia usando XHR. Para ser mais inovador, vou usar a API Fetch e a Promise que ela retorna. Se você estiver tentando fazer isso no Safari, ela não 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. Ela pode usar arquivos separados de áudio e vídeo para permitir que o áudio seja selecionado com base nas configurações de idioma.
O código real também teria várias cópias de arquivos de mídia em diferentes resoluções para que pudesse se adaptar a diferentes recursos do dispositivo e condições de rede. Esse aplicativo é capaz de carregar e reproduzir vídeos em partes usando solicitações de intervalo ou segmentos. Isso permite a adaptação às condições da rede durante a reprodução da mídia. Você pode ter ouvido os termos DASH ou HLS, que são dois métodos de fazer isso. Uma discussão completa desse tópico está além do escopo desta introdução.
Processar o objeto de resposta
O código parece estar quase pronto, mas a mídia não é reproduzida. Precisamos transferir
os 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 ao buffer. No meu código, transmita essa promessa para uma segunda cláusula then()
em que eu a anexamos 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 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
Veja o exemplo de código completo. Espero 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);
});
}