We are writing a simple plugin for VirtualDub

  • Tutorial
Despite the fact that video processing is slowly moving to OpenCL / CUDA, VirtualDub remains a convenient tool for simple video operations. Cropping a frame, adding filters or blending is much more convenient than from the ffmpeg console. In addition, over the years of existence, a mass of filters has been developed that allow you to perform many operations quickly and conveniently. Despite the simplicity of the SDK, there are some nuances when writing a plugin. The article is dedicated to working with them.


SDK is available on the link from the author’s site. The latest version is 1.1 (VDPluginSDK-1.1.zip). Download and unpack to a folder convenient for you. Inside is the PluginSDK.chm help file, of which this text is a partial arrangement. Development will be conducted in Microsoft Visual Studio Community 2015, you can use both older and newer versions. To check the environment settings, you can use the project files with examples in the src folder, Samples.sln for new versions of the studio or SamplesVC6.dsw for the good old Visual Studio 6. After collecting the examples, the SampleVideoFilter.vdf file will appear in the out \ Release or out \ Debug folder . This is a test filter. To check, just put it in the VirtualDub \ plugins folder and add it from the filter menu. If everything works, then Visual Studio is installed correctly.


As an example, let's write a filter from scratch. The tutorial is designed for beginners or recalling the Win32 API. We create in the studio an empty project of the dynamic library DLL.


Plugins for VirtualDub have a vdf extension, so in order not to rename it each time we change the extension in the project properties Properties → General → Target extension to .vdf. We change for all configurations, so do not forget to switch them on the Configuration settings tab: on All Configurations and Platform on All platforms.


Copy the include folder from the unpacked SDK into the project and add the files from it to the project via Atl-Shift-A or the menu Add → Existing Item. To work, we need header files from the include folder and a set of VDXFrame helper files. Do not forget to add the include folder to the list of folders where the system will search for them. This is done from Properties → VC ++ Directories → Include Directories, add a link to the project root in the form $ (ProjectDir) \ include.


We add the VDXFrame library to the project, in the examples it is used as a separate module, but since the license allows, we will add it in the form of source code. Create the src folder in the project directory and copy the files VideoFilter.cpp, VideoFilterEntry.cpp, VideoFilterDialog.cpp and stdafx.cpp into it from the SDK. Next, copy the header file from include \ stdafx.h to the previously created include folder. Do not forget to add the copied files to the project through Atl-Shift-A or from the menu Add → Existing Item. This completes the integration of the helper library.

We turn to writing code. Add a new main.cpp file to the project via Add → Existing Item or the keyboard shortcut Ctrl-Shift-A. Add the following lines to main

#include 
#include 
VDXFilterDefinition filterDef_blackWhite = VDXVideoFilterDefinition("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()

A plugin can contain an arbitrary number of filters described by the VDX_DECLARE_VIDEOFILTER macro with a parameter in the form of the VDXFilterDefinition class serving as a wrapper over the filter class. The filter itself is described by three text fields: Author, Title and Description. Let's create a filter class with the name BlackWhiteFilter, the author of VirtualDub names the classes using CamelCase, so we create a new class inherited from VDXVideoFilter in the BlackWhiteFilter.h file. The g_VFVAPIVersion variable will contain the version of the API. The functions defined with virtual are part of the SDK, and the ToBlackAndWhite method will implement image conversion.

#include 
#include 
#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 

We write the implementation in the BlackWhiteFilter.cpp file, the Start () method is performed first, it is intended for any preliminary steps, for example, to determine compatibility with the AVX instruction set or CUDA support. Leave it empty for now. The VDXFrame helper provides, within the scope of this class, a pointer to an instance of the VDXFilterActivation class called fa, which contains information about the frame and buffers.

The GetParams () method is used by VirtualDub to determine the compatibility of the filter; it should return a bit mask from the FILTERPARAM enumeration

  • FILTERPARAM_SWAP_BUFFERS creates two independent buffers for input and output frames, it is recommended to always use it so as not to create such buffers by hand
  • FILTERPARAM_NEEDS_LAST passes to the filter not only the current frame but also the one going in front of it, the state of which is used for filters depends on the previous frame
  • FILTERPARAM_SUPPORTS_ALTFORMATS informs VirtualDub that the plugin supports frame encoding other than RGB32, such as YUV, which allows optimizing calculations
  • FILTERPARAM_ALIGN_SCANLINES filter requires data alignment by 16 bytes, which means it does not support, for example, a string length of 13 bytes
  • FILTERPARAM_PURE_TRANSFORM filter behavior depends only on the data in the frame buffer, it allows to speed up filter processing and display
  • FILTERPARAM_NOT_SUPPORTED filter does not support input in this format and will not work

For a filter that will convert an RGB32 image to black and white, FILTERPARAM_SWAP_BUFFERS and FILTERPARAM_PURE_TRANSFORM are suitable for us. If we want to support color encoding other than RGB32 and the SDK version is less than 12, we write a check for g_VFVAPIVersion and if it is supported, we check the format of the received image in the field fa-> src.mpPixmapLayout-> format. Earlier versions of VirtualDub did not support color representation other than RGB32. To simplify the processing, we will write adhering to the RGB32 format, but in general VirtualDub supports a large list of formats listed in 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;
}

Frame processing is performed by the Run () method. The data about the frame and the input and output buffers are stored in the variable fa which is an instance of the VDXFilterActivation class. VirtualDub supports cropping, so the processing algorithm can be optimized by obtaining information about a window selected by the user with coordinates x1, y1, x2, y2. The frame data is stored in the src and dst objects, respectively, the input and output buffer.

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+)
};

If we continue to write code with SDK support less than version 12, then the implementation of the Run () method will take the following form:

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);
	}
}

The location of the raw data in the structure depends on the version that the plug-in supports. So, 6 parameters will be passed to the ToBlackAndWhite function:

  1. void * dst0 - frame output buffer
  2. ptrdiff_t dstpitch - total string length in bytes of the output buffer
  3. const void * src0 - input frame buffer
  4. ptrdiff_t srcpitch - total line length of the input buffer
  5. uint32 w - frame width in pixels
  6. uint32 h - frame height in pixels

To simplify the code, we will ignore the cropping parameters, so the frame will be processed at the same speed regardless of the Crop parameter in the settings. A point in the buffer is stored in the format kPixFormat_XRGB8888 and occupies 32 bits. We realize the simplest conversion of the frame to black and white. We don’t have an optimization task, so we will calculate according to the formula with calculation in floating point arithmetic

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

We organize two cycles, one goes through the lines and the second through the points, the boundary level for determining the color take the points equal to 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> 8) + 0.114f *((data & 0x00ff0000) >> 16);
			dstline[x] = gray < 128 ? 0x00000000 : 0x00ffffff;
		}
		src += srcpitch;
		dst += dstpitch;
	}
}

We collect the plugin, copy the Windows-VirtualDub-Plugin-BlackWhite.vdf file to the plugins VirtualDub folder and make it active. In the list it will be visible under the name that we set in the VDXFilterDefinition - Black White filter class. The plugin built for the 64-bit version will not be visible in the 32-bit version of VirtualDub, so do not forget to check the active configuration of the project.


The plug-in without settings is rather dull, we add the ability to configure and the preview button. To do this, we should dive into the jungle of the Win32 API, but enough books have been written on this topic, so we won’t go into details.

For a visual representation of the settings window, we need a dialog box. Create a new resource file through the menu Ctrl-Shift-A → Resource → Resource File with the name Resource.rc. Add a dialog box to it through the menu Add Resource → Dialog and change its name to IDD_DIALOG_BLACKWHITE_SETTING. By default, we already have two Ok and Cancel buttons. Creating resources is better in the English locale, otherwise you may get a problem with the unreadable Russian font on the Cancel button. Add the Preview button to the screen with the name IDC_SLIDER_THRESHOLD. In order not to return later, we add the rest of the controls for the settings, this will be a slider for changing the threshold value IDC_SLIDER_THRESHOLD and checkbox IDC_CHECK_INVERTED that allows you to invert the picture. This can be done for example like this.


Let's create a BlackWhiteFilterDialog dialog class inherited from VDXVideoFilterDialog.

#include 
#include 
#include 
#include 
#include 
#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 

A link to the IVDXFilterPreview class which controls the preview window is passed to the constructor; we will store the local link in the mifp variable.

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

The Show (HWND parent) method is overloaded with a call to the parent constructor and uses the resource identifier of the settings dialog IDD_DIALOG_BLACKWHITE_SETTING as a parameter

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

DlgProc is used to process messages from a dialog box and implements the processing of the dialogue life cycle in the OnInit (), OnDestroy () methods and the processing of events from controls in 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;
}

To begin, we will process the closing of the dialog by the buttons Ok and Cancel. In addition, we need a Preview handler that controls the display of the preview window through the Toggle ((VDXHWND) mhdlg) method.

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;
}

The class for working with the dialog is written, now you need to call it, for this we overload the Configure method (VDXHWND hwnd) in the BlackWhiteFilter class and implement it

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

We assemble the project, copy the plug-in file to the VirtualDub folder, add a new filter to the list and see our dialog and the available Preview button.


We have a configuration window, but the filter has no settings yet, we proceed to implementation. We will store the settings in the BlackWhiteFilterConfig class containing only two variables, mTreshold as the value of the threshold value and the inversion flag 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 

Let's edit the BlackWhiteFilterDialog class, adding two instances of the BlackWhiteFilterConfig class to store the configurations mConfigNew and mConfigOld. These variables will store the old and changed state of the settings and we will need the
Ok and Cancel buttons to work . We’ll edit the constructor by adding a parameter that stores settings and initialization of the configuration to it.

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

Settings should be stored somewhere, add the BlackWhiteFilterConfig mConfig variable to the BlackWhiteFilter class and change the initialization of the BlackWhiteFilterDialog class in the Configure method to a new one.

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

Now you need to work with Win32 controls again. In the BlackWhiteFilterDialog class, we will write two methods that link our configuration and its implementation in a dialog.

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;
}

It remains to use these two methods in the life cycle of the dialogue. In OnCommand, for the Ok button, we call SaveToConfig (), and for the Cancel button, we restore the old set of settings by assigning mConfigNew = mConfigOld. The initial parameters of the dialog are configured in the OnInit () method, the range of the slider is set to 0-255 and focus is set on it.

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;
}

Changing the settings must be displayed in the preview window using the RedoFrame () method, for this we will edit the DlgProc method adding a call to save the parameters in the method in the WM_HSCROLL handler for the slider with the check that the Preview window is enabled if (mifp && SaveToConfig ()) mifp-> RedoFrame ( ) To process the CheckBox, add the condition for the case to the IDC_CHECK_INVERTED identifier in the OnCommand method and perform the same update.

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

We rewrite the ToBlackAndWhite method to use the configuration, taking into account two parameters, inversion and threshold values. The constant BST_UNCHECKED is inherited from the Win32 API and is used as the true / false flag value.

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

We assemble the project and test the filter in VirtualDub again, the inclusion of the inversion turned the cute cat into something gothic scary.


We were left just a little before the final. VirtualDub filters support saving parameters to the settings file, for this you need to serialize our settings class. To do this, there is a macro VDXVF_DECLARE_SCRIPT_METHODS () which is added to the BlackWhiteFilter class header and a set of methods for recording and displaying the GetSettingString, GetScriptString and ScriptConfig methods for parsing parameters from the settings file. The number of arguments there is set in the macro VDXVF_DEFINE_SCRIPT_METHOD as the last parameter. The new version of the BlackWhiteFilter class will look like this

#include 
#include 
#include 
#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 

We implement methods that are not enough. We declare the number of parameters and their type in the macro VDXVF_DEFINE_SCRIPT_METHOD, we have two of them, both integer, so the initialization string will be “ii”. A list of supported formats can be found in the IVDXScriptInterpreter class; integer, fractional, and string parameters are available. The GetSettingString method displays the parameters in the settings bar; it is needed for a person who can quickly see the parameters in the Filters window, in the Filter description column. The GetScriptString method formats the parameters for saving them to the VirtualDub configuration (* .vcf) file and then reading them using the ScriptConfig method.

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();
}

By adding this code and collecting the plugin, we will be able to see the filter settings in the Filters window and save them to a file via the Save processing setting file menu.


By default, the project is built with dependencies on the VC Runtime installed in the system, if it is planned to be used on other computers, when assembling, you must specify the Multi-threaded (/ MT) parameter from the settings menu Configuration-> C / C ++ -> Code Generation-> Runtime Library . The plugin will increase its size by ten times but users will not have to select Runtime for the version of Visual Studio that the developer used.


Project code is available on github . The material is aimed at people who need to do something quickly and remember reluctance to work with the Win32 API reluctance. I needed this plugin to transfer video to a platform with a single-bit color representation, and I was tired of running the frames through XnView every time.

Also popular now: