Introduzione
Consideriamo lo schema illustrato nella Figura 1: l'input
è costituito dalla serie di immagini, l'output è
il file video. L'elaborazione è realizzata utilizzando
le JMF API.
Figura 1 - Modello di elaborazione di dati multimediali
JMF
è un package che estende le funzionalità multimediali
della piattaforma Java. Le classi che utilizziamo sono:
- javax.media.Processor:
interfaccia per processare e controllare dati multimediali;
- javax.media.protocol.DataSource:
gestisce il ciclo di vita di un contenuto multimediale mediante
un protocollo di connessione;
- javax.media.Buffer:
è un contenitore di dati multimediali;
- javax.media.protocol.PullBufferStream:
interfaccia per leggere i dati di un contenuto multimediale
e trasferirli sotto forma di oggetti di tipo Buffer;
- javax.media.protocol.PullBufferDataSource:
data-source che contiene uno o più PullBufferStream;
- javax.media.DataSink:
interfaccia per oggetti che leggono dati da DataSource e
li salvano in una certa destinazione.
- javax.media.datasink.DataSinkListener:
interfaccia per manipolare eventi asincroni generati da
DataSink;
- javax.media.format.VideoFormat:
incapsula informazioni sul formato dei video.
Definizione
delle risorse di Input ed Output del processo di creazione
video
Per semplicità facciamo riferimento a immagini di tipo
JPEG e ad un video di tipo QuickTime, ma le cose possono essere
facilmente riadattate ad altri formati di immagini e video.
JMF prevede l'utilizzo della classe Format per descrivere
le informazioni relative a contenuti multimediali. Più
nello specifico tale classe raccoglie tre sottoclassi: AudioFormat,
ContentDescriptor, VideoFormat. Tralasciando la prima delle
tre, che viene utilizzata per dati di tipo audio, concentriamoci
sulle altre due. Il ContentDescriptor è l'identificatore
del content type, ovvero del formato in cui i dati multimediali
sono memorizzati; utilizzando la sua sottoclasse FileTypeDescriptor
è possibile descrivere il tipo del contenuto in base
al formato di un file, visto che molto spesso i dati multimediali
sono acquisiti da files locali. La classe VideoFormat invece
fornisce informazioni rilevanti circa i dati video. Come possiamo
vedere nella Figura 2, diversi formati sono stati derivati
dalla classe VideoFormat per descrivere gli attributi dei
comuni formati video.
Figura 2 - Formati dei dati in JMF
Poiché
il nostro "video" in ingresso non è altro
che un "pacchetto" di JPEG, il VideoFormat a cui
faremo riferimento sarà il JPEGFormat.
Per gestire il trasferimento dei dati multimediali dall'input
all'output, si utilizza un DataSource. Un DataSource incapsula
sia la location del video sia il protocollo utilizzato per
accedere ad esso.
La location è ottenuta mediante un MediaLocator o una
URL. Un MediaLocator è simile ad una URL e può
essere costruito da una URL anche se l'handler protocol di
quest'ultima non è installato sul sistema.
Un DataSource gestisce un set di oggetti SourceStream, che
sono delle interfacce per la lettura dei dati. Uno standard
DataSource usa un array di byte come unità di trasferimento.
Un BufferDataSource utilizza un Buffer come unità di
trasferimento. JMF definisce diversi tipi di DataSource, come
si pu; vedere nella Figura 3:
Figura 3 - Modello dei dati in JMF
Noi
utilizziamo un PullBufferDataSource che prevede, per accedere
ai dati, i protocolli Http e File e utilizza il Buffer come
unità di trasferimento. Inoltre esso utilizza come
SourceStream un PullBufferStream. Noi andiamo a passare al
PullBufferStream la dimensione dei frame del video, il frame
rate e il formato del video. Inoltre modifichiamo il suo metodo
read in modo che legga i dati dai file immagini.
Tutte le informazioni riguardo all'input le inglobiamo quindi
in un oggetto da noi definito, l' ImageDataSource:
class
ImageDataSource extends PullBufferDataSource {
ImageSourceStream stream;
ImageDataSource(int width, int height, int frameRate, Vector
images {
streams = new ImageSourceStream(width, height, frameRate,
images);
}
class
ImageSourceStream implements PullBufferStream {
Vector images;
int width, height;
VideoFormat format;
float frameRate;
long seqNo = 0;
int nextImage = 0;
boolean ended = false;
public ImageSourceStream(int width, int height, int frameRate,Vector
images) {
this.width = width;
this.height = height;
this.images = images;
this.frameRate = (float) frameRate;
format = new VideoFormat(VideoFormat.JPEG, new Dimension(width,
height), Format.NOT_SPECIFIED,
Format.byteArray, frameRate);
}
ImageDataSource
ids = new ImageDataSource(width, height, frameRate, inFiles);
Ora
abbiamo tutto ciò che ci serve riguardo all'input.
Passiamo alla definizione delle risorse relative all'output.
L'approccio da seguire è il seguente: supponiamo che
il video che vogliamo ottenere sia un media stream, ovvero
un flusso di dati multimediali. Siamo abituati a pensare che
un oggetto di questo tipo debba essere acquisito da una rete,
o catturato da una videocamera. In questo caso il nostro flusso
lo otterremo da una serie di file.
Il media stream, come già visto, è identificato
dalla sua location e dal protocollo utilizzato per accedere
ad esso. Nel nostro caso, siccome ci riferiamo ad un file
locale, il protocollo utilizzato è File. Un MediaLocator
ci permette di identificare la location del media stream:
MediaLocator
outML = new MediaLocator (outputURL);
Elaborazione
di dati multimediali con JMF
A questo punto siamo in grado di passare alla fase di elaborazione
dei dati.
Il primo passo consiste nella costruzione di un Processor,
che è un Player che prende un DataSource in ingresso,
effettua alcune elaborazioni sui dati definite dall'utente
e restituisce i dati processati ad un device o ad un altro
DataSource (vedi Figura 4). Se i dati vengono inviati ad un
DataSource, esso può essere usato come ingresso per
un Player o un altro Processor, oppure per un DataSink, di
cui parleremo più avanti.
Figura 4 - Modello del Processor in JMF
Il Processor viene costruito utilizzando un Manager. Nel nostro
caso il DataSource di ingresso è l'ImageDataSource
visto prima:
Processor
p = Manager.createProcessor(ids);
Rispetto
al Player, il Processor ha due stati in più, Configuring
e Configured, così come si può vedere nella
Figura 5:
Figura 5 - Stati del Processor
Un
Processor entra nello stato Configuring quando è chiamato
il metodo configure. Mentre il Processor è in tale
stato, esso si connette al DataSource, demultiplexa i dati
in ingresso e accede alle informazioni sul loro formato. Terminata
questa fase il Processor passa nello stato Configured e viene
generato un ConfigureCompleteEvent. Mentre è in questo
stato il metodo getTrackControls può essere chiamato
per ottenere gli oggetti TrackControl relativi alle singole
tracks del media stream. Le tracks sono i canali di dati di
cui è composto il media stream, ad esempio audio e
video. Nel nostro caso la traccia video è costituita
dalle varie immagini e il formato è JPEG. Un TrackControl
fornisce una serie di opzioni per elaborare la traccia, quale
ad esempio la conversione di formato.
Quando viene chiamato il metodo realize il Processor passa
nello stato Realized ed è completamente costruito.
Invocando il metodo start vengono processati i dati:
p.configure();
TrackControl
tcs[] = p.getTrackControls();
Format f[] = tcs[0].getSupportedFormats();
tcs[0].setFormat(f[0]);
p.realize();
p.start();
In
questo modo i dati hanno subito l'elaborazione da noi richiesta
e occorre solo scriverli su file. A tale scopo viene utilizzato
il DataSink, un oggetto usato per leggere dati dal DataSource
di output di un Processor e salvarli su un file.
Il DataSource di output si ottiene dal Processor invocando
il metodo getDataOutput. Analogamente al Processor il DataSink
viene costruito utilizzando un Manager, al quale vengono passati
il DataSource di output e un MediaLocator che specifica la
location del file che si vuole scrivere. A questo punto invochiamo
prima il metodo open per aprire il file e poi il metodo start
per iniziare a scrivere i dati. Il formato dei dati scritti
sul file è controllato dal Processor attraverso il
metodo setContentDescriptor (nel nostro esempio il formato
scelto è QuickTime):
p.setContentDescriptor(new
ContentDescriptor(FileTypeDescriptor.QUICKTIME));
DataSource
ds = p.getDataOutput();
DataSink dsink = Manager.createDataSink(ds, outML);
dsink.open();
dsink.start();
Per
poter gestire gli eventi generati dal Processor e dal DataSink,
utilizziamo rispettivamente i metodi addControllerListener
e addDataSinkListener. Quando i Controller generano eventi
vengono chiamati i metodi controllerUpdate e dataSinkUpdate.
Implementazione
Riportiamo di seguito, il codice che esegue la realizzazione
del video partendo dalla sequenza di immagini. Per l'esecuzione
corretta bisogna passare come argomenti larghezza e altezza
delle immagini, frame rate, url del file video, url delle
immagini:
java
GeneratingVideoFromImages -w <width> -h <height>
-f <frame rate per sec.> -o <output URL> <input
JPEG file 1> <input JPEG file 2> ...
java
GeneratingVideoFromImages -w 320 -h 240 -f 2 -o c:/video/video.mov
c:/images/*.jpg
importjava.io.*;
importjava.util.*;
importjava.awt.Dimension;
importjavax.media.*;
importjavax.media.control.*;
importjavax.media.protocol.*;
importjavax.media.protocol.DataSource;
importjavax.media.datasink.*;
importjavax.media.format.VideoFormat;
/**
* Questo programma prende una sequenza di immagini JPEG e
le converte in un
* file video QuickTime.
*/
publicclass GeneratingVideoFromImages implements ControllerListener,
DataSinkListener {
public boolean doIt(int width, int height, int frameRate,
Vector inFiles,
MediaLocator outML) {
ImageDataSource ids = new ImageDataSource(width, height, frameRate,
inFiles);
Processor p;
try {
System.err.println("Creazione del processor dal datasource
");
p = Manager.createProcessor(ids);
} catch (Exception e) {
System.err.println("Impossibile creare un processor dal
" +
"datasource!");
return false;
}
p.addControllerListener(this);
// Mettiamo il Processor nello stato configurato in modo da
poter
// settare le opzioni del Processor
p.configure();
if (!waitForState(p, p.Configured)) {
System.err.println("Configurazione del Processor fallita.");
return false;
}
// Poniamo l'uscita del Processor di tipo QuickTime.
p.setContentDescriptor(new ContentDescriptor(
FileTypeDescriptor.QUICKTIME));
// Settiamo il formato della traccia
TrackControl tcs[] = p.getTrackControls();
Format f[] = tcs[0].getSupportedFormats();
if (f == null || f.length <= 0) {
System.err.println("Questo formato non è supportato:
"
+ tcs[0].getFormat());
return false;
}
tcs[0].setFormat(f[0]);
System.err.println("Il formato settato della traccia
è: " + f[0]);
// Realizziamo il Processor
p.realize();
if (!waitForState(p, p.Realized)) {
System.err.println("Realizzazione del Processor fallita.");
return false;
}
// Creiamo il DataSink
DataSink dsink;
if ((dsink = createDataSink(p, outML)) == null) {
System.err.println("Creazione del DataSink fallita per
il " +
"MediaLocator: " + outML);
return false;
}
dsink.addDataSinkListener(this);
fileDone = false;
System.err.println("Avviata la transcodifica..");
// Avviamo il processo di transcodifica
try {
p.start();
dsink.start();
} catch (IOException e) {
System.err.println("Errore di IO durante la transcodifica.");
return false;
}
waitForFileDone();
try {
dsink.close();
} catch (Exception e) {
}
p.removeControllerListener(this);
System.err.println("...transcodifica terminata.");
return true;
}
/**
* Creazione del DataSink
*/
DataSink createDataSink(Processor p, MediaLocator outML) {
DataSource ds;
if ((ds = p.getDataOutput()) == null) {
System.err.println("Il Processor non ha un DataSource
di " +
"output. ");
return null;
}
DataSink dsink;
try {
System.err.println("DataSink creato per il MediaLocator:
"
+ outML);
dsink = Manager.createDataSink(ds, outML);
dsink.open();
} catch (Exception e) {
System.err.println("Impossibile creare il DataSink: "
+ e);
return null;
}
return dsink;
}
Object waitSync = new Object();
boolean stateTransitionOK = true;
/**
* Aspetta finché il Processor non è transitato
nello stato desiderato.
* Ritorna false se la transizione è fallita.
*/
boolean waitForState(Processor p, int state) {
synchronized (waitSync) {
try {
while (p.getState() < state && stateTransitionOK)
waitSync.wait();
} catch (Exception e) {
}
}
return stateTransitionOK;
}
/**
* Controller Listener.
*/
public void controllerUpdate(ControllerEvent evt) {
if (evt instanceof ConfigureCompleteEvent
|| evt instanceof RealizeCompleteEvent
|| evt instanceof PrefetchCompleteEvent) {
synchronized (waitSync) {
stateTransitionOK = true;
waitSync.notifyAll();
}
} else if (evt instanceof ResourceUnavailableEvent) {
synchronized (waitSync) {
stateTransitionOK = false;
waitSync.notifyAll();
}
} else if (evt instanceof EndOfMediaEvent) {
evt.getSourceController().stop();
evt.getSourceController().close();
}
}
Object waitFileSync = new Object();
boolean fileDone = false;
boolean fileSuccess = true;
/**
* Aspetta finché non è terminata la scrittura
del file.
*/
boolean waitForFileDone() {
synchronized (waitFileSync) {
try {
while (!fileDone)
waitFileSync.wait();
} catch (Exception e) {
}
}
return fileSuccess;
}
/**
* Gestore degli eventi del file writer.
*/
public void dataSinkUpdate(DataSinkEvent evt) {
if (evt instanceof EndOfStreamEvent) {
synchronized (waitFileSync) {
fileDone = true;
waitFileSync.notifyAll();
}
} else if (evt instanceof DataSinkErrorEvent) {
synchronized (waitFileSync) {
fileDone = true;
fileSuccess = false;
waitFileSync.notifyAll();
}
}
}
public static void main(String args[]) {
if (args.length == 0)
prUsage();
// Parsing deli argomenti.
int i = 0;
int width = -1, height = -1, frameRate = 1;
Vector inputFiles = new Vector();
String outputURL = null;
while (i < args.length) {
if (args[i].equals("-w")) {
i++;
if (i >= args.length)
prUsage();
width = new Integer(args[i]).intValue();
} else if (args[i].equals("-h")) {
i++;
if (i >= args.length)
prUsage();
height = new Integer(args[i]).intValue();
} else if (args[i].equals("-f")) {
i++;
if (i >= args.length)
prUsage();
frameRate = new Integer(args[i]).intValue();
} else if (args[i].equals("-o")) {
i++;
if (i >= args.length)
prUsage();
outputURL = args[i];
} else {
inputFiles.addElement(args[i]);
}
i++;
}
if (outputURL == null || inputFiles.size() == 0)
prUsage();
// Controlla l'estensione del file video desiderato.
if (!outputURL.endsWith(".mov") && !outputURL.endsWith(".MOV"))
{
System.err.println("L'estensione del file video dovrebbe
" +
"essere .mov");
prUsage();
}
if (width < 0 || height < 0) {
System.err.println("Specificare la corretta dimensione
dell'"
+ "immagine");
prUsage();
}
// Controlla il frame rate.
if (frameRate < 1)
frameRate = 1;
// Genera il media locator .
MediaLocator oml;
if ((oml = createMediaLocator(outputURL)) == null) {
System.err.println("Impossibile creare il Media Locator:
"
+ outputURL);
System.exit(0);
}
GeneratingVideoFromImages imgToMovie = new
GeneratingVideoFromImages();
imgToMovie.doIt(width, height, frameRate, inputFiles, oml);
System.exit(0);
}
static void prUsage() {
System.err.println("Come avviare il programma: "
+
"java GeneratingVideoFromImages -w " +
" <width> -h <height> -f <frame rate>
" +
"-o <output URL> <input JPEG file 1> "
+
"<input JPEG file 2> ...");
System.exit(-1);
}
/**
* Crea un MediaLocator a partire dalla url passatagli come
argomento
*/
static MediaLocator createMediaLocator(String url) {
MediaLocator ml;
if (url.indexOf(":") > 0 && (ml = new
MediaLocator(url)) != null)
return ml;
if (url.startsWith(File.separator)) {
if ((ml = new MediaLocator("file:" + url)) != null)
return ml;
} else {
String file = "file:" + System.getProperty("user.dir")
+ File.separator + url;
if ((ml = new MediaLocator(file)) != null)
return ml;
}
return null;
}
/**
* Un DataSource per leggere i dati da una sequenza di file
immagini JPEG
* e memorizzarli in uno stream di buffer.
*/
class ImageDataSource extends PullBufferDataSource {
ImageSourceStream streams[];
ImageDataSource(int width, int height, int frameRate, Vector
images)
{
streams = new ImageSourceStream[1];
streams[0] = new ImageSourceStream(width, height, frameRate,
images);
}
public void setLocator(MediaLocator source) {
}
public MediaLocator getLocator() {
return null;
}
public String getContentType() {
return ContentDescriptor.RAW;
}
public void connect() {
}
public void disconnect() {
}
public void start() {
}
public void stop() {
}
public PullBufferStream[] getStreams() {
return streams;
}
public Time getDuration() {
return DURATION_UNKNOWN;
}
public Object[] getControls() {
return new Object[0];
}
public Object getControl(String type) {
return null;
}
}
/**
* Lo stream sorgente per scorrere l'ImageDataSource.
*/
class ImageSourceStream implements PullBufferStream {
Vector images;
int width, height;
VideoFormat format;
float frameRate;
long seqNo = 0;
// indice della prossima immagine da leggere.
int nextImage = 0;
boolean ended = false;
public ImageSourceStream(int width, int height, int frameRate,
Vector images) {
this.width = width;
this.height = height;
this.images = images;
this.frameRate = (float) frameRate;
format = new VideoFormat(VideoFormat.JPEG,
new Dimension(width,height), Format.NOT_SPECIFIED, Format.byteArray,frameRate);
}
public boolean willReadBlock() {
return false;
}
/**
* Legge i frame del video.
*/
public void read(Buffer buf) throws IOException {
if (nextImage >= images.size()) {
System.err.println("Lette tutte le immagini.");
buf.setEOM(true);
buf.setOffset(0);
buf.setLength(0);
ended = true;
return;
}
String imageFile = (String) images.elementAt(nextImage);
nextImage++;
System.err.println("Lettura del file: " + imageFile);
// Apriamo un random access file per la prossima immagine.
RandomAccessFile raFile;
raFile = new RandomAccessFile(imageFile, "r");
byte data[] = null;
if (buf.getData() instanceof byte[])
data = (byte[]) buf.getData();
if (data == null || data.length < raFile.length()) {
data = new byte[(int) raFile.length()];
buf.setData(data);
}
long time = (long) (seqNo * (1000 / frameRate) * 1000000);
buf.setTimeStamp(time);
buf.setSequenceNumber(seqNo++);
raFile.readFully(data, 0, (int) raFile.length());
buf.setOffset(0);
buf.setLength((int) raFile.length());
buf.setFormat(format);
buf.setFlags(buf.getFlags() | buf.FLAG_KEY_FRAME);
raFile.close();
}
/**
* Ritorna il formato dell'immagine...che sarà JPEG
*/
public Format getFormat() {
return format;
}
public ContentDescriptor getContentDescriptor() {
return new ContentDescriptor(ContentDescriptor.RAW);
}
public long getContentLength() {
return 0;
}
public boolean endOfStream() {
return ended;
}
public Object[] getControls() {
return new Object[0];
}
public Object getControl(String type) {
return null;
}
}
}
Conclusioni
Come visto JMF è uno strumento molto potente per la
gestione e l'elaborazione di contenuti multimediali. Oltre
all'uso classico che se ne può fare, ovvero quello
legato alla riproduzione dei contenuti multimediali, si possono
pensare anche ad applicazione di editing, mixing ed encoding.
In questo articolo abbiamo visto come poter creare un video
avendo a disposizione una serie di immagini. Un'applicazione
di questo tipo può essere utile ad esempio per realizzare
un filmato con le immagini acquisite da una web cam o da un'IP
cam. Uno dei tanti scenari futuri che JMF preannuncia, è
la possibilità di poter lavorare con formati multimediali
sempre più aggiornati, come l'MPEG-4 e il 3GPP, che
ci permetteranno di renderizzare i contenuti multimediali
anche per terminali mobili.
Bibliografia
[1] "JMF API Specification", java.sun.com/products/java-media/jmf/index.jsp
Vincenzo
Viola è nato a Formia (LT) il 03/11/1977, laureato
in Ingegneria delle Telecomunicazioni presso l'Università
Federico II di Napoli con una tesi sulla segmentazione automatica
di video digitali in scene, con sviluppo software implementato
su piattaforma Java - Oracle. Lavora come progettista software
per una Mobile Company che offre la sua consulenza ai gestori
di telefonia e alle major del settore ICT.
|