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

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

    Эта область довольно неплохо покрыта в десктопной среде — фильтры вроде 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-контекста канваса, да и сами по себе не блещут скоростью выполнения. Такие грустные дела.
    Поделиться публикацией

    Похожие публикации

    Комментарии 14

      +2
      Честно говоря, я поражен возможностями seriously.js. Мало того, что он работает в realtime, так в нем еще и куча фильтров уже реализована.
      Но вообще, как только появились те же Broadway (h264 decoder) и route9.js (vp8 decoder), стало понятно, что на js можно реализовывать очень сложные вещи. Только, с такой эффективностью, это скорее не нужно, чем нужно.
      Давайте подумаем, какие фильтры реально бы пригодились для сферического видеосервиса в вакууме (того же youtube). Мне на ум приходит только deblocking, debanding и, возможно, deblurring. И если на десктопе можно смириться со значительно возрастающей загрузкой процессора, то на мобильных устройствах (а эти фильтры было бы эффективнее всего применять именно на мобильных устройствах из-за низкого битрейта мобильного видео) это непозволительно.
      Хотя я ничего не имею против тех же Audiocogs (jsmad, flac.js, aac.js, alac.js).
        0
        Из очевидных: деблок, дебанд, повышение резкости, блюр, добавление шума, качественный ресайз. Из редко, но иногда полезных: деинтерлейс, корректировка уровня (TV->PC и наборот), шумодав (опционально — временнОй) и сглаживание движения (madVR-овский smooth motion).
        Скорее всего, для реализации потребуется еще RGB<->YUV конвертирование, ибо писать подобные фильтры прямо в RGB может быть неприятно.

        Большинство из этих фильтров по сложности в разы превосходит всё то, что сейчас предоставляет seriously.js.
        0
        Если в реализации на чистом JS основная проблема — это количество итераций цикла при большом разрешении, то попробуйте использовать алгоритм под названием «Метод Даффа».
          0
          Очевидно же что не в самом цикле проблема, а в вызовах методов.
          0
          А вообще стоит ли оно того? Я бы не хотел чтобы у меня при просмотре видео, процессор сильно грузился, пытаясь эффекты применять в realtime. Возможно для простенького веб-редактора видео, это выглядит интересно, а иначе не вижу смысла.
          Это противоречит идеи ухода в облако и неоправдано нагружает клиент, не говоря уже о мобильных устройствах.

          Или я неправильно понимаю области применения?
            0
            Основная идея подобной постобработки — устранение дефектов видео, которые мешают просмотру. Если асбтрактному пользователю ничего не мешает — он может ничего не включать. Главная ЦА — большие десктопные мониторы, на мелких экранах мобильных устройств качество стрима такой большой роли не играет.

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

            А вслед за ними плагинчик бегает и исправляет — человек ходит, а там всюду ББ хороший, горизонт ровный, красных глаз не видно.
              0
              Если бы можно было так сделать, то это было бы сделано на этапе съёмки.
                0
                В общем-то так уже делается тем же айфоном.
                  0
                  В общем-то баланс белого на нём частенько ошибается, ровного горизонта не дождёшься, и только красные глаза легко фильтруются.
                    0
                    А как вы собрались фильтрами делать ровный горизонт? Его же вообще почти никогда нельзя достоверно автоматически определить. Или баланс белого в автоматическом режиме? У любого решения есть границы применимости. А для того же баланса белого на 5S даже специальную вспышку добавили. Так что то, что можно сделать на этапе съёмки, уже делается.
                      +1
                      > Его же вообще почти никогда нельзя достоверно автоматически определить.

                      А, допустим, по акселерометру определить направление в надир на момент съемки и уже от него плясать с определением линии горизонта?
              0
              А что на счёт оптимизации с помощью WebWorkers? Создали N потоков и обрабатываете только чётные номеру воркера куски в каждом потоке.
              Хотя, конечно, JS не на столько быстр, что бы обрабатывать сложными фильтрами видео в риал-тайм.

              В теории так же можно сделать буфер на определённое количество (мили?)секунд и потом уже погнать видео как-будто-бы в реалтайме.
              PS ну и сравнивать работу кодеков с трансформацией сырого массива пикселей — странно, не находите? Сырой массив пикселей(когда видео размером в 20 гиг сжимается до 2) даже на десктопных программах вроде VirtualDub кодируется далеко не в риал-тайме.
              PSS ну и некоторые «фильтры» можно делать тупо с помощью изображения/цвета альфо-каналом поверх.
                0
                Многопоточность может помочь когда будет какой-нибудь asm.js. Пока даже в условиях идеальной параллельности, 8 потоков фактически сократят время блюра в 8 раз, или 400/8 — 50мс в хроме, что всё равно слишком медленно.

                Буфер тоже не поможет. Он подразумевает, что вы потом сможете заполнять этот буфер достаточно быстро уже во время воспроизведения, либо буфер будет достаточно большим, чтобы компенсировать лаг. В случае с JS, придется загрузить большую часть видео, что может не очень понавится пользователю.

                Ну и я не понял, при чем тут работа кодеков. В десктопных программах типа VirtualDub фильтр blur, приведенный в статье, легко может выполняться на ~800фпс на 1080p (чуть больше 1мс на кадр), что более чем достаточно для работы в реальном времене. Собственно, наличие фильтров в ffdshow подтверждает работоспособность идеи, при этом ffdshow уже много много лет.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое