MokaByte Numero 11 - Settembre 1997
  
Java Media Framework 
 
 
 
di
Massimo Carli
Le JavaMediaFramework dotano Java di strumenti multimediali. In questo articolo vedremo le caratteristiche principali del JMF attraverso la realizzazione di un lettore multimediale
 

 


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



 
 
 
 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it