Управление компонентами видеопотока.
Современные веб-технологии предоставляют широкие возможности работы с видео. 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 .