DevGang
Авторизоваться

Быстрая обработка видео JavaScript в реальном времени / постобработка

Здесь я расскажу, как управлять рендерингом видео в браузере. Это охватывает манипуляции на стороне клиента с использованием HTML и JS, а не манипуляции на стороне сервера.

Проблема, которую нужно решить: быстрая, асинхронная, потенциально аппаратно-ускоренная обработка/постобработка видео.

Пример: резкость и насыщенность видео.

Для этого я буду использовать холст, который удобно может принимать видео в качестве входного изображения. Настроим базовый код.

html
  <video src="https://v.animethemes.moe/NoGameNoLife-ED1.webm" controls autoplay muted loop></video>
  <canvas></canvas>
  <script src="./main.js" type="module"></script>
JS
class VapourSynthWannabe {
  /**
   * @param  {HTMLVideoElement} video
   * @param  {HTMLCanvasElement} canvas
   */
  constructor (video, canvas) {
    this.destroyed = false
    this.video = video
    this.canvas = canvas
  }

  destroy () {
    this.destroyed = true
  }
}

window.processor = new VapourSynthWannabe(document.querySelector('video'), document.querySelector('canvas'))
// bound to window so you can play around with it in console

Подключение элемента видео:

// constructor
    video.addEventListener('resize', this.resize.bind(this))
    video.addEventListener('loadedmetadata', this.resize.bind(this))
    this.resize()
// ...
  resize () {
    this.canvas.width = this.video.videoWidth
    this.canvas.height = this.video.videoHeight
  }

  destroy () {
// ...
    this.video.removeEventListener('resize', this.resize)
    this.video.removeEventListener('loadedmetadata', this.resize)
  }

Здесь я создал функцию, которая изменяет размер холста в зависимости от размера видео, затем я запускаю функцию один раз и подключаю ее к двум видеособытиям, resize а затем loadedmetadata. Функция запускается один раз, если объект создается после того, как видео уже было загружено, что исключает loadedmetadata событие. Даже может срабатывать в resize нескольких случаях, но мы будем беспокоиться об этих двух:

  1. смена видеодорожки [требуется AudioVideoTracks включение в движках мерцания или media.track.enabled в движках гекконов]
  2. изменение разрешения видео

Изменение разрешения является наиболее заметным, которое может быть наиболее заметно использовано .webm или .mkv для создания забавных видео.

Рендеринг видео на холсте:


// constructor
    this.ctx = canvas.getContext('2d')
    this.timeout = setTimeout(this.processFrame.bind(this), 16)
// ...
  processFrame () {
    this.ctx.drawImage(this.video, 0, 0)
    this.timeout = setTimeout(this.processFrame.bind(this), 16)
  }
  destroy () {
// ...
    clearTimeout(this.timeout)
  }

Довольно просто, мы получаем видео в исходном разрешении, однако это не относится к частоте кадров видео и цветовому пространству.

Внимательный пользователь может заметить слегка зеленый оттенок на некоторых видео, и большинство людей могут легко заметить прерывистую частоту кадров.

К сожалению, на данный момент мы мало что можем сделать с цветовым пространством, в лучшем случае можно было бы декодировать видео с помощью WebCodecsAPI, но мне важна хотя бы базовая совместимость с браузером, а WebCodecsAPI по-прежнему нестандартен, поэтому пока я пройду мимо, я мог бы повторно посетить его в более позднем посте. К счастью, эта проблема с точностью цветопередачи возникает не так часто.

Причина, по которой видео прерывистое, очень проста: выходные кадры не соответствуют видеокадрам.

Но есть проблема. Браузер не предоставляет никаких полезных метаданных видео, и поэтому у нас нет понятия частоты кадров. Опять же, FPS будет бесполезен для VFR, поэтому для решения этой проблемы мне нужно будет рисовать видеокадр на холсте каждый раз, когда видео представляет кадр.

Однако Google здесь, чтобы спасти положение с помощью requestVideoFrameCallback, который также поддерживает Safari, но, как всегда, не хватает Firefox.

К счастью, я создал полифилл, requestAnimationFrame  который getVideoPlaybackQuality верит во всех браузерах. Это не идеально, но достаточно близко. Этот API идеален, так как его функциональность соответствует ожиданиям того, что делают тайм-ауты.

Исправление проблем с частыми кадрами:

import 'https://esm.sh/rvfc-polyfill'

// ...
// constructor
    this.callback = video.requestVideoFrameCallback(this.processFrame.bind(this))
// ...
  processFrame () {
    this.ctx.drawImage(this.video, 0, 0)
    this.callback = this.video.requestVideoFrameCallback(this.processFrame.bind(this))
  }
  destroy () {
// ...
    this.video.cancelVideoFrameCallback(this.callback)
  }

Идеальный! Это решает проблему прерывистой частоты кадров, потенциальной потери кадров, и теперь нам не нужно думать о потере производительности из-за многократной обработки одного и того же кадра или обработки самого состояния видео. Видео ПВП также будут правильно, а видео с безумно высокой частотой кадров будут плавать, как масло.

Этот API заметил, был ли отрисован видеокадр, если видеоэлемент не виден и/или с визуализацией в DOM, новые кадры не будут отрисовываться или будут отрисовываться с меньшей скоростью. Браузеры делают это для экономии производительности, что вполне разумно. Если ваш элемент видео был создан с использованием document.createElement('video') DOM и никогда не добавлялся к нему, вам появлялся какой-то всегда видимый фиктивный контейнер для добавления видео, который минимум, у меня был, надежно работал:

.absolute-container {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
}
.absolute-container > video, .absolute-container > canvas {
  opacity: 0.1%;
  width: 1px;
  height: 1px;
  position: relative;
}

Благодаря этому вы можете добавлять в контейнер столько видео и холстов, сколько хотите, и благодаря низкой непрозрачности они будут почти невидимы, и браузер не будет ограничивать их.

Постобработка кадра

У меня есть несколько вариантов обработки отдельных кадров, например, с помощью ImageData, который предоставляет данные в виде массива пикселей, поэтому вы можете легко «одолжить» некоторые фильтры VapourSynth для этого:

 processFrame () {
    this.ctx.drawImage(this.video, 0, 0)
    const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
    const processed = this.doSomePostProcessing(imageData)
    this.ctx.putImageData(processed, 0, 0)
    this.callback = this.video.requestVideoFrameCallback(this.processFrame.bind(this))
  }

Или ImageBitmaps, если вы хотите поэкспериментировать с обработкой WebGL:

  async processFrame () {
    this.ctx.drawImage(this.video, 0, 0)
    const bitmap = await globalThis.createImageBitmap(this.canvas)
    if (this.destroyed) return
    const processed = this.doSomeFancyPostProcessing(bitmap)
    this.ctx.putImageData(processed, 0, 0)
    this.callback = this.video.requestVideoFrameCallback(this.processFrame.bind(this))
  }

Но я выберу то, о чем большинство людей, вероятно, никогда не слышали или даже не использовали, а именно фильтры холста, которые не требуют от меня запуска какого-либо дополнительного кода во время каждого кадра, а всякий раз, когда холст мутирует:

  resize () {
    this.canvas.width = this.video.videoWidth
    this.canvas.height = this.video.videoHeight
    this.ctx.filter = 'contrast(1.5) saturate(200%)'
  }

Это дает довольно много преимуществ, в основном то, что мы обрабатываем не постобработку, а движок браузера, что приводит к значительному приросту производительности. Эти фильтры могут показаться простыми, но параметр URL позволяет использовать фильтры SVG, которые невероятно эффективны, но не широко используются, а это означает, что вы не найдете много готовых результатов.

#JavaScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В подарок 100$ на счет при регистрации

Получить