Как ускорить воспроизведение мультимедиа за счет активной предварительной загрузки ресурсов.
Более быстрое начало воспроизведения означает, что больше людей будут смотреть ваше видео или слушать аудио. Это известный факт . В этой статье я расскажу о методах, которые вы можете использовать для ускорения воспроизведения аудио и видео за счет активной предварительной загрузки ресурсов в зависимости от вашего варианта использования.
Я опишу три метода предварительной загрузки медиафайлов, начиная с их плюсов и минусов.
Это здорово... | Но... | |
---|---|---|
Атрибут предварительной загрузки видео | Простота использования для уникального файла, размещенного на веб-сервере. | Браузеры могут полностью игнорировать этот атрибут. |
Извлечение ресурсов начинается после полной загрузки и анализа 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
.