Управление компонентами видеопотока.
Современные веб-технологии предоставляют широкие возможности работы с видео. Media Stream API , Media Recording API , Media Source API и WebRTC API составляют богатый набор инструментов для записи, передачи и воспроизведения видеопотоков. При решении некоторых задач высокого уровня эти API не позволяют веб-программистам работать с отдельными компонентами видеопотока, такими как кадры и немультиплексированные фрагменты закодированного видео или аудио. Чтобы получить низкоуровневый доступ к этим базовым компонентам, разработчики использовали WebAssembly для добавления видео- и аудиокодеков в браузер. Но учитывая, что современные браузеры уже поставляются с различными кодеками (которые часто ускоряются аппаратно), переупаковка их в WebAssembly кажется пустой тратой человеческих и компьютерных ресурсов.
WebCodecs API устраняет эту неэффективность, предоставляя программистам возможность использовать мультимедийные компоненты, которые уже присутствуют в браузере. Конкретно:
- Видео и аудио декодеры
- Видео и аудио кодеры
- Необработанные видеокадры
- Декодеры изображений
API WebCodecs полезен для веб-приложений, которым требуется полный контроль над способом обработки медиаконтента, таких как видеоредакторы, видеоконференции, потоковое видео и т. д.
Рабочий процесс обработки видео
Кадры являются центральным элементом обработки видео. Таким образом, в WebCodecs большинство классов либо потребляют, либо создают кадры. Видеокодеры преобразуют кадры в закодированные фрагменты. Видеодекодеры делают обратное.
Кроме того, VideoFrame
прекрасно взаимодействует с другими веб-API, поскольку является CanvasImageSource
и имеет конструктор , принимающий CanvasImageSource
. Поэтому его можно использовать в таких функциях, как drawImage()
и texImage2D()
. Также его можно составить из холстов, растровых изображений, видеоэлементов и других видеокадров.
WebCodecs API хорошо работает в тандеме с классами из Insertable Streams API , которые подключают WebCodecs к трекам медиапотоков .
-
MediaStreamTrackProcessor
разбивает медиа-дорожки на отдельные кадры. -
MediaStreamTrackGenerator
создает медиа-трек из потока кадров.
Веб-кодеки и веб-воркеры
По своей конструкции WebCodecs API выполняет всю тяжелую работу асинхронно и вне основного потока. Но поскольку обратные вызовы кадров и фрагментов часто могут вызываться несколько раз в секунду, они могут загромождать основной поток и, таким образом, делать веб-сайт менее отзывчивым. Поэтому предпочтительнее перенести обработку отдельных кадров и закодированных фрагментов в веб-воркер.
Чтобы помочь в этом, ReadableStream предоставляет удобный способ автоматической передачи всех кадров, поступающих с медиа-трека, в работника. Например, MediaStreamTrackProcessor
можно использовать для получения ReadableStream
для дорожки медиапотока, поступающего с веб-камеры. После этого поток передается веб-воркеру, где кадры считываются один за другим и ставятся в очередь в VideoEncoder
.
С помощью HTMLCanvasElement.transferControlToOffscreen
рендеринг можно выполнять даже вне основного потока. Но если все высокоуровневые инструменты оказались неудобными, то сам VideoFrame
переносим и может перемещаться между воркёрами.
Вебкодеки в действии
Кодирование
Все начинается с VideoFrame
. Существует три способа создания видеокадров.
Из источника изображения, такого как холст, растровое изображение или видеоэлемент.
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
Используйте
MediaStreamTrackProcessor
для извлечения кадров изMediaStreamTrack
const stream = await navigator.mediaDevices.getUserMedia({…}); const track = stream.getTracks()[0]; const trackProcessor = new MediaStreamTrackProcessor(track); const reader = trackProcessor.readable.getReader(); while (true) { const result = await reader.read(); if (result.done) break; const frameFromCamera = result.value; }
Создайте кадр из его двоичного пиксельного представления в
BufferSource
const pixelSize = 4; const init = { timestamp: 0, codedWidth: 320, codedHeight: 200, format: "RGBA", }; const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize); for (let x = 0; x < init.codedWidth; x++) { for (let y = 0; y < init.codedHeight; y++) { const offset = (y * init.codedWidth + x) * pixelSize; data[offset] = 0x7f; // Red data[offset + 1] = 0xff; // Green data[offset + 2] = 0xd4; // Blue data[offset + 3] = 0x0ff; // Alpha } } const frame = new VideoFrame(data, init);
Независимо от того, откуда они берутся, кадры могут быть закодированы в объекты EncodedVideoChunk
с помощью VideoEncoder
.
Перед кодированием VideoEncoder
необходимо передать два объекта JavaScript:
- Словарь инициализации с двумя функциями для обработки закодированных фрагментов и ошибок. Эти функции определяются разработчиком и не могут быть изменены после передачи конструктору
VideoEncoder
. - Объект конфигурации кодировщика, содержащий параметры выходного видеопотока. Вы можете изменить эти параметры позже, вызвав
configure()
.
Метод configure()
выдаст NotSupportedError
, если конфигурация не поддерживается браузером. Рекомендуется вызвать статический метод VideoEncoder.isConfigSupported()
с конфигурацией, чтобы заранее проверить, поддерживается ли конфигурация, и дождаться ее обещания.
const init = {
output: handleChunk,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
width: 640,
height: 480,
bitrate: 2_000_000, // 2 Mbps
framerate: 30,
};
const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
const encoder = new VideoEncoder(init);
encoder.configure(config);
} else {
// Try another config.
}
После настройки кодировщика он готов принимать кадры с помощью метода encode()
. И configure()
, и encode()
возвращаются немедленно, не дожидаясь завершения фактической работы. Он позволяет нескольким кадрам одновременно стоять в очереди на кодирование, а encodeQueueSize
показывает, сколько запросов ожидает завершения предыдущего кодирования в очереди. Об ошибках сообщается либо путем немедленного создания исключения, если аргументы или порядок вызовов методов нарушают контракт API, либо путем вызова обратного вызова error()
в случае проблем, возникших в реализации кодека. Если кодирование завершается успешно, вызывается обратный вызов output()
с новым закодированным фрагментом в качестве аргумента. Еще одна важная деталь заключается в том, что фреймам нужно сообщать, когда они больше не нужны, путем вызова close()
.
let frameCounter = 0;
const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);
const reader = trackProcessor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done) break;
const frame = result.value;
if (encoder.encodeQueueSize > 2) {
// Too many frames in flight, encoder is overwhelmed
// let's drop this frame.
frame.close();
} else {
frameCounter++;
const keyFrame = frameCounter % 150 == 0;
encoder.encode(frame, { keyFrame });
frame.close();
}
}
Наконец пришло время закончить кодирование кода, написав функцию, которая обрабатывает фрагменты закодированного видео по мере их выхода из кодера. Обычно эта функция отправляет фрагменты данных по сети или объединяет их в медиаконтейнер для хранения.
function handleChunk(chunk, metadata) {
if (metadata.decoderConfig) {
// Decoder needs to be configured (or reconfigured) with new parameters
// when metadata has a new decoderConfig.
// Usually it happens in the beginning or when the encoder has a new
// codec specific binary configuration. (VideoDecoderConfig.description).
fetch("/upload_extra_data", {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: metadata.decoderConfig.description,
});
}
// actual bytes of encoded data
const chunkData = new Uint8Array(chunk.byteLength);
chunk.copyTo(chunkData);
fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
method: "POST",
headers: { "Content-Type": "application/octet-stream" },
body: chunkData,
});
}
Если в какой-то момент вам понадобится убедиться, что все ожидающие запросы на кодирование выполнены, вы можете вызвать flush()
и дождаться его обещания.
await encoder.flush();
Декодирование
Настройка VideoDecoder
аналогична тому, что было сделано для VideoEncoder
: при создании декодера передаются две функции, а в configure()
передаются параметры кодека.
Набор параметров кодека варьируется от кодека к кодеку. Например, кодеку H.264 может потребоваться двоичный объект AVCC, если только он не закодирован в так называемом формате Приложения B ( encoderConfig.avc = { format: "annexb" }
).
const init = {
output: handleFrame,
error: (e) => {
console.log(e.message);
},
};
const config = {
codec: "vp8",
codedWidth: 640,
codedHeight: 480,
};
const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
const decoder = new VideoDecoder(init);
decoder.configure(config);
} else {
// Try another config.
}
После инициализации декодера вы можете начать снабжать его объектами EncodedVideoChunk
. Для создания чанка вам понадобится:
-
BufferSource
закодированных видеоданных - временная метка начала фрагмента в микросекундах (время носителя первого закодированного кадра в фрагменте)
- тип чанка, один из:
-
key
, если фрагмент может быть декодирован независимо от предыдущих фрагментов -
delta
, если фрагмент можно декодировать только после декодирования одного или нескольких предыдущих фрагментов
-
Кроме того, любые фрагменты, отправленные кодером, готовы для декодера как есть. Все сказанное выше об отчетах об ошибках и асинхронной природе методов кодировщика в равной степени справедливо и для декодеров.
const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
const chunk = new EncodedVideoChunk({
timestamp: responses[i].timestamp,
type: responses[i].key ? "key" : "delta",
data: new Uint8Array(responses[i].body),
});
decoder.decode(chunk);
}
await decoder.flush();
Теперь пришло время показать, как можно отобразить на странице только что декодированный кадр. Лучше убедиться, что обратный вызов декодера ( handleFrame()
) быстро возвращается. В приведенном ниже примере он только добавляет кадр в очередь кадров, готовых к рендерингу. Рендеринг происходит отдельно и состоит из двух этапов:
- Жду подходящего момента, чтобы показать кадр.
- Рисуем рамку на холсте.
Как только кадр больше не нужен, вызовите функцию close()
чтобы освободить базовую память до того, как к ней доберется сборщик мусора. Это уменьшит средний объем памяти, используемый веб-приложением.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;
function handleFrame(frame) {
pendingFrames.push(frame);
if (underflow) setTimeout(renderFrame, 0);
}
function calculateTimeUntilNextFrame(timestamp) {
if (baseTime == 0) baseTime = performance.now();
let mediaTime = performance.now() - baseTime;
return Math.max(0, timestamp / 1000 - mediaTime);
}
async function renderFrame() {
underflow = pendingFrames.length == 0;
if (underflow) return;
const frame = pendingFrames.shift();
// Based on the frame's timestamp calculate how much of real time waiting
// is needed before showing the next frame.
const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
await new Promise((r) => {
setTimeout(r, timeUntilNextFrame);
});
ctx.drawImage(frame, 0, 0);
frame.close();
// Immediately schedule rendering of the next frame
setTimeout(renderFrame, 0);
}
Советы разработчикам
Используйте панель мультимедиа в Chrome DevTools для просмотра журналов мультимедиа и отладки веб-кодеков.
Демо
Демонстрация ниже показывает, как создаются кадры анимации из холста:
- захватывается со скоростью 25 кадров в секунду в
ReadableStream
с помощьюMediaStreamTrackProcessor
- передан веб-работнику
- закодирован в видеоформат H.264
- снова декодируется в последовательность видеокадров
- и отображается на втором холсте с помощью
transferControlToOffscreen()
Другие демо
Также ознакомьтесь с другими нашими демо-версиями:
- Декодирование GIF-файлов с помощью ImageDecoder
- Захват данных с камеры в файл
- Воспроизведение MP4
- Другие образцы
Использование API веб-кодеков
Обнаружение функций
Чтобы проверить поддержку WebCodecs:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
Имейте в виду, что API WebCodecs доступен только в защищенных контекстах , поэтому обнаружение завершится неудачно, если self.isSecureContext
равно false.
Обратная связь
Команда Chrome хочет услышать о вашем опыте работы с API WebCodecs.
Расскажите нам о дизайне API
Что-то в API работает не так, как вы ожидали? Или вам не хватает методов или свойств, необходимых для реализации вашей идеи? У вас есть вопрос или комментарий по модели безопасности? Сообщите о проблеме спецификации в соответствующем репозитории GitHub или добавьте свои мысли к существующей проблеме.
Сообщить о проблеме с реализацией
Вы нашли ошибку в реализации Chrome? Или реализация отличается от спецификации? Сообщите об ошибке на сайте new.crbug.com . Обязательно включите как можно больше подробностей, простые инструкции по воспроизведению и введите Blink>Media>WebCodecs
в поле «Компоненты» . Glitch отлично подходит для быстрого и простого обмена репродукциями.
Показать поддержку API
Планируете ли вы использовать API WebCodecs? Ваша публичная поддержка помогает команде Chrome расставлять приоритеты для функций и показывает другим поставщикам браузеров, насколько важно их поддерживать.
Отправьте электронное письмо на адрес media-dev@chromium.org или отправьте твит на адрес @ChromiumDev , используя хэштег #WebCodecs
, и сообщите нам, где и как вы его используете.
Изображение героя , созданное Дениз Янс на Unsplash .