Reprodução rápida com pré-carregamento de áudio e vídeo

Como acelerar a reprodução de mídia pré-carregando ativamente recursos.

François Beaufort
François Beaufort

Um início de reprodução mais rápido significa que mais pessoas assistindo seu vídeo ou ouvindo seus áudio. Fato conhecido. Neste artigo, explorarei técnicas que você pode usar para acelerar a reprodução de áudio e vídeo ao realizar ativamente pré-carregando recursos dependendo do caso de uso.

Créditos: copyright Liquider Foundation | www.blender.org (link em inglês).

Vou descrever três métodos de pré-carregamento de arquivos de mídia, começando pelos prós e contras.

É ótimo... Mas…
Atributo de pré-carregamento de vídeo Simples de usar para um arquivo exclusivo hospedado em um servidor da Web. Os navegadores podem ignorar completamente o atributo.
A busca de recursos começa quando o documento HTML é totalmente carregado e analisados.
As extensões de fonte de mídia (MSE, na sigla em inglês) ignoram o atributo preload nos elementos de mídia porque o app é responsável por: fornecer mídia ao EQM.
Pré-carregamento de link Força o navegador a fazer uma solicitação de um recurso de vídeo sem bloquear evento onload do documento. As solicitações de intervalo HTTP não são compatíveis.
Compatível com MSE e segmentos de arquivo. Use somente para arquivos de mídia pequenos (menos de 5 MB) ao buscar recursos completos.
Armazenamento em buffer manual Controle total O tratamento complexo de erros é responsabilidade do site.

Atributo de pré-carregamento de vídeo

Se a origem do vídeo for um arquivo exclusivo hospedado em um servidor da Web, recomendamos use o atributo de vídeo preload para indicar ao navegador como muitas informações ou conteúdos para pré-carregar. Isso significa que as extensões de fonte de mídia (MSE) não é compatível com preload.

A busca de recursos só será iniciada quando o documento HTML inicial for completamente carregado e analisado (por exemplo, o evento DOMContentLoaded foi disparado). enquanto o evento load muito diferente será disparado quando o recurso foi realmente buscado.

Definir o atributo preload como metadata indica que o usuário não está precisa do vídeo, mas buscar os metadados (dimensões, lista, duração etc.) é desejável. A partir do Chrome 64, o valor padrão de preload é metadata. Era auto antes).

<video id="video" preload="metadata" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

Definir o atributo preload como auto indica que o navegador pode armazenar em cache. dados suficientes para que a reprodução completa seja possível sem precisar parar mais armazenamento em buffer.

<video id="video" preload="auto" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

No entanto, há algumas ressalvas. Como isso é apenas uma dica, o navegador pode ignore o atributo preload. No momento em que este artigo foi escrito, aqui estão algumas regras aplicada no Chrome:

  • Quando a Economia de dados está ativada, o Chrome força o valor preload a none.
  • No Android 4.3, o Chrome força o valor preload para none devido a um erro de Bug.
  • Em uma conexão celular (2G, 3G e 4G), o Chrome força o valor preload a metadata.

Dicas

Caso seu site tenha muitos recursos de vídeo no mesmo domínio, recomendamos que você defina o valor preload como metadata ou defina o poster atributo e defina preload como none. Dessa forma, você evitaria bater o número máximo de conexões HTTP para o mesmo domínio (seis de acordo com a especificação HTTP 1.1), que podem travar o carregamento de recursos. Isso também pode melhore a velocidade da página se os vídeos não fizerem parte da experiência principal do usuário.

Como abordado em outros artigos, o pré-carregamento de link é uma busca declarativa que permite que você force o navegador a solicitar um recurso sem bloqueando o evento load e durante o download da página. Recursos carregados via <link rel="preload"> são armazenados localmente no navegador ficam inertes até que sejam explicitamente referenciados no DOM, no JavaScript, ou CSS.

O pré-carregamento é diferente da pré-busca porque se concentra na navegação e busca recursos com prioridade baseada no tipo (script, estilo, fonte, vídeo, áudio etc.). Ela deve ser usada para aquecer o cache do navegador para de conteúdo.

Pré-carregar vídeo completo

Veja como pré-carregar um vídeo completo em seu site para que, quando seu O JavaScript pede para buscar conteúdo de vídeo. Ele é lido do cache como o recurso já pode ter sido armazenado em cache pelo navegador. Se a solicitação de pré-carregamento for concluída, ocorrerá uma busca regular de rede.

<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">

<video id="video" controls></video>

<script>
  // Later on, after some condition has been met, set video source to the
  // preloaded video URL.
  video.src = 'https://cdn.com/small-file.mp4';
  video.play().then(() => {
    // If preloaded video URL was already cached, playback started immediately.
  });
</script>

Como o recurso pré-carregado será consumido por um elemento de vídeo no No exemplo, o valor do link de pré-carregamento as é video. Se fosse um áudio elemento, seria as="audio".

Pré-carregue o primeiro segmento

O exemplo abaixo mostra como pré-carregar o primeiro segmento de um vídeo com <link rel="preload"> e usá-lo com Media Source Extensions. Se você não conhece com a API MSE JavaScript, consulte Noções básicas do MSE.

Para simplificar, vamos presumir que todo o vídeo foi dividido em arquivos menores, como file_1.webm, file_2.webm, file_3.webm etc.

<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // If video is preloaded already, fetch will return immediately a response
    // from the browser cache (memory cache). Otherwise, it will perform a
    // regular network fetch.
    fetch('https://cdn.com/file_1.webm')
    .then(response => response.arrayBuffer())
    .then(data => {
      // Append the data into the new sourceBuffer.
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch file_2.webm when user starts playing video.
    })
    .catch(error => {
      // TODO: Show "Video is not available" message to user.
    });
  }
</script>

Suporte

Você pode detectar o suporte a vários tipos de as para <link rel=preload> com o snippets abaixo:

function preloadFullVideoSupported() {
  const link = document.createElement('link');
  link.as = 'video';
  return (link.as === 'video');
}

function preloadFirstSegmentSupported() {
  const link = document.createElement('link');
  link.as = 'fetch';
  return (link.as === 'fetch');
}

Armazenamento em buffer manual

Antes de nos aprofundarmos na API Cache e nos service workers, vamos como armazenar vídeos em buffer manualmente com o EQM. O exemplo abaixo pressupõe que seu arquivo servidor oferece suporte a HTTP Range mas isso seria bem semelhante com arquivos segmentos. Algumas bibliotecas de middleware, como a Shaka do Google Player, JW Player e Video.js são criado para lidar com isso para você.

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // Fetch beginning of the video by setting the Range HTTP request header.
    fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      sourceBuffer.appendBuffer(data);
      sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
    });
  }

  function updateEnd() {
    // Video is now ready to play!
    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);

    // Fetch the next segment of video when user starts playing the video.
    video.addEventListener('playing', fetchNextSegment, { once: true });
  }

  function fetchNextSegment() {
    fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      const sourceBuffer = mediaSource.sourceBuffers[0];
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch further segment and append it.
    });
  }
</script>

Considerações

Como agora você controla toda a experiência de armazenamento em buffer de mídia, sugiro que considerar o nível de bateria do dispositivo, o "modo de economia de dados" preferência do usuário e informações da rede ao pensar no pré-carregamento.

Reconhecimento de bateria

Considere o nível de bateria dos usuários dispositivos antes de pensar sobre o pré-carregamento de vídeos. Isso preservará a duração da bateria quando o nível de energia é baixo.

Desative o pré-carregamento ou pelo menos pré-carregue um vídeo de resolução mais baixa quando o o dispositivo está ficando sem bateria.

if ('getBattery' in navigator) {
  navigator.getBattery()
  .then(battery => {
    // If battery is charging or battery level is high enough
    if (battery.charging || battery.level > 0.15) {
      // TODO: Preload the first segment of a video.
    }
  });
}

Detectar a "economia de dados"

Use o cabeçalho da solicitação de dica do cliente Save-Data para fornecer dados rápidos e leves. aplicativos a usuários que optaram pela "economia de dados" em suas navegador. Ao identificar esse cabeçalho de solicitação, seu aplicativo pode personalizar proporcionam uma experiência do usuário otimizada com limitações de custo e desempenho usuários.

Consulte Como fornecer aplicativos rápidos e leves com Save-Data para saber mais.

Carregamento inteligente com base nas informações da rede

Verifique o navigator.connection.type antes do pré-carregamento. Quando ele estiver definido como cellular, poderá impedir o pré-carregamento e informar aos usuários que sua operadora de rede móvel esteja cobrando pela largura de banda e só comece a reprodução automática de conteúdo armazenado em cache.

if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}

Confira a amostra de informações de rede para saber como reagir à rede também muda.

Pré-armazenar em cache vários primeiros segmentos

E se eu quiser pré-carregar especulativamente algum conteúdo de mídia sem sabendo qual parte da mídia o usuário escolherá? Se o usuário estiver em um página com 10 vídeos, provavelmente temos memória suficiente para buscar um arquivo de segmento de cada um, mas não devemos criar 10 arquivos <video> ocultos e 10 objetos MediaSource e começar a alimentar esses dados.

O exemplo de duas partes abaixo mostra como armazenar previamente em cache vários primeiros segmentos de usando a API Cache avançada e fácil de usar. Algo semelhante também pode ser alcançado com o IndexedDB. Ainda não estamos usando service workers porque a API Cache também pode ser acessada pelo objeto window.

Busca e cache

const videoFileUrls = [
  'bat_video_file_1.webm',
  'cow_video_file_1.webm',
  'dog_video_file_1.webm',
  'fox_video_file_1.webm',
];

// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));

function fetchAndCache(videoFileUrl, cache) {
  // Check first if video is in the cache.
  return cache.match(videoFileUrl)
  .then(cacheResponse => {
    // Let's return cached response if video is already in the cache.
    if (cacheResponse) {
      return cacheResponse;
    }
    // Otherwise, fetch the video from the network.
    return fetch(videoFileUrl)
    .then(networkResponse => {
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, networkResponse.clone());
      return networkResponse;
    });
  });
}

Se eu usasse solicitações HTTP Range, precisaria recriar manualmente um objeto Response, porque a API Cache ainda não oferece suporte a respostas Range. Tenha lembrar que chamar networkResponse.arrayBuffer() busca todo o conteúdo da resposta de uma só vez na memória do renderizador. Por isso, é melhor usar em pequenos intervalos.

Para referência, modifiquei parte do exemplo acima para salvar o intervalo HTTP para o pré-cache de vídeo.

    ...
    return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
    .then(networkResponse => networkResponse.arrayBuffer())
    .then(data => {
      const response = new Response(data);
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, response.clone());
      return response;
    });

Iniciar vídeo

Quando um usuário clica no botão de reprodução, buscamos o primeiro segmento do vídeo disponível na Cache API para que a reprodução inicie imediatamente, se disponível. Caso contrário, vamos simplesmente buscá-lo na rede. Lembre-se de que os navegadores e os usuários podem decidir limpar o cache.

Como vimos antes, usamos o EQM para alimentar o primeiro segmento do vídeo ao vídeo. .

function onPlayButtonClick(videoFileUrl) {
  video.load(); // Used to be able to play video later.

  window.caches.open('video-pre-cache')
  .then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
  .then(response => response.arrayBuffer())
  .then(data => {
    const mediaSource = new MediaSource();
    video.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

    function sourceOpen() {
      URL.revokeObjectURL(video.src);

      const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
      sourceBuffer.appendBuffer(data);

      video.play().then(() => {
        // TODO: Fetch the rest of the video when user starts playing video.
      });
    }
  });
}

Criar respostas de Range com um service worker

Agora, e se você buscou um arquivo de vídeo inteiro e o salvou a Cache API? Quando o navegador envia uma solicitação HTTP Range, você não quiser trazer todo o vídeo para a memória do renderizador, já que a API Cache não oferecem suporte a respostas Range ainda.

Vou mostrar como interceptar essas solicitações e retornar um Range personalizado. resposta de um service worker.

addEventListener('fetch', event => {
  event.respondWith(loadFromCacheOrFetch(event.request));
});

function loadFromCacheOrFetch(request) {
  // Search through all available caches for this request.
  return caches.match(request)
  .then(response => {

    // Fetch from network if it's not already in the cache.
    if (!response) {
      return fetch(request);
      // Note that we may want to add the response to the cache and return
      // network response in parallel as well.
    }

    // Browser sends a HTTP Range request. Let's provide one reconstructed
    // manually from the cache.
    if (request.headers.has('range')) {
      return response.blob()
      .then(data => {

        // Get start position from Range request header.
        const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
        const options = {
          status: 206,
          statusText: 'Partial Content',
          headers: response.headers
        }
        const slicedResponse = new Response(data.slice(pos), options);
        slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
            (data.size - 1) + '/' + data.size);
        slicedResponse.setHeaders('X-From-Cache': 'true');

        return slicedResponse;
      });
    }

    return response;
  }
}

É importante observar que eu usei response.blob() para recriar esse porque isso simplesmente me dá um identificador para o arquivo enquanto response.arrayBuffer() leva todo o arquivo para a memória do renderizador.

Meu cabeçalho HTTP X-From-Cache personalizado pode ser usado para saber se essa solicitação vieram do cache ou da rede. Ele pode ser usado por um jogador, como ShakaPlayer para ignorar o tempo de resposta como um indicador de a velocidade da rede.

Consulte o App de mídia de exemplo oficial, principalmente a biblioteca dele. arquivo ranged-response.js para uma solução completa sobre como lidar com Range solicitações.