La caratteristica di Java che ha contribuito in modo sostanziale alla sua notorietà è sicuramente la capacità di creare applet che permettono di dotare le pagine Web di oggetti multimediali con cui interagire. Una lacuna era sicuramente l'impossibilità di riprodurre, all'interno di applet o applicazioni Java, dei filmati o suoni diversi dal formato .au.. Sun, Silicon Graphics e Intel si sono uniti per porre rimedio a questa limitazione. Non sono state realizzate solo le API (ancora in versione beta) per la riproduzione audio e video ma sono in programma obiettivi più importanti che permetteranno di realizzare strumenti per l'acquisizione dati a livello di client e quindi, per la videoconferenza. In questo articolo vedremo i concetti principali relativi alla riproduzione video ed audio in Java creando un semplice lettore multimediale del tipo di quello disponibile con Window95
Che cosa sono le Java Media Framework
Come accennato gli obiettivi delle JMF sono quelli di fornire gli strumenti per la realizzazione di oggetti multimediali in Java che vadano oltre la semplice animazione ottenuta come successione di immagini. Gli strumenti che si vogliono fornire devono possedere tre caratteristiche principali:
\* Permettere la semplice realizzazione di oggetti multimediali per la riproduzione audio e video(aspetto di cui ci occuperemo in questo articolo).
\* Permettere l'estensione degli strumenti già presenti in modo da dotare loro di maggiori funzionalità.
\* Fornire le basi per l'aggiunta di player per nuovi formati audio o video non già interpretati dalle JMF.
La versione delle API che ho utilizzato per la creazione del player multimediale, è la beta e permette già di riprodurre suoni nei formati WAV, AU,e MIDI e video nei formati MPEG-1,MPEG-2, Quicktime ed AVI. Saranno questi i formati che il nostro lettore multimediale riuscirà infatti a riprodurre. Vedremo, inoltre, che la cosa sarà semplicissima. Nella gestione dei player, le JMF fanno una distinzione sulla sorgente dei dati a seconda del modo con cui essi vendono forniti al riproduttore. Esistono sorgenti:
\ Reliable* : sono sorgenti che forniscono tutti i dati relativi allo stream del media da riprodurre. In questo tipo di sorgente ogni dato che arriva al player deve essere riprodotto esattamente. Non ci devono essere delle perdite di dati. Il protocollo utilizzato in questo caso è l'HTTP (HyperText Tranfert Protocol), e la risorsa è individuata da un URL (Uniform Resurs Locator). Come avviene per le immagini (la risorsa individuata da un URL viene prelevata come vettore di byte, i primi dei quali ne determinano il formato da cui la interpretazione dei restanti come appartenenti ad una JPF o GIF), anche per queste risorse, la JMF metterà in azione l'handler appropriato. I dati ottenuti da questo tipo di sorgenti hanno la caratteristica di essere in numero finito e quindi la loro riproduzione può avvenire anche in modo ripetuto. Nel nostro esempio la sorgente è reliable.
\ Streaming*: questo tipo di sorgenti è quello che si utilizzerà nella videoconferenza. Le sorgenti preleveranno dati da uno strumento di rilevazione audio e/o video, e li riprodurranno attraverso il player adatto. Caratteristica dei dati relativi a questo tipo di sorgente è il fatto che non tutti saranno utilizzati. Molto dipenderà dalle prestazioni del player. Inoltre i dati sono riprodotti mano a mano che giungono al player e non potranno, se non registrati, essere riprodotti più volte come nel caso precedente. Il protocollo di trasmissione varia a seconda del tipo di sorgente. Per esempio, è in preparazione il protocollo VDO (Video On Demand) per la trasmissione video a richiesta.
I due tipi di dati precedenti si caratterizzano anche in relazione al funzionamento del player nello stato di stop. Nel primo caso lo stop del player causerà l'interruzione del prelevamento dei dati dalla sorgente, mentre nel secondo caso i dati continueranno a giungere al player anche se non verranno riprodotti. Le JMF sono, quindi, un insieme di classi ed interfacce che , ad oggi, permettono la implementazione di player. Esse sono raggruppate in due package principali:
\ java.media* : Contiene le basi per la realizzazione dei player e per la gestione dei controller relativi. Le interfacce sono principalmente dedicate a fornire strumenti di sincronizzazione (legati molto alla piattaforma), mentre le classi sono prevalentemente caratterizzanti tipi di eventi che, come vedremo, sono fondamentali nella programmazione con JMF.
\ java.media.protocol*: contiene interfacce e classi che forniscono strumenti per la gestione delle sorgenti dati ed i meccanismi di ottenimento di dati da esse.
Nel nostro caso utilizzeremo solamente gli strumenti del package java.
Come funzionano le JMF
Le API fornite con la beta delle JMF permettono la creazione di player. Possiamo considerare un player come una macchina software che elabora uno stream di dati provenienti da una sorgente rendendoli in maniera appropriata nei giusti tempi \[1\]. La sincronizzazione assume un ruolo fondamentale. Infatti, come vedremo, i dati prima di essere resi possono subire delle elaborazioni che necessitano di un certo tempo, per cui è necessario disporre di strumenti che notifichino lo stato dei calcoli. Infatti, un Java Media Player implementerà le seguenti interfacce:
\ Clock* : rappresenta lo strumento di sincronizzazione principale cui tutti i player si riferiranno. Dispone dei metodi principali di start() e stop() per l'avvio e lo stop della riproduzione del media.
\ Controller*: questa interfaccia estende la precedente e fornisce gli strumenti per la notifica da parte dei controller dell'avvenuta modifica di parametri di riproduzione del media. Essa contiene i metodi principali per la variazione dello stato del player.
\ Duration*: fornisce gli strumenti per determinare la durata del media relativamente al tempo scandito dal Clock.
\ Player*: estende Controller e Clock e permette la riproduzione del media.
Definizione importante è quella di media-time che rappresenta la posizione temporale dello stream relativamente alla sua riproduzione. Affinché più player siano sincronizzati, devono avere lo stesso media-time. A parte questi aspetti teorici ad ogni tipo di media JMF associa due particolari componenti con cui è possibile interagire attraverso una opportuna gestione degli eventi del tipo Delegation Model \[2\]. Ad ogni media sono associati i seguenti tipi di componenti:
\ Control-Panel*: rappresenta proprio il pannello su cui saranno messi i controlli con cui interagire con il media. In un control-panel vi saranno, per esempio, i pulsanti di start e stop di un filmato. Una volta creato un player sarà possibile verificare il numero ed il tipo di controller attraverso il metodo getControls() della interfaccia player. Oltre ad un controller di default, ogni player associato ad un particolare tipo di media dispone di eventuali altri dispositivi di controllo. Uno di questi è, per esempio, quello che permette di conoscere lo stato di bufferizzazione dei dati (CachingControl ,utile se la risorsa è prelevata dalla rete).
\ Visual-Component*: rappresenta il pannello su cui si ha la riproduzione del media. Nel caso di un video, il Visual-Panel non è altro che lo "schermo". Nel caso della riproduzione audio può non essere presente.
La realizzazione del nostro lettore multimediale non sarà altro che l'associazione di questi due componenti ad ogni tipo di media, e la gestione degli eventi che il Control-Panel emetterà. Gli eventi gestiti dal JMF sono tutti derivati dalla classe ControllerEvent e si possono classificare in tre categorie:
\ Notifiche di variazione*: sono eventi che notificano delle variazioni relative ai parametri del player e del media riprodotto. Per esempio abbiamo l'evento RateChangeEvent che notifica la variazione nella velocità di riproduzione con conseguente generazione di un evento di tipo DurationUpdateEvent che aggiorna al nuovo stato i parametri del player quali la durata di riproduzione.
\ Eventi di transizione*: questi, come vedremo nel prossimo paragrafo, sono molto importanti perché notificano la variazione dello stato di un player.
\ Eventi di notifica di un errore*: ogni volta che si verifica un errore nella gestione di un player o nella riproduzione di un media, si ha la generazione di un evento esplicativo del problema.
Possiamo allora dire che per la realizzazione di un player dobbiamo tenere conto dei seguenti aspetti fondamentali:
\* Creazione del player in relazione al tipo di media determinato dall'URL
\* Creazione del Visual-Component da inserire nel layout del player
\* Creazione del Control-Panel da inserire anch'esso nel layout del player
\* Gestione degli stati del player attraverso una controllo efficace degli eventi generati da esso o dai Controller associati
\* Rilascio delle risorse
Questo sarà anche il procedimento che seguiremo nella realizzazione del nostro esempio. L'ultimo punto è molto importante in quanto esistono delle risorse (risorse esclusive) che non possono essere utilizzate da più processi contemporaneamente. Per esempio la scheda audio non può essere condivisa per cui se un determinato player ne fa uso e termina il suo compito, deve rilasciare la risorsa a favore di un eventuale altro processo.
Stati di un player
Per la riproduzione audio e video sono necessarie delle risorse che impegnano il sistema in modo non indifferente. Le JMF hanno, allora, diviso lo stato di un player in 6 fasi, ciascuno caratterizzato dalle operazioni che è possibile effettuare. Questi stati percorrono la "vita" di un player durante la riproduzione di un media. Essi sono i seguenti (Figura 1):
\ Unrealized* : Il player si trova in questo stato all'atto della sua creazione, cioè quando è solamente a conoscenza dell'URL della risorsa da riprodurre. Un opportuno Manager è responsabile della creazione del player attraverso il metodo static createPlayer() il cui unico parametro è proprio l'URL associato.
\ Realizing*: Una volta che il player è stato creato, attraverso il metodo realize() della interfaccia Controller, è possibile portarlo in questo stato. Qui il player acquisisce le risorse di sistema che utilizzerà durante la riproduzione ma che non sono utili ad altri eventuali riproduttori. Si dice che il player si impossessa delle risorse non esclusive.
\ Realized*: Una volta che il player ha acquisito le risorse relative alla fase precedente, si porta automaticamente nello stato Realized. Qui il player è a conoscenza del suo compito per cui è qui che si rendono disponibili i Control-Panel ed il Visual-Panel. Un player nello stato realized non blocca nessun altro player candidato alla riproduzione del media, per cui possono esistere più player in questo stato.
\ Prefetching*: quando il player è nello stato Realized, è possibile utilizzare il metodo prefetch() per portarsi nello stato di Prefetching. Questo stato è caratterizzato dal fatto che il player inizia ad acquisire le risorse per la riproduzione del media, comprese le risorse esclusive se accessibili. Esso si riserva anche un certo buffer in cui farà il preloading dei primi dati da riprodurre. Il contenuto di questo buffer potrà eventualmente essere visualizzato da un Control-Panel di tipo CachingControl.
\ Prefetched*: Terminata la precedente fase, si ha il passaggio automatico allo stato di prefetched. Qui il player è pronto a partire. Tutte le risorse necessarie sono state acquisite ed eventuali altri player che utilizzassero le stesse risorse sono impossibilitati all'esecuzione fino a che le risorse stesse non saranno rilasciate.
\ Started*: Finalmente attraverso il metodo start() è possibile iniziare la riproduzione della risorsa attraverso il player.
Per il passaggio da uno stato ad un altro esistono cinque metodi principali che possono essere applicati solamente quando il player è in un particolare stato pena la generazione di una eccezione anch'essa esplicativa del problema. Esistono i seguenti cinque metodi:
\ start()* : appartiene all'interfaccia Clock e può essere eseguito solamente quando il player è nello stato Prefetched. Lo stato seguente è lo stato Started.
\ stop()*: appartiene all'interfaccia Clock e può essere eseguito solamente quando il player è nello stato Started. Lo stato seguente è lo stato Prefetched.
\ realize()*: Serve per portare il player dallo stato di Unrealized allo stato di Realizing.
\ prefetch()*; Serve per iniziare la fase di prefetching
\ deallocate()*: Serve per liberare le risorse esclusive acquisite dal player. In questo modo un eventuale altro player nello stato di Realized, può eseguire la prefetch().
Il passaggio tra gli stati è notificato attraverso la emissione di un evento la cui gestione è alla base della programmazione con le JMF.
Al Lavoro....
Facciamo un attimo il punto della situazione. Vogliamo fare in modo di leggere un determinato file audio o video e riprodurlo in maniera appropriata (Listato 1). Dal file letto, dovremmo ottenere l'URL che ci permetterà di creare il player. Per ottenere un player da un oggetto URL basta utilizzare la seguente istruzione:
Player player=Manager.createPlayer(url);
Esiste, infatti, la classe Manager che, attraverso i suoi metodi static, permette la creazione del player a partire dal suo URL (nel caso specifico si usa il metodo createPlayer()). A questo punto il manager esaminerà l'URL e creerà il player opportuno. Eseguiremo, quindi, il metodo realize() per procedere alla fase di Realizing. La generazione di un evento di tipo RealizeCompleteEvent, ci dirà che la fase di Realizing è terminata e che il player è nello stato di Realized. Esso si è "reso conto" di quello che deve fare e attende che gli venga dato il via per procedere. Ovviamente, per sentire l'evento, bisognerà registrarsi ad esso attraverso una istruzione del tipo
player.addControllerListener(this);
Attraverso l'implementazione della precedente interfaccia è possibile controllare lo stato del player in ogni istante attraverso l'ascolto di numerosi eventi che rappresentano la maggior parte delle classi del package java.media. Dovremo infatti definire il metodo
public synchronized void controllerUpdate(ControllerEvent ce)
esaminando l'oggetto ControllerEvent passato come parametro.
Un volta nello stato Realized, notificato dal fatto che è stato generato un evento del tipo realizeCompleteEvent, possiamo prelevare quelli che saranno i componenti caratteristici il media. Per fare questo utilizziamo due particolari metodi dell'interfaccia Player:
\ getVisualComponent()* : permette di ottenere il visual-component
\ getControlPanelComponent()*: permette di ottenere il Panel con gli strumenti di controllo del flusso del player.
Gli oggetti ritornati dai metodi precedenti sono oggetti Component che inseriremo nel nostro Frame principale utilizzando un layout di Java, nel nostro caso il BorderLayout. Metteremo il Visual-Component a "Center" ed il Control-Panel a "South". A questo punto il lettore multimediale si può considerare concluso. Abbiamo dotato la piccola application anche delle normali funzioni di caricamento. Abbiamo inserito anche l'opzione di chiusura in modo da permettere il rilascio delle risorse. Notiamo come la realizzazione sia stata molto semplice in quanto abbiamo voluto utilizzare gli strumenti standard offerti dalle JMF.
Conclusioni
In questo articolo abbiamo visto come si possano creare dei lettori multimediali in Java attraverso l'utilizzo delle API Java Media Framework. Abbiamo visto come alla base della programmazione con le JMF vi sia la gestione degli eventi secondo il modello Delegation Model. Si tratta, infatti, di gestire gli eventi generati dal player nelle fasi di cambiamento di stato. Le JMF permettono la riproduzione audio e video dei formati più comuni ma le aspirazioni di tali API sono molto superiori. Si vogliono, infatti, creare degli strumenti per la cattura video ed audio a livello di client che permettano di rendere Java uno strumento utile anche alla creazione di applicazioni che permettano la videoconferenza o la comunicazione audio a distanza. Staremo a vedere.
Bibliografia
[1] " Java Media Players" Version .95, JavaSoft 31 Dicembre 1996
[2] Massimo Carli, " La gestione degli eventi con il JDK1.1", Mokabyte
Marzo 1997
/*******************************************************
*
Application : MultiMediaPlayer
*
********************************************************
* Questa
applicazione rappresenta la realizzazione in *
* Java
di un lettore multimediale utilizzando le JMF *
* Environment:
JDK1.1.1 JMFBeta1.2
*
* Author:
Massimo Carli Release: 1.0 Date:24/05/97 *
********************************************************/
import
java.awt.*;
// Per GUI
import
java.awt.event.*; // Per la gestione
degli eventi
import
java.media.*;
// Per le JMF
import
java.net.*;
// Per l'URL
import
java.io.*;
// per l'I/O
public
class MultiMediaPlayer extends Frame
implements ActionListener,ControllerListener {
protected URL
url_media; // URL del media da riprodurre
protected MenuBar
menubar; // Barra del menu
protected Menu
file; // Menu
relativo ai file
protected MenuItem
open; // MenuItem
open
protected MenuItem
close; // MenuItem
close
protected Player
player; // E' il player
da creare
protected Component
visual; // Il visual component
protected Component
control; // Il control panel
protected Canvas
testa; // Intestazione
/*
*Costruttore vuoto
*/
public MultiMediaPlayer(){
super("MediaPlayer");
// Costruttore padre
crea_menu();
// Creiamo i menu
setSize(new Dimension(400,300));
// Settaggio delle dimensioni del frame
setLayout(new BorderLayout(5,5));
// Settiamo il layout
setBackground(new Color(200,200,200)); // Settiamo un
colore diverso per gli insets
enableEvents(AWTEvent.WINDOW_EVENT_MASK); // Abilitiamo gli eventi della
Window
}// fine costruttore vuoto
// Permette di creare la MenuBar
private final void crea_menu(){
// Creazione dei menu
menubar = new MenuBar();
file
= new Menu("File");
open
= new MenuItem("Open");
close = new MenuItem("Close");
// Registrazione degli ebenti ai menu
open.addActionListener(this);
close.addActionListener(this);
close.setEnabled(false);
// Composizione
file.add(open);
file.add(close);
menubar.add(file);
setMenuBar(menubar);
}// fine crea_menu
// Gestisce le scelte fatte con i menu
public void actionPerformed(ActionEvent ae){
MenuItem sorg= (MenuItem)(ae.getSource());
if (sorg.equals(open)){
manage_open();
} else if (sorg.equals(close)){
manage_close();
}
}// fine actionPerformed
// Rappresenta il metodo principale perchè permette di gestire
// i cambiamenti di stato del Player
public synchronized void controllerUpdate(ControllerEvent ce){
if (player!=null){ // Se esistem il player
if (ce instanceof RealizeCompleteEvent){
// Se il player è nello stato Realized
if ((control=player.getControlPanelComponent())!=null) // Se c'è
prendiamo il
add("South",control);
// ControlPanel
if ((visual=player.getVisualComponent())!=null) // Se esiste prendiamo
il VisualPanel
add("Center",visual);
// E lo mettiamo al centro
validate();
// Mettiamo a posto il layout
}// fine if
} // finr if esterno
}// fine controllerUpdate
// Permette di creare il player a partire dall'url
private final void crea_player(){
if (url_media!=null){ // Se e' stato creato un url
try{
player = Manager.createPlayer(url_media);
// Creiamo il player
player.addControllerListener(this);
// Sentiamo i ControllerListener
player.realize();
// Inizialo il Realizing
open.setEnabled(false);
// Disabilitiamo l'open
close.setEnabled(true);
// Abilitiamo la close
}catch(IOException ioe){
System.out.println("ERRORE "+ioe.toString());
}
}
}// fine creaplayer
// Permette di scegliere il file e di ottenere da esso
// l'url
private final void manage_open(){
url_media=null;
FileDialog fd= new FileDialog(this,"LOAD FILE",FileDialog.LOAD);
fd.setFile("*.au;*.wav;*.mid;*.avi;*.mpg");
fd.setVisible(true);
if ((fd.getFile()!=null)&&(fd.getDirectory()!=null)){
try{
String str=fd.getDirectory()+fd.getFile();
url_media=new URL("file:/"+str);
setTitle("MediaPlayer :"+fd.getFile());
} catch(MalformedURLException mue){
System.out.println("ERRORE "+mue.toString());
} catch(IOException ioe){
System.out.println("ERRORE "+ioe.toString());
}
crea_player();
}// fine if
fd.dispose();
}// fine manage_open
// Chiude il player
private final void manage_close(){
player.deallocate();
// Libera le risorse esclusive
url_media=null;
// mette l'url a null
removeAll();
// Toglie i Panel
open.setEnabled(true);
// Riabilita la open
close.setEnabled(false); // Ridisabilita
la close
setTitle("MediaPlayer"); // Aggiorna
il titolo del Frame
}// fine manage_close
/**
* Permette di mettere il Dialog in centro
*/
public void setVisible(boolean vis){
Dimension sc=Toolkit.getDefaultToolkit().getScreenSize();
// Dim. Schermo
int pos_x=((sc.width-getSize().width)/2);
// Posizione x centrata
int pos_y=((sc.height-getSize().height)/2); //
Posizione y centrata
setBounds(pos_x,pos_y,getSize().width,getSize().height);
super.setVisible(vis);
}// fine setVisible()
//
Gestisce la chiusura
protected
void processWindowEvent(WindowEvent e){
if ((e.paramString()).equals("WINDOW_CLOSING")){
System.exit(0);
if (player!=null)
player.deallocate();
} // fine if
if ((e.paramString()).equals("WINDOW_CLOSED"))
System.exit(0);
}//
fine processWindowEvent
public static void main(String[] str){
MultiMediaPlayer mp= new MultiMediaPlayer();
mp.setVisible(true);
}
}// fine
MultiMediaPlayer
