Server Customisation Copyright (C) 2005 Ushodaya Enterprises Limited Authors: Charles Yates Last Revision: 2005-03-16 INTRODUCTION This document describes how miracle can be customised. The emphasis is on showing simple examples of various aspects of the servers capabilities rather than on focussing on the MLT++ API. THE BASIC CUSTOM SERVER The most basic custom server exposes the entire DVCP protocol and is roughly equivalent to the miracle server iteself, but in this case, it lacks the initialisation from /etc/miracle.conf and the port is hardcoded to 5290: #include using namespace std; #include using namespace Mlt; int main( int argc, char **argv ) { Miracle server( "miracle++", 5290 ); if ( server.start( ) ) { server.execute( "uadd sdl" ); server.execute( "play u0" ); server.wait_for_shutdown( ); } else { cerr << "Failed to start server" << endl; } return 0; } Note that after the server is started, this example submits the hard coded commands specified - further units and property settings can of course be specified via the DVCP protocol. To specify initial DVCP commands from /etc/miracle.conf, it is sufficient to specify an additional argument in the server constructor. The wait_for_shutdown call is not required if the server is integrated in a user interface application. CUSTOMISATION This document focusses on the following areas of customisation: * the Miracle server class * extending the command set * accessing the units * the Response object * handling pushed westley documents * accessiving events THE MIRACLE SERVER CLASS The full public interface of the server is as follows: class Miracle : public Properties { public: Miracle( char *name, int port = 5290, char *config = NULL ); virtual ~Miracle( ); mlt_properties get_properties( ); bool start( ); bool is_running( ); virtual Response *execute( char *command ); virtual Response *received( char *command, char *doc ); virtual Response *push( char *command, Service *service ); void wait_for_shutdown( ); static void log_level( int ); Properties *unit( int ); }; The focus of this document is on the 3 virtual methods (execute, received and push). Some further information is provided about the unit properties method and the types of functionality that it provides. EXTENDING THE COMMAND SET The simplest customisation is carried out by overriding the the 'execute' method - the following shows a simple example: #include #include #include using namespace std; #include #include using namespace Mlt; class Custom : public Miracle { public: Custom( char *name = "Custom", int port = 5290, char *config = NULL ) : Miracle( name, port, config ) { } Response *execute( char *command ) { cerr << "command = " << command << endl; return Miracle::execute( command ); } }; int main( int argc, char **argv ) { Custom server( "miracle++", 5290 ); if ( server.start( ) ) { server.execute( "uadd sdl" ); server.execute( "play u0" ); server.wait_for_shutdown( ); } else { cerr << "Failed to start server" << endl; } return 0; } All this does is output each command and pass control over to the original implementation. When you execute this, you will see the following output: (5) Starting server on 5290. command = uadd sdl (5) miracle++ version 0.0.1 listening on port 5290 command = play u0 (7) Received signal 2 - shutting down. Note that all commands except the PUSH are passed through this method before they are executed and this includes those coming from the main function itself. ACCESSING UNIT PROPERTIES A unit consists of two objects - a playlist and a consumer. Your custom server can access these by obtaining the Properties object associated to a unit via the 'unit' method. As a simple example we can replace our execute method above with the following: Response *execute( char *command ) { if ( !strcmp( command, "debug" ) ) { int i = 0; while( unit( i ) != NULL ) unit( i ++ )->debug( ); return new Response( 200, "Diagnostics output" ); } return Miracle::execute( command ); } When this runs and you send a 'debug' command via DVCP, the server will output some information on stderr, like: (5) Starting server on 5290. (5) Server version 0.0.1 listening on port 5290 (5) Connection established with localhost (7) Object: [ ref=3, unit=0, generation=0, constructor=sdl, id=sdl, arg=(nil), consumer=0x80716a0, playlist=0x807f8a8, root=/, notifier=0x8087c28 ] (6) localhost "debug" 100 You can extract the objects using: Playlist playlist( ( mlt_playlist )( unit( i )->get_data( "playlist" ) ) ); Consumer consumer( ( mlt_consumer )( unit( i )->get_data( "consumer" ) ) ); and use the standard MLT++ wrapping methods to interact with them or you can bypass these and using the C API directly. Obviously, this opens a lot of possibilities for the types of editing operations than can be carried out over the DVCP protocol - for example, you can attach filters apply mixes/transitions between neighbouring cuts or carry out specific operations on cuts. THE RESPONSE OBJECT The example above doesn't do anything particularly useful - in order to extend things in more interesting ways, we should be able to carry information back to the client. In the code above, we introduced the Response object to carry an error code and a description - it can also be used to carry arbitrary large blocks of data. Response *execute( char *command ) { Response *response = NULL; if ( !strcmp( command, "debug" ) ) { response = new Response( 200, "Diagnostics output" ); for( int i = 0; unit( i ) != NULL; i ++ ) { Properties *properties = unit( i ); stringstream output; output << string( "Unit " ) << i << endl; for ( int j = 0; j < properties->count( ); j ++ ) output << properties->get_name( j ) << " = " << properties->get( j ) << endl; response->write( output.str( ).c_str( ) ); } } return response == NULL ? Miracle::execute( command ) : response; } Now when you connect to the server via a telnet session, you can access the 'debug' command as follows: $ telnet localhost 5290 Trying 127.0.0.1... Connected to localhost (127.0.0.1). Escape character is '^]'. 100 VTR Ready debug 201 OK Unit 0 unit = 0 generation = 0 constructor = sdl id = sdl arg = Note that the '200' return code specified is automatically promoted to a 201 because of the multiple lines. Alternatively, you can invoke response->write as many times as you like - each string submitted is simply appended to the object in a similar way to writing to a file or socket. Note that the client doesn't receive anything until the response is returned from this method (ie: there's currently no support to stream results back to the client). HANDLING PUSHED DOCUMENTS The custom class receives PUSH'd westley either via the received or push method. The default handling is to simply append a pushed document on to the end of first unit 0. You can test this in the server defined above from the command line, for example: $ inigo noise: -consumer valerie:localhost:5290 By default, the 'push' method is used - this means that the xml document received is automatically deserialised by the server itself and then offered to the push method for handling - an example of this would be: Response *push( char *command, Service *service ) { Playlist playlist( ( mlt_playlist )( unit( 0 )->get_data( "playlist" ) ) ); Producer producer( *service ); if ( producer.is_valid( ) && playlist.is_valid( ) ) { playlist.lock( ); playlist.clear( ); playlist.append( producer ); playlist.unlock( ); return new Response( 200, "OK" ); } return new Response( 400, "Invalid" ); } With this method, each service pushed into the server will automatically replace whatever is currently playing. Note that the 'received' method is not invoked by default - if you wish to receive the XML document and carry out any additional processing prior to processing, you should set the 'push-parser-off' property on the server to 1. This can be done by placing the following line in your classes constructor: set( "push-parser-off", 1 ); When this property is set, the received method is used instead of the push - in this scenario, your implementation is responsible for all handling of the document. To simulate this, you can try the following method: Response *received( char *command, char *document ) { cerr << document; Producer producer( "westley-xml", document ); return push( command, &producer ); } When you push your videos in to the server via the inigo command above (or from other tools, such as those in the shotcut suite), you will see the xml in the servers stderr output. If you need to carry out some operations on the xml document (such as replacing low quality videos used in the editing process with their original) the received mechanism is the one that you would want to use. OTHER MANIPULATIONS What you do with the received MLT Service is largely up to you. As shown above, you have flexibility in how the item is scheduled and you can carry out manipulations on either the xml document and/or the deserialised producer. Typically, shotcut and inigo produce 'tractor' objects - these can be easily manipulated in the push method - for example, to remove a track from the output, we could do something like: Response *push( char *command, Service *service ) { Playlist playlist( ( mlt_playlist )( unit( 0 )->get_data( "playlist" ) ) ); Tractor *tractor( *service ); if ( tractor.is_valid( ) && playlist.is_valid( ) ) { // Remove track 2 (NB: tracks are indexed from 0 like everything else) Producer *producer = tractor.track( 2 ); Playlist track( producer ); // If we have a valid track then hide video and audio // This is a bit pattern - 1 is video, 2 is audio if ( track.is_valid( ) ) track.set( "hide", 3 ); // You need to delete the reference to the playlist producer here delete producer; // Play it playlist.lock( ); playlist.clear( ); playlist.append( producer ); playlist.unlock( ); return new Response( 200, "OK" ); } return new Response( 400, "Invalid" ); } EVENT HANDLING The MLT framework generates events which your custom server can use to do various runtime manipulations. For the purpose of this document, I'll focus on 'consumer-frame-render' - this event is fired immediately before a frame is rendered. See example in test/server.cpp DISABLING DVCP In some cases, it is desirable to fully disable the entire DVCP command set and handle the PUSH in an application specific way (for example, the shotcut applications all do this). The simplest way of doing this is to generate a response that signifies the rejection of the command. In this example, the 'shutdown' command is also handled: Response *execute( char *command ) { if ( !strcmp( command, "shutdown" ) ) exit( 0 ); return new Response( 400, "Invalid Command" ); } If you use this method in the code above, your server does nothing - no units are defined, so even a PUSH will be rejected.