You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mltpp/CUSTOMISING

382 lines
11 KiB

Server Customisation
Copyright (C) 2005 Ushodaya Enterprises Limited
Authors: Charles Yates <charles.yates@pandora.be>
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 <iostream.h>
using namespace std;
#include <MltMiracle.h>
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 <iostream.h>
#include <string>
#include <sstring>
using namespace std;
#include <MltMiracle.h>
#include <MltResponse.h>
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.