- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

Замечания

Во время чтения статьи следует учесть:

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

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

Единственным вариантом, который позволил бы иметь одно ядро для всех целевых браузеров (Chrome и Firefox в первую очередь) является браузерное расширение. Альтернатива в виде Google Chrome Native Client [6], внезапно, работает только в 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 [7], после добавления getImageData и putImageData время выполнения растет до 20-30 миллисекунд на одну итерацию [8]. Полный код, приведенный выше, выполняется уже за 35-40мс [9], что является предельной скоростью для PAL-видео (25 кадров в секунду, 40мс на один кадр). Все замеры получены на 4770k, который является одним из наиболее мощных домашних процессоров на данный момент. Это означает, что выполнение любого более-менее сложного фильтра на предыдущих поколениях процессоров невозможно вне зависимости от производительности JavaScript. Любой, даже очень быстрый код, будет упираться в ужасную производительность самого канваса.

Но JavaScript не очень быстр сам по себе. Хотя обычные операции вроде инвертирования или прогона через LUT могут выполняться за разумное время, любой более-менее сложный фильтр вызывает ужасные лаги. Простая реализация фильтра добавления шума [10] (Math.random()*10 к каждому пикселю) требует уже 55 миллисекунд, а 3х3 ядро для блюра [11], реализованное в приведенном ниже коде, проходит за 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 [12] — средство от компании Mozilla для оптимизации выполнения JavaScript кода. Генерируемый код по-прежнему будет работать в хроме, однако надеяться не серьезный прирост производительности не стоит, поскольку поддержка asm.js, по всей видимости, еще не добавлена [13].

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

SIMD.js

SIMD.js [16] — очень новая технология ручной оптимизации JS-приложений, которая в настоящий момент “поддерживается” только в Firefox Nightly [17], но очень скоро может получить поддержку всех целевых браузеров [18]. К сожалению, API сейчас работает только с двумя типами данных [19], float32x4 и uint32x4, что делает всю затею бесполезной для большинства реальных 8-битных фильтров. Более того, тип Int32x4Array пока не реализован даже в Nightly, поэтому любая запись и чтение данных из памяти будут происходить медленно и страшно (когда реализованы подобным образом [20]). Однако, приведу код реализации обычного фильтра инвертирования (на этот раз работающего через 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 могут попробовать очередное демо [21]). Похоже, придется подождать еще достаточное количество времени, прежде чем можно будет делать хоть что-то полезное с этой технологией. К сожалению, не ясно, как будет реализована поддержка 256-битных YMM регистров (int32x4 — обычный 128-битный xmm из SSE2), и будут ли доступны инструкции из более новых технологий вроде SSSE3. Ну и SIMD.js не спасает от медленного канваса. Зато фанаты SIMD могут уже сейчас получить некоторые привычные баги [22], прямо в браузере!

WebGL

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

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

Наконец, для написания фильтров придётся использовать шейдеры, реализуемые на немного ущербном [27] языке GLSL, который (по крайней мере в WebGL-варианте) даже не поддерживает установку константных массивов [28], поэтому любые массивы надо будет либо передавать с помощью 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, он имеет действительно потрясающую для веба производительность (которую вы не можете измерить [29]). По крайней мере, он может обработать фильтр prewitt из masktools [30] (выбор максимального значения из четырех 3х3 ядер) в реальном времени на 1080p [31] и выше. Если вы ненавидите себя и не боитесь получить немного неподдерживаемый код, WebGL позволяет делать с видео довольно интересные вещи. Более разумным может быть использование библиотеки seriously.js [25], которая прячет часть шаблонного WebGL-кода, однако может оказаться недостаточно продвинутой для обработки изменения разрешения видео или реализации временных фильтров.

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

WebCL

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

Заключение

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

Автор: tp7

Источник [34]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/news/59516

Ссылки в тексте:

[1] медленных и переусложненных средств: http://avisynth.nl/index.php/Main_Page

[2] ffdshow raw video filter: http://sourceforge.net/projects/ffdshow-tryout/

[3] madVR: http://forum.doom9.org/showthread.php?t=146228

[4] MPC-BE: http://sourceforge.net/projects/mpcbe/

[5] по вине этих браузеров: https://stackoverflow.com/questions/17930964/video-element-with-looping-does-not-loop-videos-seamlessly-in-chrome-or-firefo

[6] Google Chrome Native Client: https://developer.chrome.com/native-client

[7] довольно быстр даже на 1080p: http://tp7.github.io/articles/javascript-video-filtering/pure.html?filter=none

[8] время выполнения растет до 20-30 миллисекунд на одну итерацию: http://tp7.github.io/articles/javascript-video-filtering/pure.html?filter=copy

[9] выполняется уже за 35-40мс: http://tp7.github.io/articles/javascript-video-filtering/pure.html?filter=invert

[10] Простая реализация фильтра добавления шума: http://tp7.github.io/articles/javascript-video-filtering/pure.html?filter=noise

[11] 3х3 ядро для блюра: http://tp7.github.io/articles/javascript-video-filtering/pure.html?filter=blur

[12] asm.js: http://asmjs.org/

[13] поддержка asm.js, по всей видимости, еще не добавлена: https://code.google.com/p/v8/issues/detail?id=2599

[14] Emscripten: https://github.com/kripken/emscripten

[15] то ещё удовольствие: http://habrahabr.ru/post/193642/

[16] SIMD.js: https://01.org/blogs/tlcounts/2014/bringing-simd-javascript

[17] Firefox Nightly: https://nightly.mozilla.org/

[18] может получить поддержку всех целевых браузеров: http://www.phoronix.com/scan.php?page=news_item&px=MTY0ODE

[19] работает только с двумя типами данных: http://www.2ality.com/2013/12/simd-js.html

[20] подобным образом: https://github.com/johnmccutchan/ecmascript_simd/blob/master/src/int32x4array.js#L107

[21] демо: http://tp7.github.io/articles/javascript-video-filtering/pure.html?filter=simd

[22] привычные баги: http://tp7.github.io/articles/javascript-video-filtering/pure.html?filter=buggySimd

[23] WebGL: https://en.wikipedia.org/wiki/WebGL

[24] картинки: http://www.html5rocks.com/en/tutorials/webgl/webgl_fundamentals/

[25] видео: http://seriouslyjs.org/

[26] использовать видео в качестве текстуры: https://developer.mozilla.org/en-US/docs/Web/WebGL/Animating_textures_in_WebGL

[27] немного ущербном: https://github.com/brianchirls/Seriously.js/blob/0d029c40401f98aea9cd6170bef7866f6c1750ac/effects/seriously.dither.js#L51

[28] не поддерживает установку константных массивов: https://stackoverflow.com/questions/15262729/const-float-array-in-webgl-shader

[29] которую вы не можете измерить: https://stackoverflow.com/questions/20798294/is-it-possible-to-measure-rendering-time-in-webgl-using-gl-finish

[30] masktools: http://manao4.free.fr/mt_masktools.html

[31] на 1080p: http://tp7.github.io/articles/javascript-video-filtering/webgl.html

[32] говорит: https://en.wikipedia.org/wiki/WebCL

[33] не будет поддерживаться в Firefox в ближайшем будущем: https://bugzilla.mozilla.org/show_bug.cgi?id=664147#c30

[34] Источник: http://habrahabr.ru/post/221619/