Pull to refresh

Оценка возможности постобработки видео в браузере

Reading time8 min
Views10K
В последнее время постобработка видео в рантайме приобретает всё большее значение — благодаря мощности современных ПК, почти каждый пользователь может пропустить видеоряд через сложную цепочку фильтров прямо во время просмотра, тем самым избавляясь от необходимости полноценного кодирования видео, зачастую производимого с помощью медленных и переусложненных средств.

Эта область довольно неплохо покрыта в десктопной среде — фильтры вроде ffdshow raw video filter и madVR позволяют делать практически всё, что может потребоваться для приятного просмотра. К сожалению, веб не может похвастаться аналогичным тулкитом, и вы либо наслаждаетесь всеми недостатками очередного видео на YouTube, либо открываете его во внешнем приложении вроде MPC-BE, что не очень удобно. А было бы неплохо иметь одну волшебную кнопку, активирующую фильтрацию в месте, где она и должна быть — в вашем браузере.

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

Замечания


Во время чтения статьи следует учесть:
  1. Все приведенные демонстрации основаны на html5 video с установленным атрибутом loop. Это видео может ужасно дергаться и лагать во время переключения на начало видеоряда в некоторых браузерах, по вине этих браузеров. Я не стал переусложнять код ради возможного исправления этой проблемы.
  2. Если повторяющееся видео вас раздражает, можно добавить loop=false к GET-параметрам запроса.
  3. Демки тестировались только в хроме, лисе и IE11, в остальных браузерах может не работать.
  4. Исходный код всех демок приведен прямо внутри соответствующих html-страниц, без зависимостей.
  5. В тексте много исковерканных английских слов и корявых переводов. Я плохо разбираюсь в русскоязычной терминологии, исправления приветствуются.
  6. Мы закрываем глаза на возможные проблемы с CORS, сайтами, использующими Flash-видео и т.п. Только сферические тесты в вакууме.
  7. В JavaScript я проездом, поэтому не стоит слишком доверять приведенному ниже тексту. Для большей уверенности можете поделить приведенное время на 2. Надеюсь увидеть исправления и советы в комментариях.

Принципы реализации


Единственным вариантом, который позволил бы иметь одно ядро для всех целевых браузеров (Chrome и Firefox в первую очередь) является браузерное расширение. Альтернатива в виде Google Chrome Native Client, внезапно, работает только в Chrome, и Mozilla на данный момент не собирается поддерживать NaCl в Firefox. Кроме того, я не изучал возможности доступа NaCl к элементам на странице — вполне может оказаться, что для наших целей он не сработает.

Базовый алгоритм работы (теоретического) расширения довольно прост: ищем элемент video на странице, прячем его, и сверху создаем canvas, на котором рендерятся фильтрованные кадры видео-потока. Пока всё просто.

Реальной проблемой у расширения является язык реализации — интерпретируемый JavaScript, а как мы знаем, интерпретируемые языки плохо подходят для серьезных расчетов. Но ведь это не беда! JavaScript последнее время получает море любви и оптимизаций, и существует довольно большое количество программистов, считающих, что JS — язык, подходящий для написания любых приложений и что вообще всё должно двигаться в веб. Более того, доступно множество новых технологий вроде asm.js, SIMD.js, WebGL и WebCL, которые, в теории, позволяют реализовывать всё, что душе угодно, со скоростью лишь немного меньше нативной. Так что мы не должны иметь никаких серьезных проблем с написанием набора фильтров в браузере, правда?

Не совсем.

Чистый JavaScript


Фильтрация в чистом JS работает по следующей схеме:
  1. Получаем оба необходимых элемента — спрятанный video и canvas, расположенный поверх него.
  2. Рисуем кадр из видео на канвасе через context.drawImage(video, 0, 0), где context — 2d контекст, полученный с канваса.
  3. Получаем буфер кадра (массив байтов цветов) через context.getImageData(0, 0, width, height).
  4. Обрабатываем буфер требуемыми фильтрами.
  5. Кладем обработанный массив обратно через context.putImageData(imageData, 0, 0).

Этот алгоритм работает и позволяет проводить реальную фильтрацию видео в чистом JavaScript с минимальным количеством очень похожего на C кода. Так будет выглядеть базовая (не оптимизированная) реализация фильтра invert, инвертирующего RGB байты в каждом пикселе кадра:
outputContext.drawImage(video, 0, 0);
var imageData = outputContext.getImageData(0, 0, width, height);
var source = imageData.data;
var length = source.length;
for (var i = 0; i < length; i += 4) {
    source[i  ] = 255 - source[i];
    source[i+1] = 255 - source[i+1];
    source[i+2] = 255 - source[i+2];
    // игнорируем альфу
}
outputContext.putImageData(imageData, 0, 0);

И хотя этот метод работает для демок и простых картинок, он очень быстро “сдувается” на высоких разрешениях. Хотя вызов drawImage сам по себе довольно быстр даже на 1080p, после добавления getImageData и putImageData время выполнения растет до 20-30 миллисекунд на одну итерацию. Полный код, приведенный выше, выполняется уже за 35-40мс, что является предельной скоростью для PAL-видео (25 кадров в секунду, 40мс на один кадр). Все замеры получены на 4770k, который является одним из наиболее мощных домашних процессоров на данный момент. Это означает, что выполнение любого более-менее сложного фильтра на предыдущих поколениях процессоров невозможно вне зависимости от производительности JavaScript. Любой, даже очень быстрый код, будет упираться в ужасную производительность самого канваса.

Но JavaScript не очень быстр сам по себе. Хотя обычные операции вроде инвертирования или прогона через LUT могут выполняться за разумное время, любой более-менее сложный фильтр вызывает ужасные лаги. Простая реализация фильтра добавления шума (Math.random()*10 к каждому пикселю) требует уже 55 миллисекунд, а 3х3 ядро для блюра, реализованное в приведенном ниже коде, проходит за 400мс, или 2.5 кадров в секунду.
function blur(source, width, height) {
    function blur_core(ptr, offset, stride) {
        return (ptr[offset - stride - 4] +
                ptr[offset - stride] +
                ptr[offset - stride + 4] +
                ptr[offset - 4] +
                ptr[offset] +
                ptr[offset + 4] +
                ptr[offset + stride - 4] +
                ptr[offset + stride] +
                ptr[offset + stride + 4]
                ) / 9;
    }

    var stride = width * 4;
    for (var y = 1; y < (height - 1); ++y) {
        var offset = y * stride;
        for (var x = 1; x < stride - 4; x += 4) {
            source[offset] = blur_core(source, offset, stride);
            source[offset + 1] = blur_core(source, offset + 1, stride);
            source[offset + 2] = blur_core(source, offset + 2, stride);
            offset += 4;
        }
    }
}

Firefox показывает еще более удручающие результаты с 800 мс/проход. Что интересно, IE11 опережает даже Chrome, причем в два раза (но сам canvas у него медленный, так что это не спасает). В любом случае, становится ясно, что чистый JavaScript — неправильное средство для реализации фильтров.

asm.js


Новомодный asm.js — средство от компании Mozilla для оптимизации выполнения JavaScript кода. Генерируемый код по-прежнему будет работать в хроме, однако надеяться не серьезный прирост производительности не стоит, поскольку поддержка asm.js, по всей видимости, еще не добавлена.

К сожалению, я не смог найти простой путь компиляции выбранных функций в asm.js-оптимизированный код. Emscripten генерирует около 4.5 тысяч строк кода при компиляции простой двустрочной функции, и я не понял, как можно вытащить из него только нужный код за разумное время. Писать же asm.js руками — то ещё удовольствие. В любом случае, asm.js упрётся в производительность 2d-контекста канваса, аналогично чистому JavaScript.

SIMD.js


SIMD.js — очень новая технология ручной оптимизации JS-приложений, которая в настоящий момент “поддерживается” только в Firefox Nightly, но очень скоро может получить поддержку всех целевых браузеров. К сожалению, API сейчас работает только с двумя типами данных, float32x4 и uint32x4, что делает всю затею бесполезной для большинства реальных 8-битных фильтров. Более того, тип Int32x4Array пока не реализован даже в Nightly, поэтому любая запись и чтение данных из памяти будут происходить медленно и страшно (когда реализованы подобным образом). Однако, приведу код реализации обычного фильтра инвертирования (на этот раз работающего через XOR):
function invert_frame_simd(source) {
    var fff = SIMD.int32x4.splat(0x00FFFFFF);
    var length = source.length / 4;
    var int32 = new Uint32Array(source.buffer);
    for (var i = 0; i < length; i += 4) {
        var src = SIMD.int32x4(int32[i], int32[i+1], int32[i+2], int32[i+3]);
        var dst = SIMD.int32x4.xor(src, fff);
        int32[i+0] = dst.x;
        int32[i+1] = dst.y;
        int32[i+2] = dst.z;
        int32[i+3] = dst.w;
    }
}

На данный момент приведенный код выполняется значительно медленней чистого JS — 1600мс/проход (пользователи Nighly могут попробовать очередное демо). Похоже, придется подождать еще достаточное количество времени, прежде чем можно будет делать хоть что-то полезное с этой технологией. К сожалению, не ясно, как будет реализована поддержка 256-битных YMM регистров (int32x4 — обычный 128-битный xmm из SSE2), и будут ли доступны инструкции из более новых технологий вроде SSSE3. Ну и SIMD.js не спасает от медленного канваса. Зато фанаты SIMD могут уже сейчас получить некоторые привычные баги, прямо в браузере!

WebGL


Совершенно другой способ реализации фильтров — WebGL. В самом базовом понимании WebGL — JS-интерфейс для нативной технологии OpenGL, которая позволяет выполнять разнообразный код на GPU. Обычно она используется для программирования графики в играх и т.п., однако никто не мешает обрабатывать картинки или даже видео с её помощью. WebGL также не требует вызовов getImageData, что в теории позволяет избежать типичного 20мс-лага.

Но ничто не бывает бесплатно — WebGL не является средством общего назначения и использовать это API для абстрактного неграфического кода — ужасная боль. Потребуется определять бесполезные вертексы (которые всегда будут покрывать весь кадр), правильно позиционировать текстуру (которая будет закрывать весь кадр), а затем использовать видео в качестве текстуры. К счастью, WebGL достаточно умён, чтобы запрашивать нужные кадры из видео автоматом. По крайней мере, в хроме и лисе. IE11 же обрадует ошибкой WEBGL11072: INVALID_VALUE: texImage2D: This texture source is not supported.

Наконец, для написания фильтров придётся использовать шейдеры, реализуемые на немного ущербном языке GLSL, который (по крайней мере в WebGL-варианте) даже не поддерживает установку константных массивов, поэтому любые массивы надо будет либо передавать с помощью uniforms (такие типа-глобальные переменные), либо использовать индийский способ:
float core1[9];
core1[0] = 1.0;
core1[1] = 1.0;
core1[2] = 0.0;
core1[3] = 1.0;
core1[4] = 0.0;
core1[5] = -1.0;
core1[6] = 0.0;
core1[7] = -1.0;
core1[8] = -1.0;

Он также требует чтобы пиксельный шейдер возвращал одно значение — цвет текущего пикселя, что делает невозможной типичную реализацию некоторых фильтров, обрабатывающих несколько пикселей за итерацию (то же подавление блочности). Такие фильтры придется переосмысливать и реализовывать по-другому.

В общем, технологии вроде CUDA и OpenCL были придуманы не от хорошей жизни.

В оправдание WebGL, он имеет действительно потрясающую для веба производительность (которую вы не можете измерить). По крайней мере, он может обработать фильтр prewitt из masktools (выбор максимального значения из четырех 3х3 ядер) в реальном времени на 1080p и выше. Если вы ненавидите себя и не боитесь получить немного неподдерживаемый код, WebGL позволяет делать с видео довольно интересные вещи. Более разумным может быть использование библиотеки seriously.js, которая прячет часть шаблонного WebGL-кода, однако может оказаться недостаточно продвинутой для обработки изменения разрешения видео или реализации временных фильтров.

Если же вы себя любите, то, скорее всего, вам захочется использовать что-то вроде WebCL.

WebCL


Но не получится. Википедия говорит, что WebCL 1.0 был финализирован 19-ого марта этого года, что делает технологию самой молодой из всего списка, моложе даже SIMD.js. И, в отличие от SIMD.js, она не будет поддерживаться в Firefox в ближайшем будущем. Где-то читал об аналогичном решении для Chrome, но потерял ссылку. Так что WebCL на данный момент является мёртвой технологией без ясного будущего.

Заключение


Обработка видео в реальном времени в браузере возможна, однако единственный рабочий сейчас вариант реализации — использование WebGL, программирование видео-фильтров на котором — занятие, достойное настоящих мазохистов. Все остальные методы упираются в ужасную производительность 2d-контекста канваса, да и сами по себе не блещут скоростью выполнения. Такие грустные дела.
Tags:
Hubs:
+34
Comments14

Articles

Change theme settings