Управление компонентами видеопотока.
Современные веб-технологии предоставляют множество способов работы с видео. API для потоковой передачи мультимедиа , API для записи мультимедиа , API для источников мультимедиа и API WebRTC в совокупности представляют собой богатый набор инструментов для записи, передачи и воспроизведения видеопотоков. Хотя эти API решают некоторые высокоуровневые задачи, они не позволяют веб-программистам работать с отдельными компонентами видеопотока, такими как кадры и немультиплексированные фрагменты закодированного видео или аудио. Для получения низкоуровневого доступа к этим базовым компонентам разработчики используют WebAssembly для интеграции видео- и аудиокодеков в браузер. Но учитывая, что современные браузеры уже поставляются с различными кодеками (которые часто ускоряются аппаратно), переупаковка их в WebAssembly кажется пустой тратой человеческих и компьютерных ресурсов.
API WebCodecs устраняет эту неэффективность, предоставляя программистам возможность использовать медиакомпоненты, уже присутствующие в браузере. В частности:
- Видео- и аудиодекодеры
- Видео- и аудиокодеры
- Необработанные видеокадры
- Декодеры изображений
API WebCodecs полезен для веб-приложений, которым требуется полный контроль над обработкой медиаконтента, таких как видеоредакторы, видеоконференции, потоковое видео и т. д.
Рабочий процесс обработки видео
Кадры являются центральным элементом обработки видео. Поэтому в веб-кодеках большинство классов либо обрабатывают, либо создают кадры. Видеокодеры преобразуют кадры в закодированные фрагменты. Видеодекодеры делают обратное.
Кроме того, VideoFrame хорошо взаимодействует с другими веб-API, являясь CanvasImageSource и имея конструктор , принимающий CanvasImageSource . Таким образом, его можно использовать в таких функциях, как drawImage() и texImage2D() . Также его можно создавать из холстов, растровых изображений, видеоэлементов и других видеокадров.
API WebCodecs хорошо работает в тандеме с классами из API Insertable Streams , которые связывают WebCodecs с дорожками медиапотоков .
-
MediaStreamTrackProcessorразбивает медиадорожки на отдельные кадры. -
MediaStreamTrackGeneratorсоздает медиадорожку из потока кадров.
Веб-кодеки и веб-воркеры
По своей сути API WebCodecs выполняет всю сложную работу асинхронно и вне основного потока. Но поскольку коллбэки для обработки фреймов и фрагментов часто вызываются несколько раз в секунду, они могут перегружать основной поток и, следовательно, снижать отзывчивость веб-сайта. Поэтому предпочтительнее перенести обработку отдельных фреймов и закодированных фрагментов в веб-воркер.
Для этого ReadableStream предоставляет удобный способ автоматической передачи всех кадров, поступающих с медиадорожки, в обработчик. Например, MediaStreamTrackProcessor можно использовать для получения ReadableStream для медиапотока, поступающего с веб-камеры. После этого поток передается в веб-обработчик, где кадры считываются по одному и ставятся в очередь в VideoEncoder .
С помощью HTMLCanvasElement.transferControlToOffscreen даже рендеринг может выполняться вне основного потока. Но если все высокоуровневые инструменты окажутся неудобными, сам VideoFrame является переносимым и может быть перемещен между рабочими процессами.
Вебкодеки в действии
Кодирование

Canvas или ImageBitmap к сети или к хранилищу данных. Всё начинается с VideoFrame . Существует три способа построения видеокадров.
Источником изображения может служить холст, растровое изображение или видеоэлемент.
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });Используйте
MediaStreamTrackProcessorдля извлечения кадров изMediaStreamTrackconst 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; }Создайте кадр из его бинарного пиксельного представления в объекте
BufferSourceconst 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();
Декодирование

Canvas или ImageBitmap . Настройка VideoDecoder аналогична настройке VideoEncoder : при создании декодера передаются две функции, а параметры кодека передаются в configure() .
Набор параметров кодека различается от кодека к кодеку. Например, кодеку H.264 может потребоваться бинарный блок AVCC, если он не закодирован в так называемом формате Annex 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, чтобы просматривать журналы мультимедиа и отлаживать веб-кодеки.

Демо
Демонстрация показывает, как создаются кадры анимации с холста:
- Запись производится со скоростью 25 кадров в секунду в
ReadableStreamс помощьюMediaStreamTrackProcessor - передано веб-воркеру
- закодировано в видеоформат H.264
- снова декодировано в последовательность видеокадров
- и отображено на втором холсте с помощью
transferControlToOffscreen()
Другие демонстрации
Также посмотрите другие наши демонстрационные версии:
- Расшифровка GIF-файлов с помощью ImageDecoder
- Запись видеопотока с камеры в файл.
- воспроизведение MP4
- Другие образцы
Использование API WebCodecs
Обнаружение признаков
Чтобы проверить поддержку веб-кодеков:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
Следует помнить, что API WebCodecs доступен только в защищенных контекстах , поэтому обнаружение не удастся, если self.isSecureContext имеет значение false.
Узнать больше
Если вы новичок в WebCodecs, WebCodecs Fundamentals предлагает подробные статьи со множеством примеров, которые помогут вам лучше разобраться в теме.
Обратная связь
Команда Chrome хочет узнать о вашем опыте использования API WebCodecs.
Расскажите о проектировании API.
Есть ли что-то в API, что работает не так, как вы ожидали? Или отсутствуют методы или свойства, необходимые для реализации вашей идеи? Есть вопрос или комментарий по модели безопасности? Создайте заявку в соответствующем репозитории GitHub или добавьте свои мысли в существующую заявку.
Сообщить о проблеме с реализацией
Вы обнаружили ошибку в реализации Chrome? Или реализация отличается от спецификации? Сообщите об ошибке на new.crbug.com . Обязательно укажите как можно больше подробностей, простые инструкции по воспроизведению и введите Blink>Media>WebCodecs в поле Components .
Показать поддержку API
Планируете ли вы использовать API WebCodecs? Ваша публичная поддержка помогает команде Chrome расставлять приоритеты в разработке новых функций и показывает другим производителям браузеров, насколько важно их поддерживать.
Отправляйте электронные письма на адрес media-dev@chromium.org или пишите в Твиттере @ChromiumDev , используя хэштег #WebCodecs , и сообщайте нам, где и как вы его используете.
Главное изображение предоставлено Дениз Янс на Unsplash .