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

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

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

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

Вебкодеки в действии

Кодирование

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

Скриншот Медиа-панели для отладки WebCodecs
Медиа-панель в Chrome DevTools для отладки веб-кодеков.

Демо

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

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

Другие демо

Также ознакомьтесь с другими нашими демо-версиями:

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

,

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

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

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

Вебкодеки в действии

Кодирование

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

Скриншот Медиа-панели для отладки WebCodecs
Медиа-панель в Chrome DevTools для отладки веб-кодеков.

Демо

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

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

Другие демо

Также ознакомьтесь с другими нашими демо-версиями:

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