Как ускорить воспроизведение мультимедиа за счет активной предварительной загрузки ресурсов.
Более быстрое начало воспроизведения означает, что больше людей будут смотреть ваше видео или слушать аудио. Это известный факт . В этой статье я рассмотрю методы, которые вы можете использовать для ускорения воспроизведения аудио и видео за счет активной предварительной загрузки ресурсов в зависимости от вашего варианта использования.
Кредиты: авторские права Blender Foundation | www.blender.org .
Я опишу три метода предварительной загрузки медиафайлов, начиная с их плюсов и минусов.
| Это здорово... | Но... | |
|---|---|---|
| Атрибут предварительной загрузки видео | Простота использования для уникального файла, размещенного на веб-сервере. | Браузеры могут полностью игнорировать этот атрибут. | 
| Извлечение ресурсов начинается после полной загрузки и анализа HTML-документа. | ||
 Расширения источника мультимедиа (MSE) игнорируют атрибут preload в элементах мультимедиа, поскольку приложение отвечает за предоставление мультимедиа в MSE. | ||
| Предварительная загрузка ссылки |  Заставляет браузер выполнить запрос видеоресурса, не блокируя событие onload документа. | Запросы диапазона HTTP несовместимы. | 
| Совместим с MSE и сегментами файлов. | Следует использовать только для небольших медиафайлов (<5 МБ) при получении полных ресурсов. | |
| Ручная буферизация | Полный контроль | Ответственность за обработку сложных ошибок лежит на веб-сайте. | 
Атрибут предварительной загрузки видео
 Если источником видео является уникальный файл, размещенный на веб-сервере, вы можете использовать атрибут preload видео, чтобы подсказать браузеру , какой объем информации или контента требуется предварительно загрузить . Это означает, что расширения источника мультимедиа (MSE) несовместимы с preload .
 Извлечение ресурса начнется только тогда, когда исходный HTML-документ будет полностью загружен и проанализирован (например, сработает событие DOMContentLoaded ), в то время как совершенно другое событие load будет запущено, когда ресурс действительно будет извлечен. 
 Установка атрибута preload в metadata означает, что пользователю не понадобится видео, но желательно получить его метаданные (размеры, список дорожек, продолжительность и т. д.). Обратите внимание, что начиная с Chrome 64 значением по умолчанию для preload являются metadata . (Раньше было auto ).
<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>
 Установка атрибута preload в auto означает, что браузер может кэшировать достаточно данных, чтобы обеспечить полное воспроизведение без необходимости остановки для дальнейшей буферизации.
<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>
 Однако есть некоторые предостережения. Поскольку это всего лишь подсказка, браузер может полностью игнорировать атрибут preload . На момент написания статьи в Chrome применялись следующие правила:
-  Когда функция экономии данных включена, Chrome принудительно устанавливает значение 
preloadвnone. -  В Android 4.3 Chrome принудительно устанавливает значение 
preloadвnoneиз-за ошибки Android . -  При сотовом соединении (2G, 3G и 4G) Chrome принудительно загружает значение 
preloadвmetadata. 
Советы
 Если ваш веб-сайт содержит много видеоресурсов в одном домене, я бы рекомендовал вам установить значение preload в metadata или определить атрибут poster и установить preload значение none . Таким образом, вы избежите достижения максимального количества HTTP-соединений к одному и тому же домену (6 согласно спецификации HTTP 1.1), что может привести к зависанию загрузки ресурсов. Обратите внимание, что это также может повысить скорость страницы, если видео не является частью вашего основного пользовательского опыта.
Предварительная загрузка ссылки
 Как описано в других статьях , предварительная загрузка ссылки — это декларативная выборка, которая позволяет заставить браузер выполнить запрос ресурса, не блокируя событие load и во время загрузки страницы. Ресурсы, загруженные через <link rel="preload"> хранятся локально в браузере и фактически инертны, пока на них явно не ссылаются в DOM, JavaScript или CSS.
Предварительная загрузка отличается от предварительной выборки тем, что она фокусируется на текущей навигации и извлекает ресурсы с приоритетом в зависимости от их типа (скрипт, стиль, шрифт, видео, аудио и т. д.). Его следует использовать для разогрева кеша браузера для текущих сеансов.
Предварительная загрузка полного видео
Вот как предварительно загрузить полное видео на свой веб-сайт, чтобы, когда ваш JavaScript запрашивает получение видеоконтента, оно считывалось из кеша, поскольку ресурс, возможно, уже был закеширован браузером. Если запрос на предварительную загрузку еще не завершен, произойдет обычная выборка по сети.
<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>
 Поскольку предварительно загруженный ресурс будет использоваться элементом видео в этом примере, значение ссылки as preload равно video . Если бы это был элемент audio, это было бы as="audio" .
Предварительная загрузка первого сегмента
 В приведенном ниже примере показано, как предварительно загрузить первый сегмент видео с помощью <link rel="preload"> и использовать его с расширениями источника мультимедиа. Если вы не знакомы с MSE JavaScript API, см. Основы MSE .
 Для простоты предположим, что все видео разбито на более мелкие файлы, такие как file_1.webm , file_2.webm , file_3.webm и т. д.
<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>
Поддерживать
 Вы можете обнаружить поддержку различных as для <link rel=preload> с помощью фрагментов ниже:
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');
}
Ручная буферизация
 Прежде чем мы углубимся в API Cache и сервис-воркеров, давайте посмотрим, как вручную буферизовать видео с помощью MSE. В приведенном ниже примере предполагается, что ваш веб-сервер поддерживает запросы HTTP Range , но это будет очень похоже на сегменты файлов. Обратите внимание, что некоторые библиотеки промежуточного программного обеспечения, такие как Google Shaka Player , JW Player и Video.js, созданы для того, чтобы справиться с этой задачей за вас.
<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>
Соображения
Поскольку теперь вы контролируете весь процесс буферизации мультимедиа, я предлагаю вам учитывать уровень заряда батареи устройства, пользовательские предпочтения «Режима экономии данных» и информацию о сети, когда вы думаете о предварительной загрузке.
Осведомленность о батарее
Прежде чем задуматься о предварительной загрузке видео, примите во внимание уровень заряда батареи устройств пользователей. Это продлит срок службы батареи при низком уровне мощности.
Отключите предварительную загрузку или хотя бы загрузите видео с более низким разрешением, когда в устройстве разряжается батарея.
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.
    }
  });
}
Обнаружение «Экономии данных»
 Используйте заголовок запроса подсказки клиента Save-Data , чтобы доставлять быстрые и легкие приложения пользователям, которые включили режим «экономии данных» в своем браузере. Определив этот заголовок запроса, ваше приложение может настроить и предоставить оптимизированный пользовательский интерфейс для пользователей с ограниченными затратами и производительностью.
Дополнительную информацию см. в разделе «Доставка быстрых и легких приложений с помощью данных сохранения» .
Умная загрузка на основе сетевой информации
 Возможно, вы захотите проверить navigator.connection.type перед предварительной загрузкой. Если для параметра выбрано значение cellular , вы можете запретить предварительную загрузку и сообщить пользователям, что их оператор мобильной сети может взимать плату за пропускную способность, и запускать автоматическое воспроизведение только ранее кэшированного контента.
if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}
Ознакомьтесь с примером информации о сети, чтобы узнать, как реагировать на изменения в сети.
Предварительное кэширование нескольких первых сегментов
 А что, если я захочу предварительно загрузить некоторый медиаконтент, не зная, какой фрагмент мультимедиа в конечном итоге выберет пользователь? Если пользователь находится на веб-странице, содержащей 10 видео, у нас, вероятно, достаточно памяти, чтобы получить по одному файлу сегмента из каждого, но нам определенно не следует создавать 10 скрытых элементов <video> и 10 объектов MediaSource и начинать передавать эти данные.
 В приведенном ниже примере из двух частей показано, как предварительно кэшировать несколько первых сегментов видео с помощью мощного и простого в использовании Cache API . Обратите внимание, что чего-то подобного можно добиться и с помощью IndexedDB. Мы пока не используем сервис-воркеров, поскольку API Cache также доступен из объекта window .
Получить и кэшировать
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;
    });
  });
}
 Обратите внимание: если бы я использовал HTTP-запросы Range , мне пришлось бы вручную воссоздавать объект Response , поскольку API Cache пока не поддерживает ответы Range . Помните, что вызов networkResponse.arrayBuffer() сразу извлекает все содержимое ответа в память средства рендеринга, поэтому вы можете захотеть использовать небольшие диапазоны.
Для справки: я изменил часть приведенного выше примера, чтобы сохранять запросы HTTP Range в прекэш видео.
    ...
    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;
    });
Воспроизвести видео
Когда пользователь нажимает кнопку воспроизведения, мы извлекаем первый сегмент видео, доступный в Cache API, чтобы воспроизведение началось немедленно, если оно доступно. В противном случае мы просто получим его из сети. Имейте в виду, что браузеры и пользователи могут решить очистить кэш .
Как было показано ранее, мы используем MSE для подачи этого первого сегмента видео в элемент video.
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.
      });
    }
  });
}
Создайте ответы диапазона с помощью сервисного работника
 А что, если вы получили весь видеофайл и сохранили его в Cache API? Когда браузер отправляет HTTP-запрос Range , вы, конечно, не хотите переносить все видео в память рендерера, поскольку Cache API еще не поддерживает ответы Range .
 Итак, позвольте мне показать, как перехватить эти запросы и вернуть настроенный ответ Range от сервисного работника. 
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;
  }
}
Важно отметить, что я использовал response.blob() для воссоздания этого фрагментированного ответа, поскольку это просто дает мне дескриптор файла, в то время как response.arrayBuffer() переносит весь файл в память средства рендеринга.
 Мой собственный HTTP-заголовок X-From-Cache можно использовать, чтобы узнать, пришел ли этот запрос из кеша или из сети. Его может использовать такой проигрыватель, как ShakaPlayer , чтобы игнорировать время ответа как индикатор скорости сети.
 Ознакомьтесь с официальным примером медиа-приложения и, в частности, с его файлом ranged-response.js, чтобы получить полное решение по обработке запросов Range .