Media Source Extensions (MSE) — это API JavaScript, который позволяет создавать потоки для воспроизведения из сегментов аудио или видео. Хотя это и не рассматривается в этой статье, понимание MSE необходимо, если вы хотите встроить на свой сайт видео, которые выполняют такие функции, как:
- Адаптивная потоковая передача, иначе говоря, адаптация к возможностям устройства и условиям сети.
- Адаптивное сращивание, например вставка рекламы.
- Сдвиг времени
- Контроль производительности и размера загрузки
Вы можете думать о MSE как о сети. Как показано на рисунке, между загруженным файлом и медиа-элементами находится несколько слоев.
- Элемент
<audio>
или<video>
для воспроизведения мультимедиа. - Экземпляр
MediaSource
сSourceBuffer
для подачи медиа-элемента. - Вызов
fetch()
или XHR для получения медиаданных в объектеResponse
. - Вызов
Response.arrayBuffer()
для подачиMediaSource.SourceBuffer
.
На практике цепочка выглядит так:
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);
});
}
Если вы можете разобраться во всем из объяснений, не стесняйтесь прекратить чтение прямо сейчас. Если вы хотите более подробное объяснение, продолжайте читать. Я собираюсь пройти по этой цепочке, построив базовый пример MSE. Каждый из шагов сборки добавит код к предыдущему шагу.
Примечание о ясности
Расскажет ли эта статья все, что вам нужно знать о воспроизведении мультимедиа на веб-странице? Нет, он предназначен только для того, чтобы помочь вам понять более сложный код, который вы можете найти где-то еще. Для ясности этот документ упрощает и исключает многие вещи. Мы думаем, что нам это сойдет с рук, поскольку мы также рекомендуем использовать такую библиотеку, как Google Shaka Player . Я буду отмечать все места, где намеренно упрощаю.
Несколько вещей, не охваченных
Вот несколько вещей, которые я не буду освещать в произвольном порядке.
- Управление воспроизведением. Мы получаем их бесплатно благодаря использованию элементов HTML5
<audio>
и<video>
. - Обработка ошибок.
Для использования в производственных средах
Вот некоторые вещи, которые я бы порекомендовал при использовании API, связанных с MSE:
- Прежде чем выполнять вызовы этих API, обработайте все события ошибок или исключения API и проверьте
HTMLMediaElement.readyState
иMediaSource.readyState
. Эти значения могут измениться до доставки связанных событий. - Убедитесь, что предыдущие вызовы
appendBuffer()
иremove()
еще не выполняются, проверив логическое значениеSourceBuffer.updating
перед обновлениемmode
SourceBuffer
,timestampOffset
,appendWindowStart
,appendWindowEnd
или вызовомappendBuffer()
илиremove()
вSourceBuffer
. - Для всех экземпляров
SourceBuffer
, добавленных в вашMediaSource
, убедитесь, что ни одно из ихupdating
значений не является истинным, прежде чем вызыватьMediaSource.endOfStream()
или обновлятьMediaSource.duration
. - Если значение
MediaSource.readyState
ended
, вызовы типаappendBuffer()
иremove()
или установкаSourceBuffer.mode
илиSourceBuffer.timestampOffset
приведут к переходу этого значения наopen
. Это означает, что вы должны быть готовы обрабатывать несколько событийsourceopen
. - При обработке событий
HTMLMediaElement error
содержимоеMediaError.message
может быть полезно для определения основной причины сбоя, особенно для ошибок, которые трудно воспроизвести в тестовых средах.
Прикрепите экземпляр MediaSource к медиа-элементу.
Как и многое другое в современной веб-разработке, вы начинаете с обнаружения функций. Затем получите медиа-элемент: <audio>
или <video>
. Наконец создайте экземпляр MediaSource
. Он преобразуется в URL-адрес и передается в исходный атрибут медиа-элемента.
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.');
}
То, что объект MediaSource
можно передать в атрибут src
, может показаться немного странным. Обычно это строки, но они также могут быть BLOB-объектами . Если вы проверите страницу со встроенным мультимедиа и изучите ее медиа-элемент, вы поймете, что я имею в виду.
Готов ли экземпляр MediaSource?
URL.createObjectURL()
сам по себе синхронен; однако он обрабатывает вложение асинхронно. Это вызывает небольшую задержку, прежде чем вы сможете что-либо сделать с экземпляром MediaSource
. К счастью, есть способы проверить это. Самый простой способ — использовать свойство MediaSource
под названием readyState
. Свойство readyState
описывает связь между экземпляром MediaSource
и медиа-элементом. Он может иметь одно из следующих значений:
-
closed
— экземплярMediaSource
не прикреплен к элементу мультимедиа. -
open
— экземплярMediaSource
прикреплен к элементу мультимедиа и готов к приему данных или принимает данные. -
ended
— экземплярMediaSource
прикреплен к элементу мультимедиа, и все его данные были переданы этому элементу.
Непосредственный запрос этих параметров может отрицательно повлиять на производительность. К счастью, MediaSource
также генерирует события при изменении readyState
, в частности, sourceopen
, sourceclosed
, sourceended
. В примере, который я создаю, я собираюсь использовать событие sourceopen
, чтобы сообщить мне, когда следует получить и буферизовать видео.
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>
Обратите внимание, что я также вызвал revokeObjectURL()
. Я знаю, что это кажется преждевременным, но я могу сделать это в любое время после того, как атрибут src
медиа-элемента будет подключен к экземпляру MediaSource
. Вызов этого метода не уничтожает никакие объекты. Это позволяет платформе обрабатывать сбор мусора в подходящее время, поэтому я немедленно вызываю его.
Создайте исходный буфер
Теперь пришло время создать SourceBuffer
— объект, который фактически выполняет работу по перераспределению данных между медиа-источниками и медиа-элементами. SourceBuffer
должен соответствовать типу загружаемого медиафайла.
На практике это можно сделать, вызвав addSourceBuffer()
с соответствующим значением. Обратите внимание, что в приведенном ниже примере строка типа mime содержит тип mime и два кодека. Это mime-строка для видеофайла, но она использует отдельные кодеки для видео- и аудиочастей файла.
Версия 1 спецификации MSE позволяет пользовательским агентам по-разному решать, требуют ли они как MIME-тип, так и кодек. Некоторые пользовательские агенты не требуют, но допускают только тип mime. Некоторым пользовательским агентам, например Chrome, требуется кодек для типов mime, которые не описывают свои кодеки самостоятельно. Вместо того, чтобы пытаться разобраться во всем этом, лучше просто включить оба.
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>;
}
Получить медиа-файл
Если вы выполните поиск в Интернете по примерам MSE, вы найдете множество примеров извлечения медиафайлов с использованием XHR. Чтобы быть более передовым, я собираюсь использовать Fetch API и возвращаемый им Promise . Если вы пытаетесь сделать это в Safari, это не сработает без полифила 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>;
}
Проигрыватель производственного качества будет иметь один и тот же файл в нескольких версиях для поддержки разных браузеров. Он может использовать отдельные файлы для аудио и видео, чтобы можно было выбирать звук в зависимости от языковых настроек.
Реальный код также будет иметь несколько копий медиафайлов с разными разрешениями, чтобы его можно было адаптировать к различным возможностям устройства и условиям сети. Такое приложение может загружать и воспроизводить видео частями, используя запросы диапазона или сегменты. Это позволяет адаптироваться к условиям сети во время воспроизведения мультимедиа . Возможно, вы слышали термины DASH или HLS — два метода достижения этой цели. Полное обсуждение этой темы выходит за рамки данного введения.
Обработать объект ответа
Код выглядит почти готовым, но медиафайлы не воспроизводятся. Нам нужно получить медиаданные из объекта Response
в SourceBuffer
.
Типичный способ передачи данных из объекта ответа в экземпляр MediaSource
— получить ArrayBuffer
из объекта ответа и передать его в SourceBuffer
. Начните с вызова response.arrayBuffer()
, который возвращает обещание в буфер. В моем коде я передал это обещание во второе предложение then()
, где добавляю его в 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>
}
Вызов endOfStream()
После добавления всех ArrayBuffers
и отсутствия дополнительных мультимедийных данных вызовите MediaSource.endOfStream()
. Это изменит MediaSource.readyState
на ended
и вызовет событие 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);
});
}
Окончательная версия
Вот полный пример кода. Надеюсь, вы узнали что-то о расширениях медиа-источников.
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);
});
}