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.
382 lines
11 KiB
382 lines
11 KiB
15 years ago
|
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.
|
||
|
|
||
|
|
||
|
|