This week I spent some time with the LiVES video editor, mainly trying to interface LiVES with GIMP's Script-fu capabilities. Now LiVES already has a powerful scripting/plug-in capability, however, it is mainly targeted at invocations of command line programs such as ImageMagick or its fraternal twin GraphicsMagick.
The problem with using this approach with GIMP is that GIMP startup times are typically about 5-10 seconds, even on faster systems. Processing a thousand frames of video at ten seconds per frame would tax even my patience.
Script-fu Server
Fortunately, GIMP provides a network interface through which commands and scripts can be sent so this startup time can be avoided. This interface is known as "Script-fu server" and can be found in the GIMP menus under "Filters->Script-fu". Once the Script-fu server is running, it waits for messages containing Script-fu code on TCP port 10008 (by default).
The message starts with a 3-byte header: an ASCII 'G' followed by a 16-bit 'length' value (in big endian order). 'length' bytes of code follow the message header. I am not sure if this protocol can handle UTF-8 encoding, so for now am planning on restricting myself to plain ASCII (LiVES currently suggests that RFX scripts limit themselves to the C locale, which suggests a similar limitation).
The Script-fu server will respond with a similarly encoded response after the code has been executed. This response merely states whether execution of the code completed successfully.
LiVES RFX
Now on the LiVES side there is, as already mentioned, support for adding rendered and/or realtime effects by way of RFX scripts. My mission therefore, was to write a RFX script which communicated with GIMP's Script-fu server. This ended up being surprisingly trivial to do, my only hindrances were my lack of hardly any Perl programming experience and a dearth of coverage of my project in the RFX documentation.
The RFX documentation is actually very good, it just presumes command line programs will provide the processing. There is even a nice utility called RFX Builder that greatly simplifies the task of writing a LiVES plug-in (documentation for RFX Builder in ODT).
RFX Builder
To start using RFX Builder, open up the "New Test RFX Script" dialog from the "Advanced->RFX Effects, Tools, and Utilities" menu. Fill in (at a minimum) the Name, Author, Menu Text, Action Text fields. For my first Script-fu test RFX, I created a simple "Invert" script (that merely negates the color of the selected video).

My Test Script
My script does not need any parameters, and I am postponing learning how to pass them as a future project (it would seem fairly trivial), so I ignored all of those settings, and focused mainly on the Pre-loop and Loop code editors.
Pre-Loop (setup)
The Pre-loop code gets executed once before any frames are processed (whereas the Loop code gets executed once for each frame in the current selected range of frames). It is very fortunate that this capability is available, since I wanted my RFX to start a Script-fu server if one wasn't already running.
Here is the code I came up with. While I've done some programming in just about every language there is, this is my first foray into Perl and it is quite possible that there are betters ways to accomplish this simple task of testing if a server is available and if not, starting GIMP with a new server. (Since GIMP takes a few seconds to load, I then have to wait for the server to be available.)
use IO::Socket;
use Text::Balanced;
use Time::HiRes qw( usleep );
$sf_socket = 10008;
$start_gimp_command= qq{ gimp -b "(plug-in-script-fu-server 1 $sf_socket \\\"\\\")" & };
$sock = new IO::Socket::INET ( PeerAddr => 'localhost',
PeerPort => '10008',
Proto => 'tcp',
);
if ( not defined $sock ) {
system ( $start_gimp_command );
while (not defined $sock) {
sleep (1);
$sock = new IO::Socket::INET ( PeerAddr => 'localhost',
PeerPort => '10008',
Proto => 'tcp',
);
};
};
This Pre-loop code will be the same for any RFX/Script-fu scripts, though there may be some additional code to pre-process parameters for scripts which accept those.
Loop Code (executed once for each frame)
Once the Pre-loop code has connected to the server, LiVES will execute the Loop code once for each frame in the selected range of the current clip. Each time through, the code is expected to load the original frame, process it, and then save it (with ".mgk" extension).
LiVES provides several variables ($in, $out, $frame, etc.) that are helpful to the script writer, and I found it useful to use the quoting abilities offered by the Perl Text::Balanced module so that some of these variables were available to the Script-fu code. I also assigned this code to a string variable so that its length could be determined and an appropriate TCP message header created.
Here is the code I used to invert the colors in the frame. Most of it is boilerplate that would be used by any RFX/Script-fu -- only the middle part containing '(gimp-invert layer)' would be changed.
$code= qq{
(let* ((input-file (string-append "$curtmpdir" DIR-SEPARATOR "$in"))
(output-file (string-append "$curtmpdir" DIR-SEPARATOR "$out" "$img_ext"))
(image (car (gimp-file-load RUN-NONINTERACTIVE input-file input-file)))
(layer (car (gimp-image-get-active-layer image))) )
(gimp-invert layer)
(gimp-file-save RUN-NONINTERACTIVE image layer output-file output-file)
(gimp-image-delete image) )
};
First note that LiVES RFX normally works by changing to the directory containing the input files, so the '$in' variable contains only the basename of the file; however, since we are using a server connection, GIMP needs the fully qualified path in order to open the input file and to save the output. Fortunately, the directory containing files is specified by the '$curtmpdir' variable. The '$curtmpdir' doesn't appear in the documentation, so it took me some time to find it in the LiVES source code.
I also ran into a slight problem with the naming of the output file. The 'gimp-file-save' procedure only saves to files whose names have a recognized file extension. The RFX variable '$out' however, uses the ".mgk" extension (for example, if $in is "000003.png" then $out would be "$000003.mgk"). While I could use GIMP's 'file-png-save' or 'file-jpg-save' and then use whatever extension I wanted, it was easier to just append appropriate extension to the '$out' string. This name change later has to be undone, which I will get in a minute.
Sending The Message
Now that our Script-fu code is assigned to a variable, we need to affix a header to the code and send the resulting message to the Script-fu server. My original approach to doing this was to use Perl "print $sock" commands to send this message. I then wrote a Perl loop that waited for the output file to be created. This method worked fine, even though I was ignoring the response received from the server.
However, I later came across Alan Stewart's Script-fu Perl client which included some code that actually monitored the response messages; so I basically copied that (Mr Stewart kindly released his code under GNU's General Public License).
Here is the resulting Perl code that should be included after the Script-fu string has been assigned.
my $len = length ($code);
die "ERROR: script is too long for one server request: $len > 65535\n" if $len > 65535;
# send script to GIMP
my $header = pack( 'an', 'G', $len);
syswrite( $sock, $_ ) for ($header, $code);
# wait for response
my $rin = '';
vec( $rin, fileno($sock), 1 ) = 1;
select( $rin, undef, undef, undef ); # wait (forever) for response start
select( undef, undef, undef, .1 ); # wait a bit for response to finish
# increase wait if INVALID/INCOMPLETE RESPONSE occurs
# response
my $len = sysread( $sock, $header, 4 ) or die "INVALID RESPONSE: empty response\n";
( $len == 4 and $header =~ /^G/ ) or die "INVALID RESPONSE: bad header\n";
my $status;
my ( $status, $len ) = unpack( 'xCn', $header );
my $response;
( sysread( $sock, $response, $len ) == $len ) or die "INCOMPLETE RESPONSE: $response\n";
# exit if response is not "Success"
if ( $status and $response =~ /^Error: Success\n/i ) {
die "UNSUCCESSFUL EXECUTION: Script-fu error\n";
}
rename "$out" . "$img_ext", "$out";
Again, like the Pre-loop code, this code is all pretty much boiler plate and should be included unaltered in the Loop code of any RFX/Script-fu that is created. Also, note that the last line of the code renames the temporary output file (e.g., "000003.mgk.png") to match the expectation of LiVES RFX processor.
Building And Running The Script
After you've successfully edited the Pre-loop and Loop code, save and build the script by pressing OK in the RFX Builder dialog. Next, you build the script by running the "Rebuild all RFX plugins" command in the "Advanced->RFX Effects, Tools, and Utilities" menu.
You can then run the script on the selected range of a clip by choosing the "Run Test Rendered Effect/Tool/Generator" command from the "Advanced->RFX Effects, Tools, and Utilities" menu. Start with a fairly short selected range (~100 frames or so), since the process can take some time.
After you've fully tested your plugin, you can add it permanently to LiVES (the details of which I will cover in the future).
Speed
Though I did not do too much in the way of speed benchmarking, my preliminary experiences left me pleasantly surprised to find that my simple RFX/Script-fu "Invert" script compared somewhat favorably to the LiVES built-in "Negate" function (which, like many of the LiVES effects, is based upon ImageMagick's 'convert' utility).
On my fairly puny 2 GHz dual-core machine, the script processed about two frames per second using PNG's highest compression level, but this increased to over five FPS with a PNG compression factor of "3". The lower compression level will result in larger files, but only by about 15%.
Conclusion
This method of adding visual effects opens up a wide range of opportunities for accessing all of GIMP's powerful capabilities from within LiVES. While LiVES approach to video editing -- wherein individual frames are handled as image files -- is not particular fast, it offers a great benefit with regard to flexibility and intuitiveness. Interfacing LiVES RFX scripts with GIMP's scripting capabilities is a powerful means to maximize this benefit.
This is still a work in progress and I hope to improve things as I learn more about Perl and the LiVES RFX system. Also, based on the benefit of dropping PNG compression, I will be wanting to switch the Script-fu code from using 'gimp-file-save' to 'file-png-save' so that the compression can be explicitly specified ('gimp-file-save' uses the default value that was set in GIMP).
Regards.