Оценка возможности постобработки видео в браузере
В последнее время постобработка видео в рантайме приобретает всё большее значение — благодаря мощности современных ПК, почти каждый пользователь может пропустить видеоряд через сложную цепочку фильтров прямо во время просмотра, тем самым избавляясь от необходимости полноценного кодирования видео, зачастую производимого с помощью медленных и переусложненных средств.Эта область довольно неплохо покрыта в десктопной среде — фильтры вроде ffdshow raw video filter и madVR позволяют делать практически всё, что может потребоваться для приятного просмотра. К сожалению, веб не может похвастаться аналогичным тулкитом, и вы либо наслаждаетесь всеми недостатками очередного видео на YouTube, либо открываете его во внешнем приложении вроде MPC-BE, что не очень удобно. А было бы неплохо иметь одну волшебную кнопку, активирующую фильтрацию в месте, где она и должна быть — в вашем браузере.
Данный пост представляет собой краткий отчет о моих изысканиях в этой области, где конечной целью ставилась оценка возможности проведения фильтрации в режиме реального времени на разрешении минимум 1920×1080.
Замечания Во время чтения статьи следует учесть: Все приведенные демонстрации основаны на html5 video с установленным атрибутом loop. Это видео может ужасно дергаться и лагать во время переключения на начало видеоряда в некоторых браузерах, по вине этих браузеров. Я не стал переусложнять код ради возможного исправления этой проблемы. Если повторяющееся видео вас раздражает, можно добавить loop=false к GET-параметрам запроса. Демки тестировались только в хроме, лисе и IE11, в остальных браузерах может не работать. Исходный код всех демок приведен прямо внутри соответствующих html-страниц, без зависимостей. В тексте много исковерканных английских слов и корявых переводов. Я плохо разбираюсь в русскоязычной терминологии, исправления приветствуются. Мы закрываем глаза на возможные проблемы с CORS, сайтами, использующими Flash-видео и т.п. Только сферические тесты в вакууме. В 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 работает по следующей схеме: Получаем оба необходимых элемента — спрятанный video и canvas, расположенный поверх него. Рисуем кадр из видео на канвасе через context.drawImage (video, 0, 0), где context — 2d контекст, полученный с канваса. Получаем буфер кадра (массив байтов цветов) через context.getImageData (0, 0, width, height). Обрабатываем буфер требуемыми фильтрами. Кладем обработанный массив обратно через 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 сейчас работает только с двумя типами данных, float32×4 и uint32×4, что делает всю затею бесполезной для большинства реальных 8-битных фильтров. Более того, тип Int32×4Array пока не реализован даже в Nightly, поэтому любая запись и чтение данных из памяти будут происходить медленно и страшно (когда реализованы подобным образом). Однако, приведу код реализации обычного фильтра инвертирования (на этот раз работающего через XOR): function invert_frame_simd (source) { var fff = SIMD.int32×4.splat (0×00FFFFFF); 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-контекста канваса, да и сами по себе не блещут скоростью выполнения. Такие грустные дела.