Манипулирование компонентами видеопотока.
Современные веб-технологии предоставляют множество способов работы с видео. Media Stream API , Media Recording API , Media Source API и WebRTC API дополняют богатый набор инструментов для записи, передачи и воспроизведения видеопотоков. Решая определенные высокоуровневые задачи, эти API не позволяют веб-программистам работать с отдельными компонентами видеопотока, такими как кадры и несмешанные фрагменты кодированного видео или аудио. Чтобы получить низкоуровневый доступ к этим базовым компонентам, разработчики используют WebAssembly для добавления видео- и аудиокодеков в браузер. Но учитывая, что современные браузеры уже поставляются с различными кодеками (которые часто ускоряются аппаратно), переупаковка их в WebAssembly кажется пустой тратой человеческих и компьютерных ресурсов.
API WebCodecs устраняет эту неэффективность, предоставляя программистам возможность использовать компоненты мультимедиа, которые уже присутствуют в браузере. В частности:
- Видео и аудио декодеры
- Видео и аудио кодеры
- Необработанные видеокадры
- Декодеры изображений
API WebCodecs полезен для веб-приложений, которым требуется полный контроль над обработкой медиаконтента, например, для видеоредакторов, видеоконференций, потокового видео и т. д.
Рабочий процесс обработки видео
Кадры являются центральным элементом в обработке видео. Таким образом, в WebCodecs большинство классов либо потребляют, либо производят кадры. Видеокодеры преобразуют кадры в закодированные фрагменты. Видеодекодеры делают обратное.
Также VideoFrame
прекрасно работает с другими веб-API, будучи CanvasImageSource
и имея конструктор , который принимает CanvasImageSource
. Поэтому его можно использовать в таких функциях, как drawImage()
и texImage2D()
. Также его можно создавать из холстов, растровых изображений, видеоэлементов и других видеокадров.
API WebCodecs хорошо работает в тандеме с классами из API вставляемых потоков , которые подключают WebCodecs к дорожкам медиапотока .
-
MediaStreamTrackProcessor
разбивает медиадорожки на отдельные кадры. -
MediaStreamTrackGenerator
создает медиа-дорожку из потока кадров.
Веб-кодеки и веб-работники
По замыслу API WebCodecs выполняет всю тяжелую работу асинхронно и вне основного потока. Но поскольку обратные вызовы кадров и фрагментов часто могут вызываться несколько раз в секунду, они могут загромождать основной поток и, таким образом, делать веб-сайт менее отзывчивым. Поэтому предпочтительнее перенести обработку отдельных кадров и закодированных фрагментов в веб-воркер.
Чтобы помочь с этим, ReadableStream предоставляет удобный способ автоматической передачи всех кадров, поступающих с медиа-дорожки, в worker. Например, MediaStreamTrackProcessor
можно использовать для получения ReadableStream
для медиа-дорожки, поступающей с веб-камеры. После этого поток передается в web worker, где кадры считываются один за другим и ставятся в очередь в 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
для извлечения кадров из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:
- Словарь Init с двумя функциями для обработки закодированных фрагментов и ошибок. Эти функции определяются разработчиком и не могут быть изменены после передачи в конструктор
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 DevTools для просмотра журналов мультимедиа и отладки веб-кодеков.

Демо
Демонстрация ниже показывает, как выглядят кадры анимации на холсте:
- захвачено со скоростью 25 кадров в секунду в
ReadableStream
с помощьюMediaStreamTrackProcessor
- переведен в веб-работник
- закодировано в видеоформат H.264
- декодируется снова в последовательность видеокадров
- и визуализируется на втором холсте с помощью
transferControlToOffscreen()
Другие демо
Также посмотрите наши другие демонстрации:
- Декодирование GIF-файлов с помощью ImageDecoder
- Запись входных данных камеры в файл
- Воспроизведение MP4
- Другие образцы
Использование API WebCodecs
Обнаружение особенностей
Чтобы проверить поддержку 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
в поле Components .
Показать поддержку API
Планируете ли вы использовать API WebCodecs? Ваша публичная поддержка помогает команде Chrome расставлять приоритеты функций и показывает другим поставщикам браузеров, насколько важно их поддерживать.
Отправьте электронное письмо на адрес [email protected] или отправьте твит @ChromiumDev с хэштегом #WebCodecs
и расскажите нам, где и как вы его используете.
Главное изображение от Дениз Янс на Unsplash .