Tutorial step 8: Adding job (batch) support

Only one major feature is missing from our filter: job support. As it stands, our filter will revert to default parameters if it is used in a batch operation. Time to fix that.

How VirtualDub batch operation works with filters

If you look in a virtualdub.jobs file, you'll find lines like this:

VirtualDub.video.filters.Add("rotate2");
VirtualDub.video.filters.instance[0].Config(233017,2);

The first line adds the filter to the list, while the second configures filter-specific parameters. Your filter is responsible for the second line only, through the script_obj and fssProc fields of the FilterDefinition record. When the job is run, the lines are interpreted by a primitive C-like scripting language called Sylia. The tutorial filter will interface to Sylia and add methods to the VirtualDub object tree.

Custom objects in the Sylia scripting language

This is the CScriptObject record:

CScriptObject = packed record
  Lookup:    ScriptObjectLookupFuncPtr;
  func_list: PScriptfunctionDef;
  obj_list:  PScriptObjectDef;
end;

An object can hold methods, other objects, or even variables. Because of limitations on the configuration strings that filters can generate, we're only interested in adding Config functions to our object, and thus the Lookup and obj_list will be NULL.

func_list points to an array of function definitions:

PScriptfunctionDef = ^ScriptfunctionDef;
ScriptfunctionDef = packed record
  func_ptr: ScriptfunctionPtr;
  name:     PChar;
  arg_list: PChar;
end;

arg_list describes the function's return type and parameters. The first character should be 0 (zero) for void; subsequent characters should be i for an int and s for a string. Thus, "0iis" becomes the function template (int, int, string). You can also end the parameter list with . (period) to specify a variable argument list; anything will do for the remainder.

To specify overloaded members, add one entry with a name and an arg_list, and follow up with more entries that have NULL for name. The script interpreter will pick the first one that fits. End the function list with an all NULL entry. A member list might look like this:

ScriptFunctionDef tutorial_func_defs[]={
    { (ScriptFunctionPtr)tutorialScriptConfig, "Config", "0ii" },
    { (ScriptFunctionPtr)tutorialScriptConfig, NULL,     "0iii." },
    { (ScriptFunctionPtr)tutorialScriptConfig, NULL,     "0is" },
    { NULL },
};

  Const
    tutorial_func_defs: Array [1..3] of TScriptFunctionDef = (
      (func_ptr: tutorialScriptConfig; name: 'Config'; arg_list: '0ii'),
      (func_ptr: tutorialScriptConfig; name: nil;      arg_list: '0iii.'),
      (func_ptr: tutorialScriptConfig; name: nil;      arg_list: '0is'));

This defines three methods: void Config(int, int), void Config(int, int, int, ...), and void Config(int, string). All definitions point to the same handler, which would have to check the parameter types to discover which signature fits.

Implementing the Config function

All Config script functions have the following prototype:

function configScriptProc(isi: PIScriptInterpreter; fa: PFilterActivation; argv: PCScriptValue; argc: Integer): PCScriptValue; cdecl;

fa gives your access to the filter's instance data pointer and argc/argv contains the input parameters. The parameter argv points to an array containing the several arguments passed to the script function. Each element of the array is a CObj_CScriptValue instance object. Use the following TCScriptValue class methods to get information from each argument.

  • TCScriptValue.isInt(argv^[i]) to check if an argument is an int.
  • TCScriptValue.isString(argv^[i]) to check if an argument is an string.
  • TCScriptValue.asInt(argv^[i]) to retrieve an int.
  • TCScriptValue.asString(argv^[i])^ to retrieve a string. Make sure you include the hat to deference the pointer!

Of course, argc is the argument count. The parameter list is guaranteed to fit one of the signatures specified for this function, so often it's easiest to point all the overloaded members to a single function, and use the is* functions to determine which signature is being used.

Implementing the fssProc function

fssProc's job is to generate the script function call and has this prototype:

function fssProc(fa: PFilterActivation; const ff: PFilterfunctions; buf: PChar; maxlen: Integer): Boolean; cdecl;

This function should supply the buffer to StrCopy or StrLCopy when generating the config string. VirtualDub provides the string up to instance[n]. and the semicolon, so fssProc need only fill buf with Config(...). For instance, a resampling filter with a size of 240x180 might fill buf with this:

Config(240,180)

fssProc should normally return true. If fssProc returns false, VirtualDub ignores buf and discards the entire configuration line. This is useful if the filter instance hasn't been changed from the default configuration.

Note that you must be careful when adding strings to your configuration function. In particular, watch out for backslashes and quotes. Sylia understands vC/C++ escape sequences, so the safest bet is preprocessing those characters into \" and \\. If you must include binary data, the best bet is to encode the data into a safe format such as MIME BASE64.

Adding script support to the tutorial filter

We'll use a Config(int, int) script function to control our filter, with one int for each flag:

Function tutorialFssProc(fa: PFilterActivation; const ff: PFilterFunctions; buf: PChar; maxlen: Integer): Boolean; cdecl;
var
 mfd: PFilterData;
 ste: string;
 stt: string;
 st:  string;
begin {tutorialFssProc}
mfd := PFilterData(fa^.filter_data);
Str(Integer(mfd^.fExpand), ste);
Str(Integer(mfd^.fThird), stt);
st := 'Config(' + ste + ', ' + stt + ')';
StrLCopy(buf, PChar(st), maxlen);
Result := true;
end; {tutorialFssProc}

Now, the implementation of the Config function:

Function tutorialScriptConfig(isi: PIScriptInterpreter; fa: PFilterActivation; argv: PCScriptValue; argc: Integer): PCScriptValue; cdecl;
var
 mfd: PFilterData;
begin {tutorialScriptConfig}
mfd := PFilterData(fa^.filter_data);
mfd^.fExpand := Boolean(TCScriptValue.asInt(argv^[0]));
mfd^.fThird := Boolean(TCScriptValue.asInt(argv^[1]));
end; {tutorialScriptConfig}

Const
 tutorial_func_defs: Array [1..1] of TScriptFunctionDef = (
   (func_ptr: tutorialScriptConfig; name: 'Config'; arg_list: '0ii'));
 tutorialScriptObj: TCScriptObject = (
   Lookup: nil;
   func_list: @tutorial_func_defs;
   obj_list: nil);

Finally, add the scripting support to the FilterDefinition struct:

Const
 filterDef_tutorial: TFilterDefinition = (
   (...)
   script_obj: @tutorialScriptObj;
   fssProc: tutorialFssProc;
   (...)
   );

And that's it. You have finished your first complete filter. Your final project file should look like this.

Additional notes

  • Sylia does not understand floating-point numbers. If you need to store FP values, the best way is to convert them to a fixed point integer.
  • All configuration parameters must be supplied in the Config(...) call. If a filter uses an external configuration file or registry entries, it will fsck up on batch runs!
  • Keep your parameter count low if possible. The script interpreter may reject function calls that exceed 15 parameters.