Главная » Хабрахабр » Пишем простой плагин для VirtualDub

Пишем простой плагин для 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 следующие строки

#include <vd2/VDXFrame/VideoFilter.h>
#include <BlackWhiteFilter.h> VDXFilterDefinition filterDef_blackWhite = VDXVideoFilterDefinition<BlackWhiteFilter>("Shadwork", "Black White filter", "Example for VirtualDub Plugin SDK: Applies a Black White filter to video."); VDX_DECLARE_VIDEOFILTERS_BEGIN() VDX_DECLARE_VIDEOFILTER(filterDef_blackWhite)
VDX_DECLARE_VIDEOFILTERS_END() VDX_DECLARE_VFMODULE()

Плагин может содержать в себе произвольное количество фильтров описываемых макросом VDX_DECLARE_VIDEOFILTER с параметром в виде класса VDXFilterDefinition служащим оболочкой над классом фильтра. Сам фильтр описывается тремя текстовыми полями: Автор, Название и Описание. Создадим класс фильтра с именем BlackWhiteFilter, у автора VirtualDub классы именуются с использованием CamelCase поэтому создаем новый класс унаследованный от VDXVideoFilter в файле BlackWhiteFilter.h. Переменная g_VFVAPIVersion будет содержать версию API. Функции определенные с virtual являются частью SDK, а метод ToBlackAndWhite будет реализовывать преобразование картинки.

#include <vd2/VDXFrame/VideoFilter.h>
#include <vd2/VDXFrame/VideoFilterEntry.h> #ifndef FILTER_VD_BLACK_WHITE
#define FILTER_VD_BLACK_WHITE extern int g_VFVAPIVersion; class BlackWhiteFilter : public VDXVideoFilter {
public: virtual uint32 GetParams(); virtual void Start(); virtual void Run(); protected: void ToBlackAndWhite(void *dst, ptrdiff_t dstpitch, const void *src, ptrdiff_t srcpitch, uint32 w, uint32 h);
}; #endif 

Реализацию пишем в файле 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 фильтр не поддерживает входные данные в данном формате и работать не будет

Для фильтра который будет конвертировать изображение RGB32 в черно-белое нам подойдет FILTERPARAM_SWAP_BUFFERS и FILTERPARAM_PURE_TRANSFORM. Если мы хотим поддерживать кодировку цвета отличную от RGB32 и версию SDK меньше 12 пишем проверку на g_VFVAPIVersion и если она поддержана проверяем формат полученного изображения в поле fa->src.mpPixmapLayout->format. Ранние версии VirtualDub не поддерживали представление цвета отличное от RGB32. Для упрощения обработки писать будем придерживаясь формата RGB32, но вообще VirtualDub поддерживает большой список форматов, перечисленный в VDXPixmapFormat.

uint32 BlackWhiteFilter::GetParams() { if (g_VFVAPIVersion >= 12) { switch (fa->src.mpPixmapLayout->format) { case nsVDXPixmap::kPixFormat_XRGB8888: break; default: return FILTERPARAM_NOT_SUPPORTED; } } fa->dst.offset = 0; return FILTERPARAM_SWAP_BUFFERS;
}

Обработка кадра выполняется методом Run(). Данные о кадре и входном и выходном буферах хранятся в переменной fa являющаяся экземпляром класса VDXFilterActivation. VirtualDub поддерживает обрезку кадра, поэтому алгоритм обработки можно оптимизировать получив информацию о выбранном пользователем окне с координатами x1,y1,x2,y2. Данные кадра хранятся в объектах src и dst, соответственно входной и выходной буфер.

class VDXFilterActivation {
public: const VDXFilterDefinition *filter; // void *filter_data; VDXFBitmap& dst; VDXFBitmap& src; VDXFBitmap *_reserved0; VDXFBitmap *const last; uint32 x1; uint32 y1; uint32 x2; uint32 y2; VDXFilterStateInfo *pfsi; IVDXFilterPreview *ifp; IVDXFilterPreview2 *ifp2; // (V11+) uint32 mSourceFrameCount; // (V14+) VDXFBitmap *const *mpSourceFrames; // (V14+) VDXFBitmap *const *mpOutputFrames; // (V14+)
};

Если мы продолжаем писать код с поддержкой SDK меньше 12 версии то реализация метода Run() примет такой вид:

void BlackWhiteFilter::Run() { if (g_VFVAPIVersion >= 12) { const VDXPixmap& pxdst = *fa->dst.mpPixmap; const VDXPixmap& pxsrc = *fa->src.mpPixmap; switch (pxdst.format) { case nsVDXPixmap::kPixFormat_XRGB8888: ToBlackAndWhite(pxdst.data, pxdst.pitch, pxsrc.data, pxsrc.pitch, pxsrc.w, pxsrc.h); break; } } else { ToBlackAndWhite(fa->dst.data, fa->dst.pitch, fa->src.data, fa->src.pitch, fa->dst.w, fa->dst.h); }
}

От версии которую поддерживает плагин зависит место хранения сырых данных в структуре. Итак, в функцию ToBlackAndWhite будет передано 6 параметров:

  1. void *dst0 – выходной буфер кадра
  2. ptrdiff_t dstpitch — полная длина строки в байтах выходного буфера
  3. const void *src0 — входной буфер кадра
  4. ptrdiff_t srcpitch — полная длина строки входного буфера
  5. uint32 w — ширина кадра в пикселях
  6. uint32 h — высота кадра в пикселях

Для упрощения кода мы проигнорируем параметры обрезки, поэтому кадр будет обрабатываться с одинаковой скоростью вне зависимости от параметра Crop в настройках. Точка в буфере хранится в формате kPixFormat_XRGB8888 и занимает 32 бита. Реализуем простейшее преобразование кадра в черно-белый. Задача оптимизации у нас не стоит, поэтому считать будем по формуле с расчетом в арифметике с плавающей запятой

GRAY = 0.299 * R + 0.587 * G + 0.114 * B

Организуем два цикла, один проходит по строкам а второй по точкам, граничный уровень для определения цвета точки примем равным 128.

void BlackWhiteFilter::ToBlackAndWhite(void *dst0, ptrdiff_t dstpitch, const void *src0, ptrdiff_t srcpitch, uint32 w, uint32 h) { char *dst = (char *)dst0; const char *src = (const char *)src0; for (uint32 y = 0; y<h; ++y) { // Get scanline uint32 *srcline = (uint32 *)src; uint32 *dstline = (uint32 *)dst; for (uint32 x = 0; x<w; ++x) { // Process pixels uint32 data = srcline[x]; float gray = 0.299f * (data & 0x000000ff) + 0.587f * ((data & 0x0000ff00) >> 8) + 0.114f *((data & 0x00ff0000) >> 16); dstline[x] = gray < 128 ? 0x00000000 : 0x00ffffff; } src += srcpitch; dst += dstpitch; }
}

Собираем плагин, копируем файл 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.

#include <windows.h>
#include <commctrl.h>
#include <resource.h>
#include <vd2/VDXFrame/VideoFilterDialog.h>
#include <vd2/VDXFrame/VideoFilter.h> #ifndef FILTER_VD_BLACK_WHITE_DIALOG
#define FILTER_VD_BLACK_WHITE_DIALOG class BlackWhiteFilterDialog : public VDXVideoFilterDialog {
public: BlackWhiteFilterDialog(IVDXFilterPreview *ifp); bool Show(HWND parent); virtual INT_PTR DlgProc(UINT msg, WPARAM wParam, LPARAM lParam);
protected: IVDXFilterPreview *const mifp; bool OnInit(); bool OnCommand(int cmd); void OnDestroy();
}; #endif 

В конструктор передаётся ссылка на класс IVDXFilterPreview который управляет окном предварительного просмотра, локальную ссылку мы будем хранить в переменной mifp.

BlackWhiteFilterDialog::BlackWhiteFilterDialog(IVDXFilterPreview *ifp):mifp(ifp){
}

Метод Show(HWND parent) перегружен вызовом конструктора родителя и использует в качестве параметра идентификатор ресурса диалога настроек IDD_DIALOG_BLACKWHITE_SETTING

bool BlackWhiteFilterDialog::Show(HWND parent) { return 0 != VDXVideoFilterDialog::Show(NULL, MAKEINTRESOURCE(IDD_DIALOG_BLACKWHITE_SETTING), parent);
};

DlgProc используется для обработки сообщений от диалогового окна и реализует обработку жизненного цикла диалога в методах OnInit(), OnDestroy() и обработку событий от элементов управления в OnCommand.

INT_PTR BlackWhiteFilterDialog::DlgProc(UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_INITDIALOG: return !OnInit(); case WM_DESTROY: OnDestroy(); break; case WM_COMMAND: if (OnCommand(LOWORD(wParam))) return TRUE; break; case WM_HSCROLL: if (mifp) mifp->RedoFrame(); return TRUE; } return FALSE;
}

Для начала обработаем закрытие диалога по кнопкам Ok и Cancel. Кроме того нам понадобится обработчик Preview, управляющий отображением окна предварительного просмотра через метод Toggle((VDXHWND)mhdlg).

bool BlackWhiteFilterDialog::OnCommand(int cmd) { switch (cmd) { case IDOK: EndDialog(mhdlg, true); return true; case IDCANCEL: EndDialog(mhdlg, false); return true; case IDC_PREVIEW: if (mifp) mifp->Toggle((VDXHWND)mhdlg); return true; } return false;
}

Класс для работы с диалогом написан, теперь его необходимо вызвать, для этого перегружаем в классе BlackWhiteFilter метод Configure(VDXHWND hwnd) и реализуем его

bool BlackWhiteFilter::Configure(VDXHWND hwnd) { BlackWhiteFilterDialog dlg(fa->ifp); return dlg.Show((HWND)hwnd);
}

Собираем проект, копируем файл плагина в папку VirtualDub, добавляем новый фильтр в список и видим наш диалог и доступную кнопку Preview.

Окно конфигурации у нас есть, но настроек у фильтра пока нет, приступаем к реализации. Настройки будем хранить в классе BlackWhiteFilterConfig содержащем всего две переменные, mTreshold как величину порогового значения и флаг инверсии mInvert.

#ifndef FILTER_VD_BLACK_WHITE_CONFIG
#define FILTER_VD_BLACK_WHITE_CONFIG class BlackWhiteFilterConfig {
public: BlackWhiteFilterConfig() { mTreshold = 128; mInvert = 0; } public: int mTreshold; int mInvert;
}; #endif 

Отредактируем класс BlackWhiteFilterDialog, добавив в него два экземпляра класса BlackWhiteFilterConfig для хранения конфигурации mConfigNew и mConfigOld. Эти переменные будут хранить старое и измененное состояние настроек и понадобятся нам для работы кнопки
Ok и Cancel. Отредактируем конструктор, добавив в него параметр хранящий настройки и инициализацию конфигурации.

BlackWhiteFilterDialog::BlackWhiteFilterDialog(BlackWhiteFilterConfig& config, IVDXFilterPreview *ifp):mifp(ifp){ mConfigNew = config;
}

Настройки должны где-то храниться, добавляем в класс BlackWhiteFilter переменную BlackWhiteFilterConfig mConfig и меняем инициализацию класса BlackWhiteFilterDialog в методе Configure на новую.

bool BlackWhiteFilter::Configure(VDXHWND hwnd) { BlackWhiteFilterDialog dlg(mConfig, fa->ifp); return dlg.Show((HWND)hwnd);
}

Теперь необходимо снова поработать с элементами управления Win32. В классе BlackWhiteFilterDialog напишем два метода связывающих нашу конфигурацию и ее реализацию в диалоге.

void BlackWhiteFilterDialog::LoadFromConfig() { SendDlgItemMessage(mhdlg, IDC_SLIDER_THRESHOLD, TBM_SETPOS, TRUE, mConfigNew.mTreshold); SendMessage(mhdlg, IDC_CHECK_INVERTED, mConfigNew.mInvert, 0);
} bool BlackWhiteFilterDialog::SaveToConfig() { int threshold = SendDlgItemMessage(mhdlg, IDC_SLIDER_THRESHOLD, TBM_GETPOS, 0, 0); int inverted = SendDlgItemMessage(mhdlg, IDC_CHECK_INVERTED, BM_GETCHECK, 0, 0); if (threshold != mConfigNew.mTreshold || inverted!= mConfigNew.mInvert) { mConfigNew.mTreshold = threshold; mConfigNew.mInvert = inverted; return true; } return false;
}

Осталось использовать эти два метода в жизненном цикле диалога. В OnCommand для кнопки Ok вызываем SaveToConfig(), а для кнопки Cancel восстанавливаем старый набор настроек присваиванием mConfigNew = mConfigOld. Начальные параметры диалога настраиваются в методе OnInit(), диапазон слайдера устанавливается в 0-255 и на него устанавливается фокус.

bool BlackWhiteFilterDialog::OnInit() { mConfigOld = mConfigNew; // Set up slider to range 0-255 SendDlgItemMessage(mhdlg, IDC_SLIDER_THRESHOLD, TBM_SETRANGE, TRUE, MAKELONG(0, 255)); LoadFromConfig(); // gain focus to slide control HWND hwndFirst = GetDlgItem(mhdlg, IDC_SLIDER_THRESHOLD); if (hwndFirst) SendMessage(mhdlg, WM_NEXTDLGCTL, (WPARAM)hwndFirst, TRUE); // init preview button HWND hwndPreview = GetDlgItem(mhdlg, IDC_PREVIEW); if (hwndPreview && mifp) { EnableWindow(hwndPreview, TRUE); mifp->InitButton((VDXHWND)hwndPreview); } return false;
}

Изменение настроек необходимо отобразить в окне предварительного просмотра с помощью метода RedoFrame(), для этого отредактируем метод DlgProc добавив вызов сохранения параметров в методе в обработчике WM_HSCROLL для слайдера с проверкой что окно Preview включено if(mifp && SaveToConfig())mifp->RedoFrame(). Для обработки CheckBox допишем в метод OnCommand условие для case на идентификатор IDC_CHECK_INVERTED и выполним такое же обновление.

case IDC_CHECK_INVERTED: if (mifp && SaveToConfig())mifp->RedoFrame(); return true;

Перепишем метод ToBlackAndWhite для использования конфигурации, учитывая два параметра, инверсию и пороговое значения. Константа BST_UNCHECKED унаследована от Win32 API и используется как значение флага true/false.

if (mConfig.mInvert == BST_UNCHECKED) { dstline[x] = gray < mConfig.mTreshold ? 0x00000000 : 0x00ffffff;
}
else { dstline[x] = gray > =mConfig.mTreshold ? 0x00000000 : 0x00ffffff;
}

Собираем проект и опять тестируем фильтр в VirtualDub, включение инверсии превратила милого котика в нечто готические страшное.

Нам осталось совсем чуть-чуть до финала. Фильтры VirtualDub поддерживают сохранение параметров в файл настроек, для этого нужно сериализировать наш класс настроек. Для этого существует макрос VDXVF_DECLARE_SCRIPT_METHODS() который добавляется в заголовок класса BlackWhiteFilter и набор методов для реализации записи и отображения настроек GetSettingString, GetScriptString и метод ScriptConfig для синтаксического разбора параметров из файла настроек. Количество и там аргументов задаются в макросе VDXVF_DEFINE_SCRIPT_METHOD в виде последнего параметра. Новая версия класса BlackWhiteFilter будет выглядеть так

#include <vd2/VDXFrame/VideoFilter.h>
#include <vd2/VDXFrame/VideoFilterEntry.h>
#include <BlackWhiteFilterDialog.h> #ifndef FILTER_VD_BLACK_WHITE
#define FILTER_VD_BLACK_WHITE extern int g_VFVAPIVersion; class BlackWhiteFilter : public VDXVideoFilter { public: virtual uint32 GetParams(); virtual void Start(); virtual void Run(); virtual bool Configure(VDXHWND hwnd); virtual void GetSettingString(char *buf, int maxlen); virtual void GetScriptString(char *buf, int maxlen); VDXVF_DECLARE_SCRIPT_METHODS(); protected: void ToBlackAndWhite(void *dst, ptrdiff_t dstpitch, const void *src, ptrdiff_t srcpitch, uint32 w, uint32 h); BlackWhiteFilterConfig mConfig; void ScriptConfig(IVDXScriptInterpreter *isi, const VDXScriptValue *argv, int argc);
}; #endif 

Реализуем методы которых не хватает. Декларируем количество параметров и их тип в макросе VDXVF_DEFINE_SCRIPT_METHOD, у нас их два, оба целочисленные, поэтому строка инициализации будет «ii». Список поддерживаемых форматов можно посмотреть в классе IVDXScriptInterpreter, доступны целые, дробные и строковые параметры. Метод GetSettingString отображает параметры в строке настроек, он нужен для человека который сможет быстро посмотреть параметры в окне Filters, в колонке описания Filter. Метод GetScriptString форматирует параметры для сохранения их в файл VirtualDub configuration (*.vcf) и последующего их чтения методом ScriptConfig.

VDXVF_BEGIN_SCRIPT_METHODS(BlackWhiteFilter)
VDXVF_DEFINE_SCRIPT_METHOD(BlackWhiteFilter, ScriptConfig, "ii")
VDXVF_END_SCRIPT_METHODS() void BlackWhiteFilter::GetSettingString(char *buf, int maxlen) { SafePrintf(buf, maxlen, " (Treshold:%d, Invert:%d)", mConfig.mTreshold, mConfig.mInvert);
} void BlackWhiteFilter::GetScriptString(char *buf, int maxlen) { SafePrintf(buf, maxlen, "Config(%d, %d)", mConfig.mTreshold, mConfig.mInvert);
} void BlackWhiteFilter::ScriptConfig(IVDXScriptInterpreter *isi, const VDXScriptValue *argv, int argc) { mConfig.mTreshold = argv[0].asInt(); mConfig.mInvert = argv[1].asInt();
}

Добавив данный код и собрав плагин мы получим возможность видеть настройки фильтра в окне Filters и сохранять их в файл через меню файл Save processing setting.

По умолчанию проект собирается с зависимостями от установленной в системе VC Runtime, если планируется его использование на других компьютерах, при сборке необходимо указать параметр Multi-threaded (/MT) из меню настроек Configuration->C/C++->Code Generation->Runtime Library. Плагин увеличит свой размер в десять раз но пользователям не придется подбирать Runtime под версию Visual Studio которую использовал разработчик.

Код проекта доступен на github. Материал нацелен на людей которым нужно сделать что-то быстро а вспоминать тонкости работы с Win32 API неохота. Мне этот плагин понадобился для переноса видео на платформу с однобитным представлением цвета, а прогонять каждый раз набор кадров через XnView надоело.


x

Ещё Hi-Tech Интересное!

LibreOffice: страшный сон бухгалтера

LibreOffice — мощный офисный пакет, который бесплатен для частного, образовательного и коммерческого использования. Его разработчики делают замечательный продукт, который во многих сферах используется в качестве альтернативы Microsoft Office. Команде PVS-Studio всегда интересно взглянуть на код таких известных проектов и попробовать ...

? Skype превратился в унылое подобие… и продукт, позволяющий получить полный доступ к вашей системе? Есть ли надежда?

Извините, накипело! Сегодняшний повторный инцидент с загрузкой 500+ МБ паразитного трафика за 15-20 минут, который я не заказывал, стал последней точкой, когда я всерьез задумался снести мессенджер, которым пользовался практически с самого начала его создания и отказаться от дальнейшего пользования. ...