Материал основан на разборе freecodecamp.org. Ниже — главное и практические шаги, которые можно быстро применить в работе.
- Почему WebCodecs стал важным для фронтенда
- Предварительные требования
- Введение в обработку видео
- Видеокадры
- Кодеки
- Кодирование и декодирование
- Контейнеры
- Что такое WebCodecs?
- До WebCodecs
- Основной API
- Всё вместе
- Мультиплексирование и демультиплексирование
- Демультиплексирование
- Мультиплексирование
- Создание утилиты для конвертации видео
- Перекодирование
- Трансформации
- Конвейер трансформаций
- Полное демо
- Производственные вопросы
- Кодеки
- Битрейт
- GPU и CPU
- Память
- Дополнительные ресурсы
Почему WebCodecs стал важным для фронтенда
Если вы когда-либо пытались обрабатывать видео в браузере — например, для приложения видеомонтажа или стриминга — у вас было два варианта: обрабатывать видео на сервере (дорого) или использовать ffmpeg.js (неудобно). С WebCodecs API теперь есть лучший способ сделать это.
WebCodecs — это относительно новый API, который позволяет браузерным приложениям эффективно обрабатывать видео с очень низкоуровневым контролем.
Раньше, если вы хотели создать, скажем, приложение для видеомонтажа, студию прямых трансляций или что-либо, требующее «тяжёлых вычислений», вам нужно было разрабатывать нативное десктопное приложение. Многие SaaS-инструменты, такие как Canva, обходили это с помощью серверной обработки видео, что обеспечивало значительно лучший UX, но было гораздо сложнее и дороже.
С WebCodecs теперь можно создавать такие приложения полностью в браузере, не требуя от пользователей загрузки и установки программного обеспечения, и без дорогостоящей, сложной серверной инфраструктуры.
Это не теория. Инструменты видеомонтажа, такие как Capcut, зафиксировали рост трафика на 83% после перехода на WebCodecs + WebAssembly [ 1 ]. Утилиты, такие как Remotion Convert и Free AI Video Upscaler (оба с открытым исходным кодом), обрабатывают тысячи видео в день с нулевыми серверными затратами и без необходимости установки [ 2 ].
WebCodecs используется даже для совершенно новых сценариев, например для программной генерации видео [ 3 ].
Если вы создаёте какое-либо видеоприложение, стоит хотя бы знать о WebCodecs как о варианте для работы с видео в браузере.
В этом руководстве мы:
Рассмотрим основы обработки видео
Познакомимся с WebCodecs API
Обсудим мультиплексирование и демультиплексирование для чтения и записи видеофайлов
Создадим собственную утилиту конвертации видео для преобразования видео между форматами webm и mp4, а также применим базовые трансформации
Рассмотрим некоторые вопросы производственного уровня
Обсудим дополнительные ресурсы
Цель этой статьи — стать практической точкой входа и введением в WebCodecs API для фронтенд-разработчиков. Она научит вас, как работает API и что с его помощью можно делать. Я предполагаю, что вы знаете основы JavaScript, но вам не нужно быть старшим разработчиком или видеоинженером, чтобы следовать материалу.
В конце я упомяну дополнительные учебные ресурсы и ссылки. В будущих руководствах я подробнее остановлюсь на конкретных темах, таких как создание видеоредактора или организация прямых трансляций с помощью WebCodecs. Но это руководство должно стать надёжной отправной точкой для понимания того, что такое WebCodecs, что он умеет и как создать базовое приложение с его помощью.
По этой теме полезно отдельно посмотреть Создание динамических форм в React и Next.js, чтобы расширить контекст и сравнить подходы.
Предварительные требования
Вам не нужно быть видеоинженером, чтобы следовать материалу, но вы должны уверенно владеть:
Основным JavaScript, включая async/await и колбэки
Базовыми браузерными API, такими как fetch и DOM
Пониманием того, что такое объект File и как работают файловые инпуты в HTML
Общим представлением о том, что такое HTML5 (мы будем использовать его кратко, но не будем углубляться)
Предварительные знания об обработке видео, кодеках или медиа-API не требуются — именно это охватывает первая половина данного руководства.
Введение в обработку видео
Не торопитесь, потому что прежде чем переходить к WebCodecs, я хочу убедиться, что вы понимаете, что такое кодеки, прежде чем мы вообще начнём рассматривать кодеки в вебе.
Видеокадры
Полагаю, вы знаете, что такое видео. Как ни иронично, «видео» ниже на самом деле является gif-файлом, но суть понятна.
Видео — это просто серия изображений, показываемых одно за другим в быстрой последовательности. Каждое изображение называется видеокадром (Video Frame), и каждый кадр связан с временной меткой. Когда видеоплеер воспроизводит видео, он отображает каждый видеокадр в момент времени, указанный временной меткой.
Каждый кадр в видео состоит из пикселей: кадр 4K-видео содержит приблизительно 8 миллионов пикселей (3840×2160 = 8 294 400).
Каждый пиксель сам по себе состоит из 3 компонентов: значений красного, зелёного и синего (также называемых RGB-значениями).
Каждое из значений цвета R, G и B хранится как 8-битное целое число в диапазоне от 0 до 255, где число указывает интенсивность красного, зелёного или синего цветового компонента.
Комбинирование интенсивности каждого из компонентов R, G и B позволяет представить любой произвольный цвет в цветовом спектре:
Таким образом, для каждого пикселя нам нужно 3 байта данных: 1 байт для каждого из значений цвета R, G и B (1 байт = 8 бит). Кадр 4K-видео, следовательно, будет содержать ~25 мегабайт данных.
При 30 кадрах в секунду (типичная частота кадров) часовое 4K-видео составило бы около 746 гигабайт данных. Если вы когда-либо скачивали большое видео или записывали HD-видео на камеру телефона, вы знаете, что видеофайлы могут быть большими, но никогда не бывают настолько большими.
В действительности реальные видеофайлы, которые вы можете смотреть на YouTube, записывать на камеру телефона или скачивать из интернета, примерно в 100 раз меньше. Причина, по которой реальные видеофайлы значительно меньше, — это сжатие видео, семейство очень сложных алгоритмов, которые помогают уменьшить объём данных примерно в 100 раз.
Без этого сжатия видео вы не смогли бы записать более 10 минут видео на новейших высококлассных смартфонах, и вы не смогли бы стримить что-либо в HD даже при высококлассном домашнем интернет-соединении.
Какими бы совершенными ни были наши современные устройства и интернет-соединения, без агрессивного сжатия видео мы не смогли бы смотреть, записывать или стримить что-либо в HD.
Кодеки
Кодек — это модное слово для обозначения алгоритма сжатия видео. Существует несколько устоявшихся кодеков / алгоритмов сжатия, таких как:
h264 : Наиболее распространённый кодек. Если вы видите файл mp4, скорее всего, он использует кодек h264.
vp9 : Кодек с открытым исходным кодом, широко используемый YouTube и в видеоконференциях, часто встречается в файлах webm.
av1 : Новый кодек с открытым исходным кодом, всё активнее используемый такими платформами, как YouTube и Netflix.
Принцип работы этих алгоритмов слишком сложен и выходит за рамки данного руководства. Но на очень высоком уровне вот несколько основных способов, которыми эти алгоритмы сжимают видео:
Все эти алгоритмы используют технику, называемую дискретным косинусным преобразованием (Discrete Cosine Transform), для «удаления деталей». По мере удаления «деталей» из видеокадра кадр начинает выглядеть «блочнее». Эта техника настолько эффективна, что вы можете сжать видеокадр примерно в 10 раз, прежде чем различия станут заметны человеческому глазу.
Для любопытных: вы можете посмотреть это видео от Computerphile о том, как работает алгоритм DCT.
Когда вы смотрите на последовательность видеокадров, вы заметите, что визуально они довольно похожи, и только небольшие части видео изменяются в зависимости от того, насколько много движения.
Эти кодеки/алгоритмы сжатия используют сложную математику и методы компьютерного зрения для кодирования только различий между кадрами.
Поэтому вам нужно отправить только первый кадр (ключевой кадр, Key Frame) — затем для последующих кадров вы можете отправлять «разницу между кадрами», также называемую дельта-кадрами (Delta Frames), для восстановления каждого полного кадра.
На практике для часового видео мы не просто кодируем первый кадр и храним миллионы дельта-кадров. Вместо этого алгоритмы кодируют примерно каждый 60-й кадр как ключевой кадр, а следующие 59 кадров являются дельта-кадрами.
Эта техника также весьма эффективна и позволяет сократить используемые данные ещё примерно в 10 раз. Различие между ключевыми кадрами и дельта-кадрами — одна из немногих деталей «как работают эти алгоритмы», о которых вам действительно нужно знать.
Существует ряд других деталей и техник сжатия, входящих в эти алгоритмы сжатия, которые выходят за рамки вводной статьи.
Кодирование и декодирование
Чтобы сжатие видео работало, нам нужно уметь как сжимать видео (превращать сырое видео в сжатые бинарные данные), так и распаковывать видео (превращать сжатые бинарные данные обратно в сырые видеокадры).
Превращение сырых видеокадров в сжатые бинарные данные называется кодированием (encoding), а превращение сжатых бинарных данных обратно в сырые видеокадры называется декодированием (decoding). Слово «кодек» — это просто сокращение от «encode decode» (кодирование-декодирование).
С практической, разработческой точки зрения вам не нужно знать, как работают эти кодеки, но вам нужно знать, что:
Существуют разные видеокодеки, такие как h264, vp9 и av1
Когда вы кодируете видео с помощью кодека (например, h264), вам нужен видеоплеер, поддерживающий тот же кодек, для воспроизведения видео.
Кодирование видео требует значительно больше вычислений, чем декодирование, поэтому воспроизведение 4K-видео на бюджетном телефоне — нормально, но кодирование 4K-видео на нём было бы крайне медленным.
Большинство потребительских устройств (телефоны, ноутбуки) имеют специализированные чипы, разработанные специально для кодирования и декодирования видео, что делает кодирование/декодирование значительно быстрее, чем если бы оно выполнялось на CPU как обычная программа. Это называется аппаратным ускорением (hardware acceleration).
На практике существует лишь несколько видеокодеков, потому что весь мир должен договориться о стандартах, чтобы видео, записанное на iPhone, можно было воспроизвести на устройстве с Windows.
Контейнеры
Большинство людей никогда не слышали о h264 или vp9. Когда вы думаете о видеофайлах, вы обычно думаете о форматах файлов, таких как MP4 или MKV. Они тоже важны, но это отдельная вещь, называемая контейнерами.
Видеофайл обычно содержит закодированное аудио, закодированное видео и метаданные о видеофайле. Формат файла, такой как MP4, описывает конкретный формат хранения закодированных аудио- и видеоданных, а также метаданных.
Программное обеспечение для сжатия видео сохраняет закодированные аудио/видео и метаданные в файл в соответствии со спецификацией формата файла. Это называется мультиплексированием (muxing).
Аналогично, видеоплееры следуют спецификациям формата файла для чтения метаданных и поиска закодированных аудио/видео. Это называется демультиплексированием (demuxing).
При сжатии видеофайла вам нужно как закодировать его, так и мультиплексировать (в таком порядке). Это два отдельных этапа процесса. Аналогично, при воспроизведении видеофайла вам нужно сначала демультиплексировать его, а затем декодировать (в таком порядке).
Когда видеоплеер открывает, скажем, файл mp4, логика работы следующая:
Хорошо, файл заканчивается на .mp4, значит, это файл mp4. Загружу библиотеку для разбора mp4-файлов и разберу файл.
Отлично, я разобрал mp4-файл, теперь у меня есть метаданные и я знаю, по каким байтовым смещениям получить закодированные аудио и видео.
Начну получать первые закодированные видеокадры, декодировать их и начну отображать декодированный видеокадр пользователю.
Если вы когда-либо видели сообщение «видеофайл повреждён» от видеоплеера, скорее всего, видеофайл не соответствует спецификации формата файла и при попытке разобрать / демультиплексировать видео произошла ошибка.
Что такое WebCodecs?
Теперь, когда мы разобрались с кодеками, давайте перенесём их в веб.
WebCodecs — это API, который позволяет фронтенд-разработчикам эффективно кодировать и декодировать видео в браузере (с использованием аппаратного ускорения) и с очень низкоуровневым контролем (кодирование/декодирование покадрово).
Аппаратное ускорение здесь принципиально важно, поскольку этот API нельзя просто полифилить или реализовать самостоятельно. WebCodecs предоставляет прямой доступ к специализированному оборудованию для кодирования/декодирования, что делает его таким же производительным, как десктопное видеоприложение.
До WebCodecs
Стоит на мгновение остановиться и понять, почему WebCodecs вообще появился. До появления WebCodecs API существовало несколько альтернатив для работы с видео в браузере.
HTMLVideoElement : Вы по-прежнему можете создать элемент и использовать его для декодирования видео. Это просто в использовании, но вы лишены покадрового контроля. Единственная возможность управления — задать свойство video.currentTime и ждать перемотки, что нередко приводит к пропуску или потере кадров.
Media Recorder API : По сути позволяет «записывать экран» с любого элемента canvas или видеопотока. Хотя это работает, функционально это эквивалентно записи экрана Adobe Premiere Pro вместо нажатия кнопки рендера. В сценариях редактирования вы теряете покадровый контроль и можете обрабатывать видео только в режиме реального времени.
FFMPEG.js : Порт популярного инструмента обработки видео ffmpeg, запускающий ffmpeg в браузере. Многие инструменты использовали его в прошлом, однако он лишён аппаратного ускорения, что делает его значительно медленнее WebCodecs. Кроме того, из-за работы в WebAssembly существуют ограничения на размер файла, что затрудняет работу с видео размером более 100 МБ.
WebCodecs был создан и выпущен в 2021 году для обеспечения низкоуровневого, аппаратно ускоренного декодирования и кодирования видео. Он отлично подходит для высокопроизводительного стриминга и видеомонтажа — сценариев, которые плохо поддерживались существующими API.
Основной API
Основной API WebCodecs состоит из двух новых «типов данных» — VideoFrame и EncodedVideoChunk, — а также интерфейсов VideoEncoder и VideoDecoder.
JavaScript-объект VideoFrame концептуально содержит как пиксельные данные, так и метаданные о видеокадре.
Вы можете создать новый объект VideoFrame из любого источника изображения, при условии что укажете метаданные:
const bitmapFrame = new VideoFrame(imgBitmap, {timestamp: 0});
const imageFrame = new VideoFrame(htmlImageEl, {timestamp: 0});
const videoFrame = new VideoFrame(htmlVideoEl, {timestamp: 0});
const canvasFrame = new VideoFrame(canvasEl, {timestamp: 0});
Для приложения видеомонтажа, например, вы обычно выполняете операции редактирования изображения для каждого кадра на canvas, а затем получаете каждый VideoFrame с canvas.
Вы также можете отрисовать VideoFrame на canvas с помощью контекста рендеринга Canvas 2D:
ctx.drawImage(frame, 0, 0);
Обычно это делается при рендеринге или воспроизведении видео в браузере.
EncodedVideoChunk — это просто сжатая версия VideoFrame, содержащая бинарные данные, а также те же метаданные, что и кадр.
Как правило, EncodedVideoChunk-объекты получают из библиотеки, которая извлекает их из объекта File.
import { getVideoChunks } from 'webcodecs-utils'
const chunks = <EncodedVideoChunk[]> await getVideoChunks(<File> file);
В качестве альтернативы это выходные данные, которые вы получаете от объекта VideoEncoder.
С EncodedVideoChunk-объектами не так много полезного можно сделать — это просто бинарные данные, которые вы читаете из файлов, записываете в файлы или передаёте по сети.
Ценность EncodedVideoChunk в том, что он примерно в 100 раз меньше, чем сырые видеоданные, — именно поэтому при стриминге (и записи в файл) вы отправляете EncodedVideoChunk, а не сырое видео.
VideoEncoder преобразует объекты VideoFrame в объекты EncodedVideoChunk.
Основной API выглядит примерно так: вы определяете колбэк, в котором VideoEncoder возвращает объекты EncodedVideoChunk.
const encoder = new VideoEncoder({
output: function(chunk: EncodedVideoChunk, meta: any){
// Что-то делаем с chunk
},
error: function(e: any)=> console.warn(e);
});
Имейте в виду, что это асинхронный процесс, причём не типичный асинхронный процесс. Нельзя просто рассматривать его как покадровую операцию.
// Так не работает
const frame = await encoder.encode(chunk);
Это связано с тем, как видеокодирование работает под капотом. Поэтому нужно принять, что выходные данные возвращаются через колбэк, и вы получаете их тогда, когда получаете.
После определения энкодера вы можете настроить VideoEncoder, выбрав кодек (мы вернёмся к этому), а также другие параметры: ширину, высоту, частоту кадров и битрейт.
encoder.configure({
'codec': 'vp9.00.10.08.00', // Вернёмся к этому
width: 1280,
height: 720,
bitrate: 1000000 //1 МБИТ/С,
framerate: 25
});
После этого можно начинать кодировать кадры. Здесь мы предполагаем, что объекты VideoFrame уже есть, и делаем каждый 60-й кадр ключевым кадром (Key Frame).
for (let i=0; i < frames.length; frames++){
encoder.encode(frames[i], {keyFrame: i%60 ==0})
}
VideoDecoder Видеодекодер выполняет обратную операцию, преобразуя объекты EncodedVideoChunk в объекты VideoFrame.
Вот упрощённый пример настройки VideoDecoder. Сначала извлеките объекты EncodedVideoChunk и конфигурацию декодера из видеофайла. Здесь мы не выбираем конфигурацию — её выбрал тот, кто кодировал файл. При декодировании мы извлекаем конфигурацию из файла.
import { demuxVideo } from 'webcodecs-utils';
const {chunks, config} = await demuxVideo(<File> file);
Далее мы настраиваем VideoDecoder, указывая колбэк для момента генерации объектов VideoFrame, и конфигурируем его с помощью полученного config.
const decoder = new VideoDecoder({
output: function(frame: VideoFrame){
//что-то делаем с VideoFrame
},
error: function(e: any)=> console.warn(e);
});
decoder.configure(config)
Как и в случае с VideoEncoder, кадры возвращаются через колбэк. Наконец, можно начинать декодировать чанки.
for (const chunk of chunks){
decoder.decode(chunk);
}
Всё вместе
По своей сути WebCodecs API — это просто два типа данных (EncodedVideoChunk, VideoFrame) и интерфейсы VideoEncoder и VideoDecoder, которые преобразуют данные из одного типа в другой.
Имейте в виду, что WebCodecs API на самом деле не работает с видеофайлами. Он только применяет кодирование и декодирование, а объекты EncodedVideoChunk представляют собой просто бинарные данные.
Чтение и запись видеофайлов — это отдельная задача, которая называется мультиплексированием/демультиплексированием (muxing/demuxing).
Мультиплексирование и демультиплексирование
Чтобы записать видеофайл, необходимо выполнить мультиплексирование (muxing) видео. А чтобы воспроизвести видеофайл, нужно выполнить демультиплексирование (demuxing). Это предполагает соблюдение формата видеоконтейнера, разбор видеофайла (в случае демультиплексирования) или размещение закодированных видеоданных в нужном месте записываемого файла (мультиплексирование).
Мультиплексирование и демультиплексирование не входят в состав WebCodecs API, поэтому для их выполнения потребуется отдельная библиотека.
Демультиплексирование
Чтобы воспроизвести видео в браузере, необходимо сначала демультиплексировать его, а затем декодировать — именно в таком порядке.
Существует несколько библиотек для демультиплексирования видео, в том числе MediaBunny и web-demuxer. В целях данного руководства я создал очень упрощённую обёртку вокруг этих библиотек и опубликовал её в пакете webcodecs-utils, так что демультиплексирование сводится к двум строкам кода:
import { demuxVideo } from 'webcodecs-utils' const {chunks, config} = await demuxVideo(file); Это считывает всё видео в память, поэтому на практике так делать не стоит. Но это удобно для создания простого и читаемого примера «hello world» для WebCodecs.
Следующий фрагмент принимает видеофайл (объект File), декодирует его и отрисовывает результат на canvas. Здесь мы получаем кадры из колбэка вывода и выполняем вызовы отрисовки непосредственно из этого колбэка.
import { demuxVideo } from 'webcodecs-utils' async function playFile(file: File){ const {chunks, config} = await demuxVideo(file); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const decoder = new VideoDecoder({ output(frame: VideoFrame) { ctx.drawImage(frame, 0, 0); frame.close() }, error(e) {} }); decoder.configure(config); for (const chunk of chunks){ decoder.decode(chunk) } } Вот наш предельно минималистичный демонстрационный пример воспроизведения реального видео:
Для более «правильного» примера демультиплексирования — вот как выглядит демультиплексирование с помощью MediaBunny, где можно извлекать чанки итеративно.
Мультиплексирование
Чтобы записать видеофайл, недостаточно только закодировать его (с помощью VideoEncoder) — необходимо также выполнить мультиплексирование. Это предполагает взятие закодированных чанков и размещение их в нужном месте выходного бинарного файла, в который производится запись.
Опять же, для мультиплексирования видео нужна библиотека (MediaBunny), но в демонстрационных целях я создал очень простую обёртку. Здесь мы определяем базовый ExampleMuxer.
import { ExampleMuxer } from 'webcodecs-utils' const muxer = new ExampleMuxer('video'); for (const chunk of encodedChunks){ muxer.addChunk(chunk); } const outputBlob = await muxer.finish(); В качестве полного демонстрационного примера кодирования и мультиплексирования мы создадим энкодер и настроим его так, чтобы он мультиплексировал выходные закодированные чанки сразу по мере их поступления.
const encoder = new VideoEncoder({ output: function(chunk, meta){ muxer.addChunk(chunk, meta); }, error: function(e){} }) encoder.configure({ 'codec': 'avc1.4d0034', // Мы вернёмся к этому width: 1280, height: 720, bitrate: 1000000 //1 МБИТ/С, framerate: 25 }); Затем мы определим анимацию на canvas, которая будет отображать номер текущего кадра на экране — просто чтобы убедиться, что всё работает.
const canvas = new OffscreenCanvas(640, 360); const ctx = canvas.getContext('2d'); const TOTAL_FRAMES=300; let frameNumber = 0; let chunksMuxed = 0; const fps = 30; function renderFrame(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'white'; ctx.font = bold ${Math.min(canvas.width / 10, 72)}px Arial; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(Frame ${frameNumber}, canvas.width / 2, canvas.height / 2); } Наконец, мы создадим цикл кодирования, который будет отрисовывать текущий кадр, а затем кодировать его.
let flushed = false; async function encodeLoop(){ renderFrame(); const frame = new VideoFrame(canvas, {timestamp: frameNumber/fps*1e6}); encoder.encode(frame, {keyFrame: frameNumber %60 ===0}); frame.close(); frameNumber++; if(frameNumber === TOTAL_FRAMES) { if (!flushed) encoder.flush(); } else return requestAnimationFrame(encodeLoop); } Собрав всё вместе, можно закодировать анимацию с canvas в видеофайл с точностью до кадра.
Вы можете скачать видео и воспользоваться любым инструментом для инспекции видео, чтобы убедиться, что каждый отдельный номер кадра присутствует в файле.
Это одно из ключевых отличий, которое выделяет данный подход среди других веб-API, таких как MediaRecorder, который тоже умеет кодировать видео, но не обеспечивает точности на уровне кадра. WebCodecs гарантирует, что вы можете контролировать и обеспечивать согласованность каждого кадра.
Наконец, полноценный пример мультиплексирования с использованием MediaBunny выглядел бы следующим образом:
Создание утилиты для конвертации видео
Теперь, когда мы разобрали основы WebCodecs и мультиплексирование, перейдём к созданию MVP чего-то действительно полезного: утилиты для конвертации видео. С её помощью мы сможем конвертировать между форматами mp4 и webm, а также выполнять базовые операции — изменение размера и отражение видео.
Перекодирование
Прежде чем заниматься изменением размера и отражением, давайте сначала реализуем базовую конвертацию: декодирование видео и его кодирование в новый формат. Это называется перекодированием (transcoding).
Для перекодирования видео нам нужно выстроить конвейер из следующих процессов:
Демультиплексирование (Demuxing) : Чтение EncodedVideoChunks из видеофайла
Декодирование (Decoding) : Преобразование EncodedVideoChunks в VideoFrames
Кодирование (Encoding) : Преобразование VideoFrames в новые EncodedVideoChunks
Мультиплексирование (Muxing) : Запись EncodedVideoChunks в новый видеофайл
Наш конвейер выглядит примерно так:
Используя всё, что мы рассмотрели в этой статье до сих пор, можно было бы построить полностью рабочее демо, используя только VideoEncoder и VideoDecoder, как обсуждалось. Но тогда управление состоянием и отслеживание кадров становится сложным и чреватым ошибками.
Мы добавим ещё один уровень абстракции, используя Streams API, что сделает наш конвейер таким, как показано ниже. Это напрямую соответствует нашей ментальной модели конвейера и упрощает массу деталей, таких как управление состоянием.
const transcodePipeline = demuxerReader
.pipeThrough(new VideoDecoderStream(videoDecoderConfig))
.pipeThrough(new VideoEncoderStream(videoEncoderConfig))
.pipeTo(createMuxerWriter(muxer));
await transcodePipeline;
Для этого мы создадим TransformStream для VideoDecoder и VideoEncoder.
class VideoDecoderStream extends TransformStream<{ chunk: EncodedVideoChunk; index: number }, { frame: VideoFrame; index: number }> {
constructor(config: VideoDecoderConfig) {
let pendingIndices: number[] = [];
super(
{
start(controller) {
decoder = new VideoDecoder({
output: (frame) => {
const index = pendingIndices.shift()!;
controller.enqueue({ frame, index });
},
error: (e) => controller.error(e),
});
decoder.configure(config);
},
async transform(item, controller) {
pendingIndices.push(item.index);
decoder.decode(item.chunk);
},
async flush(controller) {
await decoder.flush();
if decoder.state !== 'closed' decoder.close();
},
}
);
}
}
Не буду утомлять вас полным кодом, но я упаковал эти утилиты в пакет webcodecs-utils, который можно использовать следующим образом:
import {
SimpleDemuxer,
VideoDecodeStream,
VideoEncodeStream,
SimpleMuxer,
} from "webcodecs-utils";
Тогда наш код для перекодирования файла принимает такой вид:
const demuxer = new SimpleDemuxer(videoFile);
await demuxer.load();
const decoderConfig = await demuxer.getVideoDecoderConfig();
const encoderConfig = {/*Whatever we decide*/};
// Set up muxer
const muxer = new SimpleMuxer({ video: "avc" });
// Build the upscaling pipeline
await demuxer.videoStream()
.pipeThrough(new VideoDecodeStream(decoderConfig))
.pipeThrough(new VideoEncodeStream(encoderConfig))
.pipeTo(muxer.videoSink());
// Get output
const blob = await muxer.finalize();
Для этого промежуточного демо, чтобы перекодирование действительно заработало, мы загрузим заранее подготовленный файл и добавим переключатель для вывода в формате mp4 (используя h264) или webm (используя vp9).
Мы будем использовать avc1.4d0034 для h264 (наиболее широко поддерживаемая строка кодека h264) и vp09.00.40.08.00 для vp9 (наиболее широко поддерживаемая строка vp9).
Вот базовое демо перекодирования на CodePen:
Трансформации
Если мы хотим применять к видео какие-либо трансформации — отражение, обрезку, поворот, изменение размера и тому подобное — мы не можем работать только с чистыми объектами VideoFrame.
Самый простой способ добиться этого — добавить элемент Canvas, где мы будем использовать 2d Canvas Context для манипуляций с исходным кадром и отрисовки его на холст.
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d');
// Very easy to do transformations
ctx.drawImage(sourceFrame, 0, 0);
Затем мы будем использовать Canvas в качестве исходного изображения для нашего выходного видеокадра.
const outFrame = new VideoFrame(canvas, {timestamp: sourceFrame.timestamp});
Чтобы применить операцию изменения размера, сначала установим размеры холста равными нашей выходной высоте и ширине.
const canvas = new OffscreenCanvas(outputWidth, outputHeight);
const ctx = canvas.getContext('2d');
// Resize sourceFrame to fit output dimensions
ctx.drawImage(sourceFrame, 0, 0, outputWidth, outputHeight);
Чтобы применить горизонтальное отражение с помощью canvas2d, можно сделать следующее:
ctx.scale(-1, 1);
ctx.translate(-outputWidth, 0);
ctx.drawImage(sourceFrame, 0, 0, outputWidth, outputHeight);
Можно создать полноценную функцию рендеринга, которая применяет эти трансформации и выглядит так:
function render(videoFrame, outW, outH, flipped) {
canvas.width = outW;
canvas.height = outH;
if (flipped) {
ctx.scale(-1, 1);
ctx.translate(-outW, 0);
}
ctx.drawImage(videoFrame, 0, 0, outW, outH);
}
Вот интерактивное демо того, как выглядят эти трансформации:
Конвейер трансформаций
С учётом этих трансформаций нам нужно скорректировать конвейер, добавив в него шаг преобразования. Он будет принимать VideoFrame, применять трансформации и возвращать преобразованный кадр.
В пакете webcodecs-utils для этой цели есть объект VideoProcessStream, который принимает асинхронную функцию, получающую VideoFrame и возвращающую VideoFrame:
import { VideoProcessStream} from "webcodecs-utils";
new VideoProcessStream(async (frame) => {
// Apply transformations
return procesedFrame;
}),
Чтобы применить наши трансформации, можно настроить это следующим образом:
import { VideoProcessStream} from "webcodecs-utils";
const canvas = new OffscreenCanvas(outW, outH);
const ctx = canvas.getContext('2d');
const processStream = new VideoProcessStream(async (frame) => {
if (flipped) {
ctx.scale(-1, 1);
ctx.translate(-outW, 0);
}
ctx.drawImage(frame, 0, 0, outW, outH);
return new VideoFrame(canvas, {timestamp: frame.timestamp});
});
Тогда наш полный конвейер выглядит так:
const demuxer = new SimpleDemuxer(videoFile);
await demuxer.load();
const decoderConfig = await demuxer.getVideoDecoderConfig();
const encoderConfig = {/*Whatever we decide*/};
// Set up muxer
const muxer = new SimpleMuxer({ video: "avc" });
// Build the upscaling pipeline
await demuxer.videoStream()
.pipeThrough(new VideoDecodeStream(decoderConfig))
.pipeThrough(processStream) // Just defined this
.pipeThrough(new VideoEncodeStream(encoderConfig))
.pipeTo(muxer.videoSink());
// Get output
const blob = await muxer.finalize();
Вот полностью рабочее демо с конвейером обработки:
Полное демо
Теперь, для создания полноценного инструмента, мы внесём несколько ключевых изменений:
Вы сможете загрузить собственное видео
Мы будем предпросматривать трансформации, извлекая кадр
Мы добавим измерение прогресса
Для ввода это тривиально:
<input type="file" onchange="handler(event)" />
Для предпросмотра кадров мы могли бы использовать WebCodecs для генерации превью, но поскольку предпросмотр не требует точности на уровне кадров или высокой производительности, проще воспользоваться HTML5 VideoElement для захвата видеокадра из исходного файла.
async function getFirstFrame(file) {
const video = document.createElement("video");
video.src = URL.createObjectURL(file);
video.muted = true;
await new Promise((resolve) =>
video.addEventListener("loadeddata", resolve, { once: true })
);
video.currentTime = 0;
await new Promise((resolve) =>
video.addEventListener("seeked", resolve, { once: true })
);
return new VideoFrame(video, {timestamp: 0});
}
Наконец, мы можем рассчитывать прогресс в функции обработки, используя временную метку кадра, делённую на длительность видео.
const {duration} = await demuxer.getMediaInfo();
const processStream = new VideoProcessStream(async (frame) => {
if (flipped) {
ctx.scale(-1, 1);
ctx.translate(-outW, 0);
}
ctx.drawImage(frame, 0, 0, outW, outH);
// Frame timestamps are in microseconds, duration in seconds
const progress = frame.timestamp/(duration*1e6);
return new VideoFrame(canvas, {timestamp: frame.timestamp});
});
Собрав всё это вместе, мы наконец можем создать полностью рабочую утилиту для конвертации видео:
И это всё! Мы создали MVP чего-то действительно полезного с WebCodecs 🎉, включая демультиплексирование, декодирование, трансформации через Canvas, кодирование и мультиплексирование.
Единственное отличие между этим и полноценным браузерным редактором вроде Capcut — масштаб и охват трансформаций. Но логика обработки видео была бы практически идентичной.
Производственные вопросы
Здорово, что нам удалось создать что-то полезное, но прежде чем завершить, важно рассмотреть ряд вопросов производственного уровня.
Кодеки
Возможно, вы заметили строки вроде vp09.00.10.08 в демонстрационных примерах, но я намеренно обошёл детали стороной. Разберём их сейчас.
Во-первых, WebCodecs работает с конкретными строками кодеков, такими как vp09.00.10.08, а не просто с 'vp9'. Следующий код не будет работать:
const codec = VideoEncoder({ codec: 'vp9', //Это не сработает! //… })
Как обсуждалось ранее, при декодировании видео у вас фактически нет выбора кодека. Видео уже закодировано, поэтому нужно получить кодек из самого видео, как показано в предыдущих демонстрациях.
Упомянутые библиотеки демультиплексирования определят правильную строку кодека самостоятельно, так что беспокоиться об этом не нужно.
const decoderConfig = await demuxer.getVideoDecoderConfig(); //decoderConfig.codec = точная строка кодека для видео
При кодировании видео вы можете выбрать кодек самостоятельно. Некоторые разработчики уделяют выбору кодека большое внимание, но с сугубо практической точки зрения следующие правила подойдут большинству:
Если видео, генерируемые вашим приложением, будут скачиваться пользователями и/или вы хотите выводить файлы mp4, используйте h264.
Если генерируемые видео предназначены для внутреннего использования или вы контролируете воспроизведение видео и вам не важен формат, используйте vp9 с webm (открытый исходный код, лучшее сжатие, наиболее широко поддерживаемый кодек).
Для большинства приложений этих двух вариантов будет достаточно — углублённый выбор кодека — это кроличья нора, в которую пока не нужно лезть.
После выбора семейства кодеков необходимо выбрать конкретную строку кодека, например avc1.42001f.
Остальные числа в строке задают определённые параметры кодека, которые не так важны с точки зрения разработчика. Если ваша цель — максимальная совместимость, вот шпаргалка по строкам кодеков:
avc1.42001f — базовый профиль, максимальная совместимость, поддерживает до 720p (поддержка 99,6%)
avc1.4d0034 — основной профиль, уровень 5.2 (поддерживает до 4K) (поддержка 98,9%)
avc1.42003e — базовый профиль, уровень 6.2 (поддерживает до 8K) (поддержка 86,8%)
avc1.64003e — высокий профиль, уровень 6.2 (поддерживает до 8K) (поддержка 85,9%)
vp09.00.10.08.00 — базовый, максимальная совместимость, уровень 1 (поддержка 99,98%)
vp09.00.40.08.00 — уровень 4 (поддержка 99,96%)
vp09.00.50.08.00 — уровень 5 (поддержка 99,97%)
vp09.00.61.08.00 — уровень 6 (поддержка 99,97%)
Также можно использовать функцию getCodecString из пакета webcodecs-utils:
Исчерпывающий список кодеков и строк кодеков, доступных в WebCodecs, можно найти здесь.
Битрейт
Помимо высоты и ширины (которые вы, предположительно, знаете из своего контента) и строки кодека (которую мы только что обсудили), при кодировании видео также необходимо указать битрейт.
Алгоритмы сжатия видео предполагают компромисс между качеством и размером файла. Можно получить высококачественное видео с большими файлами или видео более низкого качества с меньшими файлами.
Вот краткая визуализация того, как выглядят разные уровни качества для видео 1080p, закодированного с разными битрейтами:
300 кбит/с
1 Мбит/с
3 Мбит/с
10 Мбит/с
Вот краткая справочная таблица по выбору битрейта:
Также можно использовать следующую вспомогательную функцию в своём приложении в качестве быстрого приближения:
Та же функция доступна и в пакете webcodecs-utils.
GPU и CPU
На большинстве пользовательских устройств есть какой-либо графический процессор (как правило, называемый встроенной графикой). Это специализированные чипы с особой кремниевой архитектурой, оптимизированной для кодирования и декодирования видео, а также для базовой графики.
Услышав «GPU», вы можете подумать об ИИ-датацентрах и геймерах. Но с точки зрения веб-приложений GPU есть практически у каждого.
Это важно, потому что если большинство задач фронтенд-разработки работает почти исключительно с CPU, то WebCodecs и обработка видео работают преимущественно на GPU.
Вот краткое руководство по тому, какой тип данных где хранится:
Перемещение данных сопряжено с затратами производительности, и это также важно учитывать при управлении памятью.
Память
Объекты VideoFrame могут быть весьма большими — 30 МБ для видео 4K. Графическая карта пользователя, как правило, резервирует часть оперативной памяти под «видеопамять» или VRAM, где и хранятся объекты VideoFrame.
Таким образом, если у пользователя 8 ГБ оперативной памяти, обычно 2 ГБ из них будут выделены под VRAM (объём определяется операционной системой).
Если объём видеоданных превысит VRAM, приложение аварийно завершит работу. Это означает, что для типичного пользователя наличие в памяти более 67 кадров 4K (~2 секунды видео) приведёт к краху программы.
Объекты VideoFrame создаются каждый раз, когда вы вызываете new VideoFrame(source), а также из VideoDecoder — конкретно из колбэка вывода. Каждый раз при создании кадра потребление памяти растёт.
Нельзя полагаться на стандартную сборку мусора для объектов VideoFrame. Необходимо явно вызывать close() на кадре, когда работа с ним завершена:
frame.close()
В коде и демонстрации Streams/Pipeline, показанных ранее, кадры фактически закрываются сразу после кодирования в интерфейсах VideoProcessStream и VideoEncodeStream.
Ещё одна причина, по которой Streams полезны для WebCodecs, — это свойство highWaterMark, которое по умолчанию равно 10. Это означает, что при выполнении следующего кода:
вы гарантируете, что в памяти одновременно находится не более 10 видеокадров. Streams API позволяет задать этот лимит, тогда как сам браузер берёт на себя логику его соблюдения.
Если вы не используете Streams API, вам придётся самостоятельно следить за лимитами памяти и количеством открытых видеокадров.
Дополнительные ресурсы
В этой статье мы рассмотрели основы обработки видео, познакомились с ключевыми концепциями WebCodecs API и создали MVP утилиты для конвертации видео. Это один из простейших возможных демонстрационных примеров, который при этом затрагивает все части API. Мы также рассмотрели некоторые базовые производственные аспекты.
Это лишь введение, которое едва касается поверхности WebCodecs. При всей кажущейся простоте API, создание полноценного, готового к продакшену приложения на WebCodecs требует выхода за рамки hello-world демонстраций.
Чтобы узнать больше о WebCodecs, вы можете обратиться к MDN и WebCodecsFundamentals — обширному онлайн-учебнику, который значительно глубже погружается в тему WebCodecs.
Вы также можете изучить исходный код существующих, проверенных в продакшене приложений, таких как Remotion Convert (исходный код), которое наиболее близко к демонстрационному приложению, рассмотренному нами, и Free AI Video Upscaler (исходный код, конвейер обработки), которое послужило вдохновением для шаблонов проектирования, представленных здесь и реализованных в webcodecs-utils.
Наконец, хотя WebCodecs сложнее, чем кажется, вы можете значительно упростить себе жизнь, используя библиотеку вроде MediaBunny, которая берёт на себя многие детали, такие как управление памятью, файловый ввод-вывод и прочее. Я использую её в своих собственных продакшен-приложениях на WebCodecs.
Независимо от того, будете ли вы создавать полноценное, готовое к продакшену приложение на WebCodecs, теперь вы по крайней мере знаете, что такая возможность существует — относительно новая, обеспечивающая лучший пользовательский опыт при меньших серверных затратах, и которую всё активнее внедряют такие известные видеоприложения, как Capcut и Descript ради её преимуществ.



