Пишем простой плагин для VirtualDub
Несмотря на то что обработка видео не спеша переезжает на OpenCL / CUDA VirtualDub остается удобным средством для простых действий с видео. Обрезка кадра, добавление фильтров или наложение выполняется гораздо удобнее чем из консоли ffmpeg. Кроме того за годы существования была разработана масса фильтров позволяющие выполнять многие операции быстро и удобно. Несмотря на простоту SDK, при написании плагина возникают некоторые нюансы. Статья посвящена работе с ними.
SDK доступно по ссылке с сайта автора. Последняя на данный момент версия 1.1 (VDPluginSDK-1.1.zip). Скачиваем и распаковываем в удобную для вас папку. Внутри находится файл справки PluginSDK.chm, частичным переложением которого этот текст и является. Разработка будет вестись в Microsoft Visual Studio Community 2015, можно использовать как более старые так и более новые версии. Для проверки настройки окружения можно воспользоваться файлами проектов с примерами лежащие в папке src, Samples.sln для новых версий студии или SamplesVC6.dsw для старого доброго Visual Studio 6. После сборки примеров в папке out\Release или out\Debug появится файл SampleVideoFilter.vdf. Это и есть тестовый фильтр. Для проверки достаточно положить его в папку VirtualDub\plugins и добавить из меню фильтров. Если всё работает значит Visual Studio установлен корректно.
В качестве примера напишем фильтр с нуля. Туториал рассчитан на начинающих или вспоминающих Win32 API. Создаём в студии пустой проект динамической библиотеки DLL.
Плагины для VirtualDub имеют расширение vdf, поэтому чтобы не переименовывать его каждый раз меняем расширение в свойства проекта Properties→General→Target extension на .vdf. Меняем для всех конфигураций, поэтому не забываем переключить их на вкладке настроек Configuration: на All Configurations и Platform на All platforms.
Копируем в проект папку include из распакованного SDK и добавляем файлы из него в проект через Atl-Shift-A или меню Add→Existing Item. Для работы нам понадобятся файлы заголовков из папки include и набор файлов хелпера VDXFrame. Не забываем добавить папку include в список папок где система будет их искать. Делается это из Properties→VC++ Directories→Include Directories, добавляем ссылку на корень проекта в виде $(ProjectDir)\include.
Добавляем в проект библиотеку VDXFrame, в примерах она используется в виде отдельного модуля, но так как лицензия позволяет, добавим её в виде исходного кода. Создадим в каталоге проекта папку src и скопируем в неё из SDK файлы VideoFilter.cpp,VideoFilterEntry.cpp,VideoFilterDialog.cpp и stdafx.cpp. Далее скопируем файл заголовка из include\stdafx.h в ранее созданную папку include. Не забываем добавить скопированные файлы в проект через Atl-Shift-A или из меню Add→Existing Item. На этом интеграция библиотеки хелпера заканчивается.
Переходим к написанию кода. Добавляем в проект новый файл main.cpp через Add→Existing Item или комбинацию клавиш Ctrl-Shift-A. Добавляем в main следующие строки
Плагин может содержать в себе произвольное количество фильтров описываемых макросом VDX_DECLARE_VIDEOFILTER с параметром в виде класса VDXFilterDefinition служащим оболочкой над классом фильтра. Сам фильтр описывается тремя текстовыми полями: Автор, Название и Описание. Создадим класс фильтра с именем BlackWhiteFilter, у автора VirtualDub классы именуются с использованием CamelCase поэтому создаем новый класс унаследованный от VDXVideoFilter в файле BlackWhiteFilter.h. Переменная g_VFVAPIVersion будет содержать версию API. Функции определенные с virtual являются частью SDK, а метод ToBlackAndWhite будет реализовывать преобразование картинки.
Реализацию пишем в файле BlackWhiteFilter.cpp, метод Start() выполняется первым, он предназначен для любых предварительных действий, например для определения совместимости с набором инструкций AVX или поддержки CUDA. Оставляем его пока пустым. Хелпер VDXFrame обеспечивает в пределах видимости этого класса указатель на экземпляр класса VDXFilterActivation с именем fa, содержащий информацию о кадре и буферах.
Метод GetParams() используется VirtualDub для определения совместимости фильтра, он должен вернуть битовую маску из перечисления FILTERPARAM
- FILTERPARAM_SWAP_BUFFERS создаётся два независимых буфера для входного и выходного кадров, рекомендуется использовать всегда чтобы не создавать такие буфера руками
- FILTERPARAM_NEEDS_LAST передаёт в фильтр не только текущий кадр но и идущий перед ним, используется для фильтров состояние которых зависит от предыдущего кадра
- FILTERPARAM_SUPPORTS_ALTFORMATS информирует VirtualDub что плагин поддерживает кодирование кадра отличное от RGB32, например YUV, что позволяет оптимизировать вычисления
- FILTERPARAM_ALIGN_SCANLINES фильтр требует выравнивания данных на 16 байт, а значит не поддерживает например длину строки 13 байт
- FILTERPARAM_PURE_TRANSFORM поведение фильтра зависит только от данных в буфере кадра, позволяет ускорить обработку и отображение фильтра
- FILTERPARAM_NOT_SUPPORTED фильтр не поддерживает входные данные в данном формате и работать не будет
Обработка кадра выполняется методом Run(). Данные о кадре и входном и выходном буферах хранятся в переменной fa являющаяся экземпляром класса VDXFilterActivation. VirtualDub поддерживает обрезку кадра, поэтому алгоритм обработки можно оптимизировать получив информацию о выбранном пользователем окне с координатами x1,y1,x2,y2. Данные кадра хранятся в объектах src и dst, соответственно входной и выходной буфер.
Если мы продолжаем писать код с поддержкой SDK меньше 12 версии то реализация метода Run() примет такой вид:
От версии которую поддерживает плагин зависит место хранения сырых данных в структуре. Итак, в функцию ToBlackAndWhite будет передано 6 параметров:
- void *dst0 – выходной буфер кадра
- ptrdiff_t dstpitch — полная длина строки в байтах выходного буфера
- const void *src0 — входной буфер кадра
- ptrdiff_t srcpitch — полная длина строки входного буфера
- uint32 w — ширина кадра в пикселях
- uint32 h — высота кадра в пикселях
GRAY = 0.299 * R + 0.587 * G + 0.114 * B
Организуем два цикла, один проходит по строкам а второй по точкам, граничный уровень для определения цвета точки примем равным 128.
Собираем плагин, копируем файл Windows-VirtualDub-Plugin-BlackWhite.vdf в папку plugins VirtualDub и делаем его активным. В списке он будет виден под названием, которое мы задали в классе VDXFilterDefinition — Black White filter. Плагин собранный для 64 битной версии не будет видно в 32 битной версии VirtualDub, поэтому не забываем проверить активную конфигурацию проекта.
Плагин без настроек довольно уныл, добавим возможность настройки и кнопку предварительного просмотра. Для этого нам бы следовало погрузиться в дебри Win32 API, но по этой теме написано достаточно книг, поэтому не будем вдаваться в детали.
Для визуального представления окна настройки нам понадобится диалоговое окно. Создаем новый файл ресурсов через меню Ctrl-Shift-A → Resource → Resource File с именем Resource.rc. Добавим в него диалоговое окно через меню Add Resource → Dialog и изменим ему имя на IDD_DIALOG_BLACKWHITE_SETTING. По умолчанию у нас уже есть две кнопки Ok и Cancel. Создавать ресурсы лучше в английской локали, иначе можно получить проблему с не читаемым русским шрифтом на кнопке Отмена. Добавим на экран кнопку Preview с именем IDC_SLIDER_THRESHOLD. Чтобы потом не возвращаться добавим остальные элементы управления для настроек, это будет слайдер для изменения порогового значения IDC_SLIDER_THRESHOLD и checkbox IDC_CHECK_INVERTED позволяющий инвертировать картинку. Сверстать это можно например так.
Создадим класс диалога BlackWhiteFilterDialog унаследованный от VDXVideoFilterDialog.
В конструктор передаётся ссылка на класс IVDXFilterPreview который управляет окном предварительного просмотра, локальную ссылку мы будем хранить в переменной mifp.
Метод Show(HWND parent) перегружен вызовом конструктора родителя и использует в качестве параметра идентификатор ресурса диалога настроек IDD_DIALOG_BLACKWHITE_SETTING
DlgProc используется для обработки сообщений от диалогового окна и реализует обработку жизненного цикла диалога в методах OnInit(), OnDestroy() и обработку событий от элементов управления в OnCommand.
Для начала обработаем закрытие диалога по кнопкам Ok и Cancel. Кроме того нам понадобится обработчик Preview, управляющий отображением окна предварительного просмотра через метод Toggle((VDXHWND)mhdlg).
Класс для работы с диалогом написан, теперь его необходимо вызвать, для этого перегружаем в классе BlackWhiteFilter метод Configure(VDXHWND hwnd) и реализуем его
Собираем проект, копируем файл плагина в папку VirtualDub, добавляем новый фильтр в список и видим наш диалог и доступную кнопку Preview.
Окно конфигурации у нас есть, но настроек у фильтра пока нет, приступаем к реализации. Настройки будем хранить в классе BlackWhiteFilterConfig содержащем всего две переменные, mTreshold как величину порогового значения и флаг инверсии mInvert.
Отредактируем класс BlackWhiteFilterDialog, добавив в него два экземпляра класса BlackWhiteFilterConfig для хранения конфигурации mConfigNew и mConfigOld. Эти переменные будут хранить старое и измененное состояние настроек и понадобятся нам для работы кнопки Ok и Cancel. Отредактируем конструктор, добавив в него параметр хранящий настройки и инициализацию конфигурации.
Настройки должны где-то храниться, добавляем в класс BlackWhiteFilter переменную BlackWhiteFilterConfig mConfig и меняем инициализацию класса BlackWhiteFilterDialog в методе Configure на новую.
Теперь необходимо снова поработать с элементами управления Win32. В классе BlackWhiteFilterDialog напишем два метода связывающих нашу конфигурацию и ее реализацию в диалоге.
Осталось использовать эти два метода в жизненном цикле диалога. В OnCommand для кнопки Ok вызываем SaveToConfig(), а для кнопки Cancel восстанавливаем старый набор настроек присваиванием mConfigNew = mConfigOld. Начальные параметры диалога настраиваются в методе OnInit(), диапазон слайдера устанавливается в 0-255 и на него устанавливается фокус.
Изменение настроек необходимо отобразить в окне предварительного просмотра с помощью метода RedoFrame(), для этого отредактируем метод DlgProc добавив вызов сохранения параметров в методе в обработчике WM_HSCROLL для слайдера с проверкой что окно Preview включено if(mifp && SaveToConfig())mifp->RedoFrame(). Для обработки CheckBox допишем в метод OnCommand условие для case на идентификатор IDC_CHECK_INVERTED и выполним такое же обновление.
Перепишем метод ToBlackAndWhite для использования конфигурации, учитывая два параметра, инверсию и пороговое значения. Константа BST_UNCHECKED унаследована от Win32 API и используется как значение флага true/false.
Собираем проект и опять тестируем фильтр в VirtualDub, включение инверсии превратила милого котика в нечто готические страшное.
Нам осталось совсем чуть-чуть до финала. Фильтры VirtualDub поддерживают сохранение параметров в файл настроек, для этого нужно сериализировать наш класс настроек. Для этого существует макрос VDXVF_DECLARE_SCRIPT_METHODS() который добавляется в заголовок класса BlackWhiteFilter и набор методов для реализации записи и отображения настроек GetSettingString, GetScriptString и метод ScriptConfig для синтаксического разбора параметров из файла настроек. Количество и там аргументов задаются в макросе VDXVF_DEFINE_SCRIPT_METHOD в виде последнего параметра. Новая версия класса BlackWhiteFilter будет выглядеть так
Реализуем методы которых не хватает. Декларируем количество параметров и их тип в макросе VDXVF_DEFINE_SCRIPT_METHOD, у нас их два, оба целочисленные, поэтому строка инициализации будет «ii». Список поддерживаемых форматов можно посмотреть в классе IVDXScriptInterpreter, доступны целые, дробные и строковые параметры. Метод GetSettingString отображает параметры в строке настроек, он нужен для человека который сможет быстро посмотреть параметры в окне Filters, в колонке описания Filter. Метод GetScriptString форматирует параметры для сохранения их в файл VirtualDub configuration (*.vcf) и последующего их чтения методом ScriptConfig.
Добавив данный код и собрав плагин мы получим возможность видеть настройки фильтра в окне Filters и сохранять их в файл через меню файл Save processing setting.
По умолчанию проект собирается с зависимостями от установленной в системе VC Runtime, если планируется его использование на других компьютерах, при сборке необходимо указать параметр Multi-threaded (/MT) из меню настроек Configuration->C/C++->Code Generation->Runtime Library. Плагин увеличит свой размер в десять раз но пользователям не придется подбирать Runtime под версию Visual Studio которую использовал разработчик.