Обработка видео с помощью WebCodecs

Управление компонентами видеопотока.

Евгений Земцов
Eugene Zemtsov
Франсуа Бофор
François Beaufort

Современные веб-технологии предоставляют множество способов работы с видео. 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 к сети или к хранилищу данных.
Путь от объекта 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:

  • Инициализируйте словарь с двумя функциями для обработки закодированных фрагментов и ошибок. Эти функции определяются разработчиком и не могут быть изменены после того, как они переданы в конструктор 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.
Путь от сети или хранилища к объекту 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() ) быстро возвращает результат. В приведенном ниже примере она только добавляет кадр в очередь кадров, готовых к рендерингу. Рендеринг происходит отдельно и состоит из двух этапов:

  1. Жду подходящего момента, чтобы показать рамку.
  2. Рисование рамки на холсте.

Как только кадр перестанет быть необходимым, вызовите 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, чтобы просматривать журналы мультимедиа и отлаживать веб-кодеки.

Скриншот панели мультимедиа для отладки веб-кодеков.
Панель «Медиа» в инструментах разработчика Chrome для отладки веб-кодеков.

Демо

Демонстрация показывает, как создаются кадры анимации с холста:

  • Запись производится со скоростью 25 кадров в секунду в ReadableStream с помощью MediaStreamTrackProcessor
  • передано веб-воркеру
  • закодировано в видеоформат H.264
  • снова декодировано в последовательность видеокадров
  • и отображено на втором холсте с помощью transferControlToOffscreen()

Другие демонстрации

Также посмотрите другие наши демонстрационные версии:

Использование 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 .